ethcontract_derive/
lib.rs

1#![deny(missing_docs, unsafe_code)]
2
3//! Implementation of procedural macro for generating type-safe bindings to an
4//! ethereum smart contract.
5
6extern crate proc_macro;
7
8mod spanned;
9
10use crate::spanned::{ParseInner, Spanned};
11use anyhow::{anyhow, Result};
12use ethcontract_common::abi::{Function, Param, ParamType};
13use ethcontract_common::abiext::{FunctionExt, ParamTypeExt};
14use ethcontract_common::artifact::truffle::TruffleLoader;
15use ethcontract_common::contract::Network;
16use ethcontract_common::Address;
17use ethcontract_generate::loaders::{HardHatFormat, HardHatLoader};
18use ethcontract_generate::{parse_address, ContractBuilder, Source};
19use proc_macro::TokenStream;
20use proc_macro2::{Span, TokenStream as TokenStream2};
21use quote::{quote, ToTokens as _};
22use std::collections::HashSet;
23use syn::ext::IdentExt;
24use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult};
25use syn::{
26    braced, parenthesized, parse_macro_input, Error as SynError, Ident, LitInt, LitStr, Path,
27    Token, Visibility,
28};
29
30/// Proc macro to generate type-safe bindings to a contract.
31///
32/// This macro accepts a path to an artifact JSON file. Note that this path
33/// is rooted in the crate's root `CARGO_MANIFEST_DIR`:
34///
35/// ```ignore
36/// contract!("build/contracts/WETH9.json");
37/// ```
38///
39/// Alternatively, other sources may be used, for full details consult the
40/// [`ethcontract_generate::source`] documentation. Some basic examples:
41///
42/// ```ignore
43/// // HTTP(S) source
44/// contract!("https://my.domain.local/path/to/contract.json")
45///
46/// // Etherscan source
47/// contract!("etherscan:0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
48///
49/// // NPM package source
50/// contract!("npm:@openzeppelin/contracts@4.2.0/build/contracts/IERC20.json")
51/// ```
52///
53/// Note that Etherscan rate-limits requests to their API, to avoid this an
54/// `ETHERSCAN_API_KEY` environment variable can be set. If it is, it will use
55/// that API key when retrieving the contract ABI.
56///
57/// Currently, the proc macro accepts additional parameters to configure some
58/// aspects of the code generation. Specifically it accepts the following.
59///
60/// - `format`: format of the artifact.
61///
62///   Available values are:
63///
64///   - `truffle` (default) to use [truffle loader];
65///   - `hardhat` to use [hardhat loader] in [single export mode];
66///   - `hardhat_multi` to use hardhat loader in [multi export mode].
67///
68///   Note that hardhat artifacts export multiple contracts. You'll have to use
69///   `contract` parameter to specify which contract to generate bindings to.
70///
71///   [truffle loader]: ethcontract_common::artifact::truffle::TruffleLoader
72///   [hardhat loader]: ethcontract_common::artifact::hardhat::HardHatLoader
73///   [single export mode]: ethcontract_common::artifact::hardhat::Format::SingleExport
74///   [multi export mode]: ethcontract_common::artifact::hardhat::Format::MultiExport
75///
76/// - `contract`: name of the contract we're generating bindings to.
77///
78///   If an artifact exports a single unnamed artifact, this parameter
79///   can be used to set its name. For example:
80///
81///   ```ignore
82///   contract!(
83///       "etherscan:0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
84///       contract = WETH9
85///   );
86///   ```
87///
88///   Otherwise, it can be used to specify which contract we're generating
89///   bindings to. Additionally, you can rename contract class by specifying
90///   a new name after the `as` keyword. For example:
91///
92///   ```ignore
93///   contract!(
94///       "build/contracts.json",
95///       format = hardhat_multi,
96///       contract = WETH9 as WrappedEthereum
97///   );
98///   ```
99///
100/// - `mod`: name of the contract module to place generated code in.
101///
102///   This defaults to the contract name converted into snake case.
103///
104///   Note that the root contract type gets re-exported in the context where the
105///   macro was invoked.
106///
107///   Example:
108///
109///   ```ignore
110///   contract!(
111///       "build/contracts/WETH9.json",
112///       contract = WETH9 as WrappedEthereum,
113///       mod = weth,
114///   );
115///   ```
116///
117/// - `deployments`: a list of additional addresses of deployed contract for
118///   specified network IDs.
119///
120///   This mapping allows generated contract's `deployed` function to work
121///   with networks that are not included in the artifact's deployment
122///   information.
123///
124///   Note that deployments defined this way **take precedence** over
125///   the ones defined in the artifact.
126///
127///   This parameter is intended to be used to manually specify contract
128///   addresses for test environments, be it testnet addresses that may defer
129///   from the originally published artifact or deterministic contract
130///   addresses on local development nodes.
131///
132///   Example:
133///
134///   ```ignore
135///   contract!(
136///       "build/contracts/WETH9.json",
137///       deployments {
138///           4 => "0x000102030405060708090a0b0c0d0e0f10111213",
139///           5777 => "0x0123456789012345678901234567890123456789",
140///       },
141///   );
142///   ```
143///
144/// - `methods`: a list of mappings from method signatures to method names
145///   allowing methods names to be explicitly set for contract methods.
146///
147///   This also provides a workaround for generating code for contracts
148///   with multiple methods with the same name.
149///
150///   Example:
151///
152///   ```ignore
153///   contract!(
154///       "build/contracts/WETH9.json",
155///       methods {
156///           approve(Address, U256) as set_allowance
157///       },
158///   );
159///   ```
160///
161/// - `event_derives`: a list of additional derives that should be added to
162///   contract event structs and enums.
163///
164///   Example:
165///
166///   ```ignore
167///   contract!(
168///       "build/contracts/WETH9.json",
169///       event_derives (serde::Deserialize, serde::Serialize),
170///   );
171///   ```
172///
173/// - `crate`: the name of the `ethcontract` crate. This is useful if the crate
174///   was renamed in the `Cargo.toml` for whatever reason.
175///
176/// Additionally, the ABI source can be preceded by a visibility modifier such
177/// as `pub` or `pub(crate)`. This visibility modifier is applied to both the
178/// generated module and contract re-export. If no visibility modifier is
179/// provided, then none is used for the generated code as well, making the
180/// module and contract private to the scope where the macro was invoked.
181///
182/// Full example:
183///
184/// ```ignore
185/// contract!(
186///     pub(crate) "build/contracts.json",
187///     format = hardhat_multi,
188///     contract = WETH9 as WrappedEthereum,
189///     mod = weth,
190///     deployments {
191///         4 => "0x000102030405060708090a0b0c0d0e0f10111213",
192///         5777 => "0x0123456789012345678901234567890123456789",
193///     },
194///     methods {
195///         myMethod(uint256,bool) as my_renamed_method;
196///     },
197///     event_derives (serde::Deserialize, serde::Serialize),
198///     crate = ethcontract_renamed,
199/// );
200/// ```
201///
202/// See [`ethcontract`](ethcontract) module level documentation for additional
203/// information.
204#[proc_macro]
205pub fn contract(input: TokenStream) -> TokenStream {
206    let args = parse_macro_input!(input as Spanned<ContractArgs>);
207    let span = args.span();
208    generate(args.into_inner())
209        .unwrap_or_else(|e| SynError::new(span, format!("{:?}", e)).to_compile_error())
210        .into()
211}
212
213fn generate(args: ContractArgs) -> Result<TokenStream2> {
214    let mut artifact_format = Format::Truffle;
215    let mut contract_name = None;
216
217    let mut builder = ContractBuilder::new();
218    builder.visibility_modifier = args.visibility;
219
220    for parameter in args.parameters.into_iter() {
221        match parameter {
222            Parameter::Mod(name) => builder.contract_mod_override = Some(name),
223            Parameter::Contract(name, alias) => {
224                builder.contract_name_override = alias.or_else(|| Some(name.clone()));
225                contract_name = Some(name);
226            }
227            Parameter::Crate(name) => builder.runtime_crate_name = name,
228            Parameter::Deployments(deployments) => {
229                for deployment in deployments {
230                    builder.networks.insert(
231                        deployment.network_id.to_string(),
232                        Network {
233                            address: deployment.address,
234                            deployment_information: None,
235                        },
236                    );
237                }
238            }
239            Parameter::Methods(methods) => {
240                for method in methods {
241                    builder
242                        .method_aliases
243                        .insert(method.signature, method.alias);
244                }
245            }
246            Parameter::EventDerives(derives) => {
247                builder.event_derives.extend(derives);
248            }
249            Parameter::Format(format) => artifact_format = format,
250        };
251    }
252
253    let source = Source::parse(&args.artifact_path)?;
254    let json = source.artifact_json()?;
255
256    match artifact_format {
257        Format::Truffle => {
258            let mut contract = TruffleLoader::new().load_contract_from_str(&json)?;
259
260            if let Some(contract_name) = contract_name {
261                if contract.name.is_empty() {
262                    contract.name = contract_name;
263                } else if contract.name != contract_name {
264                    return Err(anyhow!(
265                        "there is no contract '{}' in artifact '{}'",
266                        contract_name,
267                        args.artifact_path
268                    ));
269                }
270            }
271
272            Ok(builder.generate(&contract)?.into_tokens())
273        }
274
275        Format::HardHat(format) => {
276            let artifact = HardHatLoader::new().load_from_str(format, &json)?;
277
278            if let Some(contract_name) = contract_name {
279                if let Some(contract) = artifact.get(&contract_name) {
280                    Ok(builder.generate(contract)?.into_tokens())
281                } else {
282                    Err(anyhow!(
283                        "there is no contract '{}' in artifact '{}'",
284                        contract_name,
285                        args.artifact_path
286                    ))
287                }
288            } else {
289                Err(anyhow!(
290                    "when using hardhat artifacts, you should specify \
291                     contract name using 'contract' parameter"
292                ))
293            }
294        }
295    }
296}
297
298/// Contract procedural macro arguments.
299#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
300struct ContractArgs {
301    visibility: Option<String>,
302    artifact_path: String,
303    parameters: Vec<Parameter>,
304}
305
306impl ParseInner for ContractArgs {
307    fn spanned_parse(input: ParseStream) -> ParseResult<(Span, Self)> {
308        let visibility = match input.parse::<Visibility>()? {
309            Visibility::Inherited => None,
310            token => Some(quote!(#token).to_string()),
311        };
312
313        // TODO(nlordell): Due to limitation with the proc-macro Span API, we
314        //   can't currently get a path the the file where we were called from;
315        //   therefore, the path will always be rooted on the cargo manifest
316        //   directory. Eventually we can use the `Span::source_file` API to
317        //   have a better experience.
318        let (span, artifact_path) = {
319            let literal = input.parse::<LitStr>()?;
320            (literal.span(), literal.value())
321        };
322
323        if !input.is_empty() {
324            input.parse::<Token![,]>()?;
325        }
326        let parameters = input
327            .parse_terminated(Parameter::parse, Token![,])?
328            .into_iter()
329            .collect();
330
331        Ok((
332            span,
333            ContractArgs {
334                visibility,
335                artifact_path,
336                parameters,
337            },
338        ))
339    }
340}
341
342/// Artifact format
343#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
344enum Format {
345    Truffle,
346    HardHat(HardHatFormat),
347}
348
349/// A single procedural macro parameter.
350#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
351enum Parameter {
352    Mod(String),
353    Contract(String, Option<String>),
354    Crate(String),
355    Deployments(Vec<Deployment>),
356    Methods(Vec<Method>),
357    EventDerives(Vec<String>),
358    Format(Format),
359}
360
361impl Parse for Parameter {
362    fn parse(input: ParseStream) -> ParseResult<Self> {
363        let name = input.call(Ident::parse_any)?;
364        let param = match name.to_string().as_str() {
365            "crate" => {
366                input.parse::<Token![=]>()?;
367                let name = input.call(Ident::parse_any)?.to_string();
368                Parameter::Crate(name)
369            }
370            "mod" => {
371                input.parse::<Token![=]>()?;
372                let name = input.parse::<Ident>()?.to_string();
373                Parameter::Mod(name)
374            }
375            "format" => {
376                input.parse::<Token![=]>()?;
377                let token = input.parse::<Ident>()?;
378                let format = match token.to_string().as_str() {
379                    "truffle" => Format::Truffle,
380                    "hardhat" => Format::HardHat(HardHatFormat::SingleExport),
381                    "hardhat_multi" => Format::HardHat(HardHatFormat::MultiExport),
382                    format => {
383                        return Err(ParseError::new(
384                            token.span(),
385                            format!("unknown format {}", format),
386                        ))
387                    }
388                };
389                Parameter::Format(format)
390            }
391            "contract" => {
392                input.parse::<Token![=]>()?;
393                let name = input.parse::<Ident>()?.to_string();
394                let alias = if input.parse::<Option<Token![as]>>()?.is_some() {
395                    Some(input.parse::<Ident>()?.to_string())
396                } else {
397                    None
398                };
399
400                Parameter::Contract(name, alias)
401            }
402            "deployments" => {
403                let content;
404                braced!(content in input);
405                let deployments = {
406                    let parsed =
407                        content.parse_terminated(Spanned::<Deployment>::parse, Token![,])?;
408
409                    let mut deployments = Vec::with_capacity(parsed.len());
410                    let mut networks = HashSet::new();
411                    for deployment in parsed {
412                        if !networks.insert(deployment.network_id) {
413                            return Err(ParseError::new(
414                                deployment.span(),
415                                "duplicate network ID in `ethcontract::contract!` macro invocation",
416                            ));
417                        }
418                        deployments.push(deployment.into_inner())
419                    }
420
421                    deployments
422                };
423
424                Parameter::Deployments(deployments)
425            }
426            "methods" => {
427                let content;
428                braced!(content in input);
429                let methods = {
430                    let parsed = content.parse_terminated(Spanned::<Method>::parse, Token![;])?;
431
432                    let mut methods = Vec::with_capacity(parsed.len());
433                    let mut signatures = HashSet::new();
434                    let mut aliases = HashSet::new();
435                    for method in parsed {
436                        if !signatures.insert(method.signature.clone()) {
437                            return Err(ParseError::new(
438                                method.span(),
439                                "duplicate method signature in `ethcontract::contract!` macro invocation",
440                            ));
441                        }
442                        if !aliases.insert(method.alias.clone()) {
443                            return Err(ParseError::new(
444                                method.span(),
445                                "duplicate method alias in `ethcontract::contract!` macro invocation",
446                            ));
447                        }
448                        methods.push(method.into_inner())
449                    }
450
451                    methods
452                };
453
454                Parameter::Methods(methods)
455            }
456            "event_derives" => {
457                let content;
458                parenthesized!(content in input);
459                let derives = content
460                    .parse_terminated(Path::parse, Token![,])?
461                    .into_iter()
462                    .map(|path| path.to_token_stream().to_string())
463                    .collect();
464                Parameter::EventDerives(derives)
465            }
466            _ => {
467                return Err(ParseError::new(
468                    name.span(),
469                    format!("unexpected named parameter `{}`", name),
470                ))
471            }
472        };
473
474        Ok(param)
475    }
476}
477
478/// A manually specified dependency.
479#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
480struct Deployment {
481    network_id: u32,
482    address: Address,
483}
484
485impl Parse for Deployment {
486    fn parse(input: ParseStream) -> ParseResult<Self> {
487        let network_id = input.parse::<LitInt>()?.base10_parse()?;
488        input.parse::<Token![=>]>()?;
489        let address = {
490            let literal = input.parse::<LitStr>()?;
491            parse_address(literal.value()).map_err(|err| ParseError::new(literal.span(), err))?
492        };
493
494        Ok(Deployment {
495            network_id,
496            address,
497        })
498    }
499}
500
501/// An explicitely named contract method.
502#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
503struct Method {
504    signature: String,
505    alias: String,
506}
507
508impl Parse for Method {
509    fn parse(input: ParseStream) -> ParseResult<Self> {
510        let function = {
511            let name = input.parse::<Ident>()?.to_string();
512
513            let content;
514            parenthesized!(content in input);
515            let inputs = content
516                .parse_terminated(Ident::parse, Token![,])?
517                .iter()
518                .map(|ident| {
519                    let kind = ParamType::from_str(&ident.to_string())
520                        .map_err(|err| ParseError::new(ident.span(), err))?;
521                    Ok(Param {
522                        name: "".into(),
523                        kind,
524                        internal_type: None,
525                    })
526                })
527                .collect::<ParseResult<Vec<_>>>()?;
528
529            #[allow(deprecated)]
530            Function {
531                name,
532                inputs,
533
534                // NOTE: The output types and const-ness of the function do not
535                //   affect its signature.
536                outputs: vec![],
537                constant: None,
538                state_mutability: Default::default(),
539            }
540        };
541        let signature = function.abi_signature();
542        input.parse::<Token![as]>()?;
543        let alias = {
544            let ident = input.parse::<Ident>()?;
545            ident.to_string()
546        };
547
548        Ok(Method { signature, alias })
549    }
550}
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555
556    macro_rules! contract_args_result {
557        ($($arg:tt)*) => {{
558            use syn::parse::Parser;
559            <Spanned<ContractArgs> as Parse>::parse
560                .parse2(quote::quote! { $($arg)* })
561        }};
562    }
563    macro_rules! contract_args {
564        ($($arg:tt)*) => {
565            contract_args_result!($($arg)*)
566                .expect("failed to parse contract args")
567                .into_inner()
568        };
569    }
570    macro_rules! contract_args_err {
571        ($($arg:tt)*) => {
572            contract_args_result!($($arg)*)
573                .expect_err("expected parse contract args to error")
574        };
575    }
576
577    fn deployment(network_id: u32, address: &str) -> Deployment {
578        Deployment {
579            network_id,
580            address: parse_address(address).expect("failed to parse deployment address"),
581        }
582    }
583
584    fn method(signature: &str, alias: &str) -> Method {
585        Method {
586            signature: signature.into(),
587            alias: alias.into(),
588        }
589    }
590
591    #[test]
592    fn parse_contract_args() {
593        let args = contract_args!("path/to/artifact.json");
594        assert_eq!(args.artifact_path, "path/to/artifact.json");
595    }
596
597    #[test]
598    fn crate_parameter_accepts_keywords() {
599        let args = contract_args!("artifact.json", crate = crate);
600        assert_eq!(args.parameters, &[Parameter::Crate("crate".into())]);
601    }
602
603    #[test]
604    fn parse_contract_args_with_defaults() {
605        let args = contract_args!("artifact.json");
606        assert_eq!(
607            args,
608            ContractArgs {
609                visibility: None,
610                artifact_path: "artifact.json".into(),
611                parameters: vec![],
612            },
613        );
614    }
615
616    #[test]
617    fn parse_contract_args_with_parameters() {
618        let args = contract_args!(
619            pub(crate) "artifact.json",
620            crate = foobar,
621            mod = contract,
622            contract = Contract,
623            deployments {
624                1 => "0x000102030405060708090a0b0c0d0e0f10111213",
625                4 => "0x0123456789012345678901234567890123456789",
626            },
627            methods {
628                myMethod(uint256, bool) as my_renamed_method;
629                myOtherMethod() as my_other_renamed_method;
630            },
631            event_derives (Asdf, a::B, a::b::c::D)
632        );
633        assert_eq!(
634            args,
635            ContractArgs {
636                visibility: Some(quote!(pub(crate)).to_string()),
637                artifact_path: "artifact.json".into(),
638                parameters: vec![
639                    Parameter::Crate("foobar".into()),
640                    Parameter::Mod("contract".into()),
641                    Parameter::Contract("Contract".into(), None),
642                    Parameter::Deployments(vec![
643                        deployment(1, "0x000102030405060708090a0b0c0d0e0f10111213"),
644                        deployment(4, "0x0123456789012345678901234567890123456789"),
645                    ]),
646                    Parameter::Methods(vec![
647                        method("myMethod(uint256,bool)", "my_renamed_method"),
648                        method("myOtherMethod()", "my_other_renamed_method"),
649                    ]),
650                    Parameter::EventDerives(vec![
651                        "Asdf".into(),
652                        "a :: B".into(),
653                        "a :: b :: c :: D".into()
654                    ])
655                ],
656            },
657        );
658    }
659
660    #[test]
661    fn parse_contract_args_format() {
662        let args = contract_args!("artifact.json", format = hardhat_multi);
663        assert_eq!(
664            args,
665            ContractArgs {
666                visibility: None,
667                artifact_path: "artifact.json".into(),
668                parameters: vec![Parameter::Format(Format::HardHat(
669                    HardHatFormat::MultiExport
670                ))],
671            },
672        );
673    }
674
675    #[test]
676    fn parse_contract_args_rename() {
677        let args = contract_args!("artifact.json", contract = Contract as Renamed);
678        assert_eq!(
679            args,
680            ContractArgs {
681                visibility: None,
682                artifact_path: "artifact.json".into(),
683                parameters: vec![Parameter::Contract(
684                    "Contract".into(),
685                    Some("Renamed".into())
686                )],
687            },
688        );
689    }
690
691    #[test]
692    fn unsupported_format_error() {
693        contract_args_err!("artifact.json", format = yaml);
694    }
695
696    #[test]
697    fn duplicate_network_id_error() {
698        contract_args_err!(
699            "artifact.json",
700            deployments {
701                1 => "0x000102030405060708090a0b0c0d0e0f10111213",
702                1 => "0x0123456789012345678901234567890123456789",
703            }
704        );
705    }
706
707    #[test]
708    fn duplicate_method_rename_error() {
709        contract_args_err!(
710            "artifact.json",
711            methods {
712                myMethod(uint256) as my_method_1;
713                myMethod(uint256) as my_method_2;
714            }
715        );
716        contract_args_err!(
717            "artifact.json",
718            methods {
719                myMethod1(uint256) as my_method;
720                myMethod2(uint256) as my_method;
721            }
722        );
723    }
724
725    #[test]
726    fn method_invalid_method_parameter_type() {
727        contract_args_err!(
728            "artifact.json",
729            methods {
730                myMethod(invalid invalid) as my_method;
731            }
732        );
733    }
734}