From e05b33e0c7e6961460672964f892e4737d6af228 Mon Sep 17 00:00:00 2001 From: Tarek Date: Sat, 30 Nov 2024 01:22:00 +0200 Subject: [PATCH 1/5] feat(soroban): modify `emit_initializer` to support user-defined constructors Signed-off-by: Tarek --- src/emit/soroban/mod.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/emit/soroban/mod.rs b/src/emit/soroban/mod.rs index 71a24a933..cdf883a19 100644 --- a/src/emit/soroban/mod.rs +++ b/src/emit/soroban/mod.rs @@ -63,7 +63,7 @@ impl SorobanTarget { ); binary.internalize(export_list.as_slice()); - Self::emit_initializer(&mut binary, ns); + Self::emit_initializer(&mut binary, ns, contract.constructors(&ns).first()); Self::emit_env_meta_entries(context, &mut binary, opt); @@ -262,15 +262,17 @@ impl SorobanTarget { ); } - fn emit_initializer(binary: &mut Binary, _ns: &ast::Namespace) { + fn emit_initializer( + binary: &mut Binary, + _ns: &ast::Namespace, + constructor_cfg_no: Option<&usize>, + ) { let mut cfg = ControlFlowGraph::new("__constructor".to_string(), ASTFunction::None); cfg.public = true; let void_param = ast::Parameter::new_default(ast::Type::Void); cfg.returns = sync::Arc::new(vec![void_param]); - Self::emit_function_spec_entry(binary.context, &cfg, "__constructor".to_string(), binary); - let function_name = CString::new(STORAGE_INITIALIZER).unwrap(); let mut storage_initializers = binary .functions @@ -294,8 +296,20 @@ impl SorobanTarget { .build_call(storage_initializer, &[], "storage_initializer") .unwrap(); + // call the user defined constructor (if any) + if let Some(cfg_no) = constructor_cfg_no { + let constructor = binary.functions[cfg_no]; + let constructor_name = constructor.get_name().to_str().unwrap(); + binary + .builder + .build_call(constructor, &[], constructor_name) + .unwrap(); + } + // return zero let zero_val = binary.context.i64_type().const_int(2, false); binary.builder.build_return(Some(&zero_val)).unwrap(); + + Self::emit_function_spec_entry(binary.context, &cfg, "__constructor".to_string(), binary); } } From 399b070c97d8e1a03325756c81a1c72353ea1fe1 Mon Sep 17 00:00:00 2001 From: Tarek Date: Sat, 30 Nov 2024 01:22:30 +0200 Subject: [PATCH 2/5] tests(soroban): add constructor tests and update contract registration logic Signed-off-by: Tarek --- tests/soroban.rs | 27 +++++--- tests/soroban_testcases/constructor.rs | 92 ++++++++++++++++++++++++++ tests/soroban_testcases/math.rs | 31 +++++---- tests/soroban_testcases/mod.rs | 1 + tests/soroban_testcases/print.rs | 40 ++++++----- tests/soroban_testcases/storage.rs | 24 ++++--- 6 files changed, 165 insertions(+), 50 deletions(-) create mode 100644 tests/soroban_testcases/constructor.rs diff --git a/tests/soroban.rs b/tests/soroban.rs index 3492adcd5..793193641 100644 --- a/tests/soroban.rs +++ b/tests/soroban.rs @@ -16,7 +16,8 @@ pub struct SorobanEnv { contracts: Vec
, } -pub fn build_solidity(src: &str) -> SorobanEnv { +/// Compile a Solidity contract and return the compiled WASM blob. +pub fn build_solidity(src: &str) -> Vec { let tmp_file = OsStr::new("test.sol"); let mut cache = FileResolver::default(); cache.set_file_contents(tmp_file.to_str().unwrap(), src.to_string()); @@ -41,10 +42,11 @@ pub fn build_solidity(src: &str) -> SorobanEnv { ns.print_diagnostics_in_plain(&cache, false); assert!(!wasm.is_empty()); let wasm_blob = wasm[0].0.clone(); - SorobanEnv::new_with_contract(wasm_blob) + wasm_blob } impl SorobanEnv { + /// Create a new Soroban environment. pub fn new() -> Self { Self { env: Env::default(), @@ -52,23 +54,30 @@ impl SorobanEnv { } } - pub fn new_with_contract(contract_wasm: Vec) -> Self { + /// Create a new Soroban environment with a contract. + pub fn new_with_contract( + contract_wasm: Vec, + constructor_args: soroban_sdk::Vec, + ) -> Self { let mut env = Self::new(); - env.register_contract(contract_wasm); + env.register_contract(contract_wasm, constructor_args); env } - pub fn register_contract(&mut self, contract_wasm: Vec) -> Address { - // For now, we keep using `register_contract_wasm`. To use `register`, we have to figure - // out first what to pass for `constructor_args` - #[allow(deprecated)] + /// Register a contract given its WASM blob and constructor arguments. + pub fn register_contract( + &mut self, + contract_wasm: Vec, + constructor_args: soroban_sdk::Vec, + ) -> Address { let addr = self .env - .register_contract_wasm(None, contract_wasm.as_slice()); + .register(contract_wasm.as_slice(), constructor_args); self.contracts.push(addr.clone()); addr } + /// Invoke a contract and return the result. pub fn invoke_contract(&self, addr: &Address, function_name: &str, args: Vec) -> Val { let func = Symbol::new(&self.env, function_name); let mut args_soroban = vec![&self.env]; diff --git a/tests/soroban_testcases/constructor.rs b/tests/soroban_testcases/constructor.rs new file mode 100644 index 000000000..17f41ea13 --- /dev/null +++ b/tests/soroban_testcases/constructor.rs @@ -0,0 +1,92 @@ +use crate::{build_solidity, SorobanEnv}; +use soroban_sdk::testutils::Logs; +use soroban_sdk::{IntoVal, Val}; + +#[test] +fn test_constructor_increments_count() { + let wasm = build_solidity( + r#"contract counter { + uint64 public count = 1; + + constructor() { + count += 1; + } + + function get() public view returns (uint64) { + return count; + } + }"#, + ); + let mut src = SorobanEnv::new(); + // No constructor arguments + let constructor_args: soroban_sdk::Vec = soroban_sdk::Vec::new(&src.env); + let address = src.register_contract(wasm, constructor_args); + + let res = src.invoke_contract(&address, "get", vec![]); + let expected: Val = 2_u64.into_val(&src.env); + assert!( + expected.shallow_eq(&res), + "expected: {:?}, got: {:?}", + expected, + res + ); +} + +#[test] +fn test_constructor_logs_message_on_call() { + let wasm = build_solidity( + r#"contract counter { + uint64 public count = 1; + + constructor() { + print("Constructor called"); + } + + function get() public view returns (uint64) { + return count; + } + }"#, + ); + let mut src = SorobanEnv::new(); + // No constructor arguments + let constructor_args: soroban_sdk::Vec = soroban_sdk::Vec::new(&src.env); + let address = src.register_contract(wasm, constructor_args); + + let _res = src.invoke_contract(&address, "get", vec![]); + + let logs = src.env.logs().all(); + assert!(logs[0].contains("Constructor called")); +} + +// FIXME: Uncomment this test once the constructor arguments are supported +// #[test] +fn _test_constructor_set_count_value() { + let wasm = build_solidity( + r#"contract counter { + uint64 public count = 1; + + constructor(uint64 initial_count) { + count = initial_count; + } + + function get() public view returns (uint64) { + return count; + } + }"#, + ); + let mut src = SorobanEnv::new(); + let mut constructor_args: soroban_sdk::Vec = soroban_sdk::Vec::new(&src.env); + constructor_args.push_back(42_u64.into_val(&src.env)); + let address = src.register_contract(wasm, constructor_args); + + // Get the value of count and check it is 42 + let res = src.invoke_contract(&address, "get", vec![]); + let expected: Val = 42_u64.into_val(&src.env); + + assert!( + expected.shallow_eq(&res), + "expected: {:?}, got: {:?}", + expected, + res + ); +} diff --git a/tests/soroban_testcases/math.rs b/tests/soroban_testcases/math.rs index 00220937b..793479bcd 100644 --- a/tests/soroban_testcases/math.rs +++ b/tests/soroban_testcases/math.rs @@ -1,11 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 -use crate::build_solidity; +use crate::{build_solidity, SorobanEnv}; use soroban_sdk::{IntoVal, Val}; #[test] fn math() { - let runtime = build_solidity( + let wasm = build_solidity( r#"contract math { function max(uint64 a, uint64 b) public returns (uint64) { if (a > b) { @@ -16,20 +16,23 @@ fn math() { } }"#, ); + let mut env = SorobanEnv::new(); + // No constructor arguments + let constructor_args: soroban_sdk::Vec = soroban_sdk::Vec::new(&env.env); + let address = env.register_contract(wasm, constructor_args); - let arg: Val = 5_u64.into_val(&runtime.env); - let arg2: Val = 4_u64.into_val(&runtime.env); + let arg: Val = 5_u64.into_val(&env.env); + let arg2: Val = 4_u64.into_val(&env.env); - let addr = runtime.contracts.last().unwrap(); - let res = runtime.invoke_contract(addr, "max", vec![arg, arg2]); + let res = env.invoke_contract(&address, "max", vec![arg, arg2]); - let expected: Val = 5_u64.into_val(&runtime.env); + let expected: Val = 5_u64.into_val(&env.env); assert!(expected.shallow_eq(&res)); } #[test] fn math_same_name() { - let src = build_solidity( + let wasm = build_solidity( r#"contract math { function max(uint64 a, uint64 b) public returns (uint64) { if (a > b) { @@ -38,7 +41,7 @@ fn math_same_name() { return b; } } - + function max(uint64 a, uint64 b, uint64 c) public returns (uint64) { if (a > b) { if (a > c) { @@ -57,19 +60,21 @@ fn math_same_name() { } "#, ); - - let addr = src.contracts.last().unwrap(); + let mut src = SorobanEnv::new(); + // No constructor arguments + let constructor_args: soroban_sdk::Vec = soroban_sdk::Vec::new(&src.env); + let address = src.register_contract(wasm, constructor_args); let arg1: Val = 5_u64.into_val(&src.env); let arg2: Val = 4_u64.into_val(&src.env); - let res = src.invoke_contract(addr, "max_uint64_uint64", vec![arg1, arg2]); + let res = src.invoke_contract(&address, "max_uint64_uint64", vec![arg1, arg2]); let expected: Val = 5_u64.into_val(&src.env); assert!(expected.shallow_eq(&res)); let arg1: Val = 5_u64.into_val(&src.env); let arg2: Val = 4_u64.into_val(&src.env); let arg3: Val = 6_u64.into_val(&src.env); - let res = src.invoke_contract(addr, "max_uint64_uint64_uint64", vec![arg1, arg2, arg3]); + let res = src.invoke_contract(&address, "max_uint64_uint64_uint64", vec![arg1, arg2, arg3]); let expected: Val = 6_u64.into_val(&src.env); assert!(expected.shallow_eq(&res)); } diff --git a/tests/soroban_testcases/mod.rs b/tests/soroban_testcases/mod.rs index 080ab8938..4b54a59b2 100644 --- a/tests/soroban_testcases/mod.rs +++ b/tests/soroban_testcases/mod.rs @@ -1,4 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 +mod constructor; mod math; mod print; mod storage; diff --git a/tests/soroban_testcases/print.rs b/tests/soroban_testcases/print.rs index 58b727bd1..fc54d13a2 100644 --- a/tests/soroban_testcases/print.rs +++ b/tests/soroban_testcases/print.rs @@ -1,11 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 -use crate::build_solidity; -use soroban_sdk::testutils::Logs; +use crate::{build_solidity, SorobanEnv}; +use soroban_sdk::{testutils::Logs, Val}; #[test] fn log_runtime_error() { - let src = build_solidity( + let wasm = build_solidity( r#"contract counter { uint64 public count = 1; @@ -15,19 +15,21 @@ fn log_runtime_error() { } }"#, ); + let mut env = SorobanEnv::new(); + // No constructor arguments + let constructor_args: soroban_sdk::Vec = soroban_sdk::Vec::new(&env.env); + let address = env.register_contract(wasm, constructor_args); - let addr = src.contracts.last().unwrap(); + env.invoke_contract(&address, "decrement", vec![]); - src.invoke_contract(addr, "decrement", vec![]); - - let logs = src.invoke_contract_expect_error(addr, "decrement", vec![]); + let logs = env.invoke_contract_expect_error(&address, "decrement", vec![]); assert!(logs[0].contains("runtime_error: math overflow in test.sol:5:17-27")); } #[test] fn print() { - let src = build_solidity( + let wasm = build_solidity( r#"contract Printer { function print() public { @@ -35,10 +37,12 @@ fn print() { } }"#, ); + let mut src = SorobanEnv::new(); + // No constructor arguments + let constructor_args: soroban_sdk::Vec = soroban_sdk::Vec::new(&src.env); + let address = src.register_contract(wasm, constructor_args); - let addr = src.contracts.last().unwrap(); - - src.invoke_contract(addr, "print", vec![]); + src.invoke_contract(&address, "print", vec![]); let logs = src.env.logs().all(); @@ -47,10 +51,10 @@ fn print() { #[test] fn print_then_runtime_error() { - let src = build_solidity( + let wasm = build_solidity( r#"contract counter { uint64 public count = 1; - + function decrement() public returns (uint64){ print("Second call will FAIL!"); count -= 1; @@ -58,12 +62,14 @@ fn print_then_runtime_error() { } }"#, ); + let mut src = SorobanEnv::new(); + // No constructor arguments + let constructor_args: soroban_sdk::Vec = soroban_sdk::Vec::new(&src.env); + let address = src.register_contract(wasm, constructor_args); - let addr = src.contracts.last().unwrap(); - - src.invoke_contract(addr, "decrement", vec![]); + src.invoke_contract(&address, "decrement", vec![]); - let logs = src.invoke_contract_expect_error(addr, "decrement", vec![]); + let logs = src.invoke_contract_expect_error(&address, "decrement", vec![]); assert!(logs[0].contains("Second call will FAIL!")); assert!(logs[1].contains("Second call will FAIL!")); diff --git a/tests/soroban_testcases/storage.rs b/tests/soroban_testcases/storage.rs index b41a9432b..9c166e642 100644 --- a/tests/soroban_testcases/storage.rs +++ b/tests/soroban_testcases/storage.rs @@ -1,39 +1,41 @@ // SPDX-License-Identifier: Apache-2.0 -use crate::build_solidity; +use crate::{build_solidity, SorobanEnv}; use soroban_sdk::{IntoVal, Val}; #[test] fn counter() { - let src = build_solidity( + let wasm = build_solidity( r#"contract counter { uint64 public count = 10; - + function increment() public returns (uint64){ count += 1; return count; } - + function decrement() public returns (uint64){ count -= 1; return count; } }"#, ); + let mut src = SorobanEnv::new(); + // No constructor arguments + let constructor_args: soroban_sdk::Vec = soroban_sdk::Vec::new(&src.env); + let address = src.register_contract(wasm, constructor_args); - let addr = src.contracts.last().unwrap(); - - let res = src.invoke_contract(addr, "count", vec![]); + let res = src.invoke_contract(&address, "count", vec![]); let expected: Val = 10_u64.into_val(&src.env); assert!(expected.shallow_eq(&res)); - src.invoke_contract(addr, "increment", vec![]); - let res = src.invoke_contract(addr, "count", vec![]); + src.invoke_contract(&address, "increment", vec![]); + let res = src.invoke_contract(&address, "count", vec![]); let expected: Val = 11_u64.into_val(&src.env); assert!(expected.shallow_eq(&res)); - src.invoke_contract(addr, "decrement", vec![]); - let res = src.invoke_contract(addr, "count", vec![]); + src.invoke_contract(&address, "decrement", vec![]); + let res = src.invoke_contract(&address, "count", vec![]); let expected: Val = 10_u64.into_val(&src.env); assert!(expected.shallow_eq(&res)); } From 362c10797d79a125acf00a14490ed53f2433ecc4 Mon Sep 17 00:00:00 2001 From: Tarek Date: Sat, 30 Nov 2024 01:35:14 +0200 Subject: [PATCH 3/5] test(soroban): add no-argument constructor example Signed-off-by: Tarek --- integration/soroban/.gitignore | 1 - integration/soroban/constructor_no_args.sol | 11 +++++ integration/soroban/noargsconstructor.spec.js | 41 +++++++++++++++++++ integration/soroban/setup.js | 1 + 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 integration/soroban/constructor_no_args.sol create mode 100644 integration/soroban/noargsconstructor.spec.js diff --git a/integration/soroban/.gitignore b/integration/soroban/.gitignore index ee0ea4517..270feccc9 100644 --- a/integration/soroban/.gitignore +++ b/integration/soroban/.gitignore @@ -1,4 +1,3 @@ -*.js *.so *.key *.json diff --git a/integration/soroban/constructor_no_args.sol b/integration/soroban/constructor_no_args.sol new file mode 100644 index 000000000..886fa466a --- /dev/null +++ b/integration/soroban/constructor_no_args.sol @@ -0,0 +1,11 @@ +contract noargsconstructor { + uint64 public count = 1; + + constructor() { + count += 1; + } + + function get() public view returns (uint64) { + return count; + } +} diff --git a/integration/soroban/noargsconstructor.spec.js b/integration/soroban/noargsconstructor.spec.js new file mode 100644 index 000000000..4cf637abc --- /dev/null +++ b/integration/soroban/noargsconstructor.spec.js @@ -0,0 +1,41 @@ +import * as StellarSdk from '@stellar/stellar-sdk'; +import { readFileSync } from 'fs'; +import { expect } from 'chai'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { call_contract_function } from './test_helpers.js'; + +const __filename = fileURLToPath(import.meta.url); +const dirname = path.dirname(__filename); + +describe('CounterWithNoArgsConstructor', () => { + let keypair; + const server = new StellarSdk.SorobanRpc.Server( + "https://soroban-testnet.stellar.org:443", + ); + + let contractAddr; + let contract; + before(async () => { + + console.log('Setting up counter with no-args constructor contract tests...'); + + // read secret from file + const secret = readFileSync('alice.txt', 'utf8').trim(); + keypair = StellarSdk.Keypair.fromSecret(secret); + + let contractIdFile = path.join(dirname, '.soroban', 'contract-ids', 'noargsconstructor.txt'); + // read contract address from file + contractAddr = readFileSync(contractIdFile, 'utf8').trim().toString(); + + // load contract + contract = new StellarSdk.Contract(contractAddr); + }); + + it('make sure the constructor of the contract was called', async () => { + // get the count + let count = await call_contract_function("get", server, keypair, contract); + expect(count.toString()).eq("2"); + }); + +}); diff --git a/integration/soroban/setup.js b/integration/soroban/setup.js index 7bdb32a8b..f8c789243 100644 --- a/integration/soroban/setup.js +++ b/integration/soroban/setup.js @@ -60,4 +60,5 @@ function add_testnet() { add_testnet(); generate_alice(); +// FIXME: This will need to be refactored to allow providing constructor arguments for a specific contract deploy_all(); From 850628e3f9df010deb8dd33e27dd871b6d61741c Mon Sep 17 00:00:00 2001 From: Tarek Date: Sat, 30 Nov 2024 01:50:26 +0200 Subject: [PATCH 4/5] chore: add license to `tests/soroban_testcases/constructor.rs` Signed-off-by: Tarek --- tests/soroban_testcases/constructor.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/soroban_testcases/constructor.rs b/tests/soroban_testcases/constructor.rs index 17f41ea13..97a815eb2 100644 --- a/tests/soroban_testcases/constructor.rs +++ b/tests/soroban_testcases/constructor.rs @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + use crate::{build_solidity, SorobanEnv}; use soroban_sdk::testutils::Logs; use soroban_sdk::{IntoVal, Val}; From 4f2d6de4422a0e930023a3efacf94e31ada61f95 Mon Sep 17 00:00:00 2001 From: Tarek Date: Sat, 30 Nov 2024 02:28:44 +0200 Subject: [PATCH 5/5] fix: clippy warning Signed-off-by: Tarek --- src/emit/soroban/mod.rs | 2 +- tests/soroban.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/emit/soroban/mod.rs b/src/emit/soroban/mod.rs index cdf883a19..a0ef845f5 100644 --- a/src/emit/soroban/mod.rs +++ b/src/emit/soroban/mod.rs @@ -63,7 +63,7 @@ impl SorobanTarget { ); binary.internalize(export_list.as_slice()); - Self::emit_initializer(&mut binary, ns, contract.constructors(&ns).first()); + Self::emit_initializer(&mut binary, ns, contract.constructors(ns).first()); Self::emit_env_meta_entries(context, &mut binary, opt); diff --git a/tests/soroban.rs b/tests/soroban.rs index 793193641..93edb270a 100644 --- a/tests/soroban.rs +++ b/tests/soroban.rs @@ -41,8 +41,7 @@ pub fn build_solidity(src: &str) -> Vec { ); ns.print_diagnostics_in_plain(&cache, false); assert!(!wasm.is_empty()); - let wasm_blob = wasm[0].0.clone(); - wasm_blob + wasm[0].0.clone() } impl SorobanEnv {