@@ -4,19 +4,9 @@ import { AssertionError } from 'assert'
44import { assertObject } from './utils/assertion'
55
66import { ContractList , loadContract } from './deployments/lib/contract'
7- import { logDebug , logError } from '../logger'
7+ import { logDebug , logError , logWarn } from '../logger'
88import { Provider , Signer } from 'ethers'
99
10- // JSON format:
11- // {
12- // "<CHAIN_ID>": {
13- // "<CONTRACT_NAME>": {
14- // "address": "<ADDRESS>",
15- // "proxy": true,
16- // "implementation": { ... }
17- // ...
18- // }
19- // }
2010export type AddressBookJson <
2111 ChainId extends number = number ,
2212 ContractName extends string = string ,
@@ -29,7 +19,21 @@ export type AddressBookEntry = {
2919}
3020
3121/**
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
3337 */
3438export abstract class AddressBook <
3539 ChainId extends number = number ,
@@ -44,105 +48,54 @@ export abstract class AddressBook<
4448 // The raw contents of the address book file
4549 public addressBook : AddressBookJson < ChainId , ContractName >
4650
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 >
4862
4963 /**
5064 * Constructor for the `AddressBook` class
5165 *
5266 * @param _file the path to the address book file
5367 * @param _chainId the chain id of the network the address book should be loaded for
68+ * @param _strictAssert
5469 *
5570 * @throws AssertionError if the target file is not a valid address book
5671 * @throws Error if the target file does not exist
5772 */
58- constructor ( _file : string , _chainId : number , _strictAssert = false ) {
59- this . strictAssert = _strictAssert
73+ constructor ( _file : string , _chainId : ChainId , _strictAssert = false ) {
6074 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 )
6575 this . chainId = _chainId
6676
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 ( )
7085
7186 // If the address book is empty for this chain id, initialize it with an empty object
7287 if ( ! this . addressBook [ this . chainId ] ) {
7388 this . addressBook [ this . chainId ] = { } as Record < ContractName , AddressBookEntry >
7489 }
7590 }
7691
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-
13992 /**
14093 * List entry names in the address book
14194 *
14295 * @returns a list with all the names of the entries in the address book
14396 */
14497 listEntries ( ) : ContractName [ ] {
145- return Object . keys ( this . addressBook [ this . chainId ] ) as ContractName [ ]
98+ return this . validContracts
14699 }
147100
148101 /**
@@ -154,7 +107,9 @@ export abstract class AddressBook<
154107 */
155108 getEntry ( name : ContractName ) : AddressBookEntry {
156109 try {
157- return this . addressBook [ this . chainId ] [ name ]
110+ const entry = this . addressBook [ this . chainId ] [ name ]
111+ this . _assertAddressBookEntry ( entry )
112+ return entry
158113 } catch ( _ ) {
159114 // TODO: should we throw instead?
160115 return { address : '0x0000000000000000000000000000000000000000' }
@@ -179,36 +134,92 @@ export abstract class AddressBook<
179134 }
180135
181136 /**
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+ */
189163 _loadContracts (
190- artifactsPath : string | string [ ] ,
164+ artifactsPath : string | string [ ] | Record < ContractName , string > ,
191165 signerOrProvider ?: Signer | Provider ,
192166 ) : ContractList < ContractName > {
193167 const contracts = { } as ContractList < ContractName >
194168 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
209179 }
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
210189 }
211190
212191 return contracts
213192 }
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+ }
214225}
0 commit comments