diff --git a/Cargo.lock b/Cargo.lock index ff8ae13a9..abc19d659 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1202,8 +1202,7 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "soroban-builtin-sdk-macros" version = "22.0.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c45d2492cd44f05cc79eeb857985f153f12a4423ce51b4b746b5925024c473b1" +source = "git+https://github.com/stellar/rs-soroban-env?rev=bd0c80a1fe171e75f8d745f17975a73927d44ecd#bd0c80a1fe171e75f8d745f17975a73927d44ecd" dependencies = [ "itertools", "proc-macro2", @@ -1214,8 +1213,7 @@ dependencies = [ [[package]] name = "soroban-env-common" version = "22.0.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39b6d2ec8955243394278e1fae88be3b367fcfed9cf74e5044799a90786a8642" +source = "git+https://github.com/stellar/rs-soroban-env?rev=bd0c80a1fe171e75f8d745f17975a73927d44ecd#bd0c80a1fe171e75f8d745f17975a73927d44ecd" dependencies = [ "arbitrary", "crate-git-revision", @@ -1233,8 +1231,7 @@ dependencies = [ [[package]] name = "soroban-env-guest" version = "22.0.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4002fc582cd20cc9b9fbb73959bc5d6b5b15feda11485cbfab0c28e78ecbab3e" +source = "git+https://github.com/stellar/rs-soroban-env?rev=bd0c80a1fe171e75f8d745f17975a73927d44ecd#bd0c80a1fe171e75f8d745f17975a73927d44ecd" dependencies = [ "soroban-env-common", "static_assertions", @@ -1243,8 +1240,7 @@ dependencies = [ [[package]] name = "soroban-env-host" version = "22.0.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb9be0260d39a648db0d33e1c6f8f494ec0c4f5be2b8a0a4e15ed4b7c6a92b0" +source = "git+https://github.com/stellar/rs-soroban-env?rev=bd0c80a1fe171e75f8d745f17975a73927d44ecd#bd0c80a1fe171e75f8d745f17975a73927d44ecd" dependencies = [ "ark-bls12-381", "ark-ec", @@ -1279,8 +1275,7 @@ dependencies = [ [[package]] name = "soroban-env-macros" version = "22.0.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a328297a568ae98999fdb06902e3362dfd8a2bfa9abea40beaeb7dc93a402fe7" +source = "git+https://github.com/stellar/rs-soroban-env?rev=bd0c80a1fe171e75f8d745f17975a73927d44ecd#bd0c80a1fe171e75f8d745f17975a73927d44ecd" dependencies = [ "itertools", "proc-macro2", @@ -1383,8 +1378,7 @@ dependencies = [ [[package]] name = "soroban-wasmi" version = "0.31.1-soroban.20.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710403de32d0e0c35375518cb995d4fc056d0d48966f2e56ea471b8cb8fc9719" +source = "git+https://github.com/stellar/wasmi?rev=0ed3f3dee30dc41ebe21972399e0a73a41944aa0#0ed3f3dee30dc41ebe21972399e0a73a41944aa0" dependencies = [ "smallvec", "spin", @@ -1784,15 +1778,13 @@ checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "wasmi_arena" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "104a7f73be44570cac297b3035d76b169d6599637631cf37a1703326a0727073" +version = "0.4.0" +source = "git+https://github.com/stellar/wasmi?rev=0ed3f3dee30dc41ebe21972399e0a73a41944aa0#0ed3f3dee30dc41ebe21972399e0a73a41944aa0" [[package]] name = "wasmi_core" version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf1a7db34bff95b85c261002720c00c3a6168256dcb93041d3fa2054d19856a" +source = "git+https://github.com/stellar/wasmi?rev=0ed3f3dee30dc41ebe21972399e0a73a41944aa0#0ed3f3dee30dc41ebe21972399e0a73a41944aa0" dependencies = [ "downcast-rs", "libm", diff --git a/Cargo.toml b/Cargo.toml index 9cde3dd77..3ce474a28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,18 +25,18 @@ soroban-token-sdk = { version = "22.0.0-rc.3", path = "soroban-token-sdk" } [workspace.dependencies.soroban-env-common] version = "=22.0.0-rc.3" -#git = "https://github.com/stellar/rs-soroban-env" -#rev = "8911531dfb9a4331bc308ff8934c2223bc4e81ed" +git = "https://github.com/stellar/rs-soroban-env" +rev = "bd0c80a1fe171e75f8d745f17975a73927d44ecd" [workspace.dependencies.soroban-env-guest] version = "=22.0.0-rc.3" -#git = "https://github.com/stellar/rs-soroban-env" -#rev = "8911531dfb9a4331bc308ff8934c2223bc4e81ed" +git = "https://github.com/stellar/rs-soroban-env" +rev = "bd0c80a1fe171e75f8d745f17975a73927d44ecd" [workspace.dependencies.soroban-env-host] version = "=22.0.0-rc.3" -#git = "https://github.com/stellar/rs-soroban-env" -#rev = "8911531dfb9a4331bc308ff8934c2223bc4e81ed" +git = "https://github.com/stellar/rs-soroban-env" +rev = "bd0c80a1fe171e75f8d745f17975a73927d44ecd" [workspace.dependencies.stellar-strkey] version = "=0.0.9" @@ -48,7 +48,7 @@ features = ["curr"] #git = "https://github.com/stellar/rs-stellar-xdr" #rev = "67be5955a15f1d3a4df83fe86e6ae107f687141b" -#[patch."https://github.com/stellar/rs-soroban-env"] +#[patch.crates-io] #soroban-env-common = { path = "../rs-soroban-env/soroban-env-common" } #soroban-env-guest = { path = "../rs-soroban-env/soroban-env-guest" } #soroban-env-host = { path = "../rs-soroban-env/soroban-env-host" } diff --git a/soroban-sdk-macros/src/derive_args.rs b/soroban-sdk-macros/src/derive_args.rs new file mode 100644 index 000000000..158f5aa6b --- /dev/null +++ b/soroban-sdk-macros/src/derive_args.rs @@ -0,0 +1,104 @@ +use itertools::MultiUnzip; +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote}; +use syn::{Error, FnArg, Lifetime, Type, TypePath, TypeReference}; + +use crate::syn_ext; + +pub fn derive_args_type(ty: &str, name: &str) -> TokenStream { + let ty_str = quote!(#ty).to_string(); + let args_doc = + format!("{name} is a type for building arg lists for functions defined in {ty_str}."); + let args_ident = format_ident!("{name}"); + quote! { + #[doc = #args_doc] + pub struct #args_ident; + } +} + +pub fn derive_args_impl(name: &str, fns: &[syn_ext::Fn]) -> TokenStream { + // Map the traits methods to methods for the Args. + let mut errors = Vec::::new(); + let fns: Vec<_> = fns + .iter() + .map(|f| { + let fn_ident = &f.ident; + + // Check for the Env argument. + let env_input = f.inputs.first().and_then(|a| match a { + FnArg::Typed(pat_type) => { + let mut ty = &*pat_type.ty; + if let Type::Reference(TypeReference { elem, .. }) = ty { + ty = elem; + } + if let Type::Path(TypePath { + path: syn::Path { segments, .. }, + .. + }) = ty + { + if segments.last().map_or(false, |s| s.ident == "Env") { + Some(()) + } else { + None + } + } else { + None + } + } + FnArg::Receiver(_) => None, + }); + + // Map all remaining inputs. + let fn_input_lifetime = Lifetime::new("'i", Span::call_site()); + let (fn_input_names, fn_input_types, fn_input_fn_args): (Vec<_>, Vec<_>, Vec<_>) = f + .inputs + .iter() + .skip(if env_input.is_some() { 1 } else { 0 }) + .map(|t| { + let ident = match syn_ext::fn_arg_ident(t) { + Ok(ident) => ident, + Err(e) => { + errors.push(e); + format_ident!("_") + } + }; + let ty = match syn_ext::fn_arg_ref_type(t, Some(&fn_input_lifetime)) { + Ok(ty) => Some(ty), + Err(e) => { + errors.push(e); + None + } + }; + ( + ident, + ty, + syn_ext::fn_arg_make_ref(t, Some(&fn_input_lifetime)), + ) + }) + .multiunzip(); + + quote! { + #[inline(always)] + pub fn #fn_ident<#fn_input_lifetime>(#(#fn_input_fn_args),*) + -> (#(#fn_input_types,)*) + { + (#(#fn_input_names,)*) + } + } + }) + .collect(); + + // If errors have occurred, render them instead. + if !errors.is_empty() { + let compile_errors = errors.iter().map(Error::to_compile_error); + return quote! { #(#compile_errors)* }; + } + + // Render the Client. + let args_ident = format_ident!("{}", name); + quote! { + impl #args_ident { + #(#fns)* + } + } +} diff --git a/soroban-sdk-macros/src/derive_client.rs b/soroban-sdk-macros/src/derive_client.rs index 07a0504e1..aa896c633 100644 --- a/soroban-sdk-macros/src/derive_client.rs +++ b/soroban-sdk-macros/src/derive_client.rs @@ -192,7 +192,7 @@ pub fn derive_client_impl(crate_path: &Path, name: &str, fns: &[syn_ext::Fn]) -> format_ident!("_") } }; - (ident, syn_ext::fn_arg_make_ref(t)) + (ident, syn_ext::fn_arg_make_ref(t, None)) }) .unzip(); let fn_output = f.output(); diff --git a/soroban-sdk-macros/src/lib.rs b/soroban-sdk-macros/src/lib.rs index 69231f92c..efd2bf1d1 100644 --- a/soroban-sdk-macros/src/lib.rs +++ b/soroban-sdk-macros/src/lib.rs @@ -1,6 +1,7 @@ extern crate proc_macro; mod arbitrary; +mod derive_args; mod derive_client; mod derive_enum; mod derive_enum_int; @@ -15,6 +16,7 @@ mod path; mod symbol; mod syn_ext; +use derive_args::{derive_args_impl, derive_args_type}; use derive_client::{derive_client_impl, derive_client_type}; use derive_enum::derive_type_enum; use derive_enum_int::derive_type_enum_int; @@ -136,8 +138,11 @@ pub fn contract(metadata: TokenStream, input: TokenStream) -> TokenStream { let fn_set_registry_ident = format_ident!("__{}_fn_set_registry", ty_str.to_lowercase()); let crate_path = &args.crate_path; let client = derive_client_type(&args.crate_path, &ty_str, &client_ident); + let args_ident = format!("{ty_str}Args"); + let contract_args = derive_args_type(&ty_str, &args_ident); let mut output = quote! { #input2 + #contract_args #client }; if cfg!(feature = "testutils") { @@ -206,6 +211,18 @@ pub fn contractimpl(metadata: TokenStream, input: TokenStream) -> TokenStream { let ty = &imp.self_ty; let ty_str = quote!(#ty).to_string(); + // TODO: Use imp.trait_ in generating the args ident, to create a unique + // args for each trait impl for a contract, to avoid conflicts. + let args_ident = if let Type::Path(path) = &**ty { + path.path + .segments + .last() + .map(|name| format!("{}Args", name.ident)) + } else { + None + } + .unwrap_or_else(|| "Args".to_string()); + // TODO: Use imp.trait_ in generating the client ident, to create a unique // client for each trait impl for a contract, to avoid conflicts. let client_ident = if let Type::Path(path) = &**ty { @@ -239,6 +256,7 @@ pub fn contractimpl(metadata: TokenStream, input: TokenStream) -> TokenStream { match derived { Ok(derived_ok) => { let mut output = quote! { + #[#crate_path::contractargs(name = #args_ident, impl_only = true)] #[#crate_path::contractclient(crate_path = #crate_path_str, name = #client_ident, impl_only = true)] #[#crate_path::contractspecfn(name = #ty_str)] #imp @@ -533,6 +551,38 @@ pub fn contractfile(metadata: TokenStream) -> TokenStream { quote! { #contents_lit }.into() } +#[derive(Debug, FromMeta)] +struct ContractArgsArgs { + name: String, + #[darling(default)] + impl_only: bool, +} + +#[proc_macro_attribute] +pub fn contractargs(metadata: TokenStream, input: TokenStream) -> TokenStream { + let args = match NestedMeta::parse_meta_list(metadata.into()) { + Ok(v) => v, + Err(e) => { + return TokenStream::from(darling::Error::from(e).write_errors()); + } + }; + let args = match ContractArgsArgs::from_list(&args) { + Ok(v) => v, + Err(e) => return e.write_errors().into(), + }; + let input2: TokenStream2 = input.clone().into(); + let item = parse_macro_input!(input as HasFnsItem); + let methods: Vec<_> = item.fns(); + let args_type = (!args.impl_only).then(|| derive_args_type(&item.name(), &args.name)); + let args_impl = derive_args_impl(&args.name, &methods); + quote! { + #input2 + #args_type + #args_impl + } + .into() +} + #[derive(Debug, FromMeta)] struct ContractClientArgs { #[darling(default = "default_crate_path")] diff --git a/soroban-sdk-macros/src/syn_ext.rs b/soroban-sdk-macros/src/syn_ext.rs index 71f91216c..2d40104dc 100644 --- a/soroban-sdk-macros/src/syn_ext.rs +++ b/soroban-sdk-macros/src/syn_ext.rs @@ -9,7 +9,7 @@ use syn::{ }; use syn::{ spanned::Spanned, token::And, Error, FnArg, Ident, ImplItem, ImplItemFn, ItemImpl, ItemTrait, - Pat, PatType, TraitItem, TraitItemFn, Type, TypeReference, Visibility, + Lifetime, Pat, PatType, TraitItem, TraitItemFn, Type, TypeReference, Visibility, }; /// Gets methods from the implementation that have public visibility. For @@ -47,9 +47,30 @@ pub fn fn_arg_ident(arg: &FnArg) -> Result { )) } +/// Returns a clone of the type from the FnArg. +pub fn fn_arg_ref_type(arg: &FnArg, lifetime: Option<&Lifetime>) -> Result { + if let FnArg::Typed(pat_type) = arg { + if !matches!(*pat_type.ty, Type::Reference(_)) { + Ok(Type::Reference(TypeReference { + and_token: And::default(), + lifetime: lifetime.cloned(), + mutability: None, + elem: pat_type.ty.clone(), + })) + } else { + Ok((*pat_type.ty).clone()) + } + } else { + Err(Error::new( + arg.span(), + "argument in this form is not supported, use simple named arguments only", + )) + } +} + /// Returns a clone of FnArg with the type as a reference if the arg is a typed /// arg and its type is not already a reference. -pub fn fn_arg_make_ref(arg: &FnArg) -> FnArg { +pub fn fn_arg_make_ref(arg: &FnArg, lifetime: Option<&Lifetime>) -> FnArg { if let FnArg::Typed(pat_type) = arg { if !matches!(*pat_type.ty, Type::Reference(_)) { return FnArg::Typed(PatType { @@ -58,7 +79,7 @@ pub fn fn_arg_make_ref(arg: &FnArg) -> FnArg { colon_token: pat_type.colon_token, ty: Box::new(Type::Reference(TypeReference { and_token: And::default(), - lifetime: None, + lifetime: lifetime.cloned(), mutability: None, elem: pat_type.ty.clone(), })), diff --git a/soroban-sdk/src/env.rs b/soroban-sdk/src/env.rs index 21167c62b..8fd002ba4 100644 --- a/soroban-sdk/src/env.rs +++ b/soroban-sdk/src/env.rs @@ -582,7 +582,10 @@ impl Env { /// and is being registered natively. Pass the contract wasm bytes when the /// contract has been loaded as wasm. /// - /// Pass the arguments for the contract's constructor, or `()` if none. + /// Pass the arguments for the contract's constructor, or `()` if none. For + /// contracts with a constructor, use the contract's generated `Args` type + /// to construct the arguments with the appropropriate types for invoking + /// the constructor during registration. /// /// Returns the address of the registered contract that is the same as the /// contract id passed in. @@ -610,7 +613,7 @@ impl Env { /// # } /// # fn main() { /// let env = Env::default(); - /// let contract_id = env.register(Contract, (123_u32,)); + /// let contract_id = env.register(Contract, ContractArgs::__constructor(&123,)); /// } /// ``` /// Register a contract wasm, by specifying the wasm bytes: diff --git a/soroban-sdk/src/lib.rs b/soroban-sdk/src/lib.rs index 95207d7a9..0e218dfe5 100644 --- a/soroban-sdk/src/lib.rs +++ b/soroban-sdk/src/lib.rs @@ -593,6 +593,9 @@ pub use soroban_sdk_macros::contractmeta; /// ``` pub use soroban_sdk_macros::contracttype; +/// Generates a type that helps build function args for a contract trait. +pub use soroban_sdk_macros::contractargs; + /// Generates a client for a contract trait. /// /// Can be used to create clients for contracts that live outside the current diff --git a/soroban-spec-rust/src/lib.rs b/soroban-spec-rust/src/lib.rs index 5e62c4172..be075710e 100644 --- a/soroban-spec-rust/src/lib.rs +++ b/soroban-spec-rust/src/lib.rs @@ -89,6 +89,7 @@ pub fn generate_without_file(specs: &[ScSpecEntry]) -> TokenStream { let error_enums = spec_error_enums.iter().map(|s| generate_error_enum(s)); quote! { + #[soroban_sdk::contractargs(name = "Args")] #[soroban_sdk::contractclient(name = "Client")] #trait_ @@ -134,6 +135,7 @@ mod test { assert_eq!( rust, r#"pub const WASM: &[u8] = soroban_sdk::contractfile!(file = "", sha256 = ""); +#[soroban_sdk::contractargs(name = "Args")] #[soroban_sdk::contractclient(name = "Client")] pub trait Contract { fn add(env: soroban_sdk::Env, a: UdtEnum, b: UdtEnum) -> i64; diff --git a/tests/constructor/src/lib.rs b/tests/constructor/src/lib.rs index 3868bf933..981b9ed72 100644 --- a/tests/constructor/src/lib.rs +++ b/tests/constructor/src/lib.rs @@ -37,7 +37,7 @@ impl Contract { #[test] fn test_constructor() { let env = Env::default(); - let contract_id = env.register(Contract, (100_u32, 1000_i64)); + let contract_id = env.register(Contract, ContractArgs::__constructor(&100_u32, &1000_i64)); let client = ContractClient::new(&env, &contract_id); assert_eq!(client.get_data(&DataKey::Persistent(100)), Some(1000)); assert_eq!(client.get_data(&DataKey::Temp(200)), Some(2000)); diff --git a/tests/fuzz/fuzz/Cargo.lock b/tests/fuzz/fuzz/Cargo.lock index dd7cef164..bbfdd3d0b 100644 --- a/tests/fuzz/fuzz/Cargo.lock +++ b/tests/fuzz/fuzz/Cargo.lock @@ -1038,8 +1038,7 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "soroban-builtin-sdk-macros" version = "22.0.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c45d2492cd44f05cc79eeb857985f153f12a4423ce51b4b746b5925024c473b1" +source = "git+https://github.com/stellar/rs-soroban-env?rev=bd0c80a1fe171e75f8d745f17975a73927d44ecd#bd0c80a1fe171e75f8d745f17975a73927d44ecd" dependencies = [ "itertools", "proc-macro2", @@ -1050,8 +1049,7 @@ dependencies = [ [[package]] name = "soroban-env-common" version = "22.0.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39b6d2ec8955243394278e1fae88be3b367fcfed9cf74e5044799a90786a8642" +source = "git+https://github.com/stellar/rs-soroban-env?rev=bd0c80a1fe171e75f8d745f17975a73927d44ecd#bd0c80a1fe171e75f8d745f17975a73927d44ecd" dependencies = [ "arbitrary", "crate-git-revision", @@ -1069,8 +1067,7 @@ dependencies = [ [[package]] name = "soroban-env-guest" version = "22.0.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4002fc582cd20cc9b9fbb73959bc5d6b5b15feda11485cbfab0c28e78ecbab3e" +source = "git+https://github.com/stellar/rs-soroban-env?rev=bd0c80a1fe171e75f8d745f17975a73927d44ecd#bd0c80a1fe171e75f8d745f17975a73927d44ecd" dependencies = [ "soroban-env-common", "static_assertions", @@ -1079,8 +1076,7 @@ dependencies = [ [[package]] name = "soroban-env-host" version = "22.0.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb9be0260d39a648db0d33e1c6f8f494ec0c4f5be2b8a0a4e15ed4b7c6a92b0" +source = "git+https://github.com/stellar/rs-soroban-env?rev=bd0c80a1fe171e75f8d745f17975a73927d44ecd#bd0c80a1fe171e75f8d745f17975a73927d44ecd" dependencies = [ "ark-bls12-381", "ark-ec", @@ -1115,8 +1111,7 @@ dependencies = [ [[package]] name = "soroban-env-macros" version = "22.0.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a328297a568ae98999fdb06902e3362dfd8a2bfa9abea40beaeb7dc93a402fe7" +source = "git+https://github.com/stellar/rs-soroban-env?rev=bd0c80a1fe171e75f8d745f17975a73927d44ecd#bd0c80a1fe171e75f8d745f17975a73927d44ecd" dependencies = [ "itertools", "proc-macro2", @@ -1203,8 +1198,7 @@ dependencies = [ [[package]] name = "soroban-wasmi" version = "0.31.1-soroban.20.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710403de32d0e0c35375518cb995d4fc056d0d48966f2e56ea471b8cb8fc9719" +source = "git+https://github.com/stellar/wasmi?rev=0ed3f3dee30dc41ebe21972399e0a73a41944aa0#0ed3f3dee30dc41ebe21972399e0a73a41944aa0" dependencies = [ "smallvec", "spin", @@ -1439,15 +1433,13 @@ checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasmi_arena" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "104a7f73be44570cac297b3035d76b169d6599637631cf37a1703326a0727073" +version = "0.4.0" +source = "git+https://github.com/stellar/wasmi?rev=0ed3f3dee30dc41ebe21972399e0a73a41944aa0#0ed3f3dee30dc41ebe21972399e0a73a41944aa0" [[package]] name = "wasmi_core" version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf1a7db34bff95b85c261002720c00c3a6168256dcb93041d3fa2054d19856a" +source = "git+https://github.com/stellar/wasmi?rev=0ed3f3dee30dc41ebe21972399e0a73a41944aa0#0ed3f3dee30dc41ebe21972399e0a73a41944aa0" dependencies = [ "downcast-rs", "libm", diff --git a/tests/modular/src/feat1.rs b/tests/modular/src/feat1.rs index 49822a361..ea68d7b6e 100644 --- a/tests/modular/src/feat1.rs +++ b/tests/modular/src/feat1.rs @@ -1,6 +1,7 @@ use soroban_sdk::contractimpl; use crate::Contract; +use crate::ContractArgs; use crate::ContractClient; #[contractimpl] diff --git a/tests/modular/src/feat2.rs b/tests/modular/src/feat2.rs index b5b4aaccf..79c3a82f3 100644 --- a/tests/modular/src/feat2.rs +++ b/tests/modular/src/feat2.rs @@ -1,5 +1,6 @@ use soroban_sdk::contractimpl; +use crate::ContractArgs; use crate::ContractClient; #[contractimpl]