@@ -4,19 +4,9 @@ import { AssertionError } from 'assert'
4
4
import { assertObject } from './utils/assertion'
5
5
6
6
import { ContractList , loadContract } from './deployments/lib/contract'
7
- import { logDebug , logError } from '../logger'
7
+ import { logDebug , logError , logWarn } from '../logger'
8
8
import { Provider , Signer } from 'ethers'
9
9
10
- // JSON format:
11
- // {
12
- // "<CHAIN_ID>": {
13
- // "<CONTRACT_NAME>": {
14
- // "address": "<ADDRESS>",
15
- // "proxy": true,
16
- // "implementation": { ... }
17
- // ...
18
- // }
19
- // }
20
10
export type AddressBookJson <
21
11
ChainId extends number = number ,
22
12
ContractName extends string = string ,
@@ -29,7 +19,21 @@ export type AddressBookEntry = {
29
19
}
30
20
31
21
/**
32
- * An abstract class to manage the address book
22
+ * An abstract class to manage an address book
23
+ * The address book must be a JSON file with the following structure:
24
+ * {
25
+ * "<CHAIN_ID>": {
26
+ * "<CONTRACT_NAME>": {
27
+ * "address": "<ADDRESS>",
28
+ * "proxy": true, // optional
29
+ * "implementation": { ... } // optional, nested contract structure
30
+ * ...
31
+ * }
32
+ * }
33
+ * Uses generics to allow specifying a ContractName type to indicate which contracts should be loaded from the address book
34
+ * Implementation should provide:
35
+ * - `isContractName(name: string): name is ContractName`, a type predicate to check if a given string is a ContractName
36
+ * - `loadContracts(signerOrProvider?: Signer | Provider): ContractList<ContractName>` to load contracts from the address book
33
37
*/
34
38
export abstract class AddressBook <
35
39
ChainId extends number = number ,
@@ -44,105 +48,54 @@ export abstract class AddressBook<
44
48
// The raw contents of the address book file
45
49
public addressBook : AddressBookJson < ChainId , ContractName >
46
50
47
- public strictAssert : boolean
51
+ // Contracts in the address book of type ContractName
52
+ private validContracts : ContractName [ ] = [ ]
53
+
54
+ // Contracts in the address book that are not of type ContractName, these are ignored
55
+ private invalidContracts : string [ ] = [ ]
56
+
57
+ // Type predicate to check if a given string is a ContractName
58
+ abstract isContractName ( name : string ) : name is ContractName
59
+
60
+ // Method to load valid contracts from the address book
61
+ abstract loadContracts ( signerOrProvider ?: Signer | Provider ) : ContractList < ContractName >
48
62
49
63
/**
50
64
* Constructor for the `AddressBook` class
51
65
*
52
66
* @param _file the path to the address book file
53
67
* @param _chainId the chain id of the network the address book should be loaded for
68
+ * @param _strictAssert
54
69
*
55
70
* @throws AssertionError if the target file is not a valid address book
56
71
* @throws Error if the target file does not exist
57
72
*/
58
- constructor ( _file : string , _chainId : number , _strictAssert = false ) {
59
- this . strictAssert = _strictAssert
73
+ constructor ( _file : string , _chainId : ChainId , _strictAssert = false ) {
60
74
this . file = _file
61
- if ( ! fs . existsSync ( this . file ) ) throw new Error ( `Address book path provided does not exist!` )
62
-
63
- logDebug ( `Loading address book for chainId ${ _chainId } from ${ this . file } ` )
64
- this . assertChainId ( _chainId )
65
75
this . chainId = _chainId
66
76
67
- // Ensure file is a valid address book
68
- this . addressBook = JSON . parse ( fs . readFileSync ( this . file , 'utf8' ) || '{}' ) as AddressBookJson < ChainId , ContractName >
69
- this . assertAddressBookJson ( this . addressBook )
77
+ logDebug ( `Loading address book from ${ this . file } .` )
78
+ if ( ! fs . existsSync ( this . file ) ) throw new Error ( `Address book path provided does not exist!` )
79
+
80
+ // Load address book and validate its shape
81
+ const fileContents = JSON . parse ( fs . readFileSync ( this . file , 'utf8' ) || '{}' )
82
+ this . assertAddressBookJson ( fileContents )
83
+ this . addressBook = fileContents
84
+ this . _parseAddressBook ( )
70
85
71
86
// If the address book is empty for this chain id, initialize it with an empty object
72
87
if ( ! this . addressBook [ this . chainId ] ) {
73
88
this . addressBook [ this . chainId ] = { } as Record < ContractName , AddressBookEntry >
74
89
}
75
90
}
76
91
77
- abstract isValidContractName ( name : string ) : boolean
78
-
79
- abstract loadContracts ( chainId : number , signerOrProvider ?: Signer | Provider ) : ContractList < ContractName >
80
-
81
- // TODO: implement chain id validation?
82
- assertChainId ( chainId : string | number ) : asserts chainId is ChainId { }
83
-
84
- // Asserts the provided object is a valid address book
85
- // Logs warnings for unsupported chain ids or invalid contract names
86
- assertAddressBookJson (
87
- json : unknown ,
88
- ) : asserts json is AddressBookJson < ChainId , ContractName > {
89
- this . _assertAddressBookJson ( json )
90
-
91
- // // Validate contract names
92
- const contractList = json [ this . chainId ]
93
-
94
- const contractNames = contractList ? Object . keys ( contractList ) : [ ]
95
- for ( const contract of contractNames ) {
96
- if ( ! this . isValidContractName ( contract ) ) {
97
- const message = `Detected invalid contract in address book: ${ contract } , for chainId ${ this . chainId } `
98
- if ( this . strictAssert ) {
99
- throw new Error ( message )
100
- } else {
101
- logError ( message )
102
- }
103
- }
104
- }
105
- }
106
-
107
- _assertAddressBookJson ( json : unknown ) : asserts json is AddressBookJson {
108
- assertObject ( json , 'Assertion failed: address book is not an object' )
109
-
110
- const contractList = json [ this . chainId ]
111
- try {
112
- assertObject ( contractList , 'Assertion failed: chain contract list is not an object' )
113
- } catch ( error ) {
114
- if ( this . strictAssert ) throw error
115
- else return
116
- }
117
-
118
- const contractNames = Object . keys ( contractList )
119
- for ( const contractName of contractNames ) {
120
- this . _assertAddressBookEntry ( contractList [ contractName ] )
121
- }
122
- }
123
-
124
- _assertAddressBookEntry ( json : unknown ) : asserts json is AddressBookEntry {
125
- assertObject ( json )
126
-
127
- try {
128
- if ( typeof json . address !== 'string' ) throw new AssertionError ( { message : 'Invalid address' } )
129
- if ( json . proxy && typeof json . proxy !== 'boolean' )
130
- throw new AssertionError ( { message : 'Invalid proxy' } )
131
- if ( json . implementation && typeof json . implementation !== 'object' )
132
- throw new AssertionError ( { message : 'Invalid implementation' } )
133
- } catch ( error ) {
134
- if ( this . strictAssert ) throw error
135
- else return
136
- }
137
- }
138
-
139
92
/**
140
93
* List entry names in the address book
141
94
*
142
95
* @returns a list with all the names of the entries in the address book
143
96
*/
144
97
listEntries ( ) : ContractName [ ] {
145
- return Object . keys ( this . addressBook [ this . chainId ] ) as ContractName [ ]
98
+ return this . validContracts
146
99
}
147
100
148
101
/**
@@ -154,7 +107,9 @@ export abstract class AddressBook<
154
107
*/
155
108
getEntry ( name : ContractName ) : AddressBookEntry {
156
109
try {
157
- return this . addressBook [ this . chainId ] [ name ]
110
+ const entry = this . addressBook [ this . chainId ] [ name ]
111
+ this . _assertAddressBookEntry ( entry )
112
+ return entry
158
113
} catch ( _ ) {
159
114
// TODO: should we throw instead?
160
115
return { address : '0x0000000000000000000000000000000000000000' }
@@ -179,36 +134,92 @@ export abstract class AddressBook<
179
134
}
180
135
181
136
/**
182
- * Loads all contracts from an address book
183
- *
184
- * @param addressBook Address book to use
185
- * @param signerOrProvider Signer or provider to use
186
- * @param enableTxLogging Enable transaction logging to console and output file. Defaults to `true`
187
- * @returns the loaded contracts
188
- */
137
+ * Parse address book and separate valid and invalid contracts
138
+ */
139
+ _parseAddressBook ( ) {
140
+ const contractList = this . addressBook [ this . chainId ]
141
+
142
+ const contractNames = contractList ? Object . keys ( contractList ) : [ ]
143
+ for ( const contract of contractNames ) {
144
+ if ( ! this . isContractName ( contract ) ) {
145
+ this . invalidContracts . push ( contract )
146
+ } else {
147
+ this . validContracts . push ( contract )
148
+ }
149
+ }
150
+
151
+ if ( this . invalidContracts . length > 0 ) {
152
+ logWarn ( `Detected invalid contracts in address book - these will not be loaded: ${ this . invalidContracts . join ( ', ' ) } ` )
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Loads all valid contracts from an address book
158
+ *
159
+ * @param addressBook Address book to use
160
+ * @param signerOrProvider Signer or provider to use
161
+ * @returns the loaded contracts
162
+ */
189
163
_loadContracts (
190
- artifactsPath : string | string [ ] ,
164
+ artifactsPath : string | string [ ] | Record < ContractName , string > ,
191
165
signerOrProvider ?: Signer | Provider ,
192
166
) : ContractList < ContractName > {
193
167
const contracts = { } as ContractList < ContractName >
194
168
for ( const contractName of this . listEntries ( ) ) {
195
- try {
196
- const contract = loadContract (
197
- contractName ,
198
- this . getEntry ( contractName ) . address ,
199
- artifactsPath ,
200
- signerOrProvider ,
201
- )
202
- contracts [ contractName ] = contract
203
- } catch ( error ) {
204
- if ( error instanceof Error ) {
205
- throw new Error ( `Could not load contracts - ${ error . message } ` )
206
- } else {
207
- throw new Error ( `Could not load contracts` )
208
- }
169
+ const artifactPath = typeof artifactsPath === 'object' && ! Array . isArray ( artifactsPath )
170
+ ? artifactsPath [ contractName ]
171
+ : artifactsPath
172
+
173
+ if ( Array . isArray ( artifactPath )
174
+ ? ! artifactPath . some ( fs . existsSync )
175
+ : ! fs . existsSync ( artifactPath ) ) {
176
+ logWarn ( `Could not load contract ${ contractName } - artifact not found` )
177
+ logWarn ( artifactPath )
178
+ continue
209
179
}
180
+ logDebug ( `Loading contract ${ contractName } ` )
181
+
182
+ const contract = loadContract (
183
+ contractName ,
184
+ this . getEntry ( contractName ) . address ,
185
+ artifactPath ,
186
+ signerOrProvider ,
187
+ )
188
+ contracts [ contractName ] = contract
210
189
}
211
190
212
191
return contracts
213
192
}
193
+
194
+ // Asserts the provided object has the correct JSON format shape for an address book
195
+ // This method can be overridden by subclasses to provide custom validation
196
+ assertAddressBookJson (
197
+ json : unknown ,
198
+ ) : asserts json is AddressBookJson < ChainId , ContractName > {
199
+ this . _assertAddressBookJson ( json )
200
+ }
201
+
202
+ // Asserts the provided object is a valid address book
203
+ _assertAddressBookJson ( json : unknown ) : asserts json is AddressBookJson {
204
+ assertObject ( json , 'Assertion failed: address book is not an object' )
205
+
206
+ const contractList = json [ this . chainId ]
207
+ assertObject ( contractList , 'Assertion failed: chain contract list is not an object' )
208
+
209
+ const contractNames = Object . keys ( contractList )
210
+ for ( const contractName of contractNames ) {
211
+ this . _assertAddressBookEntry ( contractList [ contractName ] )
212
+ }
213
+ }
214
+
215
+ // Asserts the provided object is a valid address book entry
216
+ _assertAddressBookEntry ( json : unknown ) : asserts json is AddressBookEntry {
217
+ assertObject ( json )
218
+
219
+ if ( typeof json . address !== 'string' ) throw new AssertionError ( { message : 'Invalid address' } )
220
+ if ( json . proxy && typeof json . proxy !== 'boolean' )
221
+ throw new AssertionError ( { message : 'Invalid proxy' } )
222
+ if ( json . implementation && typeof json . implementation !== 'object' )
223
+ throw new AssertionError ( { message : 'Invalid implementation' } )
224
+ }
214
225
}
0 commit comments