1#![deny(missing_docs, unsafe_code)]
2
3extern 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]
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#[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 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#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
344enum Format {
345 Truffle,
346 HardHat(HardHatFormat),
347}
348
349#[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#[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#[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 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}