@@ -24,8 +24,14 @@ pub use source::Source;
24
24
pub use util:: parse_address;
25
25
26
26
use anyhow:: Result ;
27
+ use inflector:: Inflector ;
27
28
use proc_macro2:: TokenStream ;
28
- use std:: { collections:: HashMap , fs:: File , io:: Write , path:: Path } ;
29
+ use std:: {
30
+ collections:: HashMap ,
31
+ fs:: { self , File } ,
32
+ io:: Write ,
33
+ path:: Path ,
34
+ } ;
29
35
30
36
/// Builder struct for generating type-safe bindings from a contract's ABI
31
37
///
@@ -44,6 +50,7 @@ use std::{collections::HashMap, fs::File, io::Write, path::Path};
44
50
/// Abigen::new("ERC20Token", "./abi.json")?.generate()?.write_to_file("token.rs")?;
45
51
/// # Ok(())
46
52
/// # }
53
+ #[ derive( Debug , Clone ) ]
47
54
pub struct Abigen {
48
55
/// The source of the ABI JSON for the contract whose bindings
49
56
/// are being generated.
@@ -180,3 +187,286 @@ impl ContractBindings {
180
187
self . tokens
181
188
}
182
189
}
190
+
191
+ /// Generates bindings for a series of contracts
192
+ ///
193
+ /// This type can be used to generate multiple `ContractBindings` and put them all in a single rust
194
+ /// module, (eg. a `contracts` directory).
195
+ ///
196
+ /// This can be used to
197
+ /// 1) write all bindings directly into a new directory in the project's source directory, so that
198
+ /// it is included in the repository. 2) write all bindings to the value of cargo's `OUT_DIR` in a
199
+ /// build script and import the bindings as `include!(concat!(env!("OUT_DIR"), "/mod.rs"));`.
200
+ ///
201
+ /// However, the main purpose of this generator is to create bindings for option `1)` and write all
202
+ /// contracts to some `contracts` module in `src`, like `src/contracts/mod.rs` __once__ via a build
203
+ /// script or a test. After that it's recommend to remove the build script and replace it with an
204
+ /// integration test (See `MultiAbigen::ensure_consistent_bindings`) that fails if the generated
205
+ /// code is out of date. This has several advantages:
206
+ ///
207
+ /// * No need for downstream users to compile the build script
208
+ /// * No need for downstream users to run the whole `abigen!` generation steps
209
+ /// * The generated code is more usable in an IDE
210
+ /// * CI will fail if the generated code is out of date (if `abigen!` or the contract's ABI itself
211
+ /// changed)
212
+ ///
213
+ /// See `MultiAbigen::ensure_consistent_bindings` for the recommended way to set this up to generate
214
+ /// the bindings once via a test and then use the test to ensure consistency.
215
+ #[ derive( Debug , Clone ) ]
216
+ pub struct MultiAbigen {
217
+ /// whether to write all contracts in a single file instead of separated modules
218
+ single_file : bool ,
219
+
220
+ abigens : Vec < Abigen > ,
221
+ }
222
+
223
+ impl MultiAbigen {
224
+ /// Create a new instance from a series of already resolved `Abigen`
225
+ pub fn from_abigen ( abis : impl IntoIterator < Item = Abigen > ) -> Self {
226
+ Self {
227
+ single_file : false ,
228
+ abigens : abis. into_iter ( ) . map ( |abi| abi. rustfmt ( true ) ) . collect ( ) ,
229
+ }
230
+ }
231
+
232
+ /// Create a new instance from a series (`contract name`, `abi_source`)
233
+ ///
234
+ /// See `Abigen::new`
235
+ pub fn new < I , Name , Source > ( abis : I ) -> Result < Self >
236
+ where
237
+ I : IntoIterator < Item = ( Name , Source ) > ,
238
+ Name : AsRef < str > ,
239
+ Source : AsRef < str > ,
240
+ {
241
+ let abis = abis
242
+ . into_iter ( )
243
+ . map ( |( contract_name, abi_source) | Abigen :: new ( contract_name. as_ref ( ) , abi_source) )
244
+ . collect :: < Result < Vec < _ > > > ( ) ?;
245
+
246
+ Ok ( Self :: from_abigen ( abis) )
247
+ }
248
+
249
+ /// Reads all json files contained in the given `dir` and use the file name for the name of the
250
+ /// `ContractBindings`.
251
+ /// This is equivalent to calling `MultiAbigen::new` with all the json files and their filename.
252
+ ///
253
+ /// # Example
254
+ ///
255
+ /// ```text
256
+ /// abi
257
+ /// ├── ERC20.json
258
+ /// ├── Contract1.json
259
+ /// ├── Contract2.json
260
+ /// ...
261
+ /// ```
262
+ ///
263
+ /// ```no_run
264
+ /// # use ethers_contract_abigen::MultiAbigen;
265
+ /// let gen = MultiAbigen::from_json_files("./abi").unwrap();
266
+ /// ```
267
+ pub fn from_json_files ( dir : impl AsRef < Path > ) -> Result < Self > {
268
+ let mut abis = Vec :: new ( ) ;
269
+ for file in fs:: read_dir ( dir) ?. into_iter ( ) . filter_map ( std:: io:: Result :: ok) . filter ( |p| {
270
+ p. path ( ) . is_file ( ) && p. path ( ) . extension ( ) . and_then ( |ext| ext. to_str ( ) ) == Some ( "json" )
271
+ } ) {
272
+ let file: fs:: DirEntry = file;
273
+ if let Some ( file_name) = file. path ( ) . file_stem ( ) . and_then ( |s| s. to_str ( ) ) {
274
+ let content = fs:: read_to_string ( file. path ( ) ) ?;
275
+ abis. push ( ( file_name. to_string ( ) , content) ) ;
276
+ }
277
+ }
278
+ Self :: new ( abis)
279
+ }
280
+
281
+ /// Write all bindings into a single rust file instead of separate modules
282
+ #[ must_use]
283
+ pub fn single_file ( mut self ) -> Self {
284
+ self . single_file = true ;
285
+ self
286
+ }
287
+
288
+ /// Generates all the bindings and writes them to the given module
289
+ ///
290
+ /// # Example
291
+ ///
292
+ /// Read all json abi files from the `./abi` directory
293
+ /// ```text
294
+ /// abi
295
+ /// ├── ERC20.json
296
+ /// ├── Contract1.json
297
+ /// ├── Contract2.json
298
+ /// ...
299
+ /// ```
300
+ ///
301
+ /// and write them to the `./src/contracts` location as
302
+ ///
303
+ /// ```text
304
+ /// src/contracts
305
+ /// ├── mod.rs
306
+ /// ├── er20.rs
307
+ /// ├── contract1.rs
308
+ /// ├── contract2.rs
309
+ /// ...
310
+ /// ```
311
+ ///
312
+ /// ```no_run
313
+ /// # use ethers_contract_abigen::MultiAbigen;
314
+ /// let gen = MultiAbigen::from_json_files("./abi").unwrap();
315
+ /// gen.write_to_module("./src/contracts").unwrap();
316
+ /// ```
317
+ pub fn write_to_module ( self , module : impl AsRef < Path > ) -> Result < ( ) > {
318
+ let module = module. as_ref ( ) ;
319
+ fs:: create_dir_all ( module) ?;
320
+
321
+ let mut contracts_mod =
322
+ b"/// This module contains all the autogenerated abigen! contract bindings\n " . to_vec ( ) ;
323
+
324
+ let mut modules = Vec :: new ( ) ;
325
+ for abi in self . abigens {
326
+ let name = abi. contract_name . to_snake_case ( ) ;
327
+ let bindings = abi. generate ( ) ?;
328
+ if self . single_file {
329
+ // append to the mod file
330
+ bindings. write ( & mut contracts_mod) ?;
331
+ } else {
332
+ // create a contract rust file
333
+ let output = module. join ( format ! ( "{}.rs" , name) ) ;
334
+ bindings. write_to_file ( output) ?;
335
+ modules. push ( format ! ( "pub mod {};" , name) ) ;
336
+ }
337
+ }
338
+
339
+ if !modules. is_empty ( ) {
340
+ modules. sort ( ) ;
341
+ write ! ( contracts_mod, "{}" , modules. join( "\n " ) ) ?;
342
+ }
343
+
344
+ // write the mod file
345
+ fs:: write ( module. join ( "mod.rs" ) , contracts_mod) ?;
346
+
347
+ Ok ( ( ) )
348
+ }
349
+
350
+ /// This ensures that the already generated contract bindings match the output of a fresh new
351
+ /// run. Run this in a rust test, to get notified in CI if the newly generated bindings
352
+ /// deviate from the already generated ones, and it's time to generate them again. This could
353
+ /// happen if the ABI of a contract or the output that `ethers` generates changed.
354
+ ///
355
+ /// So if this functions is run within a test during CI and fails, then it's time to update all
356
+ /// bindings.
357
+ ///
358
+ /// Returns `true` if the freshly generated bindings match with the existing bindings, `false`
359
+ /// otherwise
360
+ ///
361
+ /// # Example
362
+ ///
363
+ /// Check that the generated files are up to date
364
+ ///
365
+ /// ```no_run
366
+ /// # use ethers_contract_abigen::MultiAbigen;
367
+ /// #[test]
368
+ /// fn generated_bindings_are_fresh() {
369
+ /// let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR"));
370
+ /// let abi_dir = project_root.join("abi");
371
+ /// let gen = MultiAbigen::from_json_files(&abi_dir).unwrap();
372
+ /// assert!(gen.ensure_consistent_bindings(project_root.join("src/contracts")));
373
+ /// }
374
+ ///
375
+ /// gen.write_to_module("./src/contracts").unwrap();
376
+ /// ```
377
+ #[ cfg( test) ]
378
+ pub fn ensure_consistent_bindings ( self , module : impl AsRef < Path > ) -> bool {
379
+ let module = module. as_ref ( ) ;
380
+ let dir = tempfile:: tempdir ( ) . expect ( "Failed to create temp dir" ) ;
381
+ let temp_module = dir. path ( ) . join ( "contracts" ) ;
382
+ self . write_to_module ( & temp_module) . expect ( "Failed to generate bindings" ) ;
383
+
384
+ for file in fs:: read_dir ( & temp_module) . unwrap ( ) {
385
+ let fresh_file = file. unwrap ( ) ;
386
+ let fresh_file_path = fresh_file. path ( ) ;
387
+ let file_name = fresh_file_path. file_name ( ) . and_then ( |p| p. to_str ( ) ) . unwrap ( ) ;
388
+ assert ! ( file_name. ends_with( ".rs" ) , "Expected rust file" ) ;
389
+
390
+ let existing_bindings_file = module. join ( file_name) ;
391
+
392
+ if !existing_bindings_file. is_file ( ) {
393
+ // file does not already exist
394
+ return false
395
+ }
396
+
397
+ // read the existing file
398
+ let existing_contract_bindings = fs:: read_to_string ( existing_bindings_file) . unwrap ( ) ;
399
+
400
+ let fresh_bindings = fs:: read_to_string ( fresh_file. path ( ) ) . unwrap ( ) ;
401
+
402
+ if existing_contract_bindings != fresh_bindings {
403
+ return false
404
+ }
405
+ }
406
+ true
407
+ }
408
+ }
409
+
410
+ #[ cfg( test) ]
411
+ mod tests {
412
+ use super :: * ;
413
+
414
+ #[ test]
415
+ fn can_generate_multi_abi ( ) {
416
+ let crate_root = std:: path:: Path :: new ( & env ! ( "CARGO_MANIFEST_DIR" ) ) ;
417
+
418
+ let tempdir = tempfile:: tempdir ( ) . unwrap ( ) ;
419
+ let mod_root = tempdir. path ( ) . join ( "contracts" ) ;
420
+
421
+ let console = Abigen :: new (
422
+ "Console" ,
423
+ crate_root. join ( "../tests/solidity-contracts/console.json" ) . display ( ) . to_string ( ) ,
424
+ )
425
+ . unwrap ( ) ;
426
+
427
+ let simple_storage = Abigen :: new (
428
+ "SimpleStorage" ,
429
+ crate_root
430
+ . join ( "../tests/solidity-contracts/simplestorage_abi.json" )
431
+ . display ( )
432
+ . to_string ( ) ,
433
+ )
434
+ . unwrap ( ) ;
435
+
436
+ let human_readable = Abigen :: new (
437
+ "HrContract" ,
438
+ r#"[
439
+ struct Foo { uint256 x; }
440
+ function foo(Foo memory x)
441
+ function bar(uint256 x, uint256 y, address addr)
442
+ yeet(uint256,uint256,address)
443
+ ]"# ,
444
+ )
445
+ . unwrap ( ) ;
446
+
447
+ let mut multi_gen = MultiAbigen :: from_abigen ( [ console, simple_storage, human_readable] ) ;
448
+
449
+ multi_gen. clone ( ) . write_to_module ( & mod_root) . unwrap ( ) ;
450
+ assert ! ( multi_gen. clone( ) . ensure_consistent_bindings( & mod_root) ) ;
451
+
452
+ // add another contract
453
+ multi_gen. abigens . push (
454
+ Abigen :: new (
455
+ "AdditionalContract" ,
456
+ r#"[
457
+ getValue() (uint256)
458
+ getValue(uint256 otherValue) (uint256)
459
+ getValue(uint256 otherValue, address addr) (uint256)
460
+ ]"# ,
461
+ )
462
+ . unwrap ( ) ,
463
+ ) ;
464
+
465
+ // ensure inconsistent bindings are detected
466
+ assert ! ( !multi_gen. clone( ) . ensure_consistent_bindings( & mod_root) ) ;
467
+
468
+ // update with new contract
469
+ multi_gen. clone ( ) . write_to_module ( & mod_root) . unwrap ( ) ;
470
+ assert ! ( multi_gen. clone( ) . ensure_consistent_bindings( & mod_root) ) ;
471
+ }
472
+ }
0 commit comments