diff --git a/integration-tests/public/README.md b/integration-tests/public/README.md new file mode 100644 index 00000000000..f76b4fc4a4a --- /dev/null +++ b/integration-tests/public/README.md @@ -0,0 +1,81 @@ +# ink! Smart Contract Examples & Integration Tests + +Welcome to the collection of example smart contracts and integration tests for [ink!](https://github.com/use-ink/ink), the relentless smart contract language for Polkadot and Substrate. + +This directory contains a curated set of examples demonstrates everything from basic "Hello World" contracts to advanced upgradeability patterns and cross-chain messaging (XCM). + +## Directory Structure + +The examples are organized into categories to help you navigate from foundational concepts to complex implementations. + +### Basics +*Fundamental building blocks for learning ink!.* + +- **[`flipper`](basics/flipper)**: The classic "Hello World" of smart contracts. A simple boolean value you can flip. +- **[`incrementer`](basics/incrementer)**: Demonstrates simple state mutation with integers. +- **[`dns`](basics/dns)**: Shows how to use the storage `Mapping` type. +- **[`events`](basics/events)**: Examples of defining and emitting events. +- **[`terminator`](basics/terminator)**: How to remove a contract from storage using `terminate`. + +### 🪙 Tokens +*Implementations of standard token interfaces.* + +- **[`erc20`](tokens/erc20)**: Complete implementation of the ERC-20 fungible token standard. +- **[`erc721`](tokens/erc721)**: Non-Fungible Token (NFT) standard implementation. +- **[`erc1155`](tokens/erc1155)**: Multi-Token standard implementation. + +### Traits +*Defining shared behavior using Rust traits.* + +- **[`erc20`](traits/erc20)**: ERC-20 implemented using ink! trait definitions. +- **[`flipper`](traits/flipper)**: The flipper contract, but defined via a trait. +- **[`incrementer`](traits/incrementer)**: Trait-based incrementer. +- **[`dyn-cross-contract`](traits/dyn-cross-contract)**: How to make cross-contract calls using dynamic trait dispatch. + +### Cross-Contract Interactions +*Calling other contracts from your contract.* + +- **[`basic`](cross-contract/basic)**: Simple example of one contract calling another. +- **[`advanced`](cross-contract/advanced)**: Using call builders to set gas limits and storage deposits. +- **[`multi-caller`](cross-contract/multi-caller)**: A contract that can switch between calling different target contracts. +- **[`transfer`](cross-contract/transfer)**: Sending value (tokens) to another contract. +- **[`invocation`](cross-contract/invocation)**: Instantiating new contracts from within a contract. + +### Storage +*Advanced storage layouts and data structures.* + +- **[`basic`](storage/basic)**: Overview of available storage types. +- **[`complex`](storage/complex)**: Nested structs and Enums in storage. +- **[`lazyvec`](storage/lazyvec)**: Using `Lazy` vector types for gas optimization. +- **[`allocator`](storage/allocator)**: Using a custom heap allocator (e.g., `bumpalo`). + +### Runtime & Chain +*Interacting with the Substrate chain and runtime.* + +- **[`call-contract`](runtime/call-contract)**: The runtime calling into a contract. +- **[`e2e-call`](runtime/e2e-call)**: A contract calling a runtime dispatchable (pallet function). +- **[`xcm`](runtime/xcm)**: Cross-Consensus Messaging examples. +- **[`precompile`](runtime/precompile)**: interacting with runtime precompiles. + +### Advanced +*Complex patterns and niche features.* + +- **[`upgradeable`](advanced/upgradeable)**: Contracts that can upgrade their own code (`set_code_hash`). +- **[`custom-env`](advanced/custom-env)**: Defining custom chain extensions and environment types. +- **[`fuzzing`](advanced/fuzzing)**: Setup for property-based fuzz testing. +- **[`debugging`](advanced/debugging)**: Techniques for interpreting debug buffers. + +## Running the Tests + +Most examples in this directory are standard Rust crates. You can run their tests using Cargo. + +```bash +cd basics/flipper +cargo test +``` + +For End-to-End (E2E) tests that require a running Substrate node (like `substrate-contracts-node`), look for `e2e_tests.rs` files and ensure your environment is set up correctly with the `PINK_Runtime` or a local node. + +## Contributing + +If you are adding a new example, please place it in the most appropriate category directory. Ensure it includes a `README.md` and basic tests. diff --git a/integration-tests/public/conditional-compilation/Cargo.toml b/integration-tests/public/advanced/conditional/Cargo.toml old mode 100755 new mode 100644 similarity index 100% rename from integration-tests/public/conditional-compilation/Cargo.toml rename to integration-tests/public/advanced/conditional/Cargo.toml diff --git a/integration-tests/public/conditional-compilation/lib.rs b/integration-tests/public/advanced/conditional/lib.rs old mode 100755 new mode 100644 similarity index 100% rename from integration-tests/public/conditional-compilation/lib.rs rename to integration-tests/public/advanced/conditional/lib.rs diff --git a/integration-tests/public/advanced/conditional/tests.rs b/integration-tests/public/advanced/conditional/tests.rs new file mode 100644 index 00000000000..90b621b98e2 --- /dev/null +++ b/integration-tests/public/advanced/conditional/tests.rs @@ -0,0 +1,30 @@ +use super::Flip; +use super::conditional_compilation::ConditionalCompilation; + +#[ink::test] +fn default_works() { + let flipper = ConditionalCompilation::new(); + assert!(!flipper.get()); +} + +#[ink::test] +fn it_works() { + let mut flipper = ConditionalCompilation::new(); + // Can call using universal call syntax using the trait. + assert!(!::get(&flipper)); + ::flip(&mut flipper); + // Normal call syntax possible to as long as the trait is in scope. + assert!(flipper.get()); +} + +#[cfg(feature = "foo")] +#[ink::test] +fn foo_works() { + let mut flipper = ConditionalCompilation::new_foo(false); + + flipper.inherent_flip_foo(); + assert!(flipper.get()); + + ::push_foo(&mut flipper, false); + assert!(!flipper.get()) +} \ No newline at end of file diff --git a/integration-tests/public/custom-environment/Cargo.toml b/integration-tests/public/advanced/custom-env/Cargo.toml similarity index 100% rename from integration-tests/public/custom-environment/Cargo.toml rename to integration-tests/public/advanced/custom-env/Cargo.toml diff --git a/integration-tests/public/custom-environment/README.md b/integration-tests/public/advanced/custom-env/README.md similarity index 100% rename from integration-tests/public/custom-environment/README.md rename to integration-tests/public/advanced/custom-env/README.md diff --git a/integration-tests/public/advanced/custom-env/lib.rs b/integration-tests/public/advanced/custom-env/lib.rs new file mode 100644 index 00000000000..03f7dd86154 --- /dev/null +++ b/integration-tests/public/advanced/custom-env/lib.rs @@ -0,0 +1,63 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +use ink::env::{ + DefaultEnvironment, + Environment, +}; + +/// Our custom environment diverges from the `DefaultEnvironment` in the event topics +/// limit. +#[derive(Debug, Clone, PartialEq, Eq)] +#[ink::scale_derive(TypeInfo)] +pub enum EnvironmentWithManyTopics {} + +impl Environment for EnvironmentWithManyTopics { + const NATIVE_TO_ETH_RATIO: u32 = + ::NATIVE_TO_ETH_RATIO; + const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = + ::TRUST_BACKED_ASSETS_PRECOMPILE_INDEX; + const POOL_ASSETS_PRECOMPILE_INDEX: u16 = + ::POOL_ASSETS_PRECOMPILE_INDEX; + + type AccountId = ::AccountId; + type Balance = ::Balance; + type Hash = ::Hash; + type BlockNumber = ::BlockNumber; + type Timestamp = ::Timestamp; + type EventRecord = ::EventRecord; +} + +#[ink::contract(env = crate::EnvironmentWithManyTopics)] +mod runtime_call { + /// Trivial contract with a single message that emits an event with many topics. + #[ink(storage)] + #[derive(Default)] + pub struct Topics; + + /// An event that would be forbidden in the default environment, but is completely + /// valid in our custom one. + #[ink(event)] + #[derive(Default)] + pub struct EventWithTopics { + #[ink(topic)] + first_topic: Balance, + #[ink(topic)] + second_topic: Balance, + } + + impl Topics { + #[ink(constructor)] + pub fn new() -> Self { + Default::default() + } + + /// Emit an event with many topics. + #[ink(message)] + pub fn trigger(&mut self) { + self.env().emit_event(EventWithTopics::default()); + } + } +} + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/advanced/custom-env/tests.rs b/integration-tests/public/advanced/custom-env/tests.rs new file mode 100644 index 00000000000..e7f158e6c7b --- /dev/null +++ b/integration-tests/public/advanced/custom-env/tests.rs @@ -0,0 +1,77 @@ +use super::runtime_call::{EventWithTopics, Topics, TopicsRef}; +use super::EnvironmentWithManyTopics; + +#[ink::test] +fn emits_event_with_many_topics() { + let mut contract = Topics::new(); + contract.trigger(); + + let emitted_events = ink::env::test::recorded_events(); + assert_eq!(emitted_events.len(), 1); + + let emitted_event = ::decode( + &mut &emitted_events[0].data[..], + ); + + assert!(emitted_event.is_ok()); +} + +#[cfg(all(test, feature = "e2e-tests"))] +mod e2e_tests { + use super::*; + use ink_e2e::ContractsBackend; + + type E2EResult = Result>; + + #[cfg(feature = "permissive-node")] + #[ink_e2e::test(environment = crate::EnvironmentWithManyTopics)] + async fn calling_custom_environment_works(mut client: Client) -> E2EResult<()> { + // given + let mut constructor = TopicsRef::new(); + let contract = client + .instantiate("custom-environment", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + // when + let message = call_builder.trigger(); + + let call_res = client + .call(&ink_e2e::alice(), &message) + .submit() + .await + .expect("call failed"); + + // then + assert!(call_res.contains_event("Revive", "ContractEmitted")); + + Ok(()) + } + + #[cfg(not(feature = "permissive-node"))] + #[ink_e2e::test(environment = crate::EnvironmentWithManyTopics)] + async fn calling_custom_environment_fails_if_incompatible_with_node( + mut client: Client, + ) -> E2EResult<()> { + // given + let mut constructor = TopicsRef::new(); + let contract = client + .instantiate("custom-environment", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + let message = call_builder.trigger(); + + // when + let call_res = client.call(&ink_e2e::alice(), &message).dry_run().await; + + // then + assert!(call_res.is_err()); + + Ok(()) + } +} \ No newline at end of file diff --git a/integration-tests/public/debugging-strategies/Cargo.toml b/integration-tests/public/advanced/debugging/Cargo.toml old mode 100755 new mode 100644 similarity index 100% rename from integration-tests/public/debugging-strategies/Cargo.toml rename to integration-tests/public/advanced/debugging/Cargo.toml diff --git a/integration-tests/public/advanced/debugging/lib.rs b/integration-tests/public/advanced/debugging/lib.rs new file mode 100644 index 00000000000..4b999016c65 --- /dev/null +++ b/integration-tests/public/advanced/debugging/lib.rs @@ -0,0 +1,114 @@ +//! # Debugging Strategies +//! +//! This contract illustrates a number of strategies for debugging +//! contracts: +//! +//! * Emitting debugging events. +//! * The `pallet-revive` tracing API. +//! * Causing intentional reverts with a return value, in your contract. + +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +mod debugging_strategies { + use flipper::FlipperRef; + use ink::env::call::{ + ExecutionInput, + Selector, + build_call, + build_create, + }; + #[cfg(feature = "debug")] + use ink::prelude::{ + borrow::ToOwned, + format, + string::String, + }; + + #[ink::event] + #[cfg(feature = "debug")] + pub struct DebugEvent { + message: String, + } + + /// Storage of the contract. + #[ink(storage)] + #[derive(Default)] + pub struct DebuggingStrategies { + value: bool, + } + + impl DebuggingStrategies { + #[ink(constructor)] + pub fn new() -> Self { + Self { value: true } + } + + #[ink(message)] + pub fn get(&self) -> bool { + #[cfg(feature = "debug")] + self.env().emit_event(DebugEvent { + message: format!("received {:?}", self.env().transferred_value()) + .to_owned(), + }); + self.value + } + + #[ink(message)] + pub fn intentional_revert(&self) { + #[cfg(feature = "debug")] + ink::env::return_value( + ink::env::ReturnFlags::REVERT, + &format!("reverting with info: {:?}", self.env().transferred_value()), + ); + } + + #[ink(message, payable)] + pub fn instantiate_and_call(&mut self, code_hash: ink::H256) -> bool { + let create_params = build_create::() + .code_hash(code_hash) + .endowment(0.into()) + .exec_input(ExecutionInput::new(Selector::new(ink::selector_bytes!( + Abi::Ink, + "new" + )))) + .returns::() + .params(); + + let other: FlipperRef = self.env() + .instantiate_contract(&create_params) + // todo + // we do this to make sure the instantiated contract is always at a + // different address + // .salt(self.env().addr().to_vec()) + .unwrap_or_else(|error| { + panic!( + "Received an error from `pallet-revive` while instantiating: {error:?}" + ) + }) + .unwrap_or_else(|error| { + panic!("Received a `LangError` while instantiating: {error:?}") + }); + + let call = build_call() + .call(ink::ToAddr::to_addr(&other)) + .transferred_value(0.into()) + .exec_input(ExecutionInput::new(Selector::new(ink::selector_bytes!( + Abi::Ink, + "get" + )))) + .returns::() + .params(); + + self.env() + .invoke_contract(&call) + .unwrap_or_else(|env_err| { + panic!("Received an error from the Environment: {env_err:?}") + }) + .unwrap_or_else(|lang_err| panic!("Received a `LangError`: {lang_err:?}")) + } + } +} + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/advanced/debugging/tests.rs b/integration-tests/public/advanced/debugging/tests.rs new file mode 100644 index 00000000000..889ebca52da --- /dev/null +++ b/integration-tests/public/advanced/debugging/tests.rs @@ -0,0 +1,157 @@ +use super::debugging_strategies::*; +use ink::env::Environment; +use ink_e2e::ContractsBackend; +#[cfg(feature = "debug")] +use ink::prelude::string::String; + +type E2EResult = std::result::Result>; + +/// This test illustrates how to use debugging events. +/// +/// The contract is build with the `debug` feature enabled, thus +/// we can have code in the contract that is utilized purely +/// for testing, but not for release builds. +#[cfg(feature = "debug")] +#[ink_e2e::test(features = ["debug"])] +async fn e2e_debugging_event_emitted(mut client: Client) -> E2EResult<()> { + // Given + let mut constructor = DebuggingStrategiesRef::new(); + let contract = client + .instantiate("debugging_strategies", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let call_builder = contract.call_builder::(); + + // When + let call_res = client + .call(&ink_e2e::alice(), &call_builder.get()) + .submit() + .await + .expect("calling `get` message failed"); + + // Then + // the contract will have emitted an event + assert!(call_res.contains_event("Revive", "ContractEmitted")); + let contract_events = call_res.contract_emitted_events()?; + assert_eq!(1, contract_events.len()); + let contract_event = &contract_events[0]; + let debug_event: DebugEvent = + ink::scale::Decode::decode(&mut &contract_event.event.data[..]) + .expect("encountered invalid contract event data buffer"); + assert_eq!(debug_event.message, "received 0"); + + Ok(()) +} + +/// This test illustrates how to decode a `Revive::ContractReverted`. +#[cfg(feature = "debug")] +#[ink_e2e::test(features = ["debug"])] +async fn e2e_decode_intentional_revert(mut client: Client) -> E2EResult<()> { + // Given + let mut constructor = DebuggingStrategiesRef::new(); + let contract = client + .instantiate("debugging_strategies", &ink_e2e::bob(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let call_builder = contract.call_builder::(); + + // When + let call_res = client + .call(&ink_e2e::alice(), &call_builder.intentional_revert()) + .dry_run() + .await + .expect("calling `get` message failed"); + + // Then + let return_data = call_res.return_data(); + assert!(call_res.did_revert()); + let revert_msg = String::from_utf8_lossy(return_data); + assert!(revert_msg.contains("reverting with info: 0")); + + Ok(()) +} + +/// This test illustrates how to decode a `Revive::ContractReverted`. +#[ink_e2e::test] +async fn e2e_decode_revert(mut client: Client) -> E2EResult<()> { + // Given + let mut constructor = DebuggingStrategiesRef::new(); + let contract = client + .instantiate("debugging_strategies", &ink_e2e::bob(), &mut constructor) + .value(1_337_000_000) + .dry_run() + //.submit() + .await + .expect("instantiate failed"); + + // When + let return_data = contract.return_data(); + assert!(contract.did_revert()); + let revert_msg = String::from_utf8_lossy(return_data); + assert!(revert_msg.contains("paid an unpayable message")); + + // todo show same for call + let contract = client + .instantiate("debugging_strategies", &ink_e2e::bob(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let call_builder = contract.call_builder::(); + + // When + let call_res = client + .call(&ink_e2e::alice(), &call_builder.get()) + .value(1_337_000_000) + .dry_run() + .await + .expect("calling `get` message failed"); + + // Then + let return_data = call_res.return_data(); + assert!(call_res.did_revert()); + let revert_msg = String::from_utf8_lossy(return_data); + assert!( + revert_msg.contains( + "dispatching ink! message failed: paid an unpayable message" + ) + ); + + Ok(()) +} + +/// This test illustrates how to use the `pallet-revive` tracing functionality. +#[ink_e2e::test] +async fn e2e_tracing(mut client: Client) -> E2EResult<()> { + // Given + let mut constructor = DebuggingStrategiesRef::new(); + let contract = client + .instantiate("debugging_strategies", &ink_e2e::bob(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + let call = call_builder.instantiate_and_call(contract.code_hash); + let call_res = client + .call(&ink_e2e::alice(), &call) + .value(1_337_000_000) + .submit() + .await?; + + // When + let trace: ink_e2e::CallTrace = call_res.trace.expect("trace must exist"); + + // Check that we have 2 calls (one top level, one nested instantiation/call) + assert_eq!(trace.calls.len(), 2); + + // Then + // Verify the value matches what we sent + assert_eq!( + trace.value, + Some(ink::env::DefaultEnvironment::native_to_eth(1_337_000_000)) + ); + + Ok(()) +} \ No newline at end of file diff --git a/integration-tests/public/fuzz-testing/Cargo.toml b/integration-tests/public/advanced/fuzzing/Cargo.toml similarity index 100% rename from integration-tests/public/fuzz-testing/Cargo.toml rename to integration-tests/public/advanced/fuzzing/Cargo.toml diff --git a/integration-tests/public/advanced/fuzzing/lib.rs b/integration-tests/public/advanced/fuzzing/lib.rs new file mode 100644 index 00000000000..6542d79a2c8 --- /dev/null +++ b/integration-tests/public/advanced/fuzzing/lib.rs @@ -0,0 +1,41 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +pub mod fuzz_testing { + #[ink(storage)] + pub struct FuzzTesting { + value: bool, + } + + //#[derive(PartialEq, Eq, Debug, Clone)] + //#[cfg_attr(feature = "std", derive(ink::storage::traits::StorageLayout))] + #[derive(Clone, Debug)] + #[ink::scale_derive(Encode, Decode, TypeInfo)] + pub struct Point { + pub x: i32, + pub y: i32, + } + + impl FuzzTesting { + /// Creates a new contract initialized with the given value. + #[ink(constructor)] + pub fn new(init_value: bool) -> Self { + Self { value: init_value } + } + + /// Returns the current value. + #[ink(message)] + pub fn get(&self) -> bool { + self.value + } + + /// Extracts `Point.x`. + #[ink(message)] + pub fn extract_x(&self, pt: Point) -> i32 { + pt.x + } + } +} + +#[cfg(all(test, feature = "e2e-tests"))] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/advanced/fuzzing/tests.rs b/integration-tests/public/advanced/fuzzing/tests.rs new file mode 100644 index 00000000000..375719aa91d --- /dev/null +++ b/integration-tests/public/advanced/fuzzing/tests.rs @@ -0,0 +1,77 @@ +use super::fuzz_testing::{FuzzTesting, FuzzTestingRef, Point}; +use ink_e2e::ContractsBackend; +use quickcheck_macros::quickcheck; +use quickcheck::{ + Arbitrary, + Gen, +}; + +// We need to implement `Arbitrary` for `Point`, so `quickcheck` +// knows how to fuzz the struct. +impl Arbitrary for Point { + fn arbitrary(g: &mut Gen) -> Point { + Point { + x: i32::arbitrary(g), + y: i32::arbitrary(g), + } + } +} + +/// We use `#[ink_e2e::test(runtime)]` here. It doesn't start a node for each +/// test, but instead interacts with an in-process `pallet-revive`. +/// +/// See +/// for more details. +#[ink_e2e::test(runtime, replace_test_attr = "#[quickcheck]")] +async fn fuzzing_works_runtime(val: bool) -> bool { + let mut constructor = FuzzTestingRef::new(val); + let contract = client + .instantiate("fuzz_testing", &ink_e2e::bob(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let call_builder = contract.call_builder::(); + + let get = call_builder.get(); + let get_res = client.call(&ink_e2e::bob(), &get).submit().await.unwrap(); + get_res.return_value() == val +} + +/// It's also possible to fuzz with a "real" node as the backend. +/// +/// This means that, by default, for every test run a node process will +/// be spawned. You can work around this by setting the env variable +/// `CONTRACTS_NODE_URL`. But still, interactions with a real node will +/// always be more heavy-weight than "just" interacting with an in-process +/// `pallet-revive`. +#[ink_e2e::test(runtime, replace_test_attr = "#[quickcheck]")] +async fn fuzzing_works_node(val: bool) -> bool { + let mut constructor = FuzzTestingRef::new(val); + let contract = client + .instantiate("fuzz_testing", &ink_e2e::bob(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let call_builder = contract.call_builder::(); + + let get = call_builder.get(); + let get_res = client.call(&ink_e2e::bob(), &get).submit().await.unwrap(); + get_res.return_value() == val +} + +#[ink_e2e::test(runtime, replace_test_attr = "#[quickcheck]")] +async fn fuzzing_custom_struct_works(val: Point) -> bool { + ink_e2e::tracing::info!("fuzzing with value {val:?}"); + + let mut constructor = FuzzTestingRef::new(true); + let contract = client + .instantiate("fuzz_testing", &ink_e2e::bob(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let call_builder = contract.call_builder::(); + + let get = call_builder.extract_x(val.clone()); + let get_res = client.call(&ink_e2e::bob(), &get).submit().await.unwrap(); + get_res.return_value() == val.x +} \ No newline at end of file diff --git a/integration-tests/public/upgradeable-contracts/README.md b/integration-tests/public/advanced/upgradeable/README.md similarity index 100% rename from integration-tests/public/upgradeable-contracts/README.md rename to integration-tests/public/advanced/upgradeable/README.md diff --git a/integration-tests/public/upgradeable-contracts/delegator/Cargo.toml b/integration-tests/public/advanced/upgradeable/delegator/Cargo.toml similarity index 100% rename from integration-tests/public/upgradeable-contracts/delegator/Cargo.toml rename to integration-tests/public/advanced/upgradeable/delegator/Cargo.toml diff --git a/integration-tests/public/upgradeable-contracts/delegator/delegatee/Cargo.toml b/integration-tests/public/advanced/upgradeable/delegator/delegatee/Cargo.toml similarity index 100% rename from integration-tests/public/upgradeable-contracts/delegator/delegatee/Cargo.toml rename to integration-tests/public/advanced/upgradeable/delegator/delegatee/Cargo.toml diff --git a/integration-tests/public/upgradeable-contracts/delegator/delegatee/lib.rs b/integration-tests/public/advanced/upgradeable/delegator/delegatee/lib.rs similarity index 100% rename from integration-tests/public/upgradeable-contracts/delegator/delegatee/lib.rs rename to integration-tests/public/advanced/upgradeable/delegator/delegatee/lib.rs diff --git a/integration-tests/public/upgradeable-contracts/delegator/delegatee2/Cargo.toml b/integration-tests/public/advanced/upgradeable/delegator/delegatee2/Cargo.toml similarity index 100% rename from integration-tests/public/upgradeable-contracts/delegator/delegatee2/Cargo.toml rename to integration-tests/public/advanced/upgradeable/delegator/delegatee2/Cargo.toml diff --git a/integration-tests/public/upgradeable-contracts/delegator/delegatee2/lib.rs b/integration-tests/public/advanced/upgradeable/delegator/delegatee2/lib.rs similarity index 100% rename from integration-tests/public/upgradeable-contracts/delegator/delegatee2/lib.rs rename to integration-tests/public/advanced/upgradeable/delegator/delegatee2/lib.rs diff --git a/integration-tests/public/upgradeable-contracts/delegator/lib.rs b/integration-tests/public/advanced/upgradeable/delegator/lib.rs similarity index 100% rename from integration-tests/public/upgradeable-contracts/delegator/lib.rs rename to integration-tests/public/advanced/upgradeable/delegator/lib.rs diff --git a/integration-tests/public/upgradeable-contracts/set-code-hash-migration/Cargo.toml b/integration-tests/public/advanced/upgradeable/set-code-hash-migration/Cargo.toml similarity index 100% rename from integration-tests/public/upgradeable-contracts/set-code-hash-migration/Cargo.toml rename to integration-tests/public/advanced/upgradeable/set-code-hash-migration/Cargo.toml diff --git a/integration-tests/public/upgradeable-contracts/set-code-hash-migration/e2e_tests.rs b/integration-tests/public/advanced/upgradeable/set-code-hash-migration/e2e_tests.rs similarity index 100% rename from integration-tests/public/upgradeable-contracts/set-code-hash-migration/e2e_tests.rs rename to integration-tests/public/advanced/upgradeable/set-code-hash-migration/e2e_tests.rs diff --git a/integration-tests/public/upgradeable-contracts/set-code-hash-migration/lib.rs b/integration-tests/public/advanced/upgradeable/set-code-hash-migration/lib.rs similarity index 100% rename from integration-tests/public/upgradeable-contracts/set-code-hash-migration/lib.rs rename to integration-tests/public/advanced/upgradeable/set-code-hash-migration/lib.rs diff --git a/integration-tests/public/upgradeable-contracts/set-code-hash-migration/migration/Cargo.toml b/integration-tests/public/advanced/upgradeable/set-code-hash-migration/migration/Cargo.toml similarity index 100% rename from integration-tests/public/upgradeable-contracts/set-code-hash-migration/migration/Cargo.toml rename to integration-tests/public/advanced/upgradeable/set-code-hash-migration/migration/Cargo.toml diff --git a/integration-tests/public/upgradeable-contracts/set-code-hash-migration/migration/lib.rs b/integration-tests/public/advanced/upgradeable/set-code-hash-migration/migration/lib.rs similarity index 100% rename from integration-tests/public/upgradeable-contracts/set-code-hash-migration/migration/lib.rs rename to integration-tests/public/advanced/upgradeable/set-code-hash-migration/migration/lib.rs diff --git a/integration-tests/public/upgradeable-contracts/set-code-hash-migration/updated-incrementer/Cargo.toml b/integration-tests/public/advanced/upgradeable/set-code-hash-migration/updated-incrementer/Cargo.toml similarity index 100% rename from integration-tests/public/upgradeable-contracts/set-code-hash-migration/updated-incrementer/Cargo.toml rename to integration-tests/public/advanced/upgradeable/set-code-hash-migration/updated-incrementer/Cargo.toml diff --git a/integration-tests/public/upgradeable-contracts/set-code-hash-migration/updated-incrementer/lib.rs b/integration-tests/public/advanced/upgradeable/set-code-hash-migration/updated-incrementer/lib.rs similarity index 100% rename from integration-tests/public/upgradeable-contracts/set-code-hash-migration/updated-incrementer/lib.rs rename to integration-tests/public/advanced/upgradeable/set-code-hash-migration/updated-incrementer/lib.rs diff --git a/integration-tests/public/upgradeable-contracts/set-code-hash/Cargo.toml b/integration-tests/public/advanced/upgradeable/set-code-hash/Cargo.toml similarity index 100% rename from integration-tests/public/upgradeable-contracts/set-code-hash/Cargo.toml rename to integration-tests/public/advanced/upgradeable/set-code-hash/Cargo.toml diff --git a/integration-tests/public/upgradeable-contracts/set-code-hash/lib.rs b/integration-tests/public/advanced/upgradeable/set-code-hash/lib.rs similarity index 100% rename from integration-tests/public/upgradeable-contracts/set-code-hash/lib.rs rename to integration-tests/public/advanced/upgradeable/set-code-hash/lib.rs diff --git a/integration-tests/public/upgradeable-contracts/set-code-hash/updated-incrementer/Cargo.toml b/integration-tests/public/advanced/upgradeable/set-code-hash/updated-incrementer/Cargo.toml similarity index 100% rename from integration-tests/public/upgradeable-contracts/set-code-hash/updated-incrementer/Cargo.toml rename to integration-tests/public/advanced/upgradeable/set-code-hash/updated-incrementer/Cargo.toml diff --git a/integration-tests/public/upgradeable-contracts/set-code-hash/updated-incrementer/lib.rs b/integration-tests/public/advanced/upgradeable/set-code-hash/updated-incrementer/lib.rs similarity index 100% rename from integration-tests/public/upgradeable-contracts/set-code-hash/updated-incrementer/lib.rs rename to integration-tests/public/advanced/upgradeable/set-code-hash/updated-incrementer/lib.rs diff --git a/integration-tests/public/dns/Cargo.toml b/integration-tests/public/basics/dns/Cargo.toml similarity index 100% rename from integration-tests/public/dns/Cargo.toml rename to integration-tests/public/basics/dns/Cargo.toml diff --git a/integration-tests/public/dns/lib.rs b/integration-tests/public/basics/dns/lib.rs similarity index 71% rename from integration-tests/public/dns/lib.rs rename to integration-tests/public/basics/dns/lib.rs index 164df92fdc6..4bd5b0f909b 100644 --- a/integration-tests/public/dns/lib.rs +++ b/integration-tests/public/basics/dns/lib.rs @@ -188,77 +188,7 @@ mod dns { fn zero_address() -> Address { [0u8; 20].into() } - - #[cfg(test)] - mod tests { - use super::*; - - fn default_accounts() -> ink::env::test::DefaultAccounts { - ink::env::test::default_accounts() - } - - fn set_next_caller(caller: Address) { - ink::env::test::set_caller(caller); - } - - #[ink::test] - fn register_works() { - let default_accounts = default_accounts(); - let name = H256::from([0x99; 32]); - - set_next_caller(default_accounts.alice); - let mut contract = DomainNameService::new(); - - assert_eq!(contract.register(name), Ok(())); - assert_eq!(contract.register(name), Err(Error::NameAlreadyExists)); - } - - #[ink::test] - fn set_address_works() { - let accounts = default_accounts(); - let name = H256::from([0x99; 32]); - - set_next_caller(accounts.alice); - - let mut contract = DomainNameService::new(); - assert_eq!(contract.register(name), Ok(())); - - // Caller is not owner, `set_address` should fail. - set_next_caller(accounts.bob); - assert_eq!( - contract.set_address(name, accounts.bob), - Err(Error::CallerIsNotOwner) - ); - - // Caller is owner, set_address will be successful - set_next_caller(accounts.alice); - assert_eq!(contract.set_address(name, accounts.bob), Ok(())); - assert_eq!(contract.get_address(name), accounts.bob); - } - - #[ink::test] - fn transfer_works() { - let accounts = default_accounts(); - let name = H256::from([0x99; 32]); - - set_next_caller(accounts.alice); - - let mut contract = DomainNameService::new(); - assert_eq!(contract.register(name), Ok(())); - - // Test transfer of owner. - assert_eq!(contract.transfer(name, accounts.bob), Ok(())); - - // Owner is bob, alice `set_address` should fail. - assert_eq!( - contract.set_address(name, accounts.bob), - Err(Error::CallerIsNotOwner) - ); - - set_next_caller(accounts.bob); - // Now owner is bob, `set_address` should be successful. - assert_eq!(contract.set_address(name, accounts.bob), Ok(())); - assert_eq!(contract.get_address(name), accounts.bob); - } - } } + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/basics/dns/tests.rs b/integration-tests/public/basics/dns/tests.rs new file mode 100644 index 00000000000..00617d99962 --- /dev/null +++ b/integration-tests/public/basics/dns/tests.rs @@ -0,0 +1,82 @@ +use super::dns::*; +use ink::{H256, primitives::Address}; + +// Helper to get default test accounts +fn default_accounts() -> ink::env::test::DefaultAccounts { + ink::env::test::default_accounts() +} + +// Helper to set the caller for the next contract execution +fn set_next_caller(caller: Address) { + ink::env::test::set_caller(caller); +} + +#[ink::test] +fn register_works() { + // Given + let default_accounts = default_accounts(); + let name = H256::from([0x99; 32]); + + set_next_caller(default_accounts.alice); + let mut contract = DomainNameService::new(); + + // When / Then + // Registering a new name should succeed + assert_eq!(contract.register(name), Ok(())); + + // Trying to register the same name again should fail + assert_eq!(contract.register(name), Err(Error::NameAlreadyExists)); +} + +#[ink::test] +fn set_address_works() { + // Given + let accounts = default_accounts(); + let name = H256::from([0x99; 32]); + + set_next_caller(accounts.alice); + let mut contract = DomainNameService::new(); + assert_eq!(contract.register(name), Ok(())); + + // When / Then + // Caller is not owner (Bob), `set_address` should fail. + set_next_caller(accounts.bob); + assert_eq!( + contract.set_address(name, accounts.bob), + Err(Error::CallerIsNotOwner) + ); + + // Caller is owner (Alice), `set_address` should be successful + set_next_caller(accounts.alice); + assert_eq!(contract.set_address(name, accounts.bob), Ok(())); + assert_eq!(contract.get_address(name), accounts.bob); +} + +#[ink::test] +fn transfer_works() { + // Given + let accounts = default_accounts(); + let name = H256::from([0x99; 32]); + + set_next_caller(accounts.alice); + let mut contract = DomainNameService::new(); + assert_eq!(contract.register(name), Ok(())); + + // When + // Test transfer of owner from Alice to Bob. + assert_eq!(contract.transfer(name, accounts.bob), Ok(())); + + // Then + // Owner is now Bob, so Alice calling `set_address` should fail. + assert_eq!( + contract.set_address(name, accounts.bob), + Err(Error::CallerIsNotOwner) + ); + + // Switch caller to Bob + set_next_caller(accounts.bob); + + // Now owner is Bob, `set_address` should be successful. + assert_eq!(contract.set_address(name, accounts.bob), Ok(())); + assert_eq!(contract.get_address(name), accounts.bob); +} \ No newline at end of file diff --git a/integration-tests/public/events/Cargo.toml b/integration-tests/public/basics/events/Cargo.toml similarity index 100% rename from integration-tests/public/events/Cargo.toml rename to integration-tests/public/basics/events/Cargo.toml diff --git a/integration-tests/public/events/event-def-unused/Cargo.toml b/integration-tests/public/basics/events/event-def-unused/Cargo.toml similarity index 100% rename from integration-tests/public/events/event-def-unused/Cargo.toml rename to integration-tests/public/basics/events/event-def-unused/Cargo.toml diff --git a/integration-tests/public/events/event-def-unused/src/lib.rs b/integration-tests/public/basics/events/event-def-unused/src/lib.rs similarity index 100% rename from integration-tests/public/events/event-def-unused/src/lib.rs rename to integration-tests/public/basics/events/event-def-unused/src/lib.rs diff --git a/integration-tests/public/events/event-def/Cargo.toml b/integration-tests/public/basics/events/event-def/Cargo.toml similarity index 100% rename from integration-tests/public/events/event-def/Cargo.toml rename to integration-tests/public/basics/events/event-def/Cargo.toml diff --git a/integration-tests/public/events/event-def/src/lib.rs b/integration-tests/public/basics/events/event-def/src/lib.rs similarity index 100% rename from integration-tests/public/events/event-def/src/lib.rs rename to integration-tests/public/basics/events/event-def/src/lib.rs diff --git a/integration-tests/public/events/event-def2/Cargo.toml b/integration-tests/public/basics/events/event-def2/Cargo.toml similarity index 100% rename from integration-tests/public/events/event-def2/Cargo.toml rename to integration-tests/public/basics/events/event-def2/Cargo.toml diff --git a/integration-tests/public/events/event-def2/src/lib.rs b/integration-tests/public/basics/events/event-def2/src/lib.rs similarity index 100% rename from integration-tests/public/events/event-def2/src/lib.rs rename to integration-tests/public/basics/events/event-def2/src/lib.rs diff --git a/integration-tests/public/events/lib.rs b/integration-tests/public/basics/events/lib.rs similarity index 100% rename from integration-tests/public/events/lib.rs rename to integration-tests/public/basics/events/lib.rs diff --git a/integration-tests/public/flipper/Cargo.toml b/integration-tests/public/basics/flipper/Cargo.toml similarity index 100% rename from integration-tests/public/flipper/Cargo.toml rename to integration-tests/public/basics/flipper/Cargo.toml diff --git a/integration-tests/public/basics/flipper/lib.rs b/integration-tests/public/basics/flipper/lib.rs new file mode 100644 index 00000000000..fda6c5fd1ed --- /dev/null +++ b/integration-tests/public/basics/flipper/lib.rs @@ -0,0 +1,32 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +pub mod flipper { + #[ink(storage)] + pub struct Flipper { + value: bool, + } + + impl Flipper { + /// Creates a new flipper smart contract initialized with the given value. + #[ink(constructor)] + pub fn new(init_value: bool) -> Self { + Self { value: init_value } + } + + /// Flips the current value of the Flipper's boolean. + #[ink(message)] + pub fn flip(&mut self) { + self.value = !self.value; + } + + /// Returns the current value of the Flipper's boolean. + #[ink(message)] + pub fn get(&self) -> bool { + self.value + } + } +} + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/basics/flipper/tests.rs b/integration-tests/public/basics/flipper/tests.rs new file mode 100644 index 00000000000..ce92ed696b9 --- /dev/null +++ b/integration-tests/public/basics/flipper/tests.rs @@ -0,0 +1,102 @@ +use super::flipper::*; + +#[ink::test] +fn it_works() { + let mut flipper = Flipper::new(false); + assert!(!flipper.get()); + flipper.flip(); + assert!(flipper.get()); +} + +#[cfg(all(test, feature = "e2e-tests"))] +mod e2e_tests { + use super::*; + use ink_e2e::ContractsBackend; + + type E2EResult = std::result::Result>; + + #[ink_e2e::test] + async fn it_works(mut client: Client) -> E2EResult<()> { + // given + let mut constructor = FlipperRef::new(false); + let contract = client + .instantiate("flipper", &ink_e2e::bob(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + let get = call_builder.get(); + let get_res = client.call(&ink_e2e::bob(), &get).submit().await?; + assert!(!get_res.return_value()); + + // when + let flip = call_builder.flip(); + let _flip_res = client + .call(&ink_e2e::bob(), &flip) + .submit() + .await + .expect("flip failed"); + + // then + let get = call_builder.get(); + let get_res = client.call(&ink_e2e::bob(), &get).dry_run().await?; + assert!(get_res.return_value()); + + Ok(()) + } + + /// This test illustrates how to test an existing on-chain contract. + /// + /// You can utilize this to e.g. create a snapshot of a production chain + /// and run the E2E tests against a deployed contract there. + /// This process is explained [here](https://use.ink/5.x/basics/contract-testing/chain-snapshot). + /// + /// Before executing the test: + /// * Make sure you have a node running in the background, + /// * Supply the environment variable `CONTRACT_HEX` that points to a deployed + /// flipper contract. You can take the SS58 address which `cargo contract + /// instantiate` gives you and convert it to hex using `subkey inspect + /// `. + /// + /// The test is then run like this: + /// + /// ``` + /// # The env variable needs to be set, otherwise `ink_e2e` will spawn a new + /// # node process for each test. + /// $ export CONTRACTS_NODE_URL=ws://127.0.0.1:9944 + /// + /// $ export CONTRACT_ADDR_HEX=0x2c75f0aa09dbfbfd49e6286a0f2edd3b4913f04a58b13391c79e96782f5713e3 + /// $ cargo test --features e2e-tests e2e_test_deployed_contract -- --ignored + /// ``` + /// + /// # Developer Note + /// + /// The test is marked as ignored, as it has the above pre-conditions to succeed. + #[ink_e2e::test] + #[ignore] + async fn e2e_test_deployed_contract(mut client: Client) -> E2EResult<()> { + // given + use ink::Address; + let addr = std::env::var("CONTRACT_ADDR_HEX") + .unwrap() + .replace("0x", ""); + let addr_bytes: Vec = hex::decode(addr).unwrap(); + let addr = Address::from_slice(&addr_bytes[..]); + + use std::str::FromStr; + let suri = ink_e2e::subxt_signer::SecretUri::from_str("//Alice").unwrap(); + let caller = ink_e2e::Keypair::from_uri(&suri).unwrap(); + + // when + // Invoke `Flipper::get()` from `caller`'s account + let call_builder = ink_e2e::create_call_builder::(addr); + let get = call_builder.get(); + let get_res = client.call(&caller, &get).dry_run().await?; + + // then + assert!(get_res.return_value()); + + Ok(()) + } +} \ No newline at end of file diff --git a/integration-tests/public/incrementer/Cargo.toml b/integration-tests/public/basics/incrementer/Cargo.toml similarity index 100% rename from integration-tests/public/incrementer/Cargo.toml rename to integration-tests/public/basics/incrementer/Cargo.toml diff --git a/integration-tests/public/incrementer/lib.rs b/integration-tests/public/basics/incrementer/lib.rs similarity index 54% rename from integration-tests/public/incrementer/lib.rs rename to integration-tests/public/basics/incrementer/lib.rs index ccb4ea093d6..53c353765bf 100644 --- a/integration-tests/public/incrementer/lib.rs +++ b/integration-tests/public/basics/incrementer/lib.rs @@ -1,7 +1,7 @@ #![cfg_attr(not(feature = "std"), no_std, no_main)] #[ink::contract] -mod incrementer { +pub mod incrementer { #[ink(storage)] pub struct Incrementer { value: i32, @@ -28,25 +28,7 @@ mod incrementer { self.value } } - - #[cfg(test)] - mod tests { - use super::*; - - #[ink::test] - fn default_works() { - let contract = Incrementer::new_default(); - assert_eq!(contract.get(), 0); - } - - #[ink::test] - fn it_works() { - let mut contract = Incrementer::new(42); - assert_eq!(contract.get(), 42); - contract.inc(5); - assert_eq!(contract.get(), 47); - contract.inc(-50); - assert_eq!(contract.get(), -3); - } - } } + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/basics/incrementer/tests.rs b/integration-tests/public/basics/incrementer/tests.rs new file mode 100644 index 00000000000..63036a5d1f3 --- /dev/null +++ b/integration-tests/public/basics/incrementer/tests.rs @@ -0,0 +1,29 @@ +use super::incrementer::Incrementer; + +#[ink::test] +fn default_works() { + // Given + let contract = Incrementer::new_default(); + + // Then + assert_eq!(contract.get(), 0); +} + +#[ink::test] +fn it_works() { + // Given + let mut contract = Incrementer::new(42); + assert_eq!(contract.get(), 42); + + // When + contract.inc(5); + + // Then + assert_eq!(contract.get(), 47); + + // When + contract.inc(-50); + + // Then + assert_eq!(contract.get(), -3); +} \ No newline at end of file diff --git a/integration-tests/public/contract-terminate/Cargo.toml b/integration-tests/public/basics/terminator/Cargo.toml similarity index 100% rename from integration-tests/public/contract-terminate/Cargo.toml rename to integration-tests/public/basics/terminator/Cargo.toml diff --git a/integration-tests/public/basics/terminator/lib.rs b/integration-tests/public/basics/terminator/lib.rs new file mode 100644 index 00000000000..640073aca12 --- /dev/null +++ b/integration-tests/public/basics/terminator/lib.rs @@ -0,0 +1,29 @@ +//! A smart contract which demonstrates behavior of the `self.env().terminate()` +//! function. It terminates itself once `terminate_me()` is called. + +#![cfg_attr(not(feature = "std"), no_std, no_main)] +#![allow(clippy::new_without_default)] + +#[ink::contract] +pub mod just_terminates { + /// No storage is needed for this simple contract. + #[ink(storage)] + pub struct JustTerminate {} + + impl JustTerminate { + /// Creates a new instance of this contract. + #[ink(constructor)] + pub fn new() -> Self { + Self {} + } + + /// Terminates with the caller as beneficiary. + #[ink(message)] + pub fn terminate_me(&mut self) { + self.env().terminate_contract(self.env().caller()); + } + } +} + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/basics/terminator/tests.rs b/integration-tests/public/basics/terminator/tests.rs new file mode 100644 index 00000000000..c44fa740dee --- /dev/null +++ b/integration-tests/public/basics/terminator/tests.rs @@ -0,0 +1,63 @@ +use super::just_terminates::*; + +#[ink::test] +fn terminating_works() { + // given + let accounts = ink::env::test::default_accounts(); + let contract_id = ink::env::test::callee(); + ink::env::test::set_caller(accounts.alice); + ink::env::test::set_contract_balance(contract_id, 100.into()); + let mut contract = JustTerminate::new(); + + // when + let should_terminate = move || contract.terminate_me(); + + // then + ink::env::test::assert_contract_termination::( + should_terminate, + accounts.alice, + 100.into(), + ); +} + +#[cfg(all(test, feature = "e2e-tests"))] +mod e2e_tests { + use super::*; + use ink_e2e::ContractsBackend; + + type E2EResult = std::result::Result>; + + #[ink_e2e::test] + async fn e2e_contract_terminates(mut client: Client) -> E2EResult<()> { + // given + let mut constructor = JustTerminateRef::new(); + let contract = client + .instantiate("contract_terminate", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + // when + let terminate_me = call_builder.terminate_me(); + let call_res = client + .call(&ink_e2e::alice(), &terminate_me) + .submit() + .await + .expect("terminate_me messages failed"); + + assert!( + call_res.return_data().is_empty(), + "Terminated contract never returns" + ); + + // then + assert!(call_res.contains_event("System", "KilledAccount")); + assert!(call_res.contains_event("Balances", "Withdraw")); + // todo this event below no longer exists, but we could try getting + // info for the contract and asserting that it fails. + // assert!(call_res.contains_event("Revive", "Terminated")); + + Ok(()) + } +} \ No newline at end of file diff --git a/integration-tests/public/bytes/lib.rs b/integration-tests/public/bytes/lib.rs deleted file mode 100644 index ac434fe4824..00000000000 --- a/integration-tests/public/bytes/lib.rs +++ /dev/null @@ -1,214 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -/// Example for using bytes wrapper types (i.e. `ink::sol::FixedBytes` and -/// `ink::sol::DynBytes`) as message and event arguments. -/// -/// # Note -/// -/// In Solidity ABI encoding, `uint8[]` and `uint8[N]` are encoded differently from -/// `bytes` and `bytesN`. In Rust/ink!, `Vec` and `[u8; N]` are mapped to Solidity's -/// `uint8[]` and `uint8[N]` representations, so there's a need for dedicated Rust/ink! -/// types (i.e. `ink::sol::DynBytes` and `ink::sol::FixedBytes`) that map to Solidity's -/// `bytes` and `bytesN` representations. -/// -/// # References -/// -/// - -/// - -/// - - -#[ink::event] -pub struct FixedBytesPayload { - pub data: ink::sol::FixedBytes<8>, -} - -#[ink::event] -pub struct DynBytesPayload { - pub data: ink::sol::DynBytes, -} - -#[ink::contract] -pub mod bytes { - use super::{ - DynBytesPayload, - FixedBytesPayload, - }; - - #[ink(storage)] - pub struct Bytes; - - impl Bytes { - /// Creates a new smart contract. - #[ink(constructor)] - #[allow(clippy::new_without_default)] - pub fn new() -> Self { - Self {} - } - - /// Handles fixed-size byte arrays. - #[ink(message)] - pub fn handle_fixed_bytes(&mut self, data: ink::sol::FixedBytes<8>) { - self.env().emit_event(FixedBytesPayload { data }) - } - - /// Handles dynamic size byte arrays. - #[ink(message)] - pub fn handle_dyn_bytes(&mut self, data: ink::sol::DynBytes) { - self.env().emit_event(DynBytesPayload { data }) - } - } - - #[cfg(test)] - mod tests { - use super::*; - - #[ink::test] - fn fixed_bytes_works() { - // given - let mut bytes = Bytes::new(); - - // when - let fixed_bytes = - ink::sol::FixedBytes::from([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]); - bytes.handle_fixed_bytes(fixed_bytes); - - // then - let emitted_events = ink::env::test::recorded_events(); - assert_eq!(1, emitted_events.len()); - - // then - let event = &emitted_events[0]; - let mut encoded = vec![0x0; 32]; - encoded.as_mut_slice()[..8].copy_from_slice(fixed_bytes.as_slice()); - assert_eq!(encoded, event.data); - - // then - let decoded_data = - ink::sol::decode_sequence::<(ink::sol::FixedBytes<8>,)>(&event.data) - .expect("encountered invalid contract event data buffer"); - assert_eq!(decoded_data.0, fixed_bytes); - } - - #[ink::test] - fn dyn_bytes_works() { - // given - let mut bytes = Bytes::new(); - - // when - let dyn_bytes = ink::sol::DynBytes::from(vec![0x1, 0x2, 0x3, 0x4]); - bytes.handle_dyn_bytes(dyn_bytes.clone()); - - // then - let emitted_events = ink::env::test::recorded_events(); - assert_eq!(1, emitted_events.len()); - - // then - let event = &emitted_events[0]; - let mut encoded = vec![0x0; 96]; - encoded[31] = 32; // offset - encoded[63] = dyn_bytes.len() as u8; // length - encoded.as_mut_slice()[64..64 + dyn_bytes.len()] - .copy_from_slice(dyn_bytes.as_ref()); - assert_eq!(encoded, event.data); - - // then - let decoded_data = - ink::sol::decode_sequence::<(ink::sol::DynBytes,)>(&event.data) - .expect("encountered invalid contract event data buffer"); - assert_eq!(decoded_data.0, dyn_bytes); - } - } - - #[cfg(all(test, feature = "e2e-tests"))] - mod e2e_tests { - use super::*; - - use ink_e2e::ContractsBackend; - - type E2EResult = std::result::Result>; - - #[ink_e2e::test] - async fn fixed_bytes_works(mut client: Client) -> E2EResult<()> { - // given - let mut constructor = BytesRef::new(); - let contract = client - .instantiate("bytes", &ink_e2e::alice(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); - - // when - let fixed_bytes = - ink::sol::FixedBytes::from([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]); - let handler = call_builder.handle_fixed_bytes(fixed_bytes); - let res = client - .call(&ink_e2e::bob(), &handler) - .submit() - .await - .expect("fixed bytes handler failed"); - - // then - let contract_events = res.contract_emitted_events()?; - assert_eq!(1, contract_events.len()); - - // then - let contract_event = &contract_events[0]; - let mut encoded = vec![0x0; 32]; - encoded.as_mut_slice()[..8].copy_from_slice(fixed_bytes.as_slice()); - assert_eq!(encoded, contract_event.event.data); - - // then - let decoded_data = ink::sol::decode_sequence::<(ink::sol::FixedBytes<8>,)>( - &contract_event.event.data, - ) - .expect("encountered invalid contract event data buffer"); - assert_eq!(decoded_data.0, fixed_bytes); - - Ok(()) - } - - #[ink_e2e::test] - async fn dyn_bytes_works(mut client: Client) -> E2EResult<()> { - // given - let mut constructor = BytesRef::new(); - let contract = client - .instantiate("bytes", &ink_e2e::alice(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); - - // when - let dyn_bytes = ink::sol::DynBytes::from(vec![0x1, 0x2, 0x3, 0x4]); - let handler = call_builder.handle_dyn_bytes(dyn_bytes.clone()); - let res = client - .call(&ink_e2e::bob(), &handler) - .submit() - .await - .expect("dyn bytes handler failed"); - - // then - let contract_events = res.contract_emitted_events()?; - assert_eq!(1, contract_events.len()); - - // then - let contract_event = &contract_events[0]; - let mut encoded = vec![0x0; 96]; - encoded[31] = 32; // offset - encoded[63] = dyn_bytes.len() as u8; // length - encoded.as_mut_slice()[64..64 + dyn_bytes.len()] - .copy_from_slice(dyn_bytes.as_ref()); - assert_eq!(encoded, contract_event.event.data); - - // then - let decoded_data = ink::sol::decode_sequence::<(ink::sol::DynBytes,)>( - &contract_event.event.data, - ) - .expect("encountered invalid contract event data buffer"); - assert_eq!(decoded_data.0, dyn_bytes); - - Ok(()) - } - } -} diff --git a/integration-tests/public/contract-terminate/lib.rs b/integration-tests/public/contract-terminate/lib.rs deleted file mode 100644 index 965813bd615..00000000000 --- a/integration-tests/public/contract-terminate/lib.rs +++ /dev/null @@ -1,93 +0,0 @@ -//! A smart contract which demonstrates behavior of the `self.env().terminate()` -//! function. It terminates itself once `terminate_me()` is called. - -#![cfg_attr(not(feature = "std"), no_std, no_main)] -#![allow(clippy::new_without_default)] - -#[ink::contract] -pub mod just_terminates { - /// No storage is needed for this simple contract. - #[ink(storage)] - pub struct JustTerminate {} - - impl JustTerminate { - /// Creates a new instance of this contract. - #[ink(constructor)] - pub fn new() -> Self { - Self {} - } - - /// Terminates with the caller as beneficiary. - #[ink(message)] - pub fn terminate_me(&mut self) { - self.env().terminate_contract(self.env().caller()); - } - } - - #[cfg(test)] - mod tests { - use super::*; - - #[ink::test] - fn terminating_works() { - // given - let accounts = ink::env::test::default_accounts(); - let contract_id = ink::env::test::callee(); - ink::env::test::set_caller(accounts.alice); - ink::env::test::set_contract_balance(contract_id, 100.into()); - let mut contract = JustTerminate::new(); - - // when - let should_terminate = move || contract.terminate_me(); - - // then - ink::env::test::assert_contract_termination::( - should_terminate, - accounts.alice, - 100.into(), - ); - } - } - - #[cfg(all(test, feature = "e2e-tests"))] - mod e2e_tests { - use super::*; - use ink_e2e::ContractsBackend; - - type E2EResult = std::result::Result>; - - #[ink_e2e::test] - async fn e2e_contract_terminates(mut client: Client) -> E2EResult<()> { - // given - let mut constructor = JustTerminateRef::new(); - let contract = client - .instantiate("contract_terminate", &ink_e2e::alice(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); - - // when - let terminate_me = call_builder.terminate_me(); - let call_res = client - .call(&ink_e2e::alice(), &terminate_me) - .submit() - .await - .expect("terminate_me messages failed"); - - assert!( - call_res.return_data().is_empty(), - "Terminated contract never returns" - ); - - // then - assert!(call_res.contains_event("System", "KilledAccount")); - assert!(call_res.contains_event("Balances", "Withdraw")); - // todo this event below no longer exists, but we could try getting - // info for the contract and asserting that it fails. - // assert!(call_res.contains_event("Revive", "Terminated")); - - Ok(()) - } - } -} diff --git a/integration-tests/public/contract-transfer/lib.rs b/integration-tests/public/contract-transfer/lib.rs deleted file mode 100644 index bafdbbc25d7..00000000000 --- a/integration-tests/public/contract-transfer/lib.rs +++ /dev/null @@ -1,281 +0,0 @@ -//! A smart contract which demonstrates behavior of the `self.env().transfer()` function. -//! It transfers some of it's balance to the caller. - -#![cfg_attr(not(feature = "std"), no_std, no_main)] -#![allow(clippy::new_without_default)] - -#[ink::contract] -pub mod give_me { - use ink::primitives::U256; - - /// No storage is needed for this simple contract. - #[ink(storage)] - pub struct GiveMe {} - - impl GiveMe { - /// Creates a new instance of this contract. - #[ink(constructor, payable)] - pub fn new() -> Self { - Self {} - } - - /// Transfers `value` amount of tokens to the caller. - /// - /// # Errors - /// - /// - Panics in case the requested transfer exceeds the contract balance. - /// - Panics in case the requested transfer would have brought this contract's - /// balance below the minimum balance (i.e. the chain's existential deposit). - /// - Panics in case the transfer failed for another reason. - #[ink(message)] - pub fn give_me(&mut self, value: U256) { - assert!(value <= self.env().balance(), "insufficient funds!"); - - if self.env().transfer(self.env().caller(), value).is_err() { - panic!( - "requested transfer failed. this can be the case if the contract does not\ - have sufficient free funds or if the transfer would have brought the\ - contract's balance below minimum balance." - ) - } - } - - /// Asserts that the token amount sent as payment with this call - /// is exactly `10`. This method will fail otherwise, and the - /// transaction would then be reverted. - /// - /// # Note - /// - /// The method needs to be annotated with `payable`; only then it is - /// allowed to receive value as part of the call. - #[ink(message, payable, selector = 0xCAFEBABE)] - pub fn was_it_ten(&mut self) { - /* - ink::env::debug_println!( - "received payment: {}", - self.env().transferred_value() - ); - */ - assert!( - self.env().transferred_value() == U256::from(10), - "payment was not ten" - ); - } - } - - #[cfg(test)] - mod tests { - use super::*; - - #[ink::test] - fn transfer_works() { - // given - let contract_balance = 100.into(); - let accounts = default_accounts(); - let mut give_me = create_contract(contract_balance); - - // when - set_sender(accounts.eve); - set_balance(accounts.eve, 0.into()); - give_me.give_me(80.into()); - - // then - assert_eq!(get_balance(accounts.eve), 80.into()); - } - - #[ink::test] - #[should_panic(expected = "insufficient funds!")] - fn transfer_fails_insufficient_funds() { - // given - let contract_balance = 100.into(); - let accounts = default_accounts(); - let mut give_me = create_contract(contract_balance); - - // when - set_sender(accounts.eve); - give_me.give_me(120.into()); - - // then - // `give_me` must already have panicked here - } - - #[ink::test] - fn test_transferred_value() { - use ink::codegen::Env; - // given - let accounts = default_accounts(); - let mut give_me = create_contract(100.into()); - let contract_account = give_me.env().address(); - - // when - // Push the new execution context which sets initial balances and - // sets Eve as the caller - set_balance(accounts.eve, 100.into()); - set_balance(contract_account, 0.into()); - set_sender(accounts.eve); - - // then - // we use helper macro to emulate method invocation coming with payment, - // and there must be no panic - ink::env::pay_with_call!(give_me.was_it_ten(), 10.into()); - - // and - // balances should be changed properly - let contract_new_balance = get_balance(contract_account); - let caller_new_balance = get_balance(accounts.eve); - - assert_eq!(caller_new_balance, (100 - 10).into()); - assert_eq!(contract_new_balance, 10.into()); - } - - #[ink::test] - #[should_panic(expected = "payment was not ten")] - fn test_transferred_value_must_fail() { - // given - let accounts = default_accounts(); - let mut give_me = create_contract(100.into()); - - // when - // Push the new execution context which sets Eve as caller and - // the `mock_transferred_value` as the value which the contract - // will see as transferred to it. - set_sender(accounts.eve); - ink::env::test::set_value_transferred(13.into()); - - // then - give_me.was_it_ten(); - } - - /// Creates a new instance of `GiveMe` with `initial_balance`. - /// - /// Returns the `contract_instance`. - fn create_contract(initial_balance: U256) -> GiveMe { - let accounts = default_accounts(); - set_sender(accounts.alice); - set_balance(contract_id(), initial_balance); - GiveMe::new() - } - - fn contract_id() -> Address { - ink::env::test::callee() - } - - fn set_sender(sender: Address) { - ink::env::test::set_caller(sender); - } - - fn default_accounts() -> ink::env::test::DefaultAccounts { - ink::env::test::default_accounts() - } - - fn set_balance(addr: Address, balance: U256) { - ink::env::test::set_contract_balance(addr, balance) - } - - fn get_balance(addr: Address) -> U256 { - ink::env::test::get_contract_balance::(addr) - .expect("Cannot get contract balance") - } - } - - #[cfg(all(test, feature = "e2e-tests"))] - mod e2e_tests { - use super::*; - use ink::env::Environment; - use ink_e2e::{ - ChainBackend, - ContractsBackend, - }; - - type E2EResult = std::result::Result>; - - #[ink_e2e::test] - async fn e2e_sending_value_to_give_me_must_fail( - mut client: Client, - ) -> E2EResult<()> { - // given - let mut constructor = GiveMeRef::new(); - let contract = client - .instantiate("contract_transfer", &ink_e2e::alice(), &mut constructor) - .value(1_000_000_000) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); - - // when - let transfer = call_builder.give_me(120_000_000.into()); - - let call_res = client - .call(&ink_e2e::bob(), &transfer) - .value(10_000_000) - .submit() - .await; - - // then - assert!(call_res.is_err(), "call must have errored"); - /* - // todo bug with wrong printing of message - if let Err(ink_e2e::Error::CallDryRun(dry_run)) = call_res { - assert!(dry_run.debug_message.contains("paid an unpayable message")) - } else { - panic!("Paying an unpayable message should fail") - } - */ - Ok(()) - } - - #[ink_e2e::test(runtime)] - async fn e2e_contract_must_transfer_value_to_sender( - mut client: Client, - ) -> E2EResult<()> { - // given - let mut constructor = GiveMeRef::new(); - let contract = client - .instantiate("contract_transfer", &ink_e2e::bob(), &mut constructor) - // todo convert the argument type to U256 - .value(1_337_000_000) - .submit() - .await - .expect("instantiate failed"); - let contract_addr = contract.addr; - - assert_eq!( - contract.trace.clone().unwrap().value, - Some(ink::env::DefaultEnvironment::native_to_eth(1_337_000_000)) - ); - let mut call_builder = contract.call_builder::(); - - let balance_before: Balance = client - .free_balance(contract.account_id) - .await - .expect("getting balance failed"); - - // when - let transfer = call_builder.give_me(U256::from(120_000_000_0)); - - let call_res = client - .call(&ink_e2e::eve(), &transfer) - .submit() - .await - .expect("call failed"); - - // then - let outgoing_trace = &call_res.trace.unwrap().calls[0]; - assert_eq!(outgoing_trace.value, Some(U256::from(120_000_000_0))); - assert_eq!(outgoing_trace.from, contract_addr); - assert_eq!( - outgoing_trace.to, - ink_e2e::address_from_keypair::(&ink_e2e::eve()) - ); - - let balance_after: Balance = client - .free_balance(contract.account_id) - .await - .expect("getting balance failed"); - assert_eq!(balance_before - balance_after, 12); - - Ok(()) - } - } -} diff --git a/integration-tests/public/contract-xcm/lib.rs b/integration-tests/public/contract-xcm/lib.rs deleted file mode 100644 index 0f3d8e5e4b9..00000000000 --- a/integration-tests/public/contract-xcm/lib.rs +++ /dev/null @@ -1,306 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -#[ink::contract] -mod contract_xcm { - use ink::xcm::prelude::*; - - /// A contract demonstrating usage of the XCM API. - #[ink(storage)] - #[derive(Default)] - pub struct ContractXcm; - - #[derive(Debug, PartialEq, Eq)] - #[ink::scale_derive(Encode, Decode, TypeInfo)] - pub enum RuntimeError { - XcmExecuteFailed, - XcmSendFailed, - } - - impl ContractXcm { - /// The constructor is `payable`, so that during instantiation it can be given - /// some tokens that will be further transferred when transferring funds through - /// XCM. - #[ink(constructor, payable)] - pub fn new() -> Self { - Default::default() - } - - /// Tries to transfer `value` from the contract's balance to `receiver`. - /// - /// Fails if: - /// - called in the off-chain environment - /// - the chain is not configured to support XCM - /// - the XCM program executed failed (e.g contract doesn't have enough balance) - #[ink(message)] - pub fn transfer_through_xcm( - &mut self, - receiver: AccountId, - value: Balance, - ) -> Result<(), RuntimeError> { - let asset: Asset = (Parent, value).into(); - let beneficiary = AccountId32 { - network: None, - id: *receiver.as_ref(), - }; - - let message: ink::xcm::v5::Xcm<()> = Xcm::builder() - .withdraw_asset(asset.clone()) - .buy_execution(asset.clone(), Unlimited) - .deposit_asset(asset, beneficiary) - .build(); - let msg = VersionedXcm::V5(message); - - let weight = self.env().xcm_weigh(&msg).expect("weight should work"); - - self.env() - .xcm_execute(&msg, weight) - .map_err(|_| RuntimeError::XcmExecuteFailed) - } - - /// Transfer some funds to the relay chain via XCM from the contract's derivative - /// account to the caller's account. - /// - /// Fails if: - /// - called in the off-chain environment - /// - the chain is not configured to support XCM - /// - the XCM program executed failed (e.g. contract doesn't have enough balance) - #[ink(message)] - pub fn send_funds( - &mut self, - value: Balance, - fee: Balance, - ) -> Result<(), RuntimeError> { - // The destination of the XCM message. Assuming we run the contract - // on a parachain, the parent will be the relay chain. - let destination: ink::xcm::v5::Location = ink::xcm::v5::Parent.into(); - - // The asset to be sent, since we are sending the XCM to the relay chain, - // this represents `value` amount of the relay chain's native asset. - let asset: Asset = (Here, value).into(); - - // The beneficiary of the asset. - // Here, the beneficiary is the caller's account on the relay chain. - let caller_account_id = self.env().to_account_id(self.env().caller()); - let beneficiary = AccountId32 { - network: None, - id: caller_account_id.0, - }; - - // Create an XCM message - let message: Xcm<()> = Xcm::builder() - // Withdraw the asset from the origin (the sovereign account of the - // contract on the relay chain) - .withdraw_asset(asset.clone()) - - // Buy execution to pay the fee on the relay chain - .buy_execution((Here, fee), WeightLimit::Unlimited) - - // Deposit the asset to the caller's account on the relay chain - .deposit_asset(asset, beneficiary) - .build(); - - // Send the constructed XCM message to the relay chain. - self.env() - .xcm_send( - &VersionedLocation::V5(destination), - &VersionedXcm::V5(message), - ) - .map_err(|_| RuntimeError::XcmSendFailed) - } - - #[ink(message)] - pub fn reserve_transfer( - &mut self, - amount: Balance, - fee: Balance, - ) -> Result<(), RuntimeError> { - // The beneficiary of the transfer. - // Here, the beneficiary is the caller's account on the relay chain. - let caller_account_id = self.env().to_account_id(self.env().caller()); - let beneficiary: Location = AccountId32 { - network: None, - id: caller_account_id.0, - } - .into(); - - // Create an XCM message. - let message: Xcm<()> = Xcm::builder_unsafe() - // Withdraw the relay's native token derivative from the - // contract's account. - .withdraw_asset((Parent, amount)) - - // The `initiate_reserve_withdraw` instruction takes the - // derivative token from the holding register and burns it. - // It then sends the nested XCM to the reserve in this - // example, the relay chain. - // Upon receiving the XCM, the reserve will withdraw the - // asset from our chain's sovereign account, and deposit - // on the caller's account. - .initiate_reserve_withdraw( - All, - Parent, - Xcm::builder_unsafe() - .buy_execution((Here, fee), Unlimited) - .deposit_asset(All, beneficiary) - .build(), - ) - .build(); - - let msg = VersionedXcm::V5(message); - let weight = self.env().xcm_weigh(&msg).expect("`xcm_weigh` failed"); - self.env() - .xcm_execute(&msg, weight) - .map_err(|_| RuntimeError::XcmExecuteFailed) - } - } - - #[cfg(all(test, feature = "e2e-tests"))] - mod e2e_tests { - use super::*; - use ink::primitives::AccountId; - use ink_e2e::{ - ChainBackend, - ContractsBackend, - }; - - type E2EResult = Result>; - - #[ink_e2e::test] - async fn xcm_execute_works(mut client: Client) -> E2EResult<()> { - // given - let mut constructor = ContractXcmRef::new(); - let contract = client - .instantiate("contract_xcm", &ink_e2e::alice(), &mut constructor) - .value(100_000_000_000) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); - - let receiver = AccountId::from(ink_e2e::bob().public_key().0); - - let contract_balance_before = client - .free_balance(contract.account_id) - .await - .expect("Failed to get account balance"); - let receiver_balance_before = client - .free_balance(receiver) - .await - .expect("Failed to get account balance"); - - // when - let amount = 100_000_000; - let transfer_message = call_builder.transfer_through_xcm(receiver, amount); - let call_res = client - .call(&ink_e2e::alice(), &transfer_message) - .submit() - .await - .expect("call failed"); - assert!(call_res.return_value().is_ok()); - - // then - let contract_balance_after = client - .free_balance(contract.account_id) - .await - .expect("Failed to get account balance"); - let receiver_balance_after = client - .free_balance(receiver) - .await - .expect("Failed to get account balance"); - - assert_eq!(contract_balance_after, contract_balance_before - amount); - assert_eq!(receiver_balance_after, receiver_balance_before + amount); - - Ok(()) - } - - #[ink_e2e::test] - async fn xcm_execute_failure_detection_works( - mut client: Client, - ) -> E2EResult<()> { - // todo @cmichi: This sleep is necessary until we have our `ink-node` - // support a parachain/relaychain setup. For the moment we use the - // Rococo runtime for testing the examples locally. That runtime - // only has Alice and Bob endowed. Due to the nature of the tests - // we have to use Alice for sending the transactions. If the tests - // run at the same time, we'll get an error because the nonce - // of Alice is the same for all transactions. - std::thread::sleep(std::time::Duration::from_secs(10)); - - // given - let mut constructor = ContractXcmRef::new(); - let contract = client - .instantiate("contract_xcm", &ink_e2e::alice(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); - - // when - let receiver = AccountId::from(ink_e2e::bob().public_key().0); - let amount = u128::MAX; - let transfer_message = call_builder.transfer_through_xcm(receiver, amount); - - // then - let call_res = client - .call(&ink_e2e::alice(), &transfer_message) - .submit() - .await; - assert!(call_res.is_err()); - - let expected = "revert: XCM execute failed: message may be invalid or execution constraints not satisfied"; - assert!(format!("{:?}", call_res).contains(expected)); - - Ok(()) - } - - #[ink_e2e::test] - async fn xcm_send_works(mut client: Client) -> E2EResult<()> { - // todo @cmichi: This sleep is necessary until we have our `ink-node` - // support a parachain/relaychain setup. For the moment we use the - // Rococo runtime for testing the examples locally. That runtime - // only has Alice and Bob endowed. Due to the nature of the tests - // we have to use Alice for sending the transactions. If the tests - // run at the same time, we'll get an error because the nonce - // of Alice is the same for all transactions. - std::thread::sleep(std::time::Duration::from_secs(30)); - - // given - let mut constructor = ContractXcmRef::new(); - let contract = client - .instantiate("contract_xcm", &ink_e2e::alice(), &mut constructor) - .value(100_000_000_000) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); - - let contract_balance_before = client - .free_balance(contract.account_id) - .await - .expect("Failed to get account balance"); - - // when - let amount = 100_000_000; - let transfer_message = call_builder.send_funds(amount, amount / 2); - let call_res = client - .call(&ink_e2e::alice(), &transfer_message) - .submit() - .await - .expect("call failed"); - assert!(call_res.return_value().is_ok()); - - // then - let contract_balance_after = client - .free_balance(contract.account_id) - .await - .expect("Failed to get account balance"); - - assert!( - contract_balance_after <= contract_balance_before - amount - (amount / 2) - ); - - Ok(()) - } - } -} diff --git a/integration-tests/public/cross-contract-calls-advanced/Cargo.toml b/integration-tests/public/cross-contract/advanced/Cargo.toml old mode 100755 new mode 100644 similarity index 100% rename from integration-tests/public/cross-contract-calls-advanced/Cargo.toml rename to integration-tests/public/cross-contract/advanced/Cargo.toml diff --git a/integration-tests/public/cross-contract-calls-advanced/e2e_tests.rs b/integration-tests/public/cross-contract/advanced/e2e_tests.rs similarity index 100% rename from integration-tests/public/cross-contract-calls-advanced/e2e_tests.rs rename to integration-tests/public/cross-contract/advanced/e2e_tests.rs diff --git a/integration-tests/public/cross-contract-calls-advanced/lib.rs b/integration-tests/public/cross-contract/advanced/lib.rs old mode 100755 new mode 100644 similarity index 100% rename from integration-tests/public/cross-contract-calls-advanced/lib.rs rename to integration-tests/public/cross-contract/advanced/lib.rs diff --git a/integration-tests/public/cross-contract-calls-advanced/other-contract/Cargo.toml b/integration-tests/public/cross-contract/advanced/other-contract/Cargo.toml old mode 100755 new mode 100644 similarity index 100% rename from integration-tests/public/cross-contract-calls-advanced/other-contract/Cargo.toml rename to integration-tests/public/cross-contract/advanced/other-contract/Cargo.toml diff --git a/integration-tests/public/cross-contract-calls-advanced/other-contract/lib.rs b/integration-tests/public/cross-contract/advanced/other-contract/lib.rs old mode 100755 new mode 100644 similarity index 100% rename from integration-tests/public/cross-contract-calls-advanced/other-contract/lib.rs rename to integration-tests/public/cross-contract/advanced/other-contract/lib.rs diff --git a/integration-tests/public/cross-contract-calls/Cargo.toml b/integration-tests/public/cross-contract/basic/Cargo.toml old mode 100755 new mode 100644 similarity index 100% rename from integration-tests/public/cross-contract-calls/Cargo.toml rename to integration-tests/public/cross-contract/basic/Cargo.toml diff --git a/integration-tests/public/cross-contract-calls/e2e_tests.rs b/integration-tests/public/cross-contract/basic/e2e_tests.rs similarity index 100% rename from integration-tests/public/cross-contract-calls/e2e_tests.rs rename to integration-tests/public/cross-contract/basic/e2e_tests.rs diff --git a/integration-tests/public/cross-contract-calls/lib.rs b/integration-tests/public/cross-contract/basic/lib.rs old mode 100755 new mode 100644 similarity index 100% rename from integration-tests/public/cross-contract-calls/lib.rs rename to integration-tests/public/cross-contract/basic/lib.rs diff --git a/integration-tests/public/cross-contract-calls/other-contract/Cargo.toml b/integration-tests/public/cross-contract/basic/other-contract/Cargo.toml old mode 100755 new mode 100644 similarity index 100% rename from integration-tests/public/cross-contract-calls/other-contract/Cargo.toml rename to integration-tests/public/cross-contract/basic/other-contract/Cargo.toml diff --git a/integration-tests/public/cross-contract-calls/other-contract/lib.rs b/integration-tests/public/cross-contract/basic/other-contract/lib.rs old mode 100755 new mode 100644 similarity index 100% rename from integration-tests/public/cross-contract-calls/other-contract/lib.rs rename to integration-tests/public/cross-contract/basic/other-contract/lib.rs diff --git a/integration-tests/public/contract-invocation/Cargo.toml b/integration-tests/public/cross-contract/invocation/Cargo.toml similarity index 100% rename from integration-tests/public/contract-invocation/Cargo.toml rename to integration-tests/public/cross-contract/invocation/Cargo.toml diff --git a/integration-tests/public/contract-invocation/README.md b/integration-tests/public/cross-contract/invocation/README.md similarity index 100% rename from integration-tests/public/contract-invocation/README.md rename to integration-tests/public/cross-contract/invocation/README.md diff --git a/integration-tests/public/contract-invocation/contract1/Cargo.toml b/integration-tests/public/cross-contract/invocation/contract1/Cargo.toml similarity index 100% rename from integration-tests/public/contract-invocation/contract1/Cargo.toml rename to integration-tests/public/cross-contract/invocation/contract1/Cargo.toml diff --git a/integration-tests/public/contract-invocation/contract1/lib.rs b/integration-tests/public/cross-contract/invocation/contract1/lib.rs similarity index 100% rename from integration-tests/public/contract-invocation/contract1/lib.rs rename to integration-tests/public/cross-contract/invocation/contract1/lib.rs diff --git a/integration-tests/public/contract-invocation/contract2/Cargo.toml b/integration-tests/public/cross-contract/invocation/contract2/Cargo.toml similarity index 100% rename from integration-tests/public/contract-invocation/contract2/Cargo.toml rename to integration-tests/public/cross-contract/invocation/contract2/Cargo.toml diff --git a/integration-tests/public/contract-invocation/contract2/lib.rs b/integration-tests/public/cross-contract/invocation/contract2/lib.rs similarity index 100% rename from integration-tests/public/contract-invocation/contract2/lib.rs rename to integration-tests/public/cross-contract/invocation/contract2/lib.rs diff --git a/integration-tests/public/contract-invocation/lib.rs b/integration-tests/public/cross-contract/invocation/lib.rs similarity index 100% rename from integration-tests/public/contract-invocation/lib.rs rename to integration-tests/public/cross-contract/invocation/lib.rs diff --git a/integration-tests/public/contract-invocation/virtual_contract/Cargo.toml b/integration-tests/public/cross-contract/invocation/virtual_contract/Cargo.toml similarity index 100% rename from integration-tests/public/contract-invocation/virtual_contract/Cargo.toml rename to integration-tests/public/cross-contract/invocation/virtual_contract/Cargo.toml diff --git a/integration-tests/public/contract-invocation/virtual_contract/lib.rs b/integration-tests/public/cross-contract/invocation/virtual_contract/lib.rs similarity index 100% rename from integration-tests/public/contract-invocation/virtual_contract/lib.rs rename to integration-tests/public/cross-contract/invocation/virtual_contract/lib.rs diff --git a/integration-tests/public/contract-invocation/virtual_contract_ver1/Cargo.toml b/integration-tests/public/cross-contract/invocation/virtual_contract_ver1/Cargo.toml similarity index 100% rename from integration-tests/public/contract-invocation/virtual_contract_ver1/Cargo.toml rename to integration-tests/public/cross-contract/invocation/virtual_contract_ver1/Cargo.toml diff --git a/integration-tests/public/contract-invocation/virtual_contract_ver1/lib.rs b/integration-tests/public/cross-contract/invocation/virtual_contract_ver1/lib.rs similarity index 100% rename from integration-tests/public/contract-invocation/virtual_contract_ver1/lib.rs rename to integration-tests/public/cross-contract/invocation/virtual_contract_ver1/lib.rs diff --git a/integration-tests/public/contract-invocation/virtual_contract_ver2/Cargo.toml b/integration-tests/public/cross-contract/invocation/virtual_contract_ver2/Cargo.toml similarity index 100% rename from integration-tests/public/contract-invocation/virtual_contract_ver2/Cargo.toml rename to integration-tests/public/cross-contract/invocation/virtual_contract_ver2/Cargo.toml diff --git a/integration-tests/public/contract-invocation/virtual_contract_ver2/lib.rs b/integration-tests/public/cross-contract/invocation/virtual_contract_ver2/lib.rs similarity index 100% rename from integration-tests/public/contract-invocation/virtual_contract_ver2/lib.rs rename to integration-tests/public/cross-contract/invocation/virtual_contract_ver2/lib.rs diff --git a/integration-tests/public/multi-contract-caller/.images/code-hashes.png b/integration-tests/public/cross-contract/multi-caller/.images/code-hashes.png similarity index 100% rename from integration-tests/public/multi-contract-caller/.images/code-hashes.png rename to integration-tests/public/cross-contract/multi-caller/.images/code-hashes.png diff --git a/integration-tests/public/multi-contract-caller/Cargo.toml b/integration-tests/public/cross-contract/multi-caller/Cargo.toml similarity index 100% rename from integration-tests/public/multi-contract-caller/Cargo.toml rename to integration-tests/public/cross-contract/multi-caller/Cargo.toml diff --git a/integration-tests/public/multi-contract-caller/README.md b/integration-tests/public/cross-contract/multi-caller/README.md similarity index 100% rename from integration-tests/public/multi-contract-caller/README.md rename to integration-tests/public/cross-contract/multi-caller/README.md diff --git a/integration-tests/public/multi-contract-caller/accumulator/Cargo.toml b/integration-tests/public/cross-contract/multi-caller/accumulator/Cargo.toml similarity index 100% rename from integration-tests/public/multi-contract-caller/accumulator/Cargo.toml rename to integration-tests/public/cross-contract/multi-caller/accumulator/Cargo.toml diff --git a/integration-tests/public/multi-contract-caller/accumulator/lib.rs b/integration-tests/public/cross-contract/multi-caller/accumulator/lib.rs similarity index 100% rename from integration-tests/public/multi-contract-caller/accumulator/lib.rs rename to integration-tests/public/cross-contract/multi-caller/accumulator/lib.rs diff --git a/integration-tests/public/multi-contract-caller/adder/Cargo.toml b/integration-tests/public/cross-contract/multi-caller/adder/Cargo.toml similarity index 100% rename from integration-tests/public/multi-contract-caller/adder/Cargo.toml rename to integration-tests/public/cross-contract/multi-caller/adder/Cargo.toml diff --git a/integration-tests/public/multi-contract-caller/adder/lib.rs b/integration-tests/public/cross-contract/multi-caller/adder/lib.rs similarity index 100% rename from integration-tests/public/multi-contract-caller/adder/lib.rs rename to integration-tests/public/cross-contract/multi-caller/adder/lib.rs diff --git a/integration-tests/public/multi-contract-caller/build-all.sh b/integration-tests/public/cross-contract/multi-caller/build-all.sh old mode 100755 new mode 100644 similarity index 100% rename from integration-tests/public/multi-contract-caller/build-all.sh rename to integration-tests/public/cross-contract/multi-caller/build-all.sh diff --git a/integration-tests/public/multi-contract-caller/lib.rs b/integration-tests/public/cross-contract/multi-caller/lib.rs similarity index 55% rename from integration-tests/public/multi-contract-caller/lib.rs rename to integration-tests/public/cross-contract/multi-caller/lib.rs index e86369943c6..4441b52c607 100644 --- a/integration-tests/public/multi-contract-caller/lib.rs +++ b/integration-tests/public/cross-contract/multi-caller/lib.rs @@ -1,7 +1,7 @@ #![cfg_attr(not(feature = "std"), no_std, no_main)] #[ink::contract] -mod multi_contract_caller { +pub mod multi_contract_caller { use accumulator::AccumulatorRef; use adder::AdderRef; use subber::SubberRef; @@ -117,103 +117,7 @@ mod multi_contract_caller { salt[..4].copy_from_slice(&version); Some(salt) } - - #[cfg(all(test, feature = "e2e-tests"))] - mod e2e_tests { - use super::*; - use ink_e2e::ContractsBackend; - - type E2EResult = std::result::Result>; - - #[ink_e2e::test] - async fn e2e_multi_contract_caller(mut client: Client) -> E2EResult<()> { - // given - let accumulator_hash = client - .upload("accumulator", &ink_e2e::alice()) - .submit() - .await - .expect("uploading `accumulator` failed") - .code_hash; - - let adder_hash = client - .upload("adder", &ink_e2e::alice()) - .submit() - .await - .expect("uploading `adder` failed") - .code_hash; - - let subber_hash = client - .upload("subber", &ink_e2e::alice()) - .submit() - .await - .expect("uploading `subber` failed") - .code_hash; - - let mut constructor = MultiContractCallerRef::new( - 1234, // initial value - 1337, // salt - accumulator_hash, - adder_hash, - subber_hash, - ); - - let multi_contract_caller = client - .instantiate("multi_contract_caller", &ink_e2e::alice(), &mut constructor) - .value(100_000_000_000) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = - multi_contract_caller.call_builder::(); - - // when - let get = call_builder.get(); - let value = client - .call(&ink_e2e::bob(), &get) - .dry_run() - .await? - .return_value(); - assert_eq!(value, 1234); - let change = call_builder.change(6); - let _ = client - .call(&ink_e2e::bob(), &change) - .submit() - .await - .expect("calling `change` failed"); - - // then - let get = call_builder.get(); - let value = client - .call(&ink_e2e::bob(), &get) - .dry_run() - .await? - .return_value(); - assert_eq!(value, 1234 + 6); - - // when - let switch = call_builder.switch(); - let _ = client - .call(&ink_e2e::bob(), &switch) - .submit() - .await - .expect("calling `switch` failed"); - let change = call_builder.change(3); - let _ = client - .call(&ink_e2e::bob(), &change) - .submit() - .await - .expect("calling `change` failed"); - - // then - let get = call_builder.get(); - let value = client - .call(&ink_e2e::bob(), &get) - .dry_run() - .await? - .return_value(); - assert_eq!(value, 1234 + 6 - 3); - - Ok(()) - } - } } + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/multi-contract-caller/subber/Cargo.toml b/integration-tests/public/cross-contract/multi-caller/subber/Cargo.toml similarity index 100% rename from integration-tests/public/multi-contract-caller/subber/Cargo.toml rename to integration-tests/public/cross-contract/multi-caller/subber/Cargo.toml diff --git a/integration-tests/public/multi-contract-caller/subber/lib.rs b/integration-tests/public/cross-contract/multi-caller/subber/lib.rs similarity index 100% rename from integration-tests/public/multi-contract-caller/subber/lib.rs rename to integration-tests/public/cross-contract/multi-caller/subber/lib.rs diff --git a/integration-tests/public/cross-contract/multi-caller/tests.rs b/integration-tests/public/cross-contract/multi-caller/tests.rs new file mode 100644 index 00000000000..3d7d74b9567 --- /dev/null +++ b/integration-tests/public/cross-contract/multi-caller/tests.rs @@ -0,0 +1,97 @@ +use super::multi_contract_caller::{MultiContractCaller, MultiContractCallerRef}; +use ink_e2e::ContractsBackend; + +type E2EResult = std::result::Result>; + +#[ink_e2e::test] +async fn e2e_multi_contract_caller(mut client: Client) -> E2EResult<()> { + // Given + let accumulator_hash = client + .upload("accumulator", &ink_e2e::alice()) + .submit() + .await + .expect("uploading `accumulator` failed") + .code_hash; + + let adder_hash = client + .upload("adder", &ink_e2e::alice()) + .submit() + .await + .expect("uploading `adder` failed") + .code_hash; + + let subber_hash = client + .upload("subber", &ink_e2e::alice()) + .submit() + .await + .expect("uploading `subber` failed") + .code_hash; + + let mut constructor = MultiContractCallerRef::new( + 1234, // initial value + 1337, // salt + accumulator_hash, + adder_hash, + subber_hash, + ); + + let multi_contract_caller = client + .instantiate("multi_contract_caller", &ink_e2e::alice(), &mut constructor) + .value(100_000_000_000) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = + multi_contract_caller.call_builder::(); + + // When + let get = call_builder.get(); + let value = client + .call(&ink_e2e::bob(), &get) + .dry_run() + .await? + .return_value(); + assert_eq!(value, 1234); + + let change = call_builder.change(6); + let _ = client + .call(&ink_e2e::bob(), &change) + .submit() + .await + .expect("calling `change` failed"); + + // Then + let get = call_builder.get(); + let value = client + .call(&ink_e2e::bob(), &get) + .dry_run() + .await? + .return_value(); + assert_eq!(value, 1234 + 6); + + // When + let switch = call_builder.switch(); + let _ = client + .call(&ink_e2e::bob(), &switch) + .submit() + .await + .expect("calling `switch` failed"); + + let change = call_builder.change(3); + let _ = client + .call(&ink_e2e::bob(), &change) + .submit() + .await + .expect("calling `change` failed"); + + // Then + let get = call_builder.get(); + let value = client + .call(&ink_e2e::bob(), &get) + .dry_run() + .await? + .return_value(); + assert_eq!(value, 1234 + 6 - 3); + + Ok(()) +} \ No newline at end of file diff --git a/integration-tests/public/contract-transfer/Cargo.toml b/integration-tests/public/cross-contract/transfer/Cargo.toml similarity index 100% rename from integration-tests/public/contract-transfer/Cargo.toml rename to integration-tests/public/cross-contract/transfer/Cargo.toml diff --git a/integration-tests/public/cross-contract/transfer/lib.rs b/integration-tests/public/cross-contract/transfer/lib.rs new file mode 100644 index 00000000000..20803fd59fd --- /dev/null +++ b/integration-tests/public/cross-contract/transfer/lib.rs @@ -0,0 +1,71 @@ +//! A smart contract which demonstrates behavior of the `self.env().transfer()` function. +//! It transfers some of it's balance to the caller. + +#![cfg_attr(not(feature = "std"), no_std, no_main)] +#![allow(clippy::new_without_default)] + +#[ink::contract] +pub mod give_me { + use ink::primitives::U256; + + /// No storage is needed for this simple contract. + #[ink(storage)] + pub struct GiveMe {} + + impl GiveMe { + /// Creates a new instance of this contract. + /// + /// This is a payable constructor, meaning it can receive initial funding. + #[ink(constructor, payable)] + pub fn new() -> Self { + Self {} + } + + /// Transfers `value` amount of tokens to the caller. + /// + /// # Errors + /// + /// - Panics in case the requested transfer exceeds the contract balance. + /// - Panics in case the requested transfer would have brought this contract's + /// balance below the minimum balance (i.e. the chain's existential deposit). + /// - Panics in case the transfer failed for another reason. + #[ink(message)] + pub fn give_me(&mut self, value: U256) { + assert!(value <= self.env().balance(), "insufficient funds!"); + + if self.env().transfer(self.env().caller(), value).is_err() { + panic!( + "requested transfer failed. this can be the case if the contract does not\ + have sufficient free funds or if the transfer would have brought the\ + contract's balance below minimum balance." + ) + } + } + + /// Asserts that the token amount sent as payment with this call + /// is exactly `10`. This method will fail otherwise, and the + /// transaction would then be reverted. + /// + /// # Note + /// + /// The method needs to be annotated with `payable`; only then it is + /// allowed to receive value as part of the call. + #[ink(message, payable, selector = 0xCAFEBABE)] + pub fn was_it_ten(&mut self) { + /* + ink::env::debug_println!( + "received payment: {}", + self.env().transferred_value() + ); + */ + assert!( + self.env().transferred_value() == U256::from(10), + "payment was not ten" + ); + } + } +} + +// Include the test file +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/cross-contract/transfer/tests.rs b/integration-tests/public/cross-contract/transfer/tests.rs new file mode 100644 index 00000000000..3eb47e1c08a --- /dev/null +++ b/integration-tests/public/cross-contract/transfer/tests.rs @@ -0,0 +1,232 @@ +use super::give_me::*; +use ink::primitives::{Address, U256}; + +// ================================================================================= +// UNIT TESTS +// ================================================================================= +// These tests run in the off-chain environment provided by `ink::env::test`. +// They simulate the contract logic without spinning up a full node. + +#[ink::test] +fn transfer_works() { + // given + let contract_balance = 100.into(); + let accounts = default_accounts(); + let mut give_me = create_contract(contract_balance); + + // when + set_sender(accounts.eve); + set_balance(accounts.eve, 0.into()); + // Eve requests 80 tokens + give_me.give_me(80.into()); + + // then + assert_eq!(get_balance(accounts.eve), 80.into()); +} + +#[ink::test] +#[should_panic(expected = "insufficient funds!")] +fn transfer_fails_insufficient_funds() { + // given + let contract_balance = 100.into(); + let accounts = default_accounts(); + let mut give_me = create_contract(contract_balance); + + // when + set_sender(accounts.eve); + // Eve requests 120 tokens (more than contract has) + give_me.give_me(120.into()); + + // then + // `give_me` must already have panicked here +} + +#[ink::test] +fn test_transferred_value() { + use ink::codegen::Env; + // given + let accounts = default_accounts(); + let mut give_me = create_contract(100.into()); + let contract_account = give_me.env().address(); + + // when + // Push the new execution context which sets initial balances and + // sets Eve as the caller + set_balance(accounts.eve, 100.into()); + set_balance(contract_account, 0.into()); + set_sender(accounts.eve); + + // then + // we use helper macro to emulate method invocation coming with payment, + // and there must be no panic + ink::env::pay_with_call!(give_me.was_it_ten(), 10.into()); + + // and + // balances should be changed properly + let contract_new_balance = get_balance(contract_account); + let caller_new_balance = get_balance(accounts.eve); + + assert_eq!(caller_new_balance, (100 - 10).into()); + assert_eq!(contract_new_balance, 10.into()); +} + +#[ink::test] +#[should_panic(expected = "payment was not ten")] +fn test_transferred_value_must_fail() { + // given + let accounts = default_accounts(); + let mut give_me = create_contract(100.into()); + + // when + // Push the new execution context which sets Eve as caller and + // the `mock_transferred_value` as the value which the contract + // will see as transferred to it. + set_sender(accounts.eve); + ink::env::test::set_value_transferred(13.into()); + + // then + // Expect panic because we sent 13, but contract expects 10 + give_me.was_it_ten(); +} + +// --- Helper Functions for Unit Tests --- + +/// Creates a new instance of `GiveMe` with `initial_balance`. +/// Returns the `contract_instance`. +fn create_contract(initial_balance: U256) -> GiveMe { + let accounts = default_accounts(); + set_sender(accounts.alice); + set_balance(contract_id(), initial_balance); + GiveMe::new() +} + +fn contract_id() -> Address { + ink::env::test::callee() +} + +fn set_sender(sender: Address) { + ink::env::test::set_caller(sender); +} + +fn default_accounts() -> ink::env::test::DefaultAccounts { + ink::env::test::default_accounts() +} + +fn set_balance(addr: Address, balance: U256) { + ink::env::test::set_contract_balance(addr, balance) +} + +fn get_balance(addr: Address) -> U256 { + ink::env::test::get_contract_balance::(addr) + .expect("Cannot get contract balance") +} + +// ================================================================================= +// END-TO-END (E2E) TESTS +// ================================================================================= +// These tests run against a simulated node (sandbox) using `ink_e2e`. + +#[cfg(all(test, feature = "e2e-tests"))] +mod e2e_tests { + use super::*; + use ink::env::Environment; + use ink_e2e::{ + ChainBackend, + ContractsBackend, + AccountId, + Balance, + }; + + type E2EResult = std::result::Result>; + + /// Tests that paying a method that isn't `payable` results in an error. + #[ink_e2e::test] + async fn e2e_sending_value_to_give_me_must_fail( + mut client: Client, + ) -> E2EResult<()> { + // given + let mut constructor = GiveMeRef::new(); + let contract = client + .instantiate("contract_transfer", &ink_e2e::alice(), &mut constructor) + .value(1_000_000_000) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + // when + // We try to call `give_me` (which is NOT payable) but we attach value. + let transfer = call_builder.give_me(120_000_000.into()); + + let call_res = client + .call(&ink_e2e::bob(), &transfer) + .value(10_000_000) // This is the illegal payment + .submit() + .await; + + // then + assert!(call_res.is_err(), "call must have errored"); + + Ok(()) + } + + /// Tests that the contract can successfully transfer funds back to the caller. + #[ink_e2e::test(runtime)] + async fn e2e_contract_must_transfer_value_to_sender( + mut client: Client, + ) -> E2EResult<()> { + // given + let mut constructor = GiveMeRef::new(); + let contract = client + .instantiate("contract_transfer", &ink_e2e::bob(), &mut constructor) + .value(1_337_000_000) // Initial endowment to the contract + .submit() + .await + .expect("instantiate failed"); + let contract_addr = contract.addr; + + // Check trace to verify initial value transfer + assert_eq!( + contract.trace.clone().unwrap().value, + Some(ink::env::DefaultEnvironment::native_to_eth(1_337_000_000)) + ); + let mut call_builder = contract.call_builder::(); + + let balance_before: Balance = client + .free_balance(contract.account_id) + .await + .expect("getting balance failed"); + + // when + // Eve calls the contract asking for funds + let transfer = call_builder.give_me(U256::from(120_000_000_0)); + + let call_res = client + .call(&ink_e2e::eve(), &transfer) + .submit() + .await + .expect("call failed"); + + // then + // Verify trace data + let outgoing_trace = &call_res.trace.unwrap().calls[0]; + assert_eq!(outgoing_trace.value, Some(U256::from(120_000_000_0))); + assert_eq!(outgoing_trace.from, contract_addr); + assert_eq!( + outgoing_trace.to, + ink_e2e::address_from_keypair::(&ink_e2e::eve()) + ); + + // Verify balance changes + let balance_after: Balance = client + .free_balance(contract.account_id) + .await + .expect("getting balance failed"); + + // Note: The difference includes gas costs + transferred value. + // In this specific test setup, we check the rough difference or exact if gas is handled. + assert_eq!(balance_before - balance_after, 12); + + Ok(()) + } +} \ No newline at end of file diff --git a/integration-tests/public/custom-environment/lib.rs b/integration-tests/public/custom-environment/lib.rs deleted file mode 100644 index 52ad4034e0c..00000000000 --- a/integration-tests/public/custom-environment/lib.rs +++ /dev/null @@ -1,140 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -use ink::env::{ - DefaultEnvironment, - Environment, -}; - -/// Our custom environment diverges from the `DefaultEnvironment` in the event topics -/// limit. -#[derive(Debug, Clone, PartialEq, Eq)] -#[ink::scale_derive(TypeInfo)] -pub enum EnvironmentWithManyTopics {} - -impl Environment for EnvironmentWithManyTopics { - const NATIVE_TO_ETH_RATIO: u32 = - ::NATIVE_TO_ETH_RATIO; - const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = - ::TRUST_BACKED_ASSETS_PRECOMPILE_INDEX; - const POOL_ASSETS_PRECOMPILE_INDEX: u16 = - ::POOL_ASSETS_PRECOMPILE_INDEX; - - type AccountId = ::AccountId; - type Balance = ::Balance; - type Hash = ::Hash; - type BlockNumber = ::BlockNumber; - type Timestamp = ::Timestamp; - type EventRecord = ::EventRecord; -} - -#[ink::contract(env = crate::EnvironmentWithManyTopics)] -mod runtime_call { - /// Trivial contract with a single message that emits an event with many topics. - #[ink(storage)] - #[derive(Default)] - pub struct Topics; - - /// An event that would be forbidden in the default environment, but is completely - /// valid in our custom one. - #[ink(event)] - #[derive(Default)] - pub struct EventWithTopics { - #[ink(topic)] - first_topic: Balance, - #[ink(topic)] - second_topic: Balance, - } - - impl Topics { - #[ink(constructor)] - pub fn new() -> Self { - Default::default() - } - - /// Emit an event with many topics. - #[ink(message)] - pub fn trigger(&mut self) { - self.env().emit_event(EventWithTopics::default()); - } - } - - #[cfg(test)] - mod tests { - use super::*; - - #[ink::test] - fn emits_event_with_many_topics() { - let mut contract = Topics::new(); - contract.trigger(); - - let emitted_events = ink::env::test::recorded_events(); - assert_eq!(emitted_events.len(), 1); - - let emitted_event = ::decode( - &mut &emitted_events[0].data[..], - ); - - assert!(emitted_event.is_ok()); - } - } - - #[cfg(all(test, feature = "e2e-tests"))] - mod e2e_tests { - use super::*; - use ink_e2e::ContractsBackend; - - type E2EResult = Result>; - - #[cfg(feature = "permissive-node")] - #[ink_e2e::test(environment = crate::EnvironmentWithManyTopics)] - async fn calling_custom_environment_works(mut client: Client) -> E2EResult<()> { - // given - let mut constructor = TopicsRef::new(); - let contract = client - .instantiate("custom-environment", &ink_e2e::alice(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); - - // when - let message = call_builder.trigger(); - - let call_res = client - .call(&ink_e2e::alice(), &message) - .submit() - .await - .expect("call failed"); - - // then - assert!(call_res.contains_event("Revive", "ContractEmitted")); - - Ok(()) - } - - #[cfg(not(feature = "permissive-node"))] - #[ink_e2e::test(environment = crate::EnvironmentWithManyTopics)] - async fn calling_custom_environment_fails_if_incompatible_with_node( - mut client: Client, - ) -> E2EResult<()> { - // given - let mut constructor = TopicsRef::new(); - let contract = client - .instantiate("custom-environment", &ink_e2e::alice(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); - - let message = call_builder.trigger(); - - // when - let call_res = client.call(&ink_e2e::alice(), &message).dry_run().await; - - // then - assert!(call_res.is_err()); - - Ok(()) - } - } -} diff --git a/integration-tests/public/debugging-strategies/lib.rs b/integration-tests/public/debugging-strategies/lib.rs deleted file mode 100755 index 7e12f097d32..00000000000 --- a/integration-tests/public/debugging-strategies/lib.rs +++ /dev/null @@ -1,313 +0,0 @@ -//! # Debugging Strategies -//! -//! This contract illustrates a number of strategies for debugging -//! contracts: -//! -//! * Emitting debugging events. -//! * The `pallet-revive` tracing API. -//! * Causing intentional reverts with a return value, in your contract. - -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -#[ink::contract] -mod debugging_strategies { - use flipper::FlipperRef; - use ink::env::call::{ - ExecutionInput, - Selector, - build_call, - build_create, - }; - #[cfg(feature = "debug")] - use ink::prelude::{ - borrow::ToOwned, - format, - string::String, - }; - - #[ink::event] - #[cfg(feature = "debug")] - pub struct DebugEvent { - message: String, - } - - /// Storage of the contract. - #[ink(storage)] - #[derive(Default)] - pub struct DebuggingStrategies { - value: bool, - } - - impl DebuggingStrategies { - #[ink(constructor)] - pub fn new() -> Self { - Self { value: true } - } - - #[ink(message)] - pub fn get(&self) -> bool { - #[cfg(feature = "debug")] - self.env().emit_event(DebugEvent { - message: format!("received {:?}", self.env().transferred_value()) - .to_owned(), - }); - self.value - } - - #[ink(message)] - pub fn intentional_revert(&self) { - #[cfg(feature = "debug")] - ink::env::return_value( - ink::env::ReturnFlags::REVERT, - &format!("reverting with info: {:?}", self.env().transferred_value()), - ); - } - - #[ink(message, payable)] - pub fn instantiate_and_call(&mut self, code_hash: ink::H256) -> bool { - let create_params = build_create::() - .code_hash(code_hash) - .endowment(0.into()) - .exec_input(ExecutionInput::new(Selector::new(ink::selector_bytes!( - Abi::Ink, - "new" - )))) - .returns::() - .params(); - - let other: FlipperRef = self.env() - .instantiate_contract(&create_params) - // todo - // we do this to make sure the instantiated contract is always at a - // different address - // .salt(self.env().addr().to_vec()) - .unwrap_or_else(|error| { - panic!( - "Received an error from `pallet-revive` while instantiating: {error:?}" - ) - }) - .unwrap_or_else(|error| { - panic!("Received a `LangError` while instantiating: {error:?}") - }); - - let call = build_call() - .call(ink::ToAddr::to_addr(&other)) - .transferred_value(0.into()) - .exec_input(ExecutionInput::new(Selector::new(ink::selector_bytes!( - Abi::Ink, - "get" - )))) - .returns::() - .params(); - - self.env() - .invoke_contract(&call) - .unwrap_or_else(|env_err| { - panic!("Received an error from the Environment: {env_err:?}") - }) - .unwrap_or_else(|lang_err| panic!("Received a `LangError`: {lang_err:?}")) - } - } - - #[cfg(all(test, feature = "e2e-tests"))] - mod e2e_tests { - use super::*; - use ink::env::Environment; - use ink_e2e::ContractsBackend; - - type E2EResult = std::result::Result>; - - /// This test illustrates how to use debugging events. - /// - /// The contract is build with the `debug` feature enabled, thus - /// we can have code in the contract that is utilized purely - /// for testing, but not for release builds. - #[ink_e2e::test(features = ["debug"])] - async fn e2e_debugging_event_emitted(mut client: Client) -> E2EResult<()> { - // given - let mut constructor = DebuggingStrategiesRef::new(); - let contract = client - .instantiate("debugging_strategies", &ink_e2e::alice(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let call_builder = contract.call_builder::(); - - // when - let call_res = client - .call(&ink_e2e::alice(), &call_builder.get()) - .submit() - .await - .expect("calling `get` message failed"); - - // then - // the contract will have emitted an event - assert!(call_res.contains_event("Revive", "ContractEmitted")); - let contract_events = call_res.contract_emitted_events()?; - assert_eq!(1, contract_events.len()); - let contract_event = &contract_events[0]; - let debug_event: DebugEvent = - ink::scale::Decode::decode(&mut &contract_event.event.data[..]) - .expect("encountered invalid contract event data buffer"); - assert_eq!(debug_event.message, "received 0"); - - Ok(()) - } - - /// This test illustrates how to decode a `Revive::ContractReverted`. - #[ink_e2e::test(features = ["debug"])] - async fn e2e_decode_intentional_revert(mut client: Client) -> E2EResult<()> { - // given - let mut constructor = DebuggingStrategiesRef::new(); - let contract = client - .instantiate("debugging_strategies", &ink_e2e::bob(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let call_builder = contract.call_builder::(); - - // when - let call_res = client - .call(&ink_e2e::alice(), &call_builder.intentional_revert()) - .dry_run() - .await - .expect("calling `get` message failed"); - - let return_data = call_res.return_data(); - assert!(call_res.did_revert()); - let revert_msg = String::from_utf8_lossy(return_data); - assert!(revert_msg.contains("reverting with info: 0")); - - Ok(()) - } - - /// This test illustrates how to decode a `Revive::ContractReverted`. - #[ink_e2e::test] - async fn e2e_decode_revert(mut client: Client) -> E2EResult<()> { - // given - let mut constructor = DebuggingStrategiesRef::new(); - let contract = client - .instantiate("debugging_strategies", &ink_e2e::bob(), &mut constructor) - .value(1_337_000_000) - .dry_run() - //.submit() - .await - .expect("instantiate failed"); - - // when - let return_data = contract.return_data(); - assert!(contract.did_revert()); - let revert_msg = String::from_utf8_lossy(return_data); - assert!(revert_msg.contains("paid an unpayable message")); - - // todo show same for call - let contract = client - .instantiate("debugging_strategies", &ink_e2e::bob(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let call_builder = contract.call_builder::(); - - // when - let call_res = client - .call(&ink_e2e::alice(), &call_builder.get()) - .value(1_337_000_000) - .dry_run() - .await - .expect("calling `get` message failed"); - - let return_data = call_res.return_data(); - assert!(call_res.did_revert()); - let revert_msg = String::from_utf8_lossy(return_data); - assert!( - revert_msg.contains( - "dispatching ink! message failed: paid an unpayable message" - ) - ); - - Ok(()) - } - - /// This test illustrates how to use the `pallet-revive` tracing functionality. - #[ink_e2e::test] - async fn e2e_tracing(mut client: Client) -> E2EResult<()> { - // given - let mut constructor = DebuggingStrategiesRef::new(); - let contract = client - .instantiate("debugging_strategies", &ink_e2e::bob(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); - - let call = call_builder.instantiate_and_call(contract.code_hash); - let call_res = client - .call(&ink_e2e::alice(), &call) - .value(1_337_000_000) - .submit() - .await?; - - // when - let trace: ink_e2e::CallTrace = call_res.trace.expect("trace must exist"); - assert_eq!(trace.calls.len(), 2); - // This is how the object looks: - // ``` - // CallTrace { - // from: 0x9621dde636de098b43efb0fa9b61facfe328f99d, - // gas: 1497105168000, - // gas_used: 1548337586000, - // to: 0xd71ff7085ed0e3e8b6c8e95eb6094f4311ae8e2f, - // input: Bytes( - // 0x829da98747d85e35d0b3ca3c7ceeac09b63ec2754e6a05eb6d2d5b92fb916da126364dd4, - // ), - // output: Bytes(0x0001), - // error: None, - // revert_reason: None, - // calls: [ - // CallTrace { - // from: 0xd71ff7085ed0e3e8b6c8e95eb6094f4311ae8e2f, - // gas: 711404887000, - // gas_used: 205987649000, - // to: 0xfd8bf44f34a2d2cec42b8ab31ede1bb1bc366e8e, - // input: Bytes(0x9bae9d5e), - // output: Bytes(0x0000), - // error: None, - // revert_reason: None, - // calls: [], - // logs: [], - // value: Some(0), - // call_type: Call, - // }, - // CallTrace { - // from: 0xd71ff7085ed0e3e8b6c8e95eb6094f4311ae8e2f, - // gas: 124370129000, - // gas_used: 163567881000, - // to: 0xfd8bf44f34a2d2cec42b8ab31ede1bb1bc366e8e, - // input: Bytes(0x2f865bd9), - // output: Bytes(0x0001), - // error: None, - // revert_reason: None, - // calls: [], - // logs: [], - // value: Some(0), - // call_type: Call, - // }, - // ], - // logs: [], - // value: Some(0), - // call_type: Call, - // } - // ``` - - // then - assert_eq!( - trace.value, - Some(ink::env::DefaultEnvironment::native_to_eth(1_337_000_000)) - ); - - Ok(()) - } - - // todo add the same above, but for the runtime backend - } -} diff --git a/integration-tests/public/e2e-call-runtime/lib.rs b/integration-tests/public/e2e-call-runtime/lib.rs deleted file mode 100644 index fa69b8b0103..00000000000 --- a/integration-tests/public/e2e-call-runtime/lib.rs +++ /dev/null @@ -1,99 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -#[ink::contract] -pub mod e2e_call_runtime { - #[ink(storage)] - #[derive(Default)] - pub struct Contract {} - - impl Contract { - #[ink(constructor)] - pub fn new() -> Self { - Self {} - } - - #[ink(message)] - pub fn get_contract_balance(&self) -> ink::U256 { - self.env().balance() - } - } - - #[cfg(all(test, feature = "e2e-tests"))] - mod e2e_tests { - use super::*; - use ink::env::Environment; - use ink_e2e::{ - ChainBackend, - ContractsBackend, - subxt::dynamic::Value, - }; - use static_assertions::assert_type_eq_all; - - type E2EResult = std::result::Result>; - - #[ink_e2e::test] - async fn call_runtime_works(mut client: Client) -> E2EResult<()> { - // given - let mut constructor = ContractRef::new(); - let contract = client - .instantiate("e2e_call_runtime", &ink_e2e::alice(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - - let account_id = client.to_account_id(&contract.addr).await?; - assert_eq!(account_id, contract.account_id); - - let mut call_builder = contract.call_builder::(); - - // The generic `Environment::Balance` type must be `u128` - // for this test to work. This is because we encode `Value::u128` - // in the `call_data`. - assert_type_eq_all!(Balance, u128); - let transfer_amount: u128 = 100_000_000_000; - - // when - let call_data = vec![ - // A value representing a `MultiAddress`. We want the - // "Id" variant, and that will ultimately contain the - // bytes for our destination address - Value::unnamed_variant("Id", [Value::from_bytes(contract.account_id)]), - // A value representing the amount we'd like to transfer. - Value::u128(transfer_amount), - ]; - - let get_balance = call_builder.get_contract_balance(); - let pre_balance = client - .call(&ink_e2e::alice(), &get_balance) - .dry_run() - .await? - .return_value(); - - // Send funds from Alice to the contract using Balances::transfer - client - .runtime_call( - &ink_e2e::alice(), - "Balances", - "transfer_allow_death", - call_data, - ) - .await - .expect("runtime call failed"); - - // then - let get_balance = call_builder.get_contract_balance(); - let get_balance_res = client - .call(&ink_e2e::alice(), &get_balance) - .dry_run() - .await?; - - assert_eq!( - get_balance_res.return_value(), - pre_balance - + ink::env::DefaultEnvironment::native_to_eth(transfer_amount) - ); - - Ok(()) - } - } -} diff --git a/integration-tests/public/erc20/lib.rs b/integration-tests/public/erc20/lib.rs deleted file mode 100644 index fd41432384e..00000000000 --- a/integration-tests/public/erc20/lib.rs +++ /dev/null @@ -1,668 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -#[ink::contract] -mod erc20 { - use ink::{ - U256, - storage::Mapping, - }; - - /// A simple ERC-20 contract. - #[ink(storage)] - #[derive(Default)] - pub struct Erc20 { - /// Total token supply. - total_supply: U256, - /// Mapping from owner to number of owned token. - balances: Mapping, - /// Mapping of the token amount which an account is allowed to withdraw - /// from another account. - allowances: Mapping<(Address, Address), U256>, - } - - /// Event emitted when a token transfer occurs. - #[ink(event)] - pub struct Transfer { - #[ink(topic)] - from: Option
, - #[ink(topic)] - to: Option
, - value: U256, - } - - /// Event emitted when an approval occurs that `spender` is allowed to withdraw - /// up to the amount of `value` tokens from `owner`. - #[ink(event)] - pub struct Approval { - #[ink(topic)] - owner: Address, - #[ink(topic)] - spender: Address, - value: U256, - } - - /// The ERC-20 error types. - #[derive(Debug, PartialEq, Eq)] - #[ink::error] - pub enum Error { - /// Returned if not enough balance to fulfill a request is available. - InsufficientBalance, - /// Returned if not enough allowance to fulfill a request is available. - InsufficientAllowance, - } - - /// The ERC-20 result type. - pub type Result = core::result::Result; - - impl Erc20 { - /// Creates a new ERC-20 contract with the specified initial supply. - #[ink(constructor)] - pub fn new(total_supply: U256) -> Self { - let mut balances = Mapping::default(); - let caller = Self::env().caller(); - balances.insert(caller, &total_supply); - Self::env().emit_event(Transfer { - from: None, - to: Some(caller), - value: total_supply, - }); - Self { - total_supply, - balances, - allowances: Default::default(), - } - } - - /// Returns the total token supply. - #[ink(message)] - pub fn total_supply(&self) -> U256 { - self.total_supply - } - - /// Returns the account balance for the specified `owner`. - /// - /// Returns `0` if the account is non-existent. - #[ink(message)] - pub fn balance_of(&self, owner: Address) -> U256 { - self.balance_of_impl(&owner) - } - - /// Returns the account balance for the specified `owner`. - /// - /// Returns `0` if the account is non-existent. - /// - /// # Note - /// - /// Prefer to call this method over `balance_of` since this - /// works using references which are more efficient. - #[inline] - fn balance_of_impl(&self, owner: &Address) -> U256 { - self.balances.get(owner).unwrap_or_default() - } - - /// Returns the amount which `spender` is still allowed to withdraw from `owner`. - /// - /// Returns `0` if no allowance has been set. - #[ink(message)] - pub fn allowance(&self, owner: Address, spender: Address) -> U256 { - self.allowance_impl(&owner, &spender) - } - - /// Returns the amount which `spender` is still allowed to withdraw from `owner`. - /// - /// Returns `0` if no allowance has been set. - /// - /// # Note - /// - /// Prefer to call this method over `allowance` since this - /// works using references which are more efficient. - #[inline] - fn allowance_impl(&self, owner: &Address, spender: &Address) -> U256 { - self.allowances.get((owner, spender)).unwrap_or_default() - } - - /// Transfers `value` amount of tokens from the caller's account to account `to`. - /// - /// On success a `Transfer` event is emitted. - /// - /// # Errors - /// - /// Returns `InsufficientBalance` error if there are not enough tokens on - /// the caller's account balance. - #[ink(message)] - pub fn transfer(&mut self, to: Address, value: U256) -> Result<()> { - let from = self.env().caller(); - self.transfer_from_to(&from, &to, value) - } - - /// Allows `spender` to withdraw from the caller's account multiple times, up to - /// the `value` amount. - /// - /// If this function is called again it overwrites the current allowance with - /// `value`. - /// - /// An `Approval` event is emitted. - #[ink(message)] - pub fn approve(&mut self, spender: Address, value: U256) -> Result<()> { - let owner = self.env().caller(); - self.allowances.insert((&owner, &spender), &value); - self.env().emit_event(Approval { - owner, - spender, - value, - }); - Ok(()) - } - - /// Transfers `value` tokens on the behalf of `from` to the account `to`. - /// - /// This can be used to allow a contract to transfer tokens on ones behalf and/or - /// to charge fees in sub-currencies, for example. - /// - /// On success a `Transfer` event is emitted. - /// - /// # Errors - /// - /// Returns `InsufficientAllowance` error if there are not enough tokens allowed - /// for the caller to withdraw from `from`. - /// - /// Returns `InsufficientBalance` error if there are not enough tokens on - /// the account balance of `from`. - #[ink(message)] - pub fn transfer_from( - &mut self, - from: Address, - to: Address, - value: U256, - ) -> Result<()> { - let caller = self.env().caller(); - let allowance = self.allowance_impl(&from, &caller); - if allowance < value { - return Err(Error::InsufficientAllowance) - } - self.transfer_from_to(&from, &to, value)?; - // We checked that allowance >= value - #[allow(clippy::arithmetic_side_effects)] - self.allowances - .insert((&from, &caller), &(allowance - value)); - Ok(()) - } - - /// Transfers `value` amount of tokens from the caller's account to account `to`. - /// - /// On success a `Transfer` event is emitted. - /// - /// # Errors - /// - /// Returns `InsufficientBalance` error if there are not enough tokens on - /// the caller's account balance. - fn transfer_from_to( - &mut self, - from: &Address, - to: &Address, - value: U256, - ) -> Result<()> { - let from_balance = self.balance_of_impl(from); - if from_balance < value { - return Err(Error::InsufficientBalance) - } - // We checked that from_balance >= value - #[allow(clippy::arithmetic_side_effects)] - self.balances.insert(from, &(from_balance - value)); - let to_balance = self.balance_of_impl(to); - self.balances - .insert(to, &(to_balance.checked_add(value).unwrap())); - self.env().emit_event(Transfer { - from: Some(*from), - to: Some(*to), - value, - }); - Ok(()) - } - } - - #[cfg(test)] - fn set_caller(sender: Address) { - ink::env::test::set_caller(sender); - } - - #[cfg(test)] - mod tests { - use super::*; - - use ink::primitives::{ - Clear, - Hash, - }; - - fn assert_transfer_event( - event: &ink::env::test::EmittedEvent, - expected_from: Option
, - expected_to: Option
, - expected_value: U256, - ) { - let decoded_event = - ::decode(&mut &event.data[..]) - .expect("encountered invalid contract event data buffer"); - let Transfer { from, to, value } = decoded_event; - assert_eq!(from, expected_from, "encountered invalid Transfer.from"); - assert_eq!(to, expected_to, "encountered invalid Transfer.to"); - assert_eq!(value, expected_value, "encountered invalid Transfer.value"); - - let mut expected_topics = Vec::new(); - expected_topics.push( - ink::blake2x256!("Transfer(Option
,Option
,U256)").into(), - ); - if let Some(from) = expected_from { - expected_topics.push(encoded_into_hash(from)); - } else { - expected_topics.push(Hash::CLEAR_HASH); - } - if let Some(to) = expected_to { - expected_topics.push(encoded_into_hash(to)); - } else { - expected_topics.push(Hash::CLEAR_HASH); - } - expected_topics.push(encoded_into_hash(value)); - - let topics = event.topics.clone(); - for (n, (actual_topic, expected_topic)) in - topics.iter().zip(expected_topics).enumerate() - { - let mut topic_hash = Hash::CLEAR_HASH; - let len = actual_topic.len(); - topic_hash.as_mut()[0..len].copy_from_slice(&actual_topic[0..len]); - - assert_eq!( - topic_hash, expected_topic, - "encountered invalid topic at {n}" - ); - } - } - - /// The default constructor does its job. - #[ink::test] - fn new_works() { - // Constructor works. - set_caller(Address::from([0x01; 20])); - let _erc20 = Erc20::new(100.into()); - - // Transfer event triggered during initial construction. - let emitted_events = ink::env::test::recorded_events(); - assert_eq!(1, emitted_events.len()); - - assert_transfer_event( - &emitted_events[0], - None, - Some(Address::from([0x01; 20])), - 100.into(), - ); - } - - /// The total supply was applied. - #[ink::test] - fn total_supply_works() { - // Constructor works. - set_caller(Address::from([0x01; 20])); - let erc20 = Erc20::new(100.into()); - // Transfer event triggered during initial construction. - let emitted_events = ink::env::test::recorded_events(); - assert_transfer_event( - &emitted_events[0], - None, - Some(Address::from([0x01; 20])), - 100.into(), - ); - // Get the token total supply. - assert_eq!(erc20.total_supply(), U256::from(100)); - } - - /// Get the actual balance of an account. - #[ink::test] - fn balance_of_works() { - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - - // Constructor works - let erc20 = Erc20::new(100.into()); - // Transfer event triggered during initial construction - let emitted_events = ink::env::test::recorded_events(); - assert_transfer_event( - &emitted_events[0], - None, - Some(accounts.alice), - 100.into(), - ); - let accounts = ink::env::test::default_accounts(); - // Alice owns all the tokens on contract instantiation - assert_eq!(erc20.balance_of(accounts.alice), U256::from(100)); - // Bob does not owns tokens - assert_eq!(erc20.balance_of(accounts.bob), U256::zero()); - } - - #[ink::test] - fn transfer_works() { - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - - // Constructor works. - let mut erc20 = Erc20::new(100.into()); - // Transfer event triggered during initial construction. - assert_eq!(erc20.balance_of(accounts.bob), U256::zero()); - // Alice transfers 10 tokens to Bob. - assert_eq!(erc20.transfer(accounts.bob, U256::from(10)), Ok(())); - // Bob owns 10 tokens. - assert_eq!(erc20.balance_of(accounts.bob), U256::from(10)); - - let emitted_events = ink::env::test::recorded_events(); - assert_eq!(emitted_events.len(), 2); - // Check first transfer event related to ERC-20 instantiation. - assert_transfer_event( - &emitted_events[0], - None, - Some(accounts.alice), - 100.into(), - ); - // Check the second transfer event relating to the actual transfer. - assert_transfer_event( - &emitted_events[1], - Some(accounts.alice), - Some(accounts.bob), - 10.into(), - ); - } - - #[ink::test] - fn invalid_transfer_should_fail() { - // Constructor works. - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - - let initial_supply = 100.into(); - let mut erc20 = Erc20::new(initial_supply); - - assert_eq!(erc20.balance_of(accounts.bob), U256::zero()); - - // Set the contract as callee and Bob as caller. - let contract = ink::env::address(); - ink::env::test::set_callee(contract); - set_caller(accounts.bob); - - // Bob fails to transfer 10 tokens to Eve. - assert_eq!( - erc20.transfer(accounts.eve, 10.into()), - Err(Error::InsufficientBalance) - ); - // Alice owns all the tokens. - assert_eq!(erc20.balance_of(accounts.alice), U256::from(100)); - assert_eq!(erc20.balance_of(accounts.bob), U256::zero()); - assert_eq!(erc20.balance_of(accounts.eve), U256::zero()); - - // Transfer event triggered during initial construction. - let emitted_events = ink::env::test::recorded_events(); - assert_eq!(emitted_events.len(), 1); - assert_transfer_event( - &emitted_events[0], - None, - Some(accounts.alice), - 100.into(), - ); - } - - #[ink::test] - fn transfer_from_works() { - // Constructor works. - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - - let mut erc20 = Erc20::new(100.into()); - - // Bob fails to transfer tokens owned by Alice. - assert_eq!( - erc20.transfer_from(accounts.alice, accounts.eve, 10.into()), - Err(Error::InsufficientAllowance) - ); - // Alice approves Bob for token transfers on her behalf. - assert_eq!(erc20.approve(accounts.bob, 10.into()), Ok(())); - - // The approve event takes place. - assert_eq!(ink::env::test::recorded_events().len(), 2); - - // Set the contract as callee and Bob as caller. - let contract = ink::env::address(); - ink::env::test::set_callee(contract); - ink::env::test::set_caller(accounts.bob); - - // Bob transfers tokens from Alice to Eve. - assert_eq!( - erc20.transfer_from(accounts.alice, accounts.eve, 10.into()), - Ok(()) - ); - // Eve owns tokens. - assert_eq!(erc20.balance_of(accounts.eve), U256::from(10)); - - // Check all transfer events that happened during the previous calls: - let emitted_events = ink::env::test::recorded_events(); - assert_eq!(emitted_events.len(), 3); - assert_transfer_event( - &emitted_events[0], - None, - Some(accounts.alice), - 100.into(), - ); - // The second event `emitted_events[1]` is an Approve event that we skip - // checking. - assert_transfer_event( - &emitted_events[2], - Some(accounts.alice), - Some(accounts.eve), - 10.into(), - ); - } - - #[ink::test] - fn allowance_must_not_change_on_failed_transfer() { - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - let mut erc20 = Erc20::new(100.into()); - - // Alice approves Bob for token transfers on her behalf. - let alice_balance = erc20.balance_of(accounts.alice); - let initial_allowance = alice_balance + 2; - assert_eq!(erc20.approve(accounts.bob, initial_allowance), Ok(())); - - // Get contract address. - let callee = ink::env::address(); - ink::env::test::set_callee(callee); - ink::env::test::set_caller(accounts.bob); - - // Bob tries to transfer tokens from Alice to Eve. - let emitted_events_before = ink::env::test::recorded_events().len(); - assert_eq!( - erc20.transfer_from( - accounts.alice, - accounts.eve, - alice_balance + U256::from(1) - ), - Err(Error::InsufficientBalance) - ); - // Allowance must have stayed the same - assert_eq!( - erc20.allowance(accounts.alice, accounts.bob), - initial_allowance - ); - // No more events must have been emitted - assert_eq!( - emitted_events_before, - ink::env::test::recorded_events().len() - ) - } - - fn encoded_into_hash(entity: T) -> Hash - where - T: ink::scale::Encode, - { - use ink::{ - env::hash::{ - Blake2x256, - CryptoHash, - HashOutput, - }, - primitives::Clear, - }; - - let mut result = Hash::CLEAR_HASH; - let len_result = result.as_ref().len(); - let encoded = entity.encode(); - let len_encoded = encoded.len(); - if len_encoded <= len_result { - result.as_mut()[..len_encoded].copy_from_slice(&encoded); - return result - } - let mut hash_output = - <::Type as Default>::default(); - ::hash(&encoded, &mut hash_output); - let copy_len = core::cmp::min(hash_output.len(), len_result); - result.as_mut()[0..copy_len].copy_from_slice(&hash_output[0..copy_len]); - result - } - } - - #[cfg(all(test, feature = "e2e-tests"))] - mod e2e_tests { - use super::*; - use ink_e2e::ContractsBackend; - - type E2EResult = std::result::Result>; - - #[ink_e2e::test] - async fn e2e_transfer(mut client: Client) -> E2EResult<()> { - // given - let total_supply = U256::from(1_000_000_000); - let mut constructor = Erc20Ref::new(total_supply); - let erc20 = client - .instantiate("erc20", &ink_e2e::alice(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = erc20.call_builder::(); - - // when - let total_supply_msg = call_builder.total_supply(); - let total_supply_res = client - .call(&ink_e2e::bob(), &total_supply_msg) - .dry_run() - .await?; - - let bob_account = ink_e2e::address::( - ink_e2e::Sr25519Keyring::Bob, - ); - let transfer_to_bob = U256::from(500_000_000); - let transfer = call_builder.transfer(bob_account, transfer_to_bob); - let _transfer_res = client - .call(&ink_e2e::alice(), &transfer) - .submit() - .await - .expect("transfer failed"); - - let balance_of = call_builder.balance_of(bob_account); - let balance_of_res = client - .call(&ink_e2e::alice(), &balance_of) - .dry_run() - .await?; - - // then - assert_eq!( - total_supply, - total_supply_res.return_value(), - "total_supply" - ); - assert_eq!(transfer_to_bob, balance_of_res.return_value(), "balance_of"); - - Ok(()) - } - - #[ink_e2e::test] - async fn e2e_allowances(mut client: Client) -> E2EResult<()> { - // given - let total_supply = U256::from(1_000_000_000); - let mut constructor = Erc20Ref::new(total_supply); - let erc20 = client - .instantiate("erc20", &ink_e2e::bob(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = erc20.call_builder::(); - - // when - - let bob_account = ink_e2e::address::( - ink_e2e::Sr25519Keyring::Bob, - ); - let charlie_account = ink_e2e::address::( - ink_e2e::Sr25519Keyring::Charlie, - ); - - let amount = U256::from(500_000_000); - // tx - let transfer_from = - call_builder.transfer_from(bob_account, charlie_account, amount); - let transfer_from_result = client - .call(&ink_e2e::charlie(), &transfer_from) - .submit() - .await; - - assert!( - transfer_from_result.is_err(), - "unapproved transfer_from should fail" - ); - - // Bob approves Charlie to transfer up to amount on his behalf - let approved_value = U256::from(1_000); - let approve_call = call_builder.approve(charlie_account, approved_value); - client - .call(&ink_e2e::bob(), &approve_call) - .submit() - .await - .expect("approve failed"); - - // `transfer_from` the approved amount - let transfer_from = - call_builder.transfer_from(bob_account, charlie_account, approved_value); - let transfer_from_result = client - .call(&ink_e2e::charlie(), &transfer_from) - .submit() - .await; - assert!( - transfer_from_result.is_ok(), - "approved transfer_from should succeed" - ); - - let balance_of = call_builder.balance_of(bob_account); - let balance_of_res = client - .call(&ink_e2e::alice(), &balance_of) - .dry_run() - .await?; - - // `transfer_from` again, this time exceeding the approved amount - let transfer_from = - call_builder.transfer_from(bob_account, charlie_account, 1.into()); - let transfer_from_result = client - .call(&ink_e2e::charlie(), &transfer_from) - .submit() - .await; - assert!( - transfer_from_result.is_err(), - "transfer_from exceeding the approved amount should fail" - ); - - assert_eq!( - total_supply - approved_value, - balance_of_res.return_value(), - "balance_of" - ); - - Ok(()) - } - } -} diff --git a/integration-tests/public/erc721/lib.rs b/integration-tests/public/erc721/lib.rs deleted file mode 100644 index dfa0b5ea602..00000000000 --- a/integration-tests/public/erc721/lib.rs +++ /dev/null @@ -1,698 +0,0 @@ -//! # ERC-721 -//! -//! This is an ERC-721 Token implementation. -//! -//! ## Warning -//! -//! This contract is an *example*. It is neither audited nor endorsed for production use. -//! Do **not** rely on it to keep anything of value secure. -//! -//! ## Overview -//! -//! This contract demonstrates how to build non-fungible or unique tokens using ink!. -//! -//! ## Error Handling -//! -//! Any function that modifies the state returns a `Result` type and does not changes the -//! state if the `Error` occurs. -//! The errors are defined as an `enum` type. Any other error or invariant violation -//! triggers a panic and therefore rolls back the transaction. -//! -//! ## Token Management -//! -//! After creating a new token, the function caller becomes the owner. -//! A token can be created, transferred, or destroyed. -//! -//! Token owners can assign other accounts for transferring specific tokens on their -//! behalf. It is also possible to authorize an operator (higher rights) for another -//! account to handle tokens. -//! -//! ### Token Creation -//! -//! Token creation start by calling the `mint(&mut self, id: u32)` function. -//! The token owner becomes the function caller. The Token ID needs to be specified -//! as the argument on this function call. -//! -//! ### Token Transfer -//! -//! Transfers may be initiated by: -//! - The owner of a token -//! - The approved address of a token -//! - An authorized operator of the current owner of a token -//! -//! The token owner can transfer a token by calling the `transfer` or `transfer_from` -//! functions. An approved address can make a token transfer by calling the -//! `transfer_from` function. Operators can transfer tokens on another account's behalf or -//! can approve a token transfer for a different account. -//! -//! ### Token Removal -//! -//! Tokens can be destroyed by burning them. Only the token owner is allowed to burn a -//! token. - -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -#[ink::contract] -mod erc721 { - use ink::storage::Mapping; - - /// A token ID. - pub type TokenId = u32; - - #[ink(storage)] - #[derive(Default)] - pub struct Erc721 { - /// Mapping from token to owner. - token_owner: Mapping, - /// Mapping from token to approvals users. - token_approvals: Mapping, - /// Mapping from owner to number of owned token. - owned_tokens_count: Mapping, - /// Mapping from owner to operator approvals. - operator_approvals: Mapping<(Address, Address), ()>, - } - - #[derive(Debug, PartialEq, Eq, Copy, Clone)] - #[ink::error] - pub enum Error { - NotOwner, - NotApproved, - TokenExists, - TokenNotFound, - CannotInsert, - CannotFetchValue, - NotAllowed, - } - - /// Event emitted when a token transfer occurs. - #[ink(event)] - pub struct Transfer { - #[ink(topic)] - from: Option
, - #[ink(topic)] - to: Option
, - #[ink(topic)] - id: TokenId, - } - - /// Event emitted when a token approve occurs. - #[ink(event)] - pub struct Approval { - #[ink(topic)] - from: Address, - #[ink(topic)] - to: Address, - #[ink(topic)] - id: TokenId, - } - - /// Event emitted when an operator is enabled or disabled for an owner. - /// The operator can manage all NFTs of the owner. - #[ink(event)] - pub struct ApprovalForAll { - #[ink(topic)] - owner: Address, - #[ink(topic)] - operator: Address, - approved: bool, - } - - impl Erc721 { - /// Creates a new ERC-721 token contract. - #[ink(constructor)] - pub fn new() -> Self { - Default::default() - } - - /// Returns the balance of the owner. - /// - /// This represents the amount of unique tokens the owner has. - #[ink(message)] - pub fn balance_of(&self, owner: Address) -> u32 { - self.balance_of_or_zero(&owner) - } - - /// Returns the owner of the token. - #[ink(message)] - pub fn owner_of(&self, id: TokenId) -> Option
{ - self.token_owner.get(id) - } - - /// Returns the approved account ID for this token if any. - #[ink(message)] - pub fn get_approved(&self, id: TokenId) -> Option
{ - self.token_approvals.get(id) - } - - /// Returns `true` if the operator is approved by the owner. - #[ink(message)] - pub fn is_approved_for_all(&self, owner: Address, operator: Address) -> bool { - self.approved_for_all(owner, operator) - } - - /// Approves or disapproves the operator for all tokens of the caller. - #[ink(message)] - pub fn set_approval_for_all( - &mut self, - to: Address, - approved: bool, - ) -> Result<(), Error> { - self.approve_for_all(to, approved)?; - Ok(()) - } - - /// Approves the account to transfer the specified token on behalf of the caller. - #[ink(message)] - pub fn approve(&mut self, to: Address, id: TokenId) -> Result<(), Error> { - self.approve_for(&to, id)?; - Ok(()) - } - - /// Transfers the token from the caller to the given destination. - #[ink(message)] - pub fn transfer( - &mut self, - destination: Address, - id: TokenId, - ) -> Result<(), Error> { - let caller = self.env().caller(); - self.transfer_token_from(&caller, &destination, id)?; - Ok(()) - } - - /// Transfer approved or owned token. - #[ink(message)] - pub fn transfer_from( - &mut self, - from: Address, - to: Address, - id: TokenId, - ) -> Result<(), Error> { - self.transfer_token_from(&from, &to, id)?; - Ok(()) - } - - /// Creates a new token. - #[ink(message)] - pub fn mint(&mut self, id: TokenId) -> Result<(), Error> { - let caller = self.env().caller(); - self.add_token_to(&caller, id)?; - self.env().emit_event(Transfer { - from: Some(Address::from([0x0; 20])), - to: Some(caller), - id, - }); - Ok(()) - } - - /// Deletes an existing token. Only the owner can burn the token. - #[ink(message)] - pub fn burn(&mut self, id: TokenId) -> Result<(), Error> { - let caller = self.env().caller(); - let Self { - token_owner, - owned_tokens_count, - .. - } = self; - - let owner = token_owner.get(id).ok_or(Error::TokenNotFound)?; - if owner != caller { - return Err(Error::NotOwner); - }; - - let count = owned_tokens_count - .get(caller) - .map(|c| c.checked_sub(1).unwrap()) - .ok_or(Error::CannotFetchValue)?; - owned_tokens_count.insert(caller, &count); - token_owner.remove(id); - self.clear_approval(id); - - self.env().emit_event(Transfer { - from: Some(caller), - to: Some(Address::from([0x0; 20])), - id, - }); - - Ok(()) - } - - /// Transfers token `id` `from` the sender to the `to` `Address`. - fn transfer_token_from( - &mut self, - from: &Address, - to: &Address, - id: TokenId, - ) -> Result<(), Error> { - let caller = self.env().caller(); - let owner = self.owner_of(id).ok_or(Error::TokenNotFound)?; - if !self.approved_or_owner(caller, id, owner) { - return Err(Error::NotApproved); - }; - if owner != *from { - return Err(Error::NotOwner); - }; - self.clear_approval(id); - self.remove_token_from(from, id)?; - self.add_token_to(to, id)?; - self.env().emit_event(Transfer { - from: Some(*from), - to: Some(*to), - id, - }); - Ok(()) - } - - /// Removes token `id` from the owner. - fn remove_token_from( - &mut self, - from: &Address, - id: TokenId, - ) -> Result<(), Error> { - let Self { - token_owner, - owned_tokens_count, - .. - } = self; - - if !token_owner.contains(id) { - return Err(Error::TokenNotFound); - } - - let count = owned_tokens_count - .get(from) - .map(|c| c.checked_sub(1).unwrap()) - .ok_or(Error::CannotFetchValue)?; - owned_tokens_count.insert(from, &count); - token_owner.remove(id); - - Ok(()) - } - - /// Adds the token `id` to the `to` AccountID. - fn add_token_to(&mut self, to: &Address, id: TokenId) -> Result<(), Error> { - let Self { - token_owner, - owned_tokens_count, - .. - } = self; - - if token_owner.contains(id) { - return Err(Error::TokenExists); - } - - if *to == Address::from([0x0; 20]) { - return Err(Error::NotAllowed); - }; - - let count = owned_tokens_count - .get(to) - .map(|c| c.checked_add(1).unwrap()) - .unwrap_or(1); - - owned_tokens_count.insert(to, &count); - token_owner.insert(id, to); - - Ok(()) - } - - /// Approves or disapproves the operator to transfer all tokens of the caller. - fn approve_for_all(&mut self, to: Address, approved: bool) -> Result<(), Error> { - let caller = self.env().caller(); - if to == caller { - return Err(Error::NotAllowed); - } - self.env().emit_event(ApprovalForAll { - owner: caller, - operator: to, - approved, - }); - - if approved { - self.operator_approvals.insert((&caller, &to), &()); - } else { - self.operator_approvals.remove((&caller, &to)); - } - - Ok(()) - } - - /// Approve the passed `Address` to transfer the specified token on behalf of - /// the message's sender. - fn approve_for(&mut self, to: &Address, id: TokenId) -> Result<(), Error> { - let caller = self.env().caller(); - let owner = self.owner_of(id).ok_or(Error::TokenNotFound)?; - if !(owner == caller || self.approved_for_all(owner, caller)) { - return Err(Error::NotAllowed); - }; - - if *to == Address::from([0x0; 20]) { - return Err(Error::NotAllowed); - }; - - if self.token_approvals.contains(id) { - return Err(Error::CannotInsert); - } else { - self.token_approvals.insert(id, to); - } - - self.env().emit_event(Approval { - from: caller, - to: *to, - id, - }); - - Ok(()) - } - - /// Removes existing approval from token `id`. - fn clear_approval(&mut self, id: TokenId) { - self.token_approvals.remove(id); - } - - // Returns the total number of tokens from an account. - fn balance_of_or_zero(&self, of: &Address) -> u32 { - self.owned_tokens_count.get(of).unwrap_or(0) - } - - /// Gets an operator on other Account's behalf. - fn approved_for_all(&self, owner: Address, operator: Address) -> bool { - self.operator_approvals.contains((&owner, &operator)) - } - - /// Returns true if the `Address` `from` is the owner of token `id` - /// or it has been approved on behalf of the token `id` owner. - fn approved_or_owner(&self, from: Address, id: TokenId, owner: Address) -> bool { - from != Address::from([0x0; 20]) - && (from == owner - || self.token_approvals.get(id) == Some(from) - || self.approved_for_all(owner, from)) - } - } - - /// Unit tests - #[cfg(test)] - mod tests { - /// Imports all the definitions from the outer scope so we can use them here. - use super::*; - - #[ink::test] - fn mint_works() { - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - // Create a new contract instance. - let mut erc721 = Erc721::new(); - // Token 1 does not exists. - assert_eq!(erc721.owner_of(1), None); - // Alice does not owns tokens. - assert_eq!(erc721.balance_of(accounts.alice), 0); - // Create token Id 1. - assert_eq!(erc721.mint(1), Ok(())); - // Alice owns 1 token. - assert_eq!(erc721.balance_of(accounts.alice), 1); - } - - #[ink::test] - fn mint_existing_should_fail() { - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - // Create a new contract instance. - let mut erc721 = Erc721::new(); - // Create token Id 1. - assert_eq!(erc721.mint(1), Ok(())); - // The first Transfer event takes place - assert_eq!(1, ink::env::test::recorded_events().len()); - // Alice owns 1 token. - assert_eq!(erc721.balance_of(accounts.alice), 1); - // Alice owns token Id 1. - assert_eq!(erc721.owner_of(1), Some(accounts.alice)); - // Cannot create token Id if it exists. - // Bob cannot own token Id 1. - assert_eq!(erc721.mint(1), Err(Error::TokenExists)); - } - - #[ink::test] - fn transfer_works() { - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - // Create a new contract instance. - let mut erc721 = Erc721::new(); - // Create token Id 1 for Alice - assert_eq!(erc721.mint(1), Ok(())); - // Alice owns token 1 - assert_eq!(erc721.balance_of(accounts.alice), 1); - // Bob does not owns any token - assert_eq!(erc721.balance_of(accounts.bob), 0); - // The first Transfer event takes place - assert_eq!(1, ink::env::test::recorded_events().len()); - // Alice transfers token 1 to Bob - assert_eq!(erc721.transfer(accounts.bob, 1), Ok(())); - // The second Transfer event takes place - assert_eq!(2, ink::env::test::recorded_events().len()); - // Bob owns token 1 - assert_eq!(erc721.balance_of(accounts.bob), 1); - } - - #[ink::test] - fn invalid_transfer_should_fail() { - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - // Create a new contract instance. - let mut erc721 = Erc721::new(); - // Transfer token fails if it does not exists. - assert_eq!(erc721.transfer(accounts.bob, 2), Err(Error::TokenNotFound)); - // Token Id 2 does not exists. - assert_eq!(erc721.owner_of(2), None); - // Create token Id 2. - assert_eq!(erc721.mint(2), Ok(())); - // Alice owns 1 token. - assert_eq!(erc721.balance_of(accounts.alice), 1); - // Token Id 2 is owned by Alice. - assert_eq!(erc721.owner_of(2), Some(accounts.alice)); - // Set Bob as caller - set_caller(accounts.bob); - // Bob cannot transfer not owned tokens. - assert_eq!(erc721.transfer(accounts.eve, 2), Err(Error::NotApproved)); - } - - #[ink::test] - fn approved_transfer_works() { - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - // Create a new contract instance. - let mut erc721 = Erc721::new(); - // Create token Id 1. - assert_eq!(erc721.mint(1), Ok(())); - // Token Id 1 is owned by Alice. - assert_eq!(erc721.owner_of(1), Some(accounts.alice)); - // Approve token Id 1 transfer for Bob on behalf of Alice. - assert_eq!(erc721.approve(accounts.bob, 1), Ok(())); - // Set Bob as caller - set_caller(accounts.bob); - // Bob transfers token Id 1 from Alice to Eve. - assert_eq!( - erc721.transfer_from(accounts.alice, accounts.eve, 1), - Ok(()) - ); - // TokenId 3 is owned by Eve. - assert_eq!(erc721.owner_of(1), Some(accounts.eve)); - // Alice does not owns tokens. - assert_eq!(erc721.balance_of(accounts.alice), 0); - // Bob does not owns tokens. - assert_eq!(erc721.balance_of(accounts.bob), 0); - // Eve owns 1 token. - assert_eq!(erc721.balance_of(accounts.eve), 1); - } - - #[ink::test] - fn approved_for_all_works() { - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - // Create a new contract instance. - let mut erc721 = Erc721::new(); - // Create token Id 1. - assert_eq!(erc721.mint(1), Ok(())); - // Create token Id 2. - assert_eq!(erc721.mint(2), Ok(())); - // Alice owns 2 tokens. - assert_eq!(erc721.balance_of(accounts.alice), 2); - // Approve token Id 1 transfer for Bob on behalf of Alice. - assert_eq!(erc721.set_approval_for_all(accounts.bob, true), Ok(())); - // Bob is an approved operator for Alice - assert!(erc721.is_approved_for_all(accounts.alice, accounts.bob)); - // Set Bob as caller - set_caller(accounts.bob); - // Bob transfers token Id 1 from Alice to Eve. - assert_eq!( - erc721.transfer_from(accounts.alice, accounts.eve, 1), - Ok(()) - ); - // TokenId 1 is owned by Eve. - assert_eq!(erc721.owner_of(1), Some(accounts.eve)); - // Alice owns 1 token. - assert_eq!(erc721.balance_of(accounts.alice), 1); - // Bob transfers token Id 2 from Alice to Eve. - assert_eq!( - erc721.transfer_from(accounts.alice, accounts.eve, 2), - Ok(()) - ); - // Bob does not own tokens. - assert_eq!(erc721.balance_of(accounts.bob), 0); - // Eve owns 2 tokens. - assert_eq!(erc721.balance_of(accounts.eve), 2); - // Remove operator approval for Bob on behalf of Alice. - set_caller(accounts.alice); - assert_eq!(erc721.set_approval_for_all(accounts.bob, false), Ok(())); - // Bob is not an approved operator for Alice. - assert!(!erc721.is_approved_for_all(accounts.alice, accounts.bob)); - } - - #[ink::test] - fn approve_nonexistent_token_should_fail() { - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - // Create a new contract instance. - let mut erc721 = Erc721::new(); - // Approve transfer of nonexistent token id 1 - assert_eq!(erc721.approve(accounts.bob, 1), Err(Error::TokenNotFound)); - } - - #[ink::test] - fn not_approved_transfer_should_fail() { - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - // Create a new contract instance. - let mut erc721 = Erc721::new(); - // Create token Id 1. - assert_eq!(erc721.mint(1), Ok(())); - // Alice owns 1 token. - assert_eq!(erc721.balance_of(accounts.alice), 1); - // Bob does not owns tokens. - assert_eq!(erc721.balance_of(accounts.bob), 0); - // Eve does not owns tokens. - assert_eq!(erc721.balance_of(accounts.eve), 0); - // Set Eve as caller - set_caller(accounts.eve); - // Eve is not an approved operator by Alice. - assert_eq!( - erc721.transfer_from(accounts.alice, accounts.frank, 1), - Err(Error::NotApproved) - ); - // Alice owns 1 token. - assert_eq!(erc721.balance_of(accounts.alice), 1); - // Bob does not owns tokens. - assert_eq!(erc721.balance_of(accounts.bob), 0); - // Eve does not owns tokens. - assert_eq!(erc721.balance_of(accounts.eve), 0); - } - - #[ink::test] - fn burn_works() { - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - // Create a new contract instance. - let mut erc721 = Erc721::new(); - // Create token Id 1 for Alice - assert_eq!(erc721.mint(1), Ok(())); - // Alice owns 1 token. - assert_eq!(erc721.balance_of(accounts.alice), 1); - // Alice owns token Id 1. - assert_eq!(erc721.owner_of(1), Some(accounts.alice)); - // Destroy token Id 1. - assert_eq!(erc721.burn(1), Ok(())); - // Alice does not owns tokens. - assert_eq!(erc721.balance_of(accounts.alice), 0); - // Token Id 1 does not exists - assert_eq!(erc721.owner_of(1), None); - } - - #[ink::test] - fn burn_fails_token_not_found() { - // Create a new contract instance. - let mut erc721 = Erc721::new(); - // Try burning a non existent token - assert_eq!(erc721.burn(1), Err(Error::TokenNotFound)); - } - - #[ink::test] - fn burn_fails_not_owner() { - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - // Create a new contract instance. - let mut erc721 = Erc721::new(); - // Create token Id 1 for Alice - assert_eq!(erc721.mint(1), Ok(())); - // Try burning this token with a different account - set_caller(accounts.eve); - assert_eq!(erc721.burn(1), Err(Error::NotOwner)); - } - - #[ink::test] - fn burn_clears_approval() { - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - // Create a new contract instance. - let mut erc721 = Erc721::new(); - // Create token Id 1 for Alice - assert_eq!(erc721.mint(1), Ok(())); - // Alice gives approval to Bob to transfer token Id 1 - assert_eq!(erc721.approve(accounts.bob, 1), Ok(())); - // Alice burns token - assert_eq!(erc721.burn(1), Ok(())); - // Set caller to Frank - set_caller(accounts.frank); - // Frank mints token Id 1 - assert_eq!(erc721.mint(1), Ok(())); - // Set caller to Bob - set_caller(accounts.bob); - // Bob tries to transfer token Id 1 from Frank to himself - assert_eq!( - erc721.transfer_from(accounts.frank, accounts.bob, 1), - Err(Error::NotApproved) - ); - } - - #[ink::test] - fn transfer_from_fails_not_owner() { - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - // Create a new contract instance. - let mut erc721 = Erc721::new(); - // Create token Id 1 for Alice - assert_eq!(erc721.mint(1), Ok(())); - // Bob can transfer alice's tokens - assert_eq!(erc721.set_approval_for_all(accounts.bob, true), Ok(())); - // Set caller to Frank - set_caller(accounts.frank); - // Create token Id 2 for Frank - assert_eq!(erc721.mint(2), Ok(())); - // Set caller to Bob - set_caller(accounts.bob); - // Bob makes invalid call to transfer_from (Alice is token owner, not Frank) - assert_eq!( - erc721.transfer_from(accounts.frank, accounts.bob, 1), - Err(Error::NotOwner) - ); - } - - #[ink::test] - fn transfer_fails_not_owner() { - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - // Create a new contract instance. - let mut erc721 = Erc721::new(); - // Create token Id 1 for Alice - assert_eq!(erc721.mint(1), Ok(())); - // Bob can transfer alice's tokens - assert_eq!(erc721.set_approval_for_all(accounts.bob, true), Ok(())); - // Set caller to bob - set_caller(accounts.bob); - // Bob makes invalid call to transfer (he is not token owner, Alice is) - assert_eq!(erc721.transfer(accounts.bob, 1), Err(Error::NotOwner)); - } - - fn set_caller(sender: Address) { - ink::env::test::set_caller(sender); - } - } -} diff --git a/integration-tests/public/fallible-setter/lib.rs b/integration-tests/public/fallible-setter/lib.rs deleted file mode 100644 index e32c487a4a4..00000000000 --- a/integration-tests/public/fallible-setter/lib.rs +++ /dev/null @@ -1,134 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -#[ink::error] -#[derive(Debug, PartialEq, Eq)] -/// Equivalent to multiple Solidity custom errors, one for each variant. -pub enum Error { - /// Error when `value > 100` - TooLarge, - /// Error when `value == self.value` - NoChange, -} - -#[ink::contract] -pub mod fallible_setter { - use super::Error; - - #[ink(storage)] - pub struct FallibleSetter { - value: u8, - } - - impl FallibleSetter { - /// Creates a new fallible setter smart contract initialized with the given value. - /// Returns an error if `init_value > 100`. - #[ink(constructor)] - pub fn new(init_value: u8) -> Result { - if init_value > 100 { - return Err(Error::TooLarge) - } - Ok(Self { value: init_value }) - } - - /// Sets the value of the FallibleSetter's `u8`. - /// Returns an appropriate error if any of the following is true: - /// - `value == self.value` - /// - `init_value > 100` - #[ink(message)] - pub fn try_set(&mut self, value: u8) -> Result<(), Error> { - if self.value == value { - return Err(Error::NoChange); - } - - if value > 100 { - return Err(Error::TooLarge); - } - - self.value = value; - Ok(()) - } - - /// Returns the current value of the FallibleSetter's `u8`. - #[ink(message)] - pub fn get(&self) -> u8 { - self.value - } - } - - #[cfg(test)] - mod tests { - use super::*; - - #[ink::test] - fn it_works() { - // given - let mut fallible_setter = FallibleSetter::new(0).expect("init failed"); - assert_eq!(fallible_setter.get(), 0); - - // when - let res = fallible_setter.try_set(1); - assert!(res.is_ok()); - - // when - let res = fallible_setter.try_set(1); - assert_eq!(res, Err(Error::NoChange)); - - // when - let res = fallible_setter.try_set(101); - assert_eq!(res, Err(Error::TooLarge)); - - // then - assert_eq!(fallible_setter.get(), 1); - } - } - - #[cfg(all(test, feature = "e2e-tests"))] - mod e2e_tests { - use super::*; - use ink_e2e::ContractsBackend; - - type E2EResult = std::result::Result>; - - #[ink_e2e::test] - async fn it_works(mut client: Client) -> E2EResult<()> { - // given - let mut constructor = FallibleSetterRef::new(0); - let contract = client - .instantiate("fallible_setter", &ink_e2e::bob(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); - - let get = call_builder.get(); - let get_res = client.call(&ink_e2e::bob(), &get).submit().await?; - assert_eq!(get_res.return_value(), 0); - - // when - let set = call_builder.try_set(1); - let set_res = client - .call(&ink_e2e::bob(), &set) - .submit() - .await - .expect("set failed"); - assert!(set_res.return_value().is_ok()); - - // when - let set = call_builder.try_set(1); - let set_res = client.call(&ink_e2e::bob(), &set).submit().await; - assert!(matches!(set_res, Err(ink_e2e::Error::CallExtrinsic(_, _)))); - - // when - let set = call_builder.try_set(101); - let set_res = client.call(&ink_e2e::bob(), &set).submit().await; - assert!(matches!(set_res, Err(ink_e2e::Error::CallExtrinsic(_, _)))); - - // then - let get = call_builder.get(); - let get_res = client.call(&ink_e2e::bob(), &get).dry_run().await?; - assert_eq!(get_res.return_value(), 1); - - Ok(()) - } - } -} diff --git a/integration-tests/public/flipper/lib.rs b/integration-tests/public/flipper/lib.rs deleted file mode 100644 index 9ab3412ed19..00000000000 --- a/integration-tests/public/flipper/lib.rs +++ /dev/null @@ -1,135 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -#[ink::contract] -pub mod flipper { - #[ink(storage)] - pub struct Flipper { - value: bool, - } - - impl Flipper { - /// Creates a new flipper smart contract initialized with the given value. - #[ink(constructor)] - pub fn new(init_value: bool) -> Self { - Self { value: init_value } - } - - /// Flips the current value of the Flipper's boolean. - #[ink(message)] - pub fn flip(&mut self) { - self.value = !self.value; - } - - /// Returns the current value of the Flipper's boolean. - #[ink(message)] - pub fn get(&self) -> bool { - self.value - } - } - - #[cfg(test)] - mod tests { - use super::*; - - #[ink::test] - fn it_works() { - let mut flipper = Flipper::new(false); - assert!(!flipper.get()); - flipper.flip(); - assert!(flipper.get()); - } - } - - #[cfg(all(test, feature = "e2e-tests"))] - mod e2e_tests { - use super::*; - use ink_e2e::ContractsBackend; - - type E2EResult = std::result::Result>; - - #[ink_e2e::test] - async fn it_works(mut client: Client) -> E2EResult<()> { - // given - let mut constructor = FlipperRef::new(false); - let contract = client - .instantiate("flipper", &ink_e2e::bob(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); - - let get = call_builder.get(); - let get_res = client.call(&ink_e2e::bob(), &get).submit().await?; - assert!(!get_res.return_value()); - - // when - let flip = call_builder.flip(); - let _flip_res = client - .call(&ink_e2e::bob(), &flip) - .submit() - .await - .expect("flip failed"); - - // then - let get = call_builder.get(); - let get_res = client.call(&ink_e2e::bob(), &get).dry_run().await?; - assert!(get_res.return_value()); - - Ok(()) - } - - /// This test illustrates how to test an existing on-chain contract. - /// - /// You can utilize this to e.g. create a snapshot of a production chain - /// and run the E2E tests against a deployed contract there. - /// This process is explained [here](https://use.ink/5.x/basics/contract-testing/chain-snapshot). - /// - /// Before executing the test: - /// * Make sure you have a node running in the background, - /// * Supply the environment variable `CONTRACT_HEX` that points to a deployed - /// flipper contract. You can take the SS58 address which `cargo contract - /// instantiate` gives you and convert it to hex using `subkey inspect - /// `. - /// - /// The test is then run like this: - /// - /// ``` - /// # The env variable needs to be set, otherwise `ink_e2e` will spawn a new - /// # node process for each test. - /// $ export CONTRACTS_NODE_URL=ws://127.0.0.1:9944 - /// - /// $ export CONTRACT_ADDR_HEX=0x2c75f0aa09dbfbfd49e6286a0f2edd3b4913f04a58b13391c79e96782f5713e3 - /// $ cargo test --features e2e-tests e2e_test_deployed_contract -- --ignored - /// ``` - /// - /// # Developer Note - /// - /// The test is marked as ignored, as it has the above pre-conditions to succeed. - #[ink_e2e::test] - #[ignore] - async fn e2e_test_deployed_contract(mut client: Client) -> E2EResult<()> { - // given - use ink::Address; - let addr = std::env::var("CONTRACT_ADDR_HEX") - .unwrap() - .replace("0x", ""); - let addr_bytes: Vec = hex::decode(addr).unwrap(); - let addr = Address::from_slice(&addr_bytes[..]); - - use std::str::FromStr; - let suri = ink_e2e::subxt_signer::SecretUri::from_str("//Alice").unwrap(); - let caller = ink_e2e::Keypair::from_uri(&suri).unwrap(); - - // when - // Invoke `Flipper::get()` from `caller`'s account - let call_builder = ink_e2e::create_call_builder::(addr); - let get = call_builder.get(); - let get_res = client.call(&caller, &get).dry_run().await?; - - // then - assert!(get_res.return_value()); - - Ok(()) - } - } -} diff --git a/integration-tests/public/fuzz-testing/lib.rs b/integration-tests/public/fuzz-testing/lib.rs deleted file mode 100644 index ab57f64b416..00000000000 --- a/integration-tests/public/fuzz-testing/lib.rs +++ /dev/null @@ -1,119 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -#[ink::contract] -pub mod fuzz_testing { - #[ink(storage)] - pub struct FuzzTesting { - value: bool, - } - - //#[derive(PartialEq, Eq, Debug, Clone)] - //#[cfg_attr(feature = "std", derive(ink::storage::traits::StorageLayout))] - #[derive(Clone, Debug)] - #[ink::scale_derive(Encode, Decode, TypeInfo)] - pub struct Point { - x: i32, - y: i32, - } - - impl FuzzTesting { - /// Creates a new contract initialized with the given value. - #[ink(constructor)] - pub fn new(init_value: bool) -> Self { - Self { value: init_value } - } - - /// Returns the current value. - #[ink(message)] - pub fn get(&self) -> bool { - self.value - } - - /// Extracts `Point.x`. - #[ink(message)] - pub fn extract_x(&self, pt: Point) -> i32 { - pt.x - } - } - - #[cfg(all(test, feature = "e2e-tests"))] - mod e2e_tests { - use super::*; - use ink_e2e::ContractsBackend; - use quickcheck_macros::quickcheck; - - /// We use `#[ink_e2e::test(runtime)]` here. It doesn't start a node for each - /// test, but instead interacts with an in-process `pallet-revive`. - /// - /// See - /// for more details. - #[ink_e2e::test(runtime, replace_test_attr = "#[quickcheck]")] - async fn fuzzing_works_runtime(val: bool) -> bool { - let mut constructor = FuzzTestingRef::new(val); - let contract = client - .instantiate("fuzz_testing", &ink_e2e::bob(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let call_builder = contract.call_builder::(); - - let get = call_builder.get(); - let get_res = client.call(&ink_e2e::bob(), &get).submit().await.unwrap(); - get_res.return_value() == val - } - - /// It's also possible to fuzz with a "real" node as the backend. - /// - /// This means that, by default, for every test run a node process will - /// be spawned. You can work around this by setting the env variable - /// `CONTRACTS_NODE_URL`. But still, interactions with a real node will - /// always be more heavy-weight than "just" interacting with an in-process - /// `pallet-revive`. - #[ink_e2e::test(runtime, replace_test_attr = "#[quickcheck]")] - async fn fuzzing_works_node(val: bool) -> bool { - let mut constructor = FuzzTestingRef::new(val); - let contract = client - .instantiate("fuzz_testing", &ink_e2e::bob(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let call_builder = contract.call_builder::(); - - let get = call_builder.get(); - let get_res = client.call(&ink_e2e::bob(), &get).submit().await.unwrap(); - get_res.return_value() == val - } - - // We need to implement `Arbitrary` for `Point`, so `quickcheck` - // knows how to fuzz the struct. - use quickcheck::{ - Arbitrary, - Gen, - }; - impl Arbitrary for Point { - fn arbitrary(g: &mut Gen) -> Point { - Point { - x: i32::arbitrary(g), - y: i32::arbitrary(g), - } - } - } - - #[ink_e2e::test(runtime, replace_test_attr = "#[quickcheck]")] - async fn fuzzing_custom_struct_works(val: Point) -> bool { - ink_e2e::tracing::info!("fuzzing with value {val:?}"); - - let mut constructor = FuzzTestingRef::new(true); - let contract = client - .instantiate("fuzz_testing", &ink_e2e::bob(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let call_builder = contract.call_builder::(); - - let get = call_builder.extract_x(val.clone()); - let get_res = client.call(&ink_e2e::bob(), &get).submit().await.unwrap(); - get_res.return_value() == val.x - } - } -} diff --git a/integration-tests/public/bytes/Cargo.toml b/integration-tests/public/misc/bytes/Cargo.toml similarity index 100% rename from integration-tests/public/bytes/Cargo.toml rename to integration-tests/public/misc/bytes/Cargo.toml diff --git a/integration-tests/public/misc/bytes/lib.rs b/integration-tests/public/misc/bytes/lib.rs new file mode 100644 index 00000000000..93e97901cfc --- /dev/null +++ b/integration-tests/public/misc/bytes/lib.rs @@ -0,0 +1,63 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +/// Example for using bytes wrapper types (i.e. `ink::sol::FixedBytes` and +/// `ink::sol::DynBytes`) as message and event arguments. +/// +/// # Note +/// +/// In Solidity ABI encoding, `uint8[]` and `uint8[N]` are encoded differently from +/// `bytes` and `bytesN`. In Rust/ink!, `Vec` and `[u8; N]` are mapped to Solidity's +/// `uint8[]` and `uint8[N]` representations, so there's a need for dedicated Rust/ink! +/// types (i.e. `ink::sol::DynBytes` and `ink::sol::FixedBytes`) that map to Solidity's +/// `bytes` and `bytesN` representations. +/// +/// # References +/// +/// - +/// - +/// - + +#[ink::event] +pub struct FixedBytesPayload { + pub data: ink::sol::FixedBytes<8>, +} + +#[ink::event] +pub struct DynBytesPayload { + pub data: ink::sol::DynBytes, +} + +#[ink::contract] +pub mod bytes { + use super::{ + DynBytesPayload, + FixedBytesPayload, + }; + + #[ink(storage)] + pub struct Bytes; + + impl Bytes { + /// Creates a new smart contract. + #[ink(constructor)] + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self {} + } + + /// Handles fixed-size byte arrays. + #[ink(message)] + pub fn handle_fixed_bytes(&mut self, data: ink::sol::FixedBytes<8>) { + self.env().emit_event(FixedBytesPayload { data }) + } + + /// Handles dynamic size byte arrays. + #[ink(message)] + pub fn handle_dyn_bytes(&mut self, data: ink::sol::DynBytes) { + self.env().emit_event(DynBytesPayload { data }) + } + } + + #[cfg(test)] + mod tests; +} \ No newline at end of file diff --git a/integration-tests/public/misc/bytes/tests.rs b/integration-tests/public/misc/bytes/tests.rs new file mode 100644 index 00000000000..615bdac9a25 --- /dev/null +++ b/integration-tests/public/misc/bytes/tests.rs @@ -0,0 +1,150 @@ +use super::*; + +#[ink::test] +fn fixed_bytes_works() { + // given + let mut bytes = Bytes::new(); + + // when + let fixed_bytes = + ink::sol::FixedBytes::from([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]); + bytes.handle_fixed_bytes(fixed_bytes); + + // then + let emitted_events = ink::env::test::recorded_events(); + assert_eq!(1, emitted_events.len()); + + // then + let event = &emitted_events[0]; + let mut encoded = vec![0x0; 32]; + encoded.as_mut_slice()[..8].copy_from_slice(fixed_bytes.as_slice()); + assert_eq!(encoded, event.data); + + // then + let decoded_data = + ink::sol::decode_sequence::<(ink::sol::FixedBytes<8>,)>(&event.data) + .expect("encountered invalid contract event data buffer"); + assert_eq!(decoded_data.0, fixed_bytes); +} + +#[ink::test] +fn dyn_bytes_works() { + // given + let mut bytes = Bytes::new(); + + // when + let dyn_bytes = ink::sol::DynBytes::from(vec![0x1, 0x2, 0x3, 0x4]); + bytes.handle_dyn_bytes(dyn_bytes.clone()); + + // then + let emitted_events = ink::env::test::recorded_events(); + assert_eq!(1, emitted_events.len()); + + // then + let event = &emitted_events[0]; + let mut encoded = vec![0x0; 96]; + encoded[31] = 32; // offset + encoded[63] = dyn_bytes.len() as u8; // length + encoded.as_mut_slice()[64..64 + dyn_bytes.len()] + .copy_from_slice(dyn_bytes.as_ref()); + assert_eq!(encoded, event.data); + + // then + let decoded_data = + ink::sol::decode_sequence::<(ink::sol::DynBytes,)>(&event.data) + .expect("encountered invalid contract event data buffer"); + assert_eq!(decoded_data.0, dyn_bytes); +} + +#[cfg(all(test, feature = "e2e-tests"))] +mod e2e_tests { + use super::*; + + use ink_e2e::ContractsBackend; + + type E2EResult = std::result::Result>; + + #[ink_e2e::test] + async fn fixed_bytes_works(mut client: Client) -> E2EResult<()> { + // given + let mut constructor = BytesRef::new(); + let contract = client + .instantiate("bytes", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + // when + let fixed_bytes = + ink::sol::FixedBytes::from([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]); + let handler = call_builder.handle_fixed_bytes(fixed_bytes); + let res = client + .call(&ink_e2e::bob(), &handler) + .submit() + .await + .expect("fixed bytes handler failed"); + + // then + let contract_events = res.contract_emitted_events()?; + assert_eq!(1, contract_events.len()); + + // then + let contract_event = &contract_events[0]; + let mut encoded = vec![0x0; 32]; + encoded.as_mut_slice()[..8].copy_from_slice(fixed_bytes.as_slice()); + assert_eq!(encoded, contract_event.event.data); + + // then + let decoded_data = ink::sol::decode_sequence::<(ink::sol::FixedBytes<8>,)>( + &contract_event.event.data, + ) + .expect("encountered invalid contract event data buffer"); + assert_eq!(decoded_data.0, fixed_bytes); + + Ok(()) + } + + #[ink_e2e::test] + async fn dyn_bytes_works(mut client: Client) -> E2EResult<()> { + // given + let mut constructor = BytesRef::new(); + let contract = client + .instantiate("bytes", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + // when + let dyn_bytes = ink::sol::DynBytes::from(vec![0x1, 0x2, 0x3, 0x4]); + let handler = call_builder.handle_dyn_bytes(dyn_bytes.clone()); + let res = client + .call(&ink_e2e::bob(), &handler) + .submit() + .await + .expect("dyn bytes handler failed"); + + // then + let contract_events = res.contract_emitted_events()?; + assert_eq!(1, contract_events.len()); + + // then + let contract_event = &contract_events[0]; + let mut encoded = vec![0x0; 96]; + encoded[31] = 32; // offset + encoded[63] = dyn_bytes.len() as u8; // length + encoded.as_mut_slice()[64..64 + dyn_bytes.len()] + .copy_from_slice(dyn_bytes.as_ref()); + assert_eq!(encoded, contract_event.event.data); + + // then + let decoded_data = ink::sol::decode_sequence::<(ink::sol::DynBytes,)>( + &contract_event.event.data, + ) + .expect("encountered invalid contract event data buffer"); + assert_eq!(decoded_data.0, dyn_bytes); + + Ok(()) + } +} \ No newline at end of file diff --git a/integration-tests/public/multisig/Cargo.toml b/integration-tests/public/misc/multisig/Cargo.toml old mode 100755 new mode 100644 similarity index 100% rename from integration-tests/public/multisig/Cargo.toml rename to integration-tests/public/misc/multisig/Cargo.toml diff --git a/integration-tests/public/multisig/lib.rs b/integration-tests/public/misc/multisig/lib.rs old mode 100755 new mode 100644 similarity index 59% rename from integration-tests/public/multisig/lib.rs rename to integration-tests/public/misc/multisig/lib.rs index 66b5721ed26..361a3ebfee3 --- a/integration-tests/public/multisig/lib.rs +++ b/integration-tests/public/misc/multisig/lib.rs @@ -61,7 +61,7 @@ pub use self::multisig::{ }; #[ink::contract] -mod multisig { +pub mod multisig { use ink::{ U256, env::{ @@ -145,11 +145,11 @@ mod multisig { #[ink::storage_item(packed)] pub struct Transactions { /// Just store all transaction ids packed. - transactions: Vec, + pub transactions: Vec, /// We just increment this whenever a new transaction is created. /// We never decrement or defragment. For now, the contract becomes defunct /// when the ids are exhausted. - next_id: TransactionId, + pub next_id: TransactionId, } /// Emitted when an owner confirms a transaction. @@ -235,23 +235,23 @@ mod multisig { pub struct Multisig { /// Every entry in this map represents the confirmation of an owner for a /// transaction. This is effectively a set rather than a map. - confirmations: Mapping<(TransactionId, Address), ()>, + pub confirmations: Mapping<(TransactionId, Address), ()>, /// The amount of confirmations for every transaction. This is a redundant /// information and is kept in order to prevent iterating through the /// confirmation set to check if a transaction is confirmed. - confirmation_count: Mapping, + pub confirmation_count: Mapping, /// Map the transaction id to its not-executed transaction. - transactions: Mapping, + pub transactions: Mapping, /// We need to hold a list of all transactions so that we can clean up storage /// when an owner is removed. - transaction_list: Transactions, + pub transaction_list: Transactions, /// The list is a vector because iterating over it is necessary when cleaning /// up the confirmation set. - owners: Vec
, + pub owners: Vec
, /// Redundant information to speed up the check whether a caller is an owner. - is_owner: Mapping, + pub is_owner: Mapping, /// Minimum number of owners that have to confirm a transaction to be executed. - requirement: u32, + pub requirement: u32, } impl Multisig { @@ -287,74 +287,6 @@ mod multisig { /// # Panics /// /// If the owner already exists. - /// - /// # Examples - /// - /// Since this message must be send by the wallet itself it has to be build as a - /// `Transaction` and dispatched through `submit_transaction` and - /// `invoke_transaction`: - /// ```should_panic - /// use ink::{ - /// env::{ - /// DefaultEnvironment as Env, - /// Environment, - /// call::{ - /// Call, - /// CallParams, - /// ExecutionInput, - /// Selector, - /// utils::ArgumentList, - /// }, - /// }, - /// scale::Encode, - /// selector_bytes, - /// }; - /// use multisig::{ - /// ConfirmationStatus, - /// Transaction, - /// }; - /// - /// // address of an existing `Multisig` contract - /// let wallet_id: ink::Address = [7u8; 20].into(); - /// - /// // first create the transaction that adds `alice` through `add_owner` - /// let alice: ink::Address = [1u8; 20].into(); - /// let add_owner_args = ArgumentList::empty().push_arg(&alice); - /// - /// let transaction_candidate = Transaction { - /// callee: wallet_id, - /// selector: selector_bytes!(Abi::Ink, "add_owner"), - /// input: add_owner_args.encode(), - /// transferred_value: ink::U256::zero(), - /// ref_time_limit: 0, - /// allow_reentry: true, - /// }; - /// - /// // Submit the transaction for confirmation - /// // - /// // Note that the selector bytes of the `submit_transaction` method - /// // are `[86, 244, 13, 223]`. - /// let (id, _status) = ink::env::call::build_call::() - /// .call_type(Call::new(wallet_id)) - /// .ref_time_limit(0) - /// .exec_input( - /// ExecutionInput::new(Selector::new([86, 244, 13, 223])) - /// .push_arg(&transaction_candidate), - /// ) - /// .returns::<(u32, ConfirmationStatus)>() - /// .invoke(); - /// - /// // Wait until all owners have confirmed and then execute the tx. - /// // - /// // Note that the selector bytes of the `invoke_transaction` method - /// // are `[185, 50, 225, 236]`. - /// ink::env::call::build_call::() - /// .call_type(Call::new(wallet_id)) - /// .ref_time_limit(0) - /// .exec_input(ExecutionInput::new(Selector::new([185, 50, 225, 236])).push_arg(&id)) - /// .returns::<()>() - /// .invoke(); - /// ``` #[ink(message)] pub fn add_owner(&mut self, new_owner: Address) { self.ensure_from_wallet(); @@ -727,381 +659,7 @@ mod multisig { fn ensure_requirement_is_valid(owners: u32, requirement: u32) { assert!(0 < requirement && requirement <= owners && owners <= MAX_OWNERS); } - - #[cfg(test)] - mod tests { - use super::*; - use ink::env::{ - call::utils::ArgumentList, - test, - }; - - const WALLET: [u8; 20] = [7; 20]; - - impl Transaction { - fn change_requirement(requirement: u32) -> Self { - use ink::scale::Encode; - let call_args = ArgumentList::empty().push_arg(&requirement); - - // Multisig::change_requirement() - Self { - callee: Address::from(WALLET), - selector: ink::selector_bytes!(Abi::Ink, "change_requirement"), - input: call_args.encode(), - transferred_value: U256::zero(), - ref_time_limit: 1000000, - allow_reentry: false, - } - } - } - - fn set_caller(sender: Address) { - ink::env::test::set_caller(sender); - } - - fn set_from_wallet() { - let callee = Address::from(WALLET); - set_caller(callee); - } - - fn set_from_owner() { - let accounts = default_accounts(); - set_caller(accounts.alice); - } - - fn set_from_no_owner() { - let accounts = default_accounts(); - set_caller(accounts.django); - } - - fn default_accounts() -> test::DefaultAccounts { - ink::env::test::default_accounts() - } - - fn build_contract() -> Multisig { - // Set the contract's address as `WALLET`. - let callee: Address = Address::from(WALLET); - ink::env::test::set_callee(callee); - - let accounts = default_accounts(); - let owners = vec![accounts.alice, accounts.bob, accounts.eve]; - Multisig::new(2, owners) - } - - fn submit_transaction() -> Multisig { - let mut contract = build_contract(); - let accounts = default_accounts(); - set_from_owner(); - contract.submit_transaction(Transaction::change_requirement(1)); - assert_eq!(contract.transaction_list.transactions.len(), 1); - assert_eq!(test::recorded_events().len(), 2); - let transaction = contract.transactions.get(0).unwrap(); - assert_eq!(transaction, Transaction::change_requirement(1)); - contract.confirmations.get((0, accounts.alice)).unwrap(); - assert_eq!(contract.confirmation_count.get(0).unwrap(), 1); - contract - } - - #[ink::test] - fn construction_works() { - let accounts = default_accounts(); - let owners = [accounts.alice, accounts.bob, accounts.eve]; - let contract = build_contract(); - - assert_eq!(contract.owners.len(), 3); - assert_eq!(contract.requirement, 2); - use ink::prelude::collections::HashSet; - assert_eq!( - HashSet::<&Address>::from_iter(contract.owners.iter()), - HashSet::from_iter(owners.iter()), - ); - assert!(contract.is_owner.contains(accounts.alice)); - assert!(contract.is_owner.contains(accounts.bob)); - assert!(contract.is_owner.contains(accounts.eve)); - assert!(!contract.is_owner.contains(accounts.charlie)); - assert!(!contract.is_owner.contains(accounts.django)); - assert!(!contract.is_owner.contains(accounts.frank)); - assert_eq!(contract.transaction_list.transactions.len(), 0); - } - - #[ink::test] - #[should_panic] - fn empty_owner_construction_fails() { - Multisig::new(0, vec![]); - } - - #[ink::test] - #[should_panic] - fn zero_requirement_construction_fails() { - let accounts = default_accounts(); - Multisig::new(0, vec![accounts.alice, accounts.bob]); - } - - #[ink::test] - #[should_panic] - fn too_large_requirement_construction_fails() { - let accounts = default_accounts(); - Multisig::new(3, vec![accounts.alice, accounts.bob]); - } - - #[ink::test] - fn add_owner_works() { - let accounts = default_accounts(); - let mut contract = build_contract(); - set_from_wallet(); - let owners = contract.owners.len(); - contract.add_owner(accounts.frank); - assert_eq!(contract.owners.len(), owners + 1); - assert!(contract.is_owner.contains(accounts.frank)); - assert_eq!(test::recorded_events().len(), 1); - } - - #[ink::test] - #[should_panic] - fn add_existing_owner_fails() { - let accounts = default_accounts(); - let mut contract = build_contract(); - set_from_wallet(); - contract.add_owner(accounts.bob); - } - - #[ink::test] - #[should_panic] - fn add_owner_permission_denied() { - let accounts = default_accounts(); - let mut contract = build_contract(); - set_from_owner(); - contract.add_owner(accounts.frank); - } - - #[ink::test] - fn remove_owner_works() { - let accounts = default_accounts(); - let mut contract = build_contract(); - set_from_wallet(); - let owners = contract.owners.len(); - contract.remove_owner(accounts.alice); - assert_eq!(contract.owners.len(), owners - 1); - assert!(!contract.is_owner.contains(accounts.alice)); - assert_eq!(test::recorded_events().len(), 1); - } - - #[ink::test] - #[should_panic] - fn remove_owner_nonexisting_fails() { - let accounts = default_accounts(); - let mut contract = build_contract(); - set_from_wallet(); - contract.remove_owner(accounts.django); - } - - #[ink::test] - #[should_panic] - fn remove_owner_permission_denied() { - let accounts = default_accounts(); - let mut contract = build_contract(); - set_from_owner(); - contract.remove_owner(accounts.alice); - } - - #[ink::test] - fn replace_owner_works() { - let accounts = default_accounts(); - let mut contract = build_contract(); - set_from_wallet(); - let owners = contract.owners.len(); - contract.replace_owner(accounts.alice, accounts.django); - assert_eq!(contract.owners.len(), owners); - assert!(!contract.is_owner.contains(accounts.alice)); - assert!(contract.is_owner.contains(accounts.django)); - assert_eq!(test::recorded_events().len(), 2); - } - - #[ink::test] - #[should_panic] - fn replace_owner_existing_fails() { - let accounts = default_accounts(); - let mut contract = build_contract(); - set_from_wallet(); - contract.replace_owner(accounts.alice, accounts.bob); - } - - #[ink::test] - #[should_panic] - fn replace_owner_nonexisting_fails() { - let accounts = default_accounts(); - let mut contract = build_contract(); - set_from_wallet(); - contract.replace_owner(accounts.django, accounts.frank); - } - - #[ink::test] - #[should_panic] - fn replace_owner_permission_denied() { - let accounts = default_accounts(); - let mut contract = build_contract(); - set_from_owner(); - contract.replace_owner(accounts.alice, accounts.django); - } - - #[ink::test] - fn change_requirement_works() { - let mut contract = build_contract(); - assert_eq!(contract.requirement, 2); - set_from_wallet(); - contract.change_requirement(3); - assert_eq!(contract.requirement, 3); - assert_eq!(test::recorded_events().len(), 1); - } - - #[ink::test] - #[should_panic] - fn change_requirement_too_high() { - let mut contract = build_contract(); - set_from_wallet(); - contract.change_requirement(4); - } - - #[ink::test] - #[should_panic] - fn change_requirement_zero_fails() { - let mut contract = build_contract(); - set_from_wallet(); - contract.change_requirement(0); - } - - #[ink::test] - fn submit_transaction_works() { - submit_transaction(); - } - - #[ink::test] - #[should_panic] - fn submit_transaction_no_owner_fails() { - let mut contract = build_contract(); - set_from_no_owner(); - contract.submit_transaction(Transaction::change_requirement(1)); - } - - #[ink::test] - #[should_panic] - fn submit_transaction_wallet_fails() { - let mut contract = build_contract(); - set_from_wallet(); - contract.submit_transaction(Transaction::change_requirement(1)); - } - - #[ink::test] - fn cancel_transaction_works() { - let mut contract = submit_transaction(); - set_from_wallet(); - contract.cancel_transaction(0); - assert_eq!(contract.transaction_list.transactions.len(), 0); - assert_eq!(test::recorded_events().len(), 3); - } - - #[ink::test] - fn cancel_transaction_nonexisting() { - let mut contract = submit_transaction(); - set_from_wallet(); - contract.cancel_transaction(1); - assert_eq!(contract.transaction_list.transactions.len(), 1); - assert_eq!(test::recorded_events().len(), 2); - } - - #[ink::test] - #[should_panic] - fn cancel_transaction_no_permission() { - let mut contract = submit_transaction(); - contract.cancel_transaction(0); - } - - #[ink::test] - fn confirm_transaction_works() { - let mut contract = submit_transaction(); - let accounts = default_accounts(); - set_caller(accounts.bob); - contract.confirm_transaction(0); - assert_eq!(test::recorded_events().len(), 3); - contract.confirmations.get((0, accounts.bob)).unwrap(); - assert_eq!(contract.confirmation_count.get(0).unwrap(), 2); - } - - #[ink::test] - fn revoke_confirmations() { - // given - let mut contract = submit_transaction(); - let accounts = default_accounts(); - // Confirm by Bob - set_caller(accounts.bob); - contract.confirm_transaction(0); - // Confirm by Eve - set_caller(accounts.eve); - contract.confirm_transaction(0); - assert_eq!(contract.confirmation_count.get(0).unwrap(), 3); - // Revoke from Eve - contract.revoke_confirmation(0); - assert_eq!(contract.confirmation_count.get(0).unwrap(), 2); - // Revoke from Bob - set_caller(accounts.bob); - contract.revoke_confirmation(0); - assert_eq!(contract.confirmation_count.get(0).unwrap(), 1); - } - - #[ink::test] - fn confirm_transaction_already_confirmed() { - let mut contract = submit_transaction(); - let accounts = default_accounts(); - set_caller(accounts.alice); - contract.confirm_transaction(0); - assert_eq!(test::recorded_events().len(), 2); - contract.confirmations.get((0, accounts.alice)).unwrap(); - assert_eq!(contract.confirmation_count.get(0).unwrap(), 1); - } - - #[ink::test] - #[should_panic] - fn confirm_transaction_no_owner_fail() { - let mut contract = submit_transaction(); - set_from_no_owner(); - contract.confirm_transaction(0); - } - - #[ink::test] - fn revoke_transaction_works() { - let mut contract = submit_transaction(); - let accounts = default_accounts(); - set_caller(accounts.alice); - contract.revoke_confirmation(0); - assert_eq!(test::recorded_events().len(), 3); - assert!(!contract.confirmations.contains((0, accounts.alice))); - assert_eq!(contract.confirmation_count.get(0).unwrap(), 0); - } - - #[ink::test] - fn revoke_transaction_no_confirmer() { - let mut contract = submit_transaction(); - let accounts = default_accounts(); - set_caller(accounts.bob); - contract.revoke_confirmation(0); - assert_eq!(test::recorded_events().len(), 2); - assert!(contract.confirmations.contains((0, accounts.alice))); - assert_eq!(contract.confirmation_count.get(0).unwrap(), 1); - } - - #[ink::test] - #[should_panic] - fn revoke_transaction_no_owner_fail() { - let mut contract = submit_transaction(); - let accounts = default_accounts(); - set_caller(accounts.django); - contract.revoke_confirmation(0); - } - - #[ink::test] - fn execute_transaction_works() { - // Execution of calls is currently unsupported in off-chain test. - // Calling `execute_transaction` panics in any case. - } - } } + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/misc/multisig/tests.rs b/integration-tests/public/misc/multisig/tests.rs new file mode 100644 index 00000000000..5907caee041 --- /dev/null +++ b/integration-tests/public/misc/multisig/tests.rs @@ -0,0 +1,374 @@ +use super::multisig::*; +use ink::env::{ + call::utils::ArgumentList, + test, +}; +use ink::{Address, U256}; + +const WALLET: [u8; 20] = [7; 20]; + +impl Transaction { + fn change_requirement(requirement: u32) -> Self { + use ink::scale::Encode; + let call_args = ArgumentList::empty().push_arg(&requirement); + + // Multisig::change_requirement() + Self { + callee: Address::from(WALLET), + selector: ink::selector_bytes!(Abi::Ink, "change_requirement"), + input: call_args.encode(), + transferred_value: U256::zero(), + ref_time_limit: 1000000, + allow_reentry: false, + } + } +} + +fn set_caller(sender: Address) { + ink::env::test::set_caller(sender); +} + +fn set_from_wallet() { + let callee = Address::from(WALLET); + set_caller(callee); +} + +fn set_from_owner() { + let accounts = default_accounts(); + set_caller(accounts.alice); +} + +fn set_from_no_owner() { + let accounts = default_accounts(); + set_caller(accounts.django); +} + +fn default_accounts() -> test::DefaultAccounts { + ink::env::test::default_accounts() +} + +fn build_contract() -> Multisig { + // Set the contract's address as `WALLET`. + let callee: Address = Address::from(WALLET); + ink::env::test::set_callee(callee); + + let accounts = default_accounts(); + let owners = vec![accounts.alice, accounts.bob, accounts.eve]; + Multisig::new(2, owners) +} + +fn submit_transaction() -> Multisig { + let mut contract = build_contract(); + let accounts = default_accounts(); + set_from_owner(); + contract.submit_transaction(Transaction::change_requirement(1)); + assert_eq!(contract.transaction_list.transactions.len(), 1); + assert_eq!(test::recorded_events().len(), 2); + let transaction = contract.transactions.get(0).unwrap(); + assert_eq!(transaction, Transaction::change_requirement(1)); + contract.confirmations.get((0, accounts.alice)).unwrap(); + assert_eq!(contract.confirmation_count.get(0).unwrap(), 1); + contract +} + +#[ink::test] +fn construction_works() { + let accounts = default_accounts(); + let owners = [accounts.alice, accounts.bob, accounts.eve]; + let contract = build_contract(); + + assert_eq!(contract.owners.len(), 3); + assert_eq!(contract.requirement, 2); + use ink::prelude::collections::HashSet; + assert_eq!( + HashSet::<&Address>::from_iter(contract.owners.iter()), + HashSet::from_iter(owners.iter()), + ); + assert!(contract.is_owner.contains(accounts.alice)); + assert!(contract.is_owner.contains(accounts.bob)); + assert!(contract.is_owner.contains(accounts.eve)); + assert!(!contract.is_owner.contains(accounts.charlie)); + assert!(!contract.is_owner.contains(accounts.django)); + assert!(!contract.is_owner.contains(accounts.frank)); + assert_eq!(contract.transaction_list.transactions.len(), 0); +} + +#[ink::test] +#[should_panic] +fn empty_owner_construction_fails() { + Multisig::new(0, vec![]); +} + +#[ink::test] +#[should_panic] +fn zero_requirement_construction_fails() { + let accounts = default_accounts(); + Multisig::new(0, vec![accounts.alice, accounts.bob]); +} + +#[ink::test] +#[should_panic] +fn too_large_requirement_construction_fails() { + let accounts = default_accounts(); + Multisig::new(3, vec![accounts.alice, accounts.bob]); +} + +#[ink::test] +fn add_owner_works() { + let accounts = default_accounts(); + let mut contract = build_contract(); + set_from_wallet(); + let owners = contract.owners.len(); + contract.add_owner(accounts.frank); + assert_eq!(contract.owners.len(), owners + 1); + assert!(contract.is_owner.contains(accounts.frank)); + assert_eq!(test::recorded_events().len(), 1); +} + +#[ink::test] +#[should_panic] +fn add_existing_owner_fails() { + let accounts = default_accounts(); + let mut contract = build_contract(); + set_from_wallet(); + contract.add_owner(accounts.bob); +} + +#[ink::test] +#[should_panic] +fn add_owner_permission_denied() { + let accounts = default_accounts(); + let mut contract = build_contract(); + set_from_owner(); + contract.add_owner(accounts.frank); +} + +#[ink::test] +fn remove_owner_works() { + let accounts = default_accounts(); + let mut contract = build_contract(); + set_from_wallet(); + let owners = contract.owners.len(); + contract.remove_owner(accounts.alice); + assert_eq!(contract.owners.len(), owners - 1); + assert!(!contract.is_owner.contains(accounts.alice)); + assert_eq!(test::recorded_events().len(), 1); +} + +#[ink::test] +#[should_panic] +fn remove_owner_nonexisting_fails() { + let accounts = default_accounts(); + let mut contract = build_contract(); + set_from_wallet(); + contract.remove_owner(accounts.django); +} + +#[ink::test] +#[should_panic] +fn remove_owner_permission_denied() { + let accounts = default_accounts(); + let mut contract = build_contract(); + set_from_owner(); + contract.remove_owner(accounts.alice); +} + +#[ink::test] +fn replace_owner_works() { + let accounts = default_accounts(); + let mut contract = build_contract(); + set_from_wallet(); + let owners = contract.owners.len(); + contract.replace_owner(accounts.alice, accounts.django); + assert_eq!(contract.owners.len(), owners); + assert!(!contract.is_owner.contains(accounts.alice)); + assert!(contract.is_owner.contains(accounts.django)); + assert_eq!(test::recorded_events().len(), 2); +} + +#[ink::test] +#[should_panic] +fn replace_owner_existing_fails() { + let accounts = default_accounts(); + let mut contract = build_contract(); + set_from_wallet(); + contract.replace_owner(accounts.alice, accounts.bob); +} + +#[ink::test] +#[should_panic] +fn replace_owner_nonexisting_fails() { + let accounts = default_accounts(); + let mut contract = build_contract(); + set_from_wallet(); + contract.replace_owner(accounts.django, accounts.frank); +} + +#[ink::test] +#[should_panic] +fn replace_owner_permission_denied() { + let accounts = default_accounts(); + let mut contract = build_contract(); + set_from_owner(); + contract.replace_owner(accounts.alice, accounts.django); +} + +#[ink::test] +fn change_requirement_works() { + let mut contract = build_contract(); + assert_eq!(contract.requirement, 2); + set_from_wallet(); + contract.change_requirement(3); + assert_eq!(contract.requirement, 3); + assert_eq!(test::recorded_events().len(), 1); +} + +#[ink::test] +#[should_panic] +fn change_requirement_too_high() { + let mut contract = build_contract(); + set_from_wallet(); + contract.change_requirement(4); +} + +#[ink::test] +#[should_panic] +fn change_requirement_zero_fails() { + let mut contract = build_contract(); + set_from_wallet(); + contract.change_requirement(0); +} + +#[ink::test] +fn submit_transaction_works() { + submit_transaction(); +} + +#[ink::test] +#[should_panic] +fn submit_transaction_no_owner_fails() { + let mut contract = build_contract(); + set_from_no_owner(); + contract.submit_transaction(Transaction::change_requirement(1)); +} + +#[ink::test] +#[should_panic] +fn submit_transaction_wallet_fails() { + let mut contract = build_contract(); + set_from_wallet(); + contract.submit_transaction(Transaction::change_requirement(1)); +} + +#[ink::test] +fn cancel_transaction_works() { + let mut contract = submit_transaction(); + set_from_wallet(); + contract.cancel_transaction(0); + assert_eq!(contract.transaction_list.transactions.len(), 0); + assert_eq!(test::recorded_events().len(), 3); +} + +#[ink::test] +fn cancel_transaction_nonexisting() { + let mut contract = submit_transaction(); + set_from_wallet(); + contract.cancel_transaction(1); + assert_eq!(contract.transaction_list.transactions.len(), 1); + assert_eq!(test::recorded_events().len(), 2); +} + +#[ink::test] +#[should_panic] +fn cancel_transaction_no_permission() { + let mut contract = submit_transaction(); + contract.cancel_transaction(0); +} + +#[ink::test] +fn confirm_transaction_works() { + let mut contract = submit_transaction(); + let accounts = default_accounts(); + set_caller(accounts.bob); + contract.confirm_transaction(0); + assert_eq!(test::recorded_events().len(), 3); + contract.confirmations.get((0, accounts.bob)).unwrap(); + assert_eq!(contract.confirmation_count.get(0).unwrap(), 2); +} + +#[ink::test] +fn revoke_confirmations() { + // given + let mut contract = submit_transaction(); + let accounts = default_accounts(); + // Confirm by Bob + set_caller(accounts.bob); + contract.confirm_transaction(0); + // Confirm by Eve + set_caller(accounts.eve); + contract.confirm_transaction(0); + assert_eq!(contract.confirmation_count.get(0).unwrap(), 3); + // Revoke from Eve + contract.revoke_confirmation(0); + assert_eq!(contract.confirmation_count.get(0).unwrap(), 2); + // Revoke from Bob + set_caller(accounts.bob); + contract.revoke_confirmation(0); + assert_eq!(contract.confirmation_count.get(0).unwrap(), 1); +} + +#[ink::test] +fn confirm_transaction_already_confirmed() { + let mut contract = submit_transaction(); + let accounts = default_accounts(); + set_caller(accounts.alice); + contract.confirm_transaction(0); + assert_eq!(test::recorded_events().len(), 2); + contract.confirmations.get((0, accounts.alice)).unwrap(); + assert_eq!(contract.confirmation_count.get(0).unwrap(), 1); +} + +#[ink::test] +#[should_panic] +fn confirm_transaction_no_owner_fail() { + let mut contract = submit_transaction(); + set_from_no_owner(); + contract.confirm_transaction(0); +} + +#[ink::test] +fn revoke_transaction_works() { + let mut contract = submit_transaction(); + let accounts = default_accounts(); + set_caller(accounts.alice); + contract.revoke_confirmation(0); + assert_eq!(test::recorded_events().len(), 3); + assert!(!contract.confirmations.contains((0, accounts.alice))); + assert_eq!(contract.confirmation_count.get(0).unwrap(), 0); +} + +#[ink::test] +fn revoke_transaction_no_confirmer() { + let mut contract = submit_transaction(); + let accounts = default_accounts(); + set_caller(accounts.bob); + contract.revoke_confirmation(0); + assert_eq!(test::recorded_events().len(), 2); + assert!(contract.confirmations.contains((0, accounts.alice))); + assert_eq!(contract.confirmation_count.get(0).unwrap(), 1); +} + +#[ink::test] +#[should_panic] +fn revoke_transaction_no_owner_fail() { + let mut contract = submit_transaction(); + let accounts = default_accounts(); + set_caller(accounts.django); + contract.revoke_confirmation(0); +} + +#[ink::test] +fn execute_transaction_works() { + // Execution of calls is currently unsupported in off-chain test. + // Calling `execute_transaction` panics in any case. +} \ No newline at end of file diff --git a/integration-tests/public/payment-channel/Cargo.toml b/integration-tests/public/misc/payment-channel/Cargo.toml old mode 100755 new mode 100644 similarity index 50% rename from integration-tests/public/payment-channel/Cargo.toml rename to integration-tests/public/misc/payment-channel/Cargo.toml index 98d68ed38cc..5b0a5ab8816 --- a/integration-tests/public/payment-channel/Cargo.toml +++ b/integration-tests/public/misc/payment-channel/Cargo.toml @@ -1,16 +1,17 @@ [package] -name = "payment_channel" +name = "payment-channel" version = "6.0.0-beta.1" authors = ["Use Ink "] edition = "2024" publish = false [dependencies] -ink = { path = "../../../crates/ink", default-features = false, features = ["unstable-hostfn"] } -sp-core = { version = "38.0.0", default-features = false } +ink = { path = "../../../crates/ink", default-features = false } [dev-dependencies] -hex-literal = "1" +ink_e2e = { path = "../../../crates/e2e" } +hex-literal = "0.4" +sp-core = { version = "34.0.0", default-features = false, features = ["full_crypto"] } [lib] path = "lib.rs" @@ -20,8 +21,8 @@ default = ["std"] std = [ "ink/std", ] - ink-as-dependency = [] +e2e-tests = [] [package.metadata.ink-lang] -abi = "ink" +abi = "ink" \ No newline at end of file diff --git a/integration-tests/public/misc/payment-channel/lib.rs b/integration-tests/public/misc/payment-channel/lib.rs new file mode 100644 index 00000000000..43cb7000a67 --- /dev/null +++ b/integration-tests/public/misc/payment-channel/lib.rs @@ -0,0 +1,289 @@ +//! # Payment Channel +//! +//! This implements a payment channel between two parties. +//! +//! ## Warning +//! +//! This contract is an *example*. It is neither audited nor endorsed for production use. +//! Do **not** rely on it to keep anything of value secure. +//! +//! ## Overview +//! +//! Each instantiation of this contract creates a payment channel between a `sender` and a +//! `recipient`. It uses ECDSA signatures to ensure that the `recipient` can only claim +//! the funds if it is signed by the `sender`. +//! +//! ## Error Handling +//! +//! The only panic in the contract is when the signature is invalid. For all other +//! error cases an error is returned. Possible errors are defined in the `Error` enum. +//! +//! ## Interface +//! +//! The interface is modelled after [this blog post](https://programtheblockchain.com/posts/2018/03/02/building-long-lived-payment-channels) +//! +//! ### Deposits +//! +//! The creator of the contract, i.e. the `sender`, can deposit funds to the payment +//! channel while creating the payment channel. Any subsequent deposits can be made by +//! transferring funds to the contract's address. +//! +//! ### Withdrawals +//! +//! The `recipient` can `withdraw` from the payment channel anytime by submitting the last +//! `signature` received from the `sender`. +//! +//! The `sender` can only `withdraw` by terminating the payment channel. This is +//! done by calling `start_sender_close` to set an expiration with a subsequent call +//! of `claim_timeout` to claim the funds. This will terminate the payment channel. + +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +pub mod payment_channel { + use ink::U256; + + /// Struct for storing the payment channel details. + /// The creator of the contract, i.e. the `sender`, can deposit funds to the payment + /// channel while deploying the contract. + #[ink(storage)] + pub struct PaymentChannel { + /// The `Address` of the sender of the payment channel. + sender: Address, + /// The `Address` of the recipient of the payment channel. + recipient: Address, + /// The `Timestamp` at which the contract expires. The field is optional. + /// The contract never expires if set to `None`. + expiration: Option, + /// The `Amount` withdrawn by the recipient. + withdrawn: U256, + /// The `Timestamp` which will be added to the current time when the sender + /// wishes to close the channel. This will be set at the time of contract + /// instantiation. + close_duration: Timestamp, + } + + /// Errors that can occur upon calling this contract. + #[derive(Debug, PartialEq, Eq)] + #[ink::error] + pub enum Error { + /// Returned if caller is not the `sender` while required to. + CallerIsNotSender, + /// Returned if caller is not the `recipient` while required to. + CallerIsNotRecipient, + /// Returned if the requested withdrawal amount is less than the amount + /// that is already withdrawn. + AmountIsLessThanWithdrawn, + /// Returned if the requested transfer failed. This can be the case if the + /// contract does not have sufficient free funds or if the transfer would + /// have brought the contract's balance below minimum balance. + TransferFailed, + /// Returned if the contract hasn't expired yet and the `sender` wishes to + /// close the channel. + NotYetExpired, + /// Returned if the signature is invalid. + InvalidSignature, + } + + /// Type alias for the contract's `Result` type. + pub type Result = core::result::Result; + + /// Emitted when the sender starts closing the channel. + #[ink(event)] + pub struct SenderCloseStarted { + expiration: Timestamp, + close_duration: Timestamp, + } + + impl PaymentChannel { + /// The only constructor of the contract. + /// + /// The arguments `recipient` and `close_duration` are required. + /// + /// `expiration` will be set to `None`, so that the contract will + /// never expire. `sender` can call `start_sender_close` to override + /// this. `sender` will be able to claim the remaining balance by calling + /// `claim_timeout` after `expiration` has passed. + #[ink(constructor)] + pub fn new(recipient: Address, close_duration: Timestamp) -> Self { + Self { + sender: Self::env().caller(), + recipient, + expiration: None, + withdrawn: 0.into(), + close_duration, + } + } + + /// The `recipient` can close the payment channel anytime. The specified + /// `amount` will be sent to the `recipient` and the remainder will go + /// back to the `sender`. + #[ink(message)] + pub fn close(&mut self, amount: U256, signature: [u8; 65]) -> Result<()> { + self.close_inner(amount, signature)?; + self.env().terminate_contract(self.sender); + } + + /// We split this out in order to make testing `close` simpler. + pub fn close_inner(&mut self, amount: U256, signature: [u8; 65]) -> Result<()> { + if self.env().caller() != self.recipient { + return Err(Error::CallerIsNotRecipient) + } + + if amount < self.withdrawn { + return Err(Error::AmountIsLessThanWithdrawn) + } + + // Signature validation + if !self.is_signature_valid(amount, signature) { + return Err(Error::InvalidSignature) + } + + // We checked that amount >= self.withdrawn + #[allow(clippy::arithmetic_side_effects)] + self.env() + .transfer(self.recipient, amount - self.withdrawn) + .map_err(|_| Error::TransferFailed)?; + + Ok(()) + } + + /// If the `sender` wishes to close the channel and withdraw the funds they can + /// do so by setting the `expiration`. If the `expiration` is reached, the + /// sender will be able to call `claim_timeout` to claim the remaining funds + /// and the channel will be terminated. This emits an event that the recipient can + /// listen to in order to withdraw the funds before the `expiration`. + #[ink(message)] + pub fn start_sender_close(&mut self) -> Result<()> { + if self.env().caller() != self.sender { + return Err(Error::CallerIsNotSender) + } + + let now = self.env().block_timestamp(); + let expiration = now.checked_add(self.close_duration).unwrap(); + + self.env().emit_event(SenderCloseStarted { + expiration, + close_duration: self.close_duration, + }); + + self.expiration = Some(expiration); + + Ok(()) + } + + /// If the timeout is reached (`current_time >= expiration`) without the + /// recipient closing the channel, then the remaining balance is released + /// back to the `sender`. + #[ink(message)] + pub fn claim_timeout(&mut self) -> Result<()> { + match self.expiration { + Some(expiration) => { + // expiration is set. Check if it's reached and if so, release the + // funds and terminate the contract. + let now = self.env().block_timestamp(); + if now < expiration { + return Err(Error::NotYetExpired) + } + + self.env().terminate_contract(self.sender); + } + + None => Err(Error::NotYetExpired), + } + } + + /// The `recipient` can withdraw the funds from the channel at any time. + #[ink(message)] + pub fn withdraw(&mut self, amount: U256, signature: [u8; 65]) -> Result<()> { + if self.env().caller() != self.recipient { + return Err(Error::CallerIsNotRecipient) + } + + // Signature validation + if !self.is_signature_valid(amount, signature) { + return Err(Error::InvalidSignature) + } + + // Make sure there's something to withdraw (guards against underflow) + if amount < self.withdrawn { + return Err(Error::AmountIsLessThanWithdrawn) + } + + // We checked that amount >= self.withdrawn + #[allow(clippy::arithmetic_side_effects)] + let amount_to_withdraw = amount - self.withdrawn; + self.withdrawn.checked_add(amount_to_withdraw).unwrap(); + + self.env() + .transfer(self.recipient, amount_to_withdraw) + .map_err(|_| Error::TransferFailed)?; + + Ok(()) + } + + /// Returns the `sender` of the contract. + #[ink(message)] + pub fn get_sender(&self) -> Address { + self.sender + } + + /// Returns the `recipient` of the contract. + #[ink(message)] + pub fn get_recipient(&self) -> Address { + self.recipient + } + + /// Returns the `expiration` of the contract. + #[ink(message)] + pub fn get_expiration(&self) -> Option { + self.expiration + } + + /// Returns the `withdrawn` amount of the contract. + #[ink(message)] + pub fn get_withdrawn(&self) -> U256 { + self.withdrawn + } + + /// Returns the `close_duration` of the contract. + #[ink(message)] + pub fn get_close_duration(&self) -> Timestamp { + self.close_duration + } + + /// Returns the `balance` of the contract. + #[ink(message)] + pub fn get_balance(&self) -> U256 { + self.env().balance() + } + } + + #[ink(impl)] + impl PaymentChannel { + fn is_signature_valid(&self, amount: U256, signature: [u8; 65]) -> bool { + let encodable = (self.env().address(), amount); + let mut message = + ::Type::default(); + ink::env::hash_encoded::( + &encodable, + &mut message, + ); + + let mut pub_key = [0; 33]; + ink::env::ecdsa_recover(&signature, &message, &mut pub_key) + .unwrap_or_else(|err| panic!("recover failed: {err:?}")); + let mut signature_account_id = [0u8; 32]; + ::hash( + &pub_key, + &mut signature_account_id, + ); + + self.recipient + == ink::primitives::AccountIdMapper::to_address(&signature_account_id) + } + } +} + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/misc/payment-channel/tests.rs b/integration-tests/public/misc/payment-channel/tests.rs new file mode 100644 index 00000000000..4866636a7fe --- /dev/null +++ b/integration-tests/public/misc/payment-channel/tests.rs @@ -0,0 +1,301 @@ +use super::payment_channel::*; +use ink::U256; +use ink::primitives::AccountId as Address; + +use hex_literal; +use sp_core::{ + Encode, + Pair, +}; + +fn default_accounts() -> ink::env::test::DefaultAccounts { + ink::env::test::default_accounts() +} + +fn set_next_caller(caller: Address) { + ink::env::test::set_caller(caller); +} + +fn set_contract_balance(addr: Address, balance: U256) { + ink::env::test::set_contract_balance(addr, balance); +} + +fn get_contract_balance(addr: Address) -> U256 { + ink::env::test::get_contract_balance::(addr) + .expect("Cannot get contract balance") +} + +fn advance_block() { + ink::env::test::advance_block::(); +} + +fn get_current_time() -> u64 { + let since_the_epoch = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("Time went backwards"); + since_the_epoch.as_secs() + + since_the_epoch.subsec_nanos() as u64 / 1_000_000_000 +} + +fn get_dan() -> Address { + // Use Dan's seed + // `subkey inspect //Dan --scheme Ecdsa --output-type json | jq .secretSeed` + let seed = hex_literal::hex!( + "c31fa562972de437802e0df146b16146349590b444db41f7e3eb9deedeee6f64" + ); + let pair = sp_core::ecdsa::Pair::from_seed(&seed); + let pub_key = pair.public(); + let compressed_pub_key: [u8; 33] = pub_key.encode()[..] + .try_into() + .expect("slice with incorrect length"); + let mut account_id = [0; 32]; + ::hash( + &compressed_pub_key, + &mut account_id, + ); + ink::primitives::AccountIdMapper::to_address(&account_id) +} + +fn contract_id() -> Address { + let accounts = default_accounts(); + let contract_id = accounts.charlie; + ink::env::test::set_callee(contract_id); + contract_id +} + +fn sign(contract_id: Address, amount: U256) -> [u8; 65] { + let encodable = (contract_id, amount); + let mut hash = + ::Type::default(); // 256-bit buffer + ink::env::hash_encoded::(&encodable, &mut hash); + + // Use Dan's seed + // `subkey inspect //Dan --scheme Ecdsa --output-type json | jq .secretSeed` + let seed = hex_literal::hex!( + "c31fa562972de437802e0df146b16146349590b444db41f7e3eb9deedeee6f64" + ); + let pair = sp_core::ecdsa::Pair::from_seed(&seed); + + let signature = pair.sign_prehashed(&hash); + signature.0 +} + +#[ink::test] +fn test_deposit() { + // given + let accounts = default_accounts(); + let initial_balance = 10_000.into(); + let close_duration = 360_000; + let mock_deposit_value = 1_000.into(); + set_contract_balance(accounts.alice, initial_balance); + set_contract_balance(accounts.bob, initial_balance); + + // when + // Push the new execution context with Alice as the caller and + // the `mock_deposit_value` as the value deposited. + // Note: Currently there is no way to transfer funds to the contract. + set_next_caller(accounts.alice); + let payment_channel = PaymentChannel::new(accounts.bob, close_duration); + let contract_id = contract_id(); + set_contract_balance(contract_id, mock_deposit_value); + + // then + assert_eq!(payment_channel.get_balance(), mock_deposit_value); +} + +#[ink::test] +fn test_close() { + // given + let accounts = default_accounts(); + let dan = get_dan(); + let close_duration = 360_000; + let mock_deposit_value = 1_000.into(); + let amount = 500.into(); + let initial_balance = 10_000.into(); + set_contract_balance(accounts.alice, initial_balance); + set_contract_balance(dan, initial_balance); + + // when + set_next_caller(accounts.alice); + let mut payment_channel = PaymentChannel::new(dan, close_duration); + let contract_id = contract_id(); + set_contract_balance(contract_id, mock_deposit_value); + set_next_caller(dan); + let signature = sign(contract_id, amount); + + // then + let should_close = move || payment_channel.close(amount, signature).unwrap(); + ink::env::test::assert_contract_termination::( + should_close, + accounts.alice, + amount, + ); + assert_eq!(get_contract_balance(dan), initial_balance + amount); +} + +#[ink::test] +fn close_fails_invalid_signature() { + // given + let accounts = default_accounts(); + let dan = get_dan(); + let mock_deposit_value = 1_000.into(); + let close_duration = 360_000; + let amount = 400.into(); + let unexpected_amount = amount + U256::from(1); + let initial_balance = 10_000.into(); + set_contract_balance(accounts.alice, initial_balance); + set_contract_balance(dan, initial_balance); + + // when + set_next_caller(accounts.alice); + let mut payment_channel = PaymentChannel::new(dan, close_duration); + let contract_id = contract_id(); + set_contract_balance(contract_id, mock_deposit_value); + set_next_caller(dan); + let signature = sign(contract_id, amount); + + // then + let res = payment_channel.close_inner(unexpected_amount, signature); + assert!(res.is_err(), "Expected an error, got {res:?} instead."); + assert_eq!(res.unwrap_err(), Error::InvalidSignature,); +} + +#[ink::test] +fn test_withdraw() { + // given + let accounts = default_accounts(); + let dan = get_dan(); + let initial_balance = 10_000.into(); + let mock_deposit_value = 1_000.into(); + let close_duration = 360_000; + let amount = 500.into(); + set_contract_balance(accounts.alice, initial_balance); + set_contract_balance(dan, initial_balance); + + // when + set_next_caller(accounts.alice); + let mut payment_channel = PaymentChannel::new(dan, close_duration); + let contract_id = contract_id(); + set_contract_balance(contract_id, mock_deposit_value); + + set_next_caller(dan); + let signature = sign(contract_id, amount); + payment_channel + .withdraw(amount, signature) + .expect("withdraw failed"); + + // then + assert_eq!(payment_channel.get_balance(), amount); + assert_eq!(get_contract_balance(dan), initial_balance + amount); +} + +#[ink::test] +fn withdraw_fails_invalid_signature() { + // given + let accounts = default_accounts(); + let dan = get_dan(); + let initial_balance = 10_000.into(); + let close_duration = 360_000; + let amount = 400.into(); + let unexpected_amount = amount + U256::from(1); + let mock_deposit_value = 1_000.into(); + set_contract_balance(accounts.alice, initial_balance); + set_contract_balance(dan, initial_balance); + + // when + set_next_caller(accounts.alice); + let mut payment_channel = PaymentChannel::new(dan, close_duration); + let contract_id = contract_id(); + set_contract_balance(contract_id, mock_deposit_value); + set_next_caller(dan); + let signature = sign(contract_id, amount); + + // then + let res = payment_channel.withdraw(unexpected_amount, signature); + assert!(res.is_err(), "Expected an error, got {res:?} instead."); + assert_eq!(res.unwrap_err(), Error::InvalidSignature,); +} + +#[ink::test] +fn test_start_sender_close() { + // given + let accounts = default_accounts(); + let initial_balance = 10_000.into(); + let mock_deposit_value = 1_000.into(); + let close_duration = 1; + set_contract_balance(accounts.alice, initial_balance); + set_contract_balance(accounts.bob, initial_balance); + + // when + set_next_caller(accounts.alice); + let mut payment_channel = PaymentChannel::new(accounts.bob, close_duration); + let contract_id = contract_id(); + set_contract_balance(contract_id, mock_deposit_value); + + payment_channel + .start_sender_close() + .expect("start_sender_close failed"); + advance_block(); + + // then + let now = get_current_time(); + assert!(now > payment_channel.get_expiration().unwrap()); +} + +#[ink::test] +fn test_claim_timeout() { + // given + let accounts = default_accounts(); + let initial_balance = 10_000.into(); + let close_duration = 1; + let mock_deposit_value = 1_000.into(); + set_contract_balance(accounts.alice, initial_balance); + set_contract_balance(accounts.bob, initial_balance); + + // when + set_next_caller(accounts.alice); + let contract_id = contract_id(); + let mut payment_channel = PaymentChannel::new(accounts.bob, close_duration); + set_contract_balance(contract_id, mock_deposit_value); + + payment_channel + .start_sender_close() + .expect("start_sender_close failed"); + advance_block(); + + // then + let should_close = move || payment_channel.claim_timeout().unwrap(); + ink::env::test::assert_contract_termination::( + should_close, + accounts.alice, + mock_deposit_value, + ); + assert_eq!( + get_contract_balance(accounts.alice), + initial_balance + mock_deposit_value + ); +} + +#[ink::test] +fn test_getters() { + // given + let accounts = default_accounts(); + let initial_balance = 10_000.into(); + let mock_deposit_value = 1_000.into(); + let close_duration = 360_000; + set_contract_balance(accounts.alice, initial_balance); + set_contract_balance(accounts.bob, initial_balance); + + // when + set_next_caller(accounts.alice); + let contract_id = contract_id(); + let payment_channel = PaymentChannel::new(accounts.bob, close_duration); + set_contract_balance(contract_id, mock_deposit_value); + + // then + assert_eq!(payment_channel.get_sender(), accounts.alice); + assert_eq!(payment_channel.get_recipient(), accounts.bob); + assert_eq!(payment_channel.get_balance(), mock_deposit_value); + assert_eq!(payment_channel.get_close_duration(), close_duration); + assert_eq!(payment_channel.get_withdrawn(), U256::zero()); +} \ No newline at end of file diff --git a/integration-tests/public/wildcard-selector/Cargo.toml b/integration-tests/public/misc/wildcard/Cargo.toml similarity index 100% rename from integration-tests/public/wildcard-selector/Cargo.toml rename to integration-tests/public/misc/wildcard/Cargo.toml diff --git a/integration-tests/public/misc/wildcard/lib.rs b/integration-tests/public/misc/wildcard/lib.rs new file mode 100644 index 00000000000..5bd37fb2389 --- /dev/null +++ b/integration-tests/public/misc/wildcard/lib.rs @@ -0,0 +1,61 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +pub mod wildcard_selector { + #[cfg(feature = "emit-event")] + use ink::prelude::format; + use ink::prelude::string::String; + + #[cfg(feature = "emit-event")] + #[ink::event] + pub struct Event { + pub msg: String, + } + + #[ink(storage)] + pub struct WildcardSelector {} + + struct MessageInput([u8; 4], String); + impl ink::env::DecodeDispatch for MessageInput { + fn decode_dispatch(input: &mut &[u8]) -> Result { + // todo improve code here + let mut selector: [u8; 4] = [0u8; 4]; + selector.copy_from_slice(&input[..4]); + let arg: String = ink::scale::Decode::decode(&mut &input[4..]).unwrap(); + Ok(MessageInput(selector, arg)) + } + } + + impl WildcardSelector { + /// Creates a new wildcard selector smart contract. + #[ink(constructor)] + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self {} + } + + /// Wildcard selector handles messages with any selector. + #[ink(message, selector = _)] + pub fn wildcard(&mut self) { + let MessageInput(_selector, _message) = + ink::env::decode_input::().unwrap(); + + #[cfg(feature = "emit-event")] + self.env().emit_event(Event { + msg: format!("Wildcard selector: {_selector:?}, message: {_message}"), + }) + } + + /// Wildcard complement handles messages with a well-known reserved selector. + #[ink(message, selector = @)] + pub fn wildcard_complement(&mut self, _message: String) { + #[cfg(feature = "emit-event")] + self.env().emit_event(Event { + msg: format!("Wildcard complement message: {_message}"), + }); + } + } +} + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/misc/wildcard/tests.rs b/integration-tests/public/misc/wildcard/tests.rs new file mode 100644 index 00000000000..f1756c54a73 --- /dev/null +++ b/integration-tests/public/misc/wildcard/tests.rs @@ -0,0 +1,127 @@ +use super::wildcard_selector::*; + +#[cfg(all(test, feature = "e2e-tests"))] +mod e2e_tests { + use super::*; + use ink_e2e::ContractsBackend; + + use ink::{ + env::call::utils::{ + Argument, + ArgumentList, + EmptyArgumentList, + }, + primitives::abi::Ink, + }; + + type E2EResult = std::result::Result>; + type Environment = ::Env; + + fn build_message( + addr: AccountId, // Changed to AccountId (which is H160 in v6) to match Address alias usage usually found + selector: [u8; 4], + message: String, + ) -> ink_e2e::CallBuilderFinal< + Environment, + ArgumentList, EmptyArgumentList, Ink>, + (), + Ink, + > { + ink::env::call::build_call::() + .call(addr) + .exec_input( + ink::env::call::ExecutionInput::new(ink::env::call::Selector::new( + selector, + )) + .push_arg(message), + ) + .returns::<()>() + } + + #[ink_e2e::test(features = ["emit-event"])] + async fn arbitrary_selectors_handled_by_wildcard( + mut client: Client, + ) -> E2EResult<()> { + // Given + let mut constructor = WildcardSelectorRef::new(); + let contract_acc_id = client + .instantiate("wildcard_selector", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed") + .addr; + + // When + const ARBITRARY_SELECTOR: [u8; 4] = [0xF9, 0xF9, 0xF9, 0xF9]; + let wildcard_message = "WILDCARD_MESSAGE 1".to_string(); + let wildcard = build_message( + contract_acc_id, + ARBITRARY_SELECTOR, + wildcard_message.clone(), + ); + + let _result = client + .call(&ink_e2e::bob(), &wildcard) + .submit() + .await + .expect("wildcard failed"); + + const ARBITRARY_SELECTOR_2: [u8; 4] = [0x01, 0x23, 0x45, 0x67]; + let wildcard_message2 = "WILDCARD_MESSAGE 2".to_string(); + let wildcard2 = build_message( + contract_acc_id, + ARBITRARY_SELECTOR_2, + wildcard_message2.clone(), + ); + + let _result2 = client + .call(&ink_e2e::bob(), &wildcard2) + .submit() + .await + .expect("wildcard failed"); + + // Then + let contract_events = _result.contract_emitted_events()?; + assert_eq!(1, contract_events.len()); + let contract_event = contract_events.first().expect("first event must exist"); + let event: Event = + ink::scale::Decode::decode(&mut &contract_event.event.data[..]) + .expect("encountered invalid contract event data buffer"); + assert_eq!( + event.msg, + format!( + "Wildcard selector: {ARBITRARY_SELECTOR:?}, message: {wildcard_message}" + ) + ); + + Ok(()) + } + + #[ink_e2e::test] + async fn wildcard_complement_works(mut client: Client) -> E2EResult<()> { + // Given + let mut constructor = WildcardSelectorRef::new(); + let contract_acc_id = client + .instantiate("wildcard_selector", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed") + .addr; + + // When + let wildcard_complement_message = "WILDCARD COMPLEMENT MESSAGE".to_string(); + let wildcard = build_message( + contract_acc_id, + ink::IIP2_WILDCARD_COMPLEMENT_SELECTOR, + wildcard_complement_message.clone(), + ); + + let _result = client + .call(&ink_e2e::bob(), &wildcard) + .submit() + .await + .expect("wildcard failed"); + + Ok(()) + } +} \ No newline at end of file diff --git a/integration-tests/public/payment-channel/lib.rs b/integration-tests/public/payment-channel/lib.rs deleted file mode 100755 index 5fe1ae35165..00000000000 --- a/integration-tests/public/payment-channel/lib.rs +++ /dev/null @@ -1,589 +0,0 @@ -//! # Payment Channel -//! -//! This implements a payment channel between two parties. -//! -//! ## Warning -//! -//! This contract is an *example*. It is neither audited nor endorsed for production use. -//! Do **not** rely on it to keep anything of value secure. -//! -//! ## Overview -//! -//! Each instantiation of this contract creates a payment channel between a `sender` and a -//! `recipient`. It uses ECDSA signatures to ensure that the `recipient` can only claim -//! the funds if it is signed by the `sender`. -//! -//! ## Error Handling -//! -//! The only panic in the contract is when the signature is invalid. For all other -//! error cases an error is returned. Possible errors are defined in the `Error` enum. -//! -//! ## Interface -//! -//! The interface is modelled after [this blog post](https://programtheblockchain.com/posts/2018/03/02/building-long-lived-payment-channels) -//! -//! ### Deposits -//! -//! The creator of the contract, i.e. the `sender`, can deposit funds to the payment -//! channel while creating the payment channel. Any subsequent deposits can be made by -//! transferring funds to the contract's address. -//! -//! ### Withdrawals -//! -//! The `recipient` can `withdraw` from the payment channel anytime by submitting the last -//! `signature` received from the `sender`. -//! -//! The `sender` can only `withdraw` by terminating the payment channel. This is -//! done by calling `start_sender_close` to set an expiration with a subsequent call -//! of `claim_timeout` to claim the funds. This will terminate the payment channel. - -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -#[ink::contract] -mod payment_channel { - use ink::U256; - - /// Struct for storing the payment channel details. - /// The creator of the contract, i.e. the `sender`, can deposit funds to the payment - /// channel while deploying the contract. - #[ink(storage)] - pub struct PaymentChannel { - /// The `Address` of the sender of the payment channel. - sender: Address, - /// The `Address` of the recipient of the payment channel. - recipient: Address, - /// The `Timestamp` at which the contract expires. The field is optional. - /// The contract never expires if set to `None`. - expiration: Option, - /// The `Amount` withdrawn by the recipient. - withdrawn: U256, - /// The `Timestamp` which will be added to the current time when the sender - /// wishes to close the channel. This will be set at the time of contract - /// instantiation. - close_duration: Timestamp, - } - - /// Errors that can occur upon calling this contract. - #[derive(Debug, PartialEq, Eq)] - #[ink::error] - pub enum Error { - /// Returned if caller is not the `sender` while required to. - CallerIsNotSender, - /// Returned if caller is not the `recipient` while required to. - CallerIsNotRecipient, - /// Returned if the requested withdrawal amount is less than the amount - /// that is already withdrawn. - AmountIsLessThanWithdrawn, - /// Returned if the requested transfer failed. This can be the case if the - /// contract does not have sufficient free funds or if the transfer would - /// have brought the contract's balance below minimum balance. - TransferFailed, - /// Returned if the contract hasn't expired yet and the `sender` wishes to - /// close the channel. - NotYetExpired, - /// Returned if the signature is invalid. - InvalidSignature, - } - - /// Type alias for the contract's `Result` type. - pub type Result = core::result::Result; - - /// Emitted when the sender starts closing the channel. - #[ink(event)] - pub struct SenderCloseStarted { - expiration: Timestamp, - close_duration: Timestamp, - } - - impl PaymentChannel { - /// The only constructor of the contract. - /// - /// The arguments `recipient` and `close_duration` are required. - /// - /// `expiration` will be set to `None`, so that the contract will - /// never expire. `sender` can call `start_sender_close` to override - /// this. `sender` will be able to claim the remaining balance by calling - /// `claim_timeout` after `expiration` has passed. - #[ink(constructor)] - pub fn new(recipient: Address, close_duration: Timestamp) -> Self { - Self { - sender: Self::env().caller(), - recipient, - expiration: None, - withdrawn: 0.into(), - close_duration, - } - } - - /// The `recipient` can close the payment channel anytime. The specified - /// `amount` will be sent to the `recipient` and the remainder will go - /// back to the `sender`. - #[ink(message)] - pub fn close(&mut self, amount: U256, signature: [u8; 65]) -> Result<()> { - self.close_inner(amount, signature)?; - self.env().terminate_contract(self.sender); - } - - /// We split this out in order to make testing `close` simpler. - fn close_inner(&mut self, amount: U256, signature: [u8; 65]) -> Result<()> { - if self.env().caller() != self.recipient { - return Err(Error::CallerIsNotRecipient) - } - - if amount < self.withdrawn { - return Err(Error::AmountIsLessThanWithdrawn) - } - - // Signature validation - if !self.is_signature_valid(amount, signature) { - return Err(Error::InvalidSignature) - } - - // We checked that amount >= self.withdrawn - #[allow(clippy::arithmetic_side_effects)] - self.env() - .transfer(self.recipient, amount - self.withdrawn) - .map_err(|_| Error::TransferFailed)?; - - Ok(()) - } - - /// If the `sender` wishes to close the channel and withdraw the funds they can - /// do so by setting the `expiration`. If the `expiration` is reached, the - /// sender will be able to call `claim_timeout` to claim the remaining funds - /// and the channel will be terminated. This emits an event that the recipient can - /// listen to in order to withdraw the funds before the `expiration`. - #[ink(message)] - pub fn start_sender_close(&mut self) -> Result<()> { - if self.env().caller() != self.sender { - return Err(Error::CallerIsNotSender) - } - - let now = self.env().block_timestamp(); - let expiration = now.checked_add(self.close_duration).unwrap(); - - self.env().emit_event(SenderCloseStarted { - expiration, - close_duration: self.close_duration, - }); - - self.expiration = Some(expiration); - - Ok(()) - } - - /// If the timeout is reached (`current_time >= expiration`) without the - /// recipient closing the channel, then the remaining balance is released - /// back to the `sender`. - #[ink(message)] - pub fn claim_timeout(&mut self) -> Result<()> { - match self.expiration { - Some(expiration) => { - // expiration is set. Check if it's reached and if so, release the - // funds and terminate the contract. - let now = self.env().block_timestamp(); - if now < expiration { - return Err(Error::NotYetExpired) - } - - self.env().terminate_contract(self.sender); - } - - None => Err(Error::NotYetExpired), - } - } - - /// The `recipient` can withdraw the funds from the channel at any time. - #[ink(message)] - pub fn withdraw(&mut self, amount: U256, signature: [u8; 65]) -> Result<()> { - if self.env().caller() != self.recipient { - return Err(Error::CallerIsNotRecipient) - } - - // Signature validation - if !self.is_signature_valid(amount, signature) { - return Err(Error::InvalidSignature) - } - - // Make sure there's something to withdraw (guards against underflow) - if amount < self.withdrawn { - return Err(Error::AmountIsLessThanWithdrawn) - } - - // We checked that amount >= self.withdrawn - #[allow(clippy::arithmetic_side_effects)] - let amount_to_withdraw = amount - self.withdrawn; - self.withdrawn.checked_add(amount_to_withdraw).unwrap(); - - self.env() - .transfer(self.recipient, amount_to_withdraw) - .map_err(|_| Error::TransferFailed)?; - - Ok(()) - } - - /// Returns the `sender` of the contract. - #[ink(message)] - pub fn get_sender(&self) -> Address { - self.sender - } - - /// Returns the `recipient` of the contract. - #[ink(message)] - pub fn get_recipient(&self) -> Address { - self.recipient - } - - /// Returns the `expiration` of the contract. - #[ink(message)] - pub fn get_expiration(&self) -> Option { - self.expiration - } - - /// Returns the `withdrawn` amount of the contract. - #[ink(message)] - pub fn get_withdrawn(&self) -> U256 { - self.withdrawn - } - - /// Returns the `close_duration` of the contract. - #[ink(message)] - pub fn get_close_duration(&self) -> Timestamp { - self.close_duration - } - - /// Returns the `balance` of the contract. - #[ink(message)] - pub fn get_balance(&self) -> U256 { - self.env().balance() - } - } - - #[ink(impl)] - impl PaymentChannel { - fn is_signature_valid(&self, amount: U256, signature: [u8; 65]) -> bool { - let encodable = (self.env().address(), amount); - let mut message = - ::Type::default(); - ink::env::hash_encoded::( - &encodable, - &mut message, - ); - - let mut pub_key = [0; 33]; - ink::env::ecdsa_recover(&signature, &message, &mut pub_key) - .unwrap_or_else(|err| panic!("recover failed: {err:?}")); - let mut signature_account_id = [0u8; 32]; - ::hash( - &pub_key, - &mut signature_account_id, - ); - - self.recipient - == ink::primitives::AccountIdMapper::to_address(&signature_account_id) - } - } - - #[cfg(test)] - mod tests { - use super::*; - - use hex_literal; - use sp_core::{ - Encode, - Pair, - }; - - fn default_accounts() -> ink::env::test::DefaultAccounts { - ink::env::test::default_accounts() - } - - fn set_next_caller(caller: Address) { - ink::env::test::set_caller(caller); - } - - fn set_contract_balance(addr: Address, balance: U256) { - ink::env::test::set_contract_balance(addr, balance); - } - - fn get_contract_balance(addr: Address) -> U256 { - ink::env::test::get_contract_balance::(addr) - .expect("Cannot get contract balance") - } - - fn advance_block() { - ink::env::test::advance_block::(); - } - - fn get_current_time() -> Timestamp { - let since_the_epoch = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("Time went backwards"); - since_the_epoch.as_secs() - + since_the_epoch.subsec_nanos() as u64 / 1_000_000_000 - } - - fn get_dan() -> Address { - // Use Dan's seed - // `subkey inspect //Dan --scheme Ecdsa --output-type json | jq .secretSeed` - let seed = hex_literal::hex!( - "c31fa562972de437802e0df146b16146349590b444db41f7e3eb9deedeee6f64" - ); - let pair = sp_core::ecdsa::Pair::from_seed(&seed); - let pub_key = pair.public(); - let compressed_pub_key: [u8; 33] = pub_key.encode()[..] - .try_into() - .expect("slice with incorrect length"); - let mut account_id = [0; 32]; - ::hash( - &compressed_pub_key, - &mut account_id, - ); - ink::primitives::AccountIdMapper::to_address(&account_id) - } - - fn contract_id() -> Address { - let accounts = default_accounts(); - let contract_id = accounts.charlie; - ink::env::test::set_callee(contract_id); - contract_id - } - - fn sign(contract_id: Address, amount: U256) -> [u8; 65] { - let encodable = (contract_id, amount); - let mut hash = - ::Type::default(); // 256-bit buffer - ink::env::hash_encoded::(&encodable, &mut hash); - - // Use Dan's seed - // `subkey inspect //Dan --scheme Ecdsa --output-type json | jq .secretSeed` - let seed = hex_literal::hex!( - "c31fa562972de437802e0df146b16146349590b444db41f7e3eb9deedeee6f64" - ); - let pair = sp_core::ecdsa::Pair::from_seed(&seed); - - let signature = pair.sign_prehashed(&hash); - signature.0 - } - - #[ink::test] - fn test_deposit() { - // given - let accounts = default_accounts(); - let initial_balance = 10_000.into(); - let close_duration = 360_000; - let mock_deposit_value = 1_000.into(); - set_contract_balance(accounts.alice, initial_balance); - set_contract_balance(accounts.bob, initial_balance); - - // when - // Push the new execution context with Alice as the caller and - // the `mock_deposit_value` as the value deposited. - // Note: Currently there is no way to transfer funds to the contract. - set_next_caller(accounts.alice); - let payment_channel = PaymentChannel::new(accounts.bob, close_duration); - let contract_id = contract_id(); - set_contract_balance(contract_id, mock_deposit_value); - - // then - assert_eq!(payment_channel.get_balance(), mock_deposit_value); - } - - #[ink::test] - fn test_close() { - // given - let accounts = default_accounts(); - let dan = get_dan(); - let close_duration = 360_000; - let mock_deposit_value = 1_000.into(); - let amount = 500.into(); - let initial_balance = 10_000.into(); - set_contract_balance(accounts.alice, initial_balance); - set_contract_balance(dan, initial_balance); - - // when - set_next_caller(accounts.alice); - let mut payment_channel = PaymentChannel::new(dan, close_duration); - let contract_id = contract_id(); - set_contract_balance(contract_id, mock_deposit_value); - set_next_caller(dan); - let signature = sign(contract_id, amount); - - // then - let should_close = move || payment_channel.close(amount, signature).unwrap(); - ink::env::test::assert_contract_termination::( - should_close, - accounts.alice, - amount, - ); - assert_eq!(get_contract_balance(dan), initial_balance + amount); - } - - #[ink::test] - fn close_fails_invalid_signature() { - // given - let accounts = default_accounts(); - let dan = get_dan(); - let mock_deposit_value = 1_000.into(); - let close_duration = 360_000; - let amount = 400.into(); - let unexpected_amount = amount + U256::from(1); - let initial_balance = 10_000.into(); - set_contract_balance(accounts.alice, initial_balance); - set_contract_balance(dan, initial_balance); - - // when - set_next_caller(accounts.alice); - let mut payment_channel = PaymentChannel::new(dan, close_duration); - let contract_id = contract_id(); - set_contract_balance(contract_id, mock_deposit_value); - set_next_caller(dan); - let signature = sign(contract_id, amount); - - // then - let res = payment_channel.close_inner(unexpected_amount, signature); - assert!(res.is_err(), "Expected an error, got {res:?} instead."); - assert_eq!(res.unwrap_err(), Error::InvalidSignature,); - } - - #[ink::test] - fn test_withdraw() { - // given - let accounts = default_accounts(); - let dan = get_dan(); - let initial_balance = 10_000.into(); - let mock_deposit_value = 1_000.into(); - let close_duration = 360_000; - let amount = 500.into(); - set_contract_balance(accounts.alice, initial_balance); - set_contract_balance(dan, initial_balance); - - // when - set_next_caller(accounts.alice); - let mut payment_channel = PaymentChannel::new(dan, close_duration); - let contract_id = contract_id(); - set_contract_balance(contract_id, mock_deposit_value); - - set_next_caller(dan); - let signature = sign(contract_id, amount); - payment_channel - .withdraw(amount, signature) - .expect("withdraw failed"); - - // then - assert_eq!(payment_channel.get_balance(), amount); - assert_eq!(get_contract_balance(dan), initial_balance + amount); - } - - #[ink::test] - fn withdraw_fails_invalid_signature() { - // given - let accounts = default_accounts(); - let dan = get_dan(); - let initial_balance = 10_000.into(); - let close_duration = 360_000; - let amount = 400.into(); - let unexpected_amount = amount + U256::from(1); - let mock_deposit_value = 1_000.into(); - set_contract_balance(accounts.alice, initial_balance); - set_contract_balance(dan, initial_balance); - - // when - set_next_caller(accounts.alice); - let mut payment_channel = PaymentChannel::new(dan, close_duration); - let contract_id = contract_id(); - set_contract_balance(contract_id, mock_deposit_value); - set_next_caller(dan); - let signature = sign(contract_id, amount); - - // then - let res = payment_channel.withdraw(unexpected_amount, signature); - assert!(res.is_err(), "Expected an error, got {res:?} instead."); - assert_eq!(res.unwrap_err(), Error::InvalidSignature,); - } - - #[ink::test] - fn test_start_sender_close() { - // given - let accounts = default_accounts(); - let initial_balance = 10_000.into(); - let mock_deposit_value = 1_000.into(); - let close_duration = 1; - set_contract_balance(accounts.alice, initial_balance); - set_contract_balance(accounts.bob, initial_balance); - - // when - set_next_caller(accounts.alice); - let mut payment_channel = PaymentChannel::new(accounts.bob, close_duration); - let contract_id = contract_id(); - set_contract_balance(contract_id, mock_deposit_value); - - payment_channel - .start_sender_close() - .expect("start_sender_close failed"); - advance_block(); - - // then - let now = get_current_time(); - assert!(now > payment_channel.get_expiration().unwrap()); - } - - #[ink::test] - fn test_claim_timeout() { - // given - let accounts = default_accounts(); - let initial_balance = 10_000.into(); - let close_duration = 1; - let mock_deposit_value = 1_000.into(); - set_contract_balance(accounts.alice, initial_balance); - set_contract_balance(accounts.bob, initial_balance); - - // when - set_next_caller(accounts.alice); - let contract_id = contract_id(); - let mut payment_channel = PaymentChannel::new(accounts.bob, close_duration); - set_contract_balance(contract_id, mock_deposit_value); - - payment_channel - .start_sender_close() - .expect("start_sender_close failed"); - advance_block(); - - // then - let should_close = move || payment_channel.claim_timeout().unwrap(); - ink::env::test::assert_contract_termination::( - should_close, - accounts.alice, - mock_deposit_value, - ); - assert_eq!( - get_contract_balance(accounts.alice), - initial_balance + mock_deposit_value - ); - } - - #[ink::test] - fn test_getters() { - // given - let accounts = default_accounts(); - let initial_balance = 10_000.into(); - let mock_deposit_value = 1_000.into(); - let close_duration = 360_000; - set_contract_balance(accounts.alice, initial_balance); - set_contract_balance(accounts.bob, initial_balance); - - // when - set_next_caller(accounts.alice); - let contract_id = contract_id(); - let payment_channel = PaymentChannel::new(accounts.bob, close_duration); - set_contract_balance(contract_id, mock_deposit_value); - - // then - assert_eq!(payment_channel.get_sender(), accounts.alice); - assert_eq!(payment_channel.get_recipient(), accounts.bob); - assert_eq!(payment_channel.get_balance(), mock_deposit_value); - assert_eq!(payment_channel.get_close_duration(), close_duration); - assert_eq!(payment_channel.get_withdrawn(), U256::zero()); - } - } -} diff --git a/integration-tests/public/assets-precompile/Cargo.toml b/integration-tests/public/runtime/assets-precompile/Cargo.toml similarity index 100% rename from integration-tests/public/assets-precompile/Cargo.toml rename to integration-tests/public/runtime/assets-precompile/Cargo.toml diff --git a/integration-tests/public/assets-precompile/README.md b/integration-tests/public/runtime/assets-precompile/README.md similarity index 100% rename from integration-tests/public/assets-precompile/README.md rename to integration-tests/public/runtime/assets-precompile/README.md diff --git a/integration-tests/public/runtime/assets-precompile/lib.rs b/integration-tests/public/runtime/assets-precompile/lib.rs new file mode 100644 index 00000000000..f543a3a1487 --- /dev/null +++ b/integration-tests/public/runtime/assets-precompile/lib.rs @@ -0,0 +1,153 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +use ink::{ + H160, + U256, + prelude::string::ToString, +}; +pub use ink_precompiles::erc20::{ + AssetId, + erc20, +}; + +#[ink::contract] +mod asset_hub_precompile { + use super::*; + use ink::prelude::string::String; + use ink_precompiles::erc20::{ + Erc20, + Erc20Ref, + }; + + #[ink(storage)] + pub struct AssetHubPrecompile { + asset_id: AssetId, + /// The owner of this contract. Only the owner can call transfer, approve, and + /// transfer_from. This is necessary because the contract holds tokens + /// and without access control, anyone could transfer tokens that the + /// contract holds, which would be a security issue. + owner: H160, + precompile: Erc20Ref, + } + + impl AssetHubPrecompile { + /// Creates a new contract instance for a specific asset ID. + #[ink(constructor, payable)] + pub fn new(asset_id: AssetId) -> Self { + Self { + asset_id, + owner: Self::env().caller(), + precompile: erc20(TRUST_BACKED_ASSETS_PRECOMPILE_INDEX, asset_id), + } + } + + /// Returns the asset ID this contract is configured for. + #[ink(message)] + pub fn asset_id(&self) -> AssetId { + self.asset_id + } + + /// Returns the owner of this contract. + #[ink(message)] + pub fn owner(&self) -> H160 { + self.owner + } + + /// Ensures only the owner can call this function. + fn ensure_owner(&self) -> Result<(), String> { + if self.env().caller() != self.owner { + return Err("Only owner can call this function".to_string()); + } + Ok(()) + } + + /// Gets the total supply by calling the precompile. + #[ink(message)] + pub fn total_supply(&self) -> U256 { + self.precompile.totalSupply() + } + + /// Gets the balance of an account. + #[ink(message)] + pub fn balance_of(&self, account: Address) -> U256 { + self.precompile.balanceOf(account) + } + + /// Transfers tokens to another account. + #[ink(message)] + pub fn transfer(&mut self, to: Address, value: U256) -> Result { + self.ensure_owner()?; + if !self.precompile.transfer(to, value) { + return Err("Transfer failed".to_string()); + } + self.env().emit_event(Transfer { + from: self.env().address(), + to, + value, + }); + Ok(true) + } + + /// Approves a spender. + #[ink(message)] + pub fn approve(&mut self, spender: Address, value: U256) -> Result { + self.ensure_owner()?; + if !self.precompile.approve(spender, value) { + return Err("Approval failed".to_string()); + } + self.env().emit_event(Approval { + owner: self.env().address(), + spender, + value, + }); + Ok(true) + } + + /// Gets the allowance for a spender. + #[ink(message)] + pub fn allowance(&self, owner: Address, spender: Address) -> U256 { + self.precompile.allowance(owner, spender) + } + + /// Transfers tokens from one account to another using allowance. + #[ink(message)] + pub fn transfer_from( + &mut self, + from: Address, + to: Address, + value: U256, + ) -> Result { + self.ensure_owner()?; + if !self.precompile.transferFrom(from, to, value) { + return Err("Transfer failed".to_string()); + } + self.env().emit_event(Transfer { from, to, value }); + Ok(true) + } + } + + /// Event emitted when allowance by `owner` to `spender` changes. + #[derive(Debug, PartialEq)] + #[ink::event] + pub struct Approval { + #[ink(topic)] + pub owner: Address, + #[ink(topic)] + pub spender: Address, + pub value: U256, + } + + /// Event emitted when transfer of tokens occurs. + #[derive(Debug, PartialEq)] + #[ink::event] + pub struct Transfer { + #[ink(topic)] + pub from: Address, + #[ink(topic)] + pub to: Address, + pub value: U256, + } +} + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/assets-precompile/lib.rs b/integration-tests/public/runtime/assets-precompile/tests.rs similarity index 66% rename from integration-tests/public/assets-precompile/lib.rs rename to integration-tests/public/runtime/assets-precompile/tests.rs index 86d455b3822..e054153ad82 100644 --- a/integration-tests/public/assets-precompile/lib.rs +++ b/integration-tests/public/runtime/assets-precompile/tests.rs @@ -1,177 +1,24 @@ -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -use ink::{ - H160, - U256, - prelude::string::ToString, -}; -pub use ink_precompiles::erc20::{ - AssetId, - erc20, -}; - -#[ink::contract] -mod asset_hub_precompile { - use super::*; - use ink::prelude::string::String; - use ink_precompiles::erc20::{ - Erc20, - Erc20Ref, - }; +use super::*; +use ink::{H160, U256}; - #[ink(storage)] - pub struct AssetHubPrecompile { - asset_id: AssetId, - /// The owner of this contract. Only the owner can call transfer, approve, and - /// transfer_from. This is necessary because the contract holds tokens - /// and without access control, anyone could transfer tokens that the - /// contract holds, which would be a security issue. - owner: H160, - precompile: Erc20Ref, - } - - impl AssetHubPrecompile { - /// Creates a new contract instance for a specific asset ID. - #[ink(constructor, payable)] - pub fn new(asset_id: AssetId) -> Self { - Self { - asset_id, - owner: Self::env().caller(), - precompile: erc20(TRUST_BACKED_ASSETS_PRECOMPILE_INDEX, asset_id), - } - } - - /// Returns the asset ID this contract is configured for. - #[ink(message)] - pub fn asset_id(&self) -> AssetId { - self.asset_id - } - - /// Returns the owner of this contract. - #[ink(message)] - pub fn owner(&self) -> H160 { - self.owner - } - - /// Ensures only the owner can call this function. - fn ensure_owner(&self) -> Result<(), String> { - if self.env().caller() != self.owner { - return Err("Only owner can call this function".to_string()); - } - Ok(()) - } - - /// Gets the total supply by calling the precompile. - #[ink(message)] - pub fn total_supply(&self) -> U256 { - self.precompile.totalSupply() - } - - /// Gets the balance of an account. - #[ink(message)] - pub fn balance_of(&self, account: Address) -> U256 { - self.precompile.balanceOf(account) - } - - /// Transfers tokens to another account. - #[ink(message)] - pub fn transfer(&mut self, to: Address, value: U256) -> Result { - self.ensure_owner()?; - if !self.precompile.transfer(to, value) { - return Err("Transfer failed".to_string()); - } - self.env().emit_event(Transfer { - from: self.env().address(), - to, - value, - }); - Ok(true) - } - - /// Approves a spender. - #[ink(message)] - pub fn approve(&mut self, spender: Address, value: U256) -> Result { - self.ensure_owner()?; - if !self.precompile.approve(spender, value) { - return Err("Approval failed".to_string()); - } - self.env().emit_event(Approval { - owner: self.env().address(), - spender, - value, - }); - Ok(true) - } - - /// Gets the allowance for a spender. - #[ink(message)] - pub fn allowance(&self, owner: Address, spender: Address) -> U256 { - self.precompile.allowance(owner, spender) - } - - /// Transfers tokens from one account to another using allowance. - #[ink(message)] - pub fn transfer_from( - &mut self, - from: Address, - to: Address, - value: U256, - ) -> Result { - self.ensure_owner()?; - if !self.precompile.transferFrom(from, to, value) { - return Err("Transfer failed".to_string()); - } - self.env().emit_event(Transfer { from, to, value }); - Ok(true) - } - } +#[test] +fn contract_stores_asset_id() { + use asset_hub_precompile::AssetHubPrecompile; - /// Event emitted when allowance by `owner` to `spender` changes. - #[derive(Debug, PartialEq)] - #[ink::event] - pub struct Approval { - #[ink(topic)] - pub owner: Address, - #[ink(topic)] - pub spender: Address, - pub value: U256, - } + let contract = AssetHubPrecompile::new(1337); - /// Event emitted when transfer of tokens occurs. - #[derive(Debug, PartialEq)] - #[ink::event] - pub struct Transfer { - #[ink(topic)] - pub from: Address, - #[ink(topic)] - pub to: Address, - pub value: U256, - } + assert_eq!(contract.asset_id(), 1337); } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn contract_stores_asset_id() { - use asset_hub_precompile::AssetHubPrecompile; - - let contract = AssetHubPrecompile::new(1337); +#[test] +fn contract_stores_owner() { + use asset_hub_precompile::AssetHubPrecompile; - assert_eq!(contract.asset_id(), 1337); - } - - #[test] - fn contract_stores_owner() { - use asset_hub_precompile::AssetHubPrecompile; - - let contract = AssetHubPrecompile::new(1337); + let contract = AssetHubPrecompile::new(1337); - assert_eq!(contract.asset_id(), 1337); - // Note: In unit tests, the caller is always the zero address - assert_eq!(contract.owner(), H160::from([0u8; 20])); - } + assert_eq!(contract.asset_id(), 1337); + // Note: In unit tests, the caller is always the zero address + assert_eq!(contract.owner(), H160::from([0u8; 20])); } #[cfg(all(test, feature = "e2e-tests"))] @@ -504,4 +351,4 @@ mod e2e_tests { assert_noop!(result, "Unapproved"); Ok(()) } -} +} \ No newline at end of file diff --git a/integration-tests/public/runtime-call-contract/Cargo.toml b/integration-tests/public/runtime/call-contract/Cargo.toml similarity index 100% rename from integration-tests/public/runtime-call-contract/Cargo.toml rename to integration-tests/public/runtime/call-contract/Cargo.toml diff --git a/integration-tests/public/runtime-call-contract/custom-runtime/Cargo.toml b/integration-tests/public/runtime/call-contract/custom-runtime/Cargo.toml similarity index 100% rename from integration-tests/public/runtime-call-contract/custom-runtime/Cargo.toml rename to integration-tests/public/runtime/call-contract/custom-runtime/Cargo.toml diff --git a/integration-tests/public/runtime-call-contract/custom-runtime/pallet-revive-caller/Cargo.toml b/integration-tests/public/runtime/call-contract/custom-runtime/pallet-revive-caller/Cargo.toml similarity index 100% rename from integration-tests/public/runtime-call-contract/custom-runtime/pallet-revive-caller/Cargo.toml rename to integration-tests/public/runtime/call-contract/custom-runtime/pallet-revive-caller/Cargo.toml diff --git a/integration-tests/public/runtime-call-contract/custom-runtime/pallet-revive-caller/src/executor.rs b/integration-tests/public/runtime/call-contract/custom-runtime/pallet-revive-caller/src/executor.rs similarity index 100% rename from integration-tests/public/runtime-call-contract/custom-runtime/pallet-revive-caller/src/executor.rs rename to integration-tests/public/runtime/call-contract/custom-runtime/pallet-revive-caller/src/executor.rs diff --git a/integration-tests/public/runtime-call-contract/custom-runtime/pallet-revive-caller/src/lib.rs b/integration-tests/public/runtime/call-contract/custom-runtime/pallet-revive-caller/src/lib.rs similarity index 100% rename from integration-tests/public/runtime-call-contract/custom-runtime/pallet-revive-caller/src/lib.rs rename to integration-tests/public/runtime/call-contract/custom-runtime/pallet-revive-caller/src/lib.rs diff --git a/integration-tests/public/runtime-call-contract/custom-runtime/src/lib.rs b/integration-tests/public/runtime/call-contract/custom-runtime/src/lib.rs similarity index 100% rename from integration-tests/public/runtime-call-contract/custom-runtime/src/lib.rs rename to integration-tests/public/runtime/call-contract/custom-runtime/src/lib.rs diff --git a/integration-tests/public/runtime-call-contract/e2e_tests.rs b/integration-tests/public/runtime/call-contract/e2e_tests.rs similarity index 100% rename from integration-tests/public/runtime-call-contract/e2e_tests.rs rename to integration-tests/public/runtime/call-contract/e2e_tests.rs diff --git a/integration-tests/public/runtime-call-contract/lib.rs b/integration-tests/public/runtime/call-contract/lib.rs similarity index 100% rename from integration-tests/public/runtime-call-contract/lib.rs rename to integration-tests/public/runtime/call-contract/lib.rs diff --git a/integration-tests/public/runtime-call-contract/traits/Cargo.toml b/integration-tests/public/runtime/call-contract/traits/Cargo.toml similarity index 100% rename from integration-tests/public/runtime-call-contract/traits/Cargo.toml rename to integration-tests/public/runtime/call-contract/traits/Cargo.toml diff --git a/integration-tests/public/runtime-call-contract/traits/lib.rs b/integration-tests/public/runtime/call-contract/traits/lib.rs similarity index 100% rename from integration-tests/public/runtime-call-contract/traits/lib.rs rename to integration-tests/public/runtime/call-contract/traits/lib.rs diff --git a/integration-tests/public/e2e-call-runtime/Cargo.toml b/integration-tests/public/runtime/e2e-call/Cargo.toml similarity index 100% rename from integration-tests/public/e2e-call-runtime/Cargo.toml rename to integration-tests/public/runtime/e2e-call/Cargo.toml diff --git a/integration-tests/public/runtime/e2e-call/lib.rs b/integration-tests/public/runtime/e2e-call/lib.rs new file mode 100644 index 00000000000..f0b15742287 --- /dev/null +++ b/integration-tests/public/runtime/e2e-call/lib.rs @@ -0,0 +1,23 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +pub mod e2e_call_runtime { + #[ink(storage)] + #[derive(Default)] + pub struct Contract {} + + impl Contract { + #[ink(constructor)] + pub fn new() -> Self { + Self {} + } + + #[ink(message)] + pub fn get_contract_balance(&self) -> ink::U256 { + self.env().balance() + } + } +} + +#[cfg(all(test, feature = "e2e-tests"))] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/runtime/e2e-call/tests.rs b/integration-tests/public/runtime/e2e-call/tests.rs new file mode 100644 index 00000000000..51e65ffcdea --- /dev/null +++ b/integration-tests/public/runtime/e2e-call/tests.rs @@ -0,0 +1,75 @@ +use crate::e2e_call_runtime::{Contract, ContractRef}; +use ink::env::Environment; +use ink_e2e::{ + ChainBackend, + ContractsBackend, + subxt::dynamic::Value, +}; +use static_assertions::assert_type_eq_all; + +type E2EResult = std::result::Result>; + +#[ink_e2e::test] +async fn call_runtime_works(mut client: Client) -> E2EResult<()> { + // given + let mut constructor = ContractRef::new(); + let contract = client + .instantiate("e2e_call_runtime", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + + let account_id = client.to_account_id(&contract.addr).await?; + assert_eq!(account_id, contract.account_id); + + let mut call_builder = contract.call_builder::(); + + // The generic `Environment::Balance` type must be `u128` + // for this test to work. This is because we encode `Value::u128` + // in the `call_data`. + assert_type_eq_all!(Balance, u128); + let transfer_amount: u128 = 100_000_000_000; + + // when + let call_data = vec![ + // A value representing a `MultiAddress`. We want the + // "Id" variant, and that will ultimately contain the + // bytes for our destination address + Value::unnamed_variant("Id", [Value::from_bytes(contract.account_id)]), + // A value representing the amount we'd like to transfer. + Value::u128(transfer_amount), + ]; + + let get_balance = call_builder.get_contract_balance(); + let pre_balance = client + .call(&ink_e2e::alice(), &get_balance) + .dry_run() + .await? + .return_value(); + + // Send funds from Alice to the contract using Balances::transfer + client + .runtime_call( + &ink_e2e::alice(), + "Balances", + "transfer_allow_death", + call_data, + ) + .await + .expect("runtime call failed"); + + // then + let get_balance = call_builder.get_contract_balance(); + let get_balance_res = client + .call(&ink_e2e::alice(), &get_balance) + .dry_run() + .await?; + + assert_eq!( + get_balance_res.return_value(), + pre_balance + + ink::env::DefaultEnvironment::native_to_eth(transfer_amount) + ); + + Ok(()) +} \ No newline at end of file diff --git a/integration-tests/public/precompile-demo/Cargo.toml b/integration-tests/public/runtime/precompile/Cargo.toml similarity index 100% rename from integration-tests/public/precompile-demo/Cargo.toml rename to integration-tests/public/runtime/precompile/Cargo.toml diff --git a/integration-tests/public/precompile-demo/lib.rs b/integration-tests/public/runtime/precompile/lib.rs similarity index 62% rename from integration-tests/public/precompile-demo/lib.rs rename to integration-tests/public/runtime/precompile/lib.rs index e3a0e324978..08f4272ddf1 100644 --- a/integration-tests/public/precompile-demo/lib.rs +++ b/integration-tests/public/runtime/precompile/lib.rs @@ -29,7 +29,7 @@ pub trait System { } #[ink::contract] -mod precompile_demo { +pub mod precompile_demo { use super::System; use ink::prelude::vec::Vec; @@ -56,39 +56,7 @@ mod precompile_demo { out_bytes.0 } } - - #[cfg(all(test, feature = "e2e-tests"))] - mod e2e_tests { - use super::*; - use ink_e2e::ContractsBackend; - - type E2EResult = std::result::Result>; - - #[ink_e2e::test] - async fn call_echo_works(mut client: ink_e2e::Client) -> E2EResult<()> { - // given - let mut constructor = PrecompileDemoRef::new(); - let contract = client - .instantiate("precompile_demo", &ink_e2e::bob(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let call_builder = contract.call_builder::(); - - // when - let data = vec![0x1, 0x2, 0x3, 0x4]; - let expected = data.clone(); - let call_echo = call_builder.call_echo(data); - let res = client - .call(&ink_e2e::bob(), &call_echo) - .submit() - .await - .expect("call_echo failed"); - - // then - assert_eq!(res.return_value(), expected); - - Ok(()) - } - } } + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/runtime/precompile/tests.rs b/integration-tests/public/runtime/precompile/tests.rs new file mode 100644 index 00000000000..3ec27a877e5 --- /dev/null +++ b/integration-tests/public/runtime/precompile/tests.rs @@ -0,0 +1,31 @@ +use super::precompile_demo::{PrecompileDemo, PrecompileDemoRef}; +use ink_e2e::ContractsBackend; + +type E2EResult = std::result::Result>; + +#[ink_e2e::test] +async fn call_echo_works(mut client: Client) -> E2EResult<()> { + // Given + let mut constructor = PrecompileDemoRef::new(); + let contract = client + .instantiate("precompile_demo", &ink_e2e::bob(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let call_builder = contract.call_builder::(); + + // When + let data = vec![0x1, 0x2, 0x3, 0x4]; + let expected = data.clone(); + let call_echo = call_builder.call_echo(data); + let res = client + .call(&ink_e2e::bob(), &call_echo) + .submit() + .await + .expect("call_echo failed"); + + // Then + assert_eq!(res.return_value(), expected); + + Ok(()) +} \ No newline at end of file diff --git a/integration-tests/public/contract-xcm/Cargo.toml b/integration-tests/public/runtime/xcm/Cargo.toml similarity index 100% rename from integration-tests/public/contract-xcm/Cargo.toml rename to integration-tests/public/runtime/xcm/Cargo.toml diff --git a/integration-tests/public/runtime/xcm/lib.rs b/integration-tests/public/runtime/xcm/lib.rs new file mode 100644 index 00000000000..76bdcee80ab --- /dev/null +++ b/integration-tests/public/runtime/xcm/lib.rs @@ -0,0 +1,150 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +mod contract_xcm { + use ink::xcm::prelude::*; + + /// A contract demonstrating usage of the XCM API. + #[ink(storage)] + #[derive(Default)] + pub struct ContractXcm; + + #[derive(Debug, PartialEq, Eq)] + #[ink::scale_derive(Encode, Decode, TypeInfo)] + pub enum RuntimeError { + XcmExecuteFailed, + XcmSendFailed, + } + + impl ContractXcm { + /// The constructor is `payable`, so that during instantiation it can be given + /// some tokens that will be further transferred when transferring funds through + /// XCM. + #[ink(constructor, payable)] + pub fn new() -> Self { + Default::default() + } + + /// Tries to transfer `value` from the contract's balance to `receiver` + /// on the SAME chain using XCM execution. + /// + /// This demonstrates `xcm_execute`, which executes an XCM message locally. + #[ink(message)] + pub fn transfer_through_xcm( + &mut self, + receiver: AccountId, + value: Balance, + ) -> Result<(), RuntimeError> { + // Define the asset as the native currency (Parent) with amount `value`. + let asset: Asset = (Parent, value).into(); + + // Define beneficiary on the current network. + let beneficiary = AccountId32 { + network: None, + id: *receiver.as_ref(), + }; + + // Build the XCM message: + // 1. Withdraw asset from contract's account. + // 2. Buy execution time (gas) for the XCM VM. + // 3. Deposit the asset into the beneficiary's account. + let message: ink::xcm::v5::Xcm<()> = Xcm::builder() + .withdraw_asset(asset.clone()) + .buy_execution(asset.clone(), Unlimited) + .deposit_asset(asset, beneficiary) + .build(); + let msg = VersionedXcm::V5(message); + + // Calculate the weight (gas) required for this XCM message. + let weight = self.env().xcm_weigh(&msg).expect("weight should work"); + + // Execute the XCM message locally. + self.env() + .xcm_execute(&msg, weight) + .map_err(|_| RuntimeError::XcmExecuteFailed) + } + + /// Transfer some funds to the relay chain via XCM using `xcm_send`. + /// + /// This sends an XCM message to another chain (the Parent/Relay Chain). + #[ink(message)] + pub fn send_funds( + &mut self, + value: Balance, + fee: Balance, + ) -> Result<(), RuntimeError> { + // Target destination: The Parent chain (Relay Chain). + let destination: ink::xcm::v5::Location = ink::xcm::v5::Parent.into(); + + // Asset: Native token of the relay chain (represented as Here relative to Parent). + let asset: Asset = (Here, value).into(); + + // Beneficiary: The caller's account on the Relay Chain. + let caller_account_id = self.env().to_account_id(self.env().caller()); + let beneficiary = AccountId32 { + network: None, + id: caller_account_id.0, + }; + + // Build XCM: + // 1. Withdraw asset from this chain's sovereign account on the Relay Chain. + // 2. Buy execution on the Relay Chain using the withdrawn asset. + // 3. Deposit asset to the caller's account on the Relay Chain. + let message: Xcm<()> = Xcm::builder() + .withdraw_asset(asset.clone()) + .buy_execution((Here, fee), WeightLimit::Unlimited) + .deposit_asset(asset, beneficiary) + .build(); + + // Send the message to the Relay Chain. + self.env() + .xcm_send( + &VersionedLocation::V5(destination), + &VersionedXcm::V5(message), + ) + .map_err(|_| RuntimeError::XcmSendFailed) + } + + /// Initiates a reserve transfer, burning tokens here and releasing them on the Parent chain. + #[ink(message)] + pub fn reserve_transfer( + &mut self, + amount: Balance, + fee: Balance, + ) -> Result<(), RuntimeError> { + let caller_account_id = self.env().to_account_id(self.env().caller()); + let beneficiary: Location = AccountId32 { + network: None, + id: caller_account_id.0, + } + .into(); + + // Build XCM using `builder_unsafe` for advanced operations like reserve transfers. + let message: Xcm<()> = Xcm::builder_unsafe() + // Withdraw the derivative token (Parent) from contract's local account. + .withdraw_asset((Parent, amount)) + + // Burn the local derivative and send an instruction to the Reserve (Parent) + // to release the real asset to the beneficiary. + .initiate_reserve_withdraw( + All, + Parent, + Xcm::builder_unsafe() + .buy_execution((Here, fee), Unlimited) + .deposit_asset(All, beneficiary) + .build(), + ) + .build(); + + let msg = VersionedXcm::V5(message); + let weight = self.env().xcm_weigh(&msg).expect("`xcm_weigh` failed"); + + self.env() + .xcm_execute(&msg, weight) + .map_err(|_| RuntimeError::XcmExecuteFailed) + } + } +} + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/runtime/xcm/tests.rs b/integration-tests/public/runtime/xcm/tests.rs new file mode 100644 index 00000000000..1b7388accda --- /dev/null +++ b/integration-tests/public/runtime/xcm/tests.rs @@ -0,0 +1,142 @@ +#[cfg(all(test, feature = "e2e-tests"))] +mod e2e_tests { + use super::contract_xcm::{ContractXcm, ContractXcmRef}; + use ink::primitives::AccountId; + use ink_e2e::{ + ChainBackend, + ContractsBackend, + }; + + type E2EResult = Result>; + + /// Tests that `xcm_execute` correctly transfers funds locally via XCM instructions. + #[ink_e2e::test] + async fn xcm_execute_works(mut client: Client) -> E2EResult<()> { + // Given: Instantiate contract with an initial endowment. + let mut constructor = ContractXcmRef::new(); + let contract = client + .instantiate("contract_xcm", &ink_e2e::alice(), &mut constructor) + .value(100_000_000_000) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + let receiver = AccountId::from(ink_e2e::bob().public_key().0); + + let contract_balance_before = client + .free_balance(contract.account_id) + .await + .expect("Failed to get account balance"); + let receiver_balance_before = client + .free_balance(receiver) + .await + .expect("Failed to get account balance"); + + // When: Execute XCM transfer of 100_000_000 units to receiver. + let amount = 100_000_000; + let transfer_message = call_builder.transfer_through_xcm(receiver, amount); + let call_res = client + .call(&ink_e2e::alice(), &transfer_message) + .submit() + .await + .expect("call failed"); + assert!(call_res.return_value().is_ok()); + + // Then: Verify balances updated correctly. + let contract_balance_after = client + .free_balance(contract.account_id) + .await + .expect("Failed to get account balance"); + let receiver_balance_after = client + .free_balance(receiver) + .await + .expect("Failed to get account balance"); + + assert_eq!(contract_balance_after, contract_balance_before - amount); + assert_eq!(receiver_balance_after, receiver_balance_before + amount); + + Ok(()) + } + + /// Tests that `xcm_execute` fails gracefully when funds are insufficient. + #[ink_e2e::test] + async fn xcm_execute_failure_detection_works( + mut client: Client, + ) -> E2EResult<()> { + // Sleep to avoid nonce collision with other tests using Alice. + std::thread::sleep(std::time::Duration::from_secs(10)); + + // Given: Instantiate contract. + let mut constructor = ContractXcmRef::new(); + let contract = client + .instantiate("contract_xcm", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + // When: Try to transfer `u128::MAX` (impossible amount). + let receiver = AccountId::from(ink_e2e::bob().public_key().0); + let amount = u128::MAX; + let transfer_message = call_builder.transfer_through_xcm(receiver, amount); + + // Then: The call should return an error indicating execution failure. + let call_res = client + .call(&ink_e2e::alice(), &transfer_message) + .submit() + .await; + assert!(call_res.is_err()); + + // Verify the specific error message. + let expected = "revert: XCM execute failed: message may be invalid or execution constraints not satisfied"; + assert!(format!("{:?}", call_res).contains(expected)); + + Ok(()) + } + + /// Tests that `xcm_send` successfully dispatches a message to the Relay Chain. + /// Note: This tests the *dispatch*, not the successful execution on the remote chain. + #[ink_e2e::test] + async fn xcm_send_works(mut client: Client) -> E2EResult<()> { + // Sleep to avoid nonce collision. + std::thread::sleep(std::time::Duration::from_secs(30)); + + // Given: Instantiate contract with funds. + let mut constructor = ContractXcmRef::new(); + let contract = client + .instantiate("contract_xcm", &ink_e2e::alice(), &mut constructor) + .value(100_000_000_000) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + let contract_balance_before = client + .free_balance(contract.account_id) + .await + .expect("Failed to get account balance"); + + // When: Send funds via XCM to the Relay Chain. + let amount = 100_000_000; + let transfer_message = call_builder.send_funds(amount, amount / 2); + let call_res = client + .call(&ink_e2e::alice(), &transfer_message) + .submit() + .await + .expect("call failed"); + assert!(call_res.return_value().is_ok()); + + // Then: Contract balance should decrease (amount sent + execution fees). + let contract_balance_after = client + .free_balance(contract.account_id) + .await + .expect("Failed to get account balance"); + + assert!( + contract_balance_after <= contract_balance_before - amount - (amount / 2) + ); + + Ok(()) + } +} \ No newline at end of file diff --git a/integration-tests/public/custom-allocator/Cargo.toml b/integration-tests/public/storage/allocator/Cargo.toml old mode 100755 new mode 100644 similarity index 100% rename from integration-tests/public/custom-allocator/Cargo.toml rename to integration-tests/public/storage/allocator/Cargo.toml diff --git a/integration-tests/public/custom-allocator/lib.rs b/integration-tests/public/storage/allocator/lib.rs old mode 100755 new mode 100644 similarity index 55% rename from integration-tests/public/custom-allocator/lib.rs rename to integration-tests/public/storage/allocator/lib.rs index f77aec9ffe0..1c1e89f64ef --- a/integration-tests/public/custom-allocator/lib.rs +++ b/integration-tests/public/storage/allocator/lib.rs @@ -2,7 +2,7 @@ //! //! This example demonstrates how to opt-out of the ink! provided global memory allocator. //! -//! We will use [`dlmalloc`](https://github.com/alexcrichton/dlmalloc-rs) instead. +//! We will use a custom bump allocator implementation as an example. //! //! ## Warning! //! @@ -30,15 +30,6 @@ #![feature(sync_unsafe_cell)] #![feature(allocator_api)] -// todo -// Here we set `dlmalloc` to be the global memory allocator. -// -// The [`GlobalAlloc`](https://doc.rust-lang.org/std/alloc/trait.GlobalAlloc.html) trait is -// important to understand if you're swapping our your allocator. -//#[cfg(not(feature = "std"))] -//#[global_allocator] -//static ALLOC: dlmalloc::GlobalDlmalloc = dlmalloc::GlobalDlmalloc; - use core::{ alloc::{ GlobalAlloc, @@ -139,86 +130,7 @@ mod custom_allocator { self.value[0] } } - - #[cfg(test)] - mod tests { - use super::*; - - #[ink::test] - fn default_works() { - let custom_allocator = CustomAllocator::default(); - assert!(!custom_allocator.get()); - } - - #[ink::test] - fn it_works() { - let mut custom_allocator = CustomAllocator::new(false); - assert!(!custom_allocator.get()); - custom_allocator.flip(); - assert!(custom_allocator.get()); - } - } - - #[cfg(all(test, feature = "e2e-tests"))] - mod e2e_tests { - use super::*; - use ink_e2e::ContractsBackend; - - type E2EResult = std::result::Result>; - - /// We test that we can upload and instantiate the contract using its default - /// constructor. - #[ink_e2e::test] - async fn default_works(mut client: Client) -> E2EResult<()> { - // Given - let mut constructor = CustomAllocatorRef::default(); - - // When - let contract = client - .instantiate("custom_allocator", &ink_e2e::alice(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let call_builder = contract.call_builder::(); - - // Then - let get = call_builder.get(); - let get_result = client.call(&ink_e2e::alice(), &get).dry_run().await?; - assert!(!get_result.return_value()); - - Ok(()) - } - - /// We test that we can read and write a value from the on-chain contract. - #[ink_e2e::test] - async fn it_works(mut client: Client) -> E2EResult<()> { - // Given - let mut constructor = CustomAllocatorRef::new(false); - let contract = client - .instantiate("custom_allocator", &ink_e2e::bob(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); - - let get = call_builder.get(); - let get_result = client.call(&ink_e2e::bob(), &get).dry_run().await?; - assert!(!get_result.return_value()); - - // When - let flip = call_builder.flip(); - let _flip_result = client - .call(&ink_e2e::bob(), &flip) - .submit() - .await - .expect("flip failed"); - - // Then - let get = call_builder.get(); - let get_result = client.call(&ink_e2e::bob(), &get).dry_run().await?; - assert!(get_result.return_value()); - - Ok(()) - } - } } + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/storage/allocator/tests.rs b/integration-tests/public/storage/allocator/tests.rs new file mode 100644 index 00000000000..341f92fef18 --- /dev/null +++ b/integration-tests/public/storage/allocator/tests.rs @@ -0,0 +1,78 @@ +use super::custom_allocator::*; + +#[ink::test] +fn default_works() { + let custom_allocator = CustomAllocator::default(); + assert!(!custom_allocator.get()); +} + +#[ink::test] +fn it_works() { + let mut custom_allocator = CustomAllocator::new(false); + assert!(!custom_allocator.get()); + custom_allocator.flip(); + assert!(custom_allocator.get()); +} + +#[cfg(all(test, feature = "e2e-tests"))] +mod e2e_tests { + use super::*; + use ink_e2e::ContractsBackend; + + type E2EResult = std::result::Result>; + + /// We test that we can upload and instantiate the contract using its default + /// constructor. + #[ink_e2e::test] + async fn default_works(mut client: Client) -> E2EResult<()> { + // Given + let mut constructor = CustomAllocatorRef::default(); + + // When + let contract = client + .instantiate("custom_allocator", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let call_builder = contract.call_builder::(); + + // Then + let get = call_builder.get(); + let get_result = client.call(&ink_e2e::alice(), &get).dry_run().await?; + assert!(!get_result.return_value()); + + Ok(()) + } + + /// We test that we can read and write a value from the on-chain contract. + #[ink_e2e::test] + async fn it_works(mut client: Client) -> E2EResult<()> { + // Given + let mut constructor = CustomAllocatorRef::new(false); + let contract = client + .instantiate("custom_allocator", &ink_e2e::bob(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + let get = call_builder.get(); + let get_result = client.call(&ink_e2e::bob(), &get).dry_run().await?; + assert!(!get_result.return_value()); + + // When + let flip = call_builder.flip(); + let _flip_result = client + .call(&ink_e2e::bob(), &flip) + .submit() + .await + .expect("flip failed"); + + // Then + let get = call_builder.get(); + let get_result = client.call(&ink_e2e::bob(), &get).dry_run().await?; + assert!(get_result.return_value()); + + Ok(()) + } +} \ No newline at end of file diff --git a/integration-tests/public/contract-storage/Cargo.toml b/integration-tests/public/storage/basic/Cargo.toml old mode 100755 new mode 100644 similarity index 100% rename from integration-tests/public/contract-storage/Cargo.toml rename to integration-tests/public/storage/basic/Cargo.toml diff --git a/integration-tests/public/contract-storage/e2e_tests.rs b/integration-tests/public/storage/basic/e2e_tests.rs similarity index 100% rename from integration-tests/public/contract-storage/e2e_tests.rs rename to integration-tests/public/storage/basic/e2e_tests.rs diff --git a/integration-tests/public/contract-storage/lib.rs b/integration-tests/public/storage/basic/lib.rs old mode 100755 new mode 100644 similarity index 100% rename from integration-tests/public/contract-storage/lib.rs rename to integration-tests/public/storage/basic/lib.rs diff --git a/integration-tests/public/complex-storage-structures/Cargo.toml b/integration-tests/public/storage/complex/Cargo.toml similarity index 100% rename from integration-tests/public/complex-storage-structures/Cargo.toml rename to integration-tests/public/storage/complex/Cargo.toml diff --git a/integration-tests/public/complex-storage-structures/README.md b/integration-tests/public/storage/complex/README.md similarity index 100% rename from integration-tests/public/complex-storage-structures/README.md rename to integration-tests/public/storage/complex/README.md diff --git a/integration-tests/public/complex-storage-structures/lib.rs b/integration-tests/public/storage/complex/lib.rs similarity index 99% rename from integration-tests/public/complex-storage-structures/lib.rs rename to integration-tests/public/storage/complex/lib.rs index 9c134076060..ce3f8dd957b 100644 --- a/integration-tests/public/complex-storage-structures/lib.rs +++ b/integration-tests/public/storage/complex/lib.rs @@ -107,3 +107,6 @@ pub mod complex_structures { } } } + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/storage/complex/tests.rs b/integration-tests/public/storage/complex/tests.rs new file mode 100644 index 00000000000..6fe9c7a7fac --- /dev/null +++ b/integration-tests/public/storage/complex/tests.rs @@ -0,0 +1,25 @@ +use super::complex_structures::*; +use ink_e2e::ContractsBackend; + +type E2EResult = std::result::Result>; + +#[ink_e2e::test] +async fn deployment_works(mut client: Client) -> E2EResult<()> { + let mut constructor = ContractRef::new(); + + let contract = client + .instantiate("complex_storage_structures", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let call_builder = contract.call_builder:: let get_balance = call_builder.get_balances_state(); + let get_balance_result = client + .call(&ink_e2e::alice(), &get_balance) + .submit() + .await + .expect("call failed"); + + assert_eq!(get_balance_result.return_value(), 0); + + Ok(()) +} \ No newline at end of file diff --git a/integration-tests/public/fallible-setter/Cargo.toml b/integration-tests/public/storage/fallible/Cargo.toml similarity index 100% rename from integration-tests/public/fallible-setter/Cargo.toml rename to integration-tests/public/storage/fallible/Cargo.toml diff --git a/integration-tests/public/storage/fallible/lib.rs b/integration-tests/public/storage/fallible/lib.rs new file mode 100644 index 00000000000..dd4aac85c4b --- /dev/null +++ b/integration-tests/public/storage/fallible/lib.rs @@ -0,0 +1,60 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::error] +#[derive(Debug, PartialEq, Eq)] +/// Equivalent to multiple Solidity custom errors, one for each variant. +pub enum Error { + /// Error when `value > 100` + TooLarge, + /// Error when `value == self.value` + NoChange, +} + +#[ink::contract] +pub mod fallible_setter { + use super::Error; + + #[ink(storage)] + pub struct FallibleSetter { + value: u8, + } + + impl FallibleSetter { + /// Creates a new fallible setter smart contract initialized with the given value. + /// Returns an error if `init_value > 100`. + #[ink(constructor)] + pub fn new(init_value: u8) -> Result { + if init_value > 100 { + return Err(Error::TooLarge) + } + Ok(Self { value: init_value }) + } + + /// Sets the value of the FallibleSetter's `u8`. + /// Returns an appropriate error if any of the following is true: + /// - `value == self.value` + /// - `init_value > 100` + #[ink(message)] + pub fn try_set(&mut self, value: u8) -> Result<(), Error> { + if self.value == value { + return Err(Error::NoChange); + } + + if value > 100 { + return Err(Error::TooLarge); + } + + self.value = value; + Ok(()) + } + + /// Returns the current value of the FallibleSetter's `u8`. + #[ink(message)] + pub fn get(&self) -> u8 { + self.value + } + } +} + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/storage/fallible/tests.rs b/integration-tests/public/storage/fallible/tests.rs new file mode 100644 index 00000000000..d6c6e71b868 --- /dev/null +++ b/integration-tests/public/storage/fallible/tests.rs @@ -0,0 +1,76 @@ +use super::Error; +use super::fallible_setter::*; + +#[ink::test] +fn it_works() { + // given + let mut fallible_setter = FallibleSetter::new(0).expect("init failed"); + assert_eq!(fallible_setter.get(), 0); + + // when + let res = fallible_setter.try_set(1); + assert!(res.is_ok()); + + // when: trying to set same value + let res = fallible_setter.try_set(1); + assert_eq!(res, Err(Error::NoChange)); + + // when: trying to set value > 100 + let res = fallible_setter.try_set(101); + assert_eq!(res, Err(Error::TooLarge)); + + // then + assert_eq!(fallible_setter.get(), 1); +} + +#[cfg(all(test, feature = "e2e-tests"))] +mod e2e_tests { + use super::*; + use ink_e2e::ContractsBackend; + + type E2EResult = std::result::Result>; + + #[ink_e2e::test] + async fn it_works(mut client: Client) -> E2EResult<()> { + // given + let mut constructor = FallibleSetterRef::new(0); + let contract = client + .instantiate("fallible_setter", &ink_e2e::bob(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + let get = call_builder.get(); + let get_res = client.call(&ink_e2e::bob(), &get).submit().await?; + assert_eq!(get_res.return_value(), 0); + + // when + let set = call_builder.try_set(1); + let set_res = client + .call(&ink_e2e::bob(), &set) + .submit() + .await + .expect("set failed"); + assert!(set_res.return_value().is_ok()); + + // when: trying to set same value (should fail) + let set = call_builder.try_set(1); + let set_res = client.call(&ink_e2e::bob(), &set).submit().await; + // In this specific e2e environment configuration, the contract returning Result::Err + // is surfacing as a CallExtrinsic error. + assert!(matches!(set_res, Err(ink_e2e::Error::CallExtrinsic(_, _)))); + + // when: trying to set value > 100 (should fail) + let set = call_builder.try_set(101); + let set_res = client.call(&ink_e2e::bob(), &set).submit().await; + assert!(matches!(set_res, Err(ink_e2e::Error::CallExtrinsic(_, _)))); + + // then + let get = call_builder.get(); + let get_res = client.call(&ink_e2e::bob(), &get).dry_run().await?; + assert_eq!(get_res.return_value(), 1); + + Ok(()) + } +} \ No newline at end of file diff --git a/integration-tests/public/lazyvec/Cargo.toml b/integration-tests/public/storage/lazyvec/Cargo.toml old mode 100755 new mode 100644 similarity index 100% rename from integration-tests/public/lazyvec/Cargo.toml rename to integration-tests/public/storage/lazyvec/Cargo.toml diff --git a/integration-tests/public/lazyvec/lib.rs b/integration-tests/public/storage/lazyvec/lib.rs old mode 100755 new mode 100644 similarity index 54% rename from integration-tests/public/lazyvec/lib.rs rename to integration-tests/public/storage/lazyvec/lib.rs index 8635334bd86..5b3842ef8a8 --- a/integration-tests/public/lazyvec/lib.rs +++ b/integration-tests/public/storage/lazyvec/lib.rs @@ -3,7 +3,7 @@ #![cfg_attr(not(feature = "std"), no_std, no_main)] #[ink::contract] -mod lazyvec { +pub mod lazyvec { use ink::{ prelude::vec::Vec, storage::StorageVec, @@ -11,10 +11,10 @@ mod lazyvec { #[ink::storage_item(packed)] pub struct Proposal { - data: Vec, - until: BlockNumber, - approvals: u32, - min_approvals: u32, + pub data: Vec, + pub until: BlockNumber, + pub approvals: u32, + pub min_approvals: u32, } impl Proposal { @@ -87,63 +87,7 @@ mod lazyvec { self.proposals.get(at) } } - - #[cfg(all(test, feature = "e2e-tests"))] - mod e2e_tests { - use super::*; - use ink_e2e::ContractsBackend; - - type E2EResult = std::result::Result>; - - #[ink_e2e::test] - async fn create_and_vote(mut client: Client) -> E2EResult<()> { - // given - let mut constructor = LazyVectorRef::default(); - let contract = client - .instantiate("lazyvec", &ink_e2e::alice(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); - - // when - let create = call_builder.create_proposal(vec![0x41], 15, 1); - let _ = client - .call(&ink_e2e::alice(), &create) - .submit() - .await - .expect("Calling `create_proposal` failed"); - - let approve = call_builder.approve(); - let _ = client - .call(&ink_e2e::alice(), &approve) - .submit() - .await - .expect("Voting failed"); - let _ = client - .call(&ink_e2e::bob(), &approve) - .submit() - .await - .expect("Voting failed"); - - // then - let value = client - .call(&ink_e2e::alice(), &create) - .dry_run() - .await - .expect("create trapped when it shouldn't") - .return_value(); - assert_eq!(value, None); - - let value = client - .call(&ink_e2e::alice(), &call_builder.get(0)) - .dry_run() - .await - .expect("get trapped when it shouldn't") - .return_value(); - assert_eq!(value.unwrap().approvals, 2); - - Ok(()) - } - } } + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/storage/lazyvec/tests.rs b/integration-tests/public/storage/lazyvec/tests.rs new file mode 100644 index 00000000000..338e9493933 --- /dev/null +++ b/integration-tests/public/storage/lazyvec/tests.rs @@ -0,0 +1,57 @@ +use super::lazyvec::{LazyVector, LazyVectorRef}; +use ink_e2e::ContractsBackend; + +type E2EResult = std::result::Result>; + +#[ink_e2e::test] +async fn create_and_vote(mut client: Client) -> E2EResult<()> { + // Given + let mut constructor = LazyVectorRef::default(); + let contract = client + .instantiate("lazyvec", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + // When + let create = call_builder.create_proposal(vec![0x41], 15, 1); + let _ = client + .call(&ink_e2e::alice(), &create) + .submit() + .await + .expect("Calling `create_proposal` failed"); + + let approve = call_builder.approve(); + let _ = client + .call(&ink_e2e::alice(), &approve) + .submit() + .await + .expect("Voting failed"); + let _ = client + .call(&ink_e2e::bob(), &approve) + .submit() + .await + .expect("Voting failed"); + + // Then + let value = client + .call(&ink_e2e::alice(), &create) + .dry_run() + .await + .expect("create trapped when it shouldn't") + .return_value(); + assert_eq!(value, None); + + let value = client + .call(&ink_e2e::alice(), &call_builder.get(0)) + .dry_run() + .await + .expect("get trapped when it shouldn't") + .return_value(); + + // We can access .approvals here because we made the struct fields pub in lib.rs + assert_eq!(value.unwrap().approvals, 2); + + Ok(()) +} \ No newline at end of file diff --git a/integration-tests/public/erc1155/Cargo.toml b/integration-tests/public/tokens/erc1155/Cargo.toml similarity index 100% rename from integration-tests/public/erc1155/Cargo.toml rename to integration-tests/public/tokens/erc1155/Cargo.toml diff --git a/integration-tests/public/erc1155/lib.rs b/integration-tests/public/tokens/erc1155/lib.rs similarity index 76% rename from integration-tests/public/erc1155/lib.rs rename to integration-tests/public/tokens/erc1155/lib.rs index 744422331ad..57007de15fc 100644 --- a/integration-tests/public/erc1155/lib.rs +++ b/integration-tests/public/tokens/erc1155/lib.rs @@ -188,7 +188,7 @@ pub trait Erc1155TokenReceiver { } #[ink::contract] -mod erc1155 { +pub mod erc1155 { use super::*; use ink::{ @@ -630,215 +630,7 @@ mod erc1155 { fn zero_address() -> Address { [0u8; 20].into() } - - #[cfg(test)] - mod tests { - /// Imports all the definitions from the outer scope so we can use them here. - use super::*; - use crate::Erc1155; - - fn set_sender(sender: Address) { - ink::env::test::set_caller(sender); - } - - fn default_accounts() -> ink::env::test::DefaultAccounts { - ink::env::test::default_accounts() - } - - fn alice() -> Address { - default_accounts().alice - } - - fn bob() -> Address { - default_accounts().bob - } - - fn charlie() -> Address { - default_accounts().charlie - } - - fn init_contract() -> Contract { - set_sender(alice()); - let mut erc = Contract::new(); - erc.balances.insert((alice(), 1), &U256::from(10)); - erc.balances.insert((alice(), 2), &U256::from(20)); - erc.balances.insert((bob(), 1), &U256::from(10)); - - erc - } - - #[ink::test] - fn can_get_correct_balance_of() { - let erc = init_contract(); - - assert_eq!(erc.balance_of(alice(), 1), U256::from(10)); - assert_eq!(erc.balance_of(alice(), 2), U256::from(20)); - assert_eq!(erc.balance_of(alice(), 3), U256::zero()); - assert_eq!(erc.balance_of(bob(), 2), U256::zero()); - } - - #[ink::test] - fn can_get_correct_batch_balance_of() { - let erc = init_contract(); - - assert_eq!( - erc.balance_of_batch(vec![alice()], vec![1, 2, 3]), - vec![U256::from(10), 20.into(), 0.into()] - ); - assert_eq!( - erc.balance_of_batch(vec![alice(), bob()], vec![1]), - vec![U256::from(10), 10.into()] - ); - - assert_eq!( - erc.balance_of_batch(vec![alice(), bob(), charlie()], vec![1, 2]), - vec![ - U256::from(10), - 20.into(), - 10.into(), - 0.into(), - 0.into(), - 0.into() - ] - ); - } - - #[ink::test] - fn can_send_tokens_between_accounts() { - let mut erc = init_contract(); - - assert!( - erc.safe_transfer_from(alice(), bob(), 1, 5.into(), vec![]) - .is_ok() - ); - assert_eq!(erc.balance_of(alice(), 1), U256::from(5)); - assert_eq!(erc.balance_of(bob(), 1), U256::from(15)); - - assert!( - erc.safe_transfer_from(alice(), bob(), 2, 5.into(), vec![]) - .is_ok() - ); - assert_eq!(erc.balance_of(alice(), 2), U256::from(15)); - assert_eq!(erc.balance_of(bob(), 2), U256::from(5)); - } - - #[ink::test] - fn sending_too_many_tokens_fails() { - let mut erc = init_contract(); - let res = erc.safe_transfer_from(alice(), bob(), 1, 99.into(), vec![]); - assert_eq!(res.unwrap_err(), Error::InsufficientU256); - } - - #[ink::test] - fn sending_tokens_to_zero_address_fails() { - let burn: Address = [0; 20].into(); - - let mut erc = init_contract(); - let res = erc.safe_transfer_from(alice(), burn, 1, 10.into(), vec![]); - assert_eq!(res.unwrap_err(), Error::ZeroAddressTransfer); - } - - #[ink::test] - fn can_send_batch_tokens() { - let mut erc = init_contract(); - assert!( - erc.safe_batch_transfer_from( - alice(), - bob(), - vec![1, 2], - vec![U256::from(5), U256::from(10)], - vec![] - ) - .is_ok() - ); - - let balances = erc.balance_of_batch(vec![alice(), bob()], vec![1, 2]); - assert_eq!( - balances, - vec![U256::from(5), 10.into(), 15.into(), 10.into()] - ); - } - - #[ink::test] - fn rejects_batch_if_lengths_dont_match() { - let mut erc = init_contract(); - let res = erc.safe_batch_transfer_from( - alice(), - bob(), - vec![1, 2, 3], - vec![U256::from(5)], - vec![], - ); - assert_eq!(res.unwrap_err(), Error::BatchTransferMismatch); - } - - #[ink::test] - fn batch_transfers_fail_if_len_is_zero() { - let mut erc = init_contract(); - let res = - erc.safe_batch_transfer_from(alice(), bob(), vec![], vec![], vec![]); - assert_eq!(res.unwrap_err(), Error::BatchTransferMismatch); - } - - #[ink::test] - fn operator_can_send_tokens() { - let mut erc = init_contract(); - - let owner = alice(); - let operator = bob(); - - set_sender(owner); - assert!(erc.set_approval_for_all(operator, true).is_ok()); - - set_sender(operator); - assert!( - erc.safe_transfer_from(owner, charlie(), 1, 5.into(), vec![]) - .is_ok() - ); - assert_eq!(erc.balance_of(alice(), 1), U256::from(5)); - assert_eq!(erc.balance_of(charlie(), 1), U256::from(5)); - } - - #[ink::test] - fn approvals_work() { - let mut erc = init_contract(); - let owner = alice(); - let operator = bob(); - let another_operator = charlie(); - - // Note: All of these tests are from the context of the owner who is either - // allowing or disallowing an operator to control their funds. - set_sender(owner); - assert!(!erc.is_approved_for_all(owner, operator)); - - assert!(erc.set_approval_for_all(operator, true).is_ok()); - assert!(erc.is_approved_for_all(owner, operator)); - - assert!(erc.set_approval_for_all(another_operator, true).is_ok()); - assert!(erc.is_approved_for_all(owner, another_operator)); - - assert!(erc.set_approval_for_all(operator, false).is_ok()); - assert!(!erc.is_approved_for_all(owner, operator)); - } - - #[ink::test] - fn minting_tokens_works() { - let mut erc = Contract::new(); - - set_sender(alice()); - assert_eq!(erc.create(0.into()), 1); - assert_eq!(erc.balance_of(alice(), 1), U256::zero()); - - assert!(erc.mint(1, 123.into()).is_ok()); - assert_eq!(erc.balance_of(alice(), 1), U256::from(123)); - } - - #[ink::test] - fn minting_not_allowed_for_nonexistent_tokens() { - let mut erc = Contract::new(); - - let res = erc.mint(1, 123.into()); - assert_eq!(res.unwrap_err(), Error::UnexistentToken); - } - } } + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/tokens/erc1155/tests.rs b/integration-tests/public/tokens/erc1155/tests.rs new file mode 100644 index 00000000000..2a56ae2d92a --- /dev/null +++ b/integration-tests/public/tokens/erc1155/tests.rs @@ -0,0 +1,247 @@ +use crate::erc1155::*; +use crate::{Erc1155, Error}; +use ink::{Address, U256}; + +fn set_sender(sender: Address) { + ink::env::test::set_caller(sender); +} + +fn default_accounts() -> ink::env::test::DefaultAccounts { + ink::env::test::default_accounts() +} + +fn alice() -> Address { + default_accounts().alice +} + +fn bob() -> Address { + default_accounts().bob +} + +fn charlie() -> Address { + default_accounts().charlie +} + +fn init_contract() -> Contract { + set_sender(alice()); + let mut erc = Contract::new(); + // We can access internal storage directly for setup because the test module + // is part of the crate hierarchy, though typically fields should be pub + // or accessed via methods. In ink! storage structs are usually generated + // with pub fields by the macro or we are checking behavior via public API. + // + // Note: In the refactor, we are using the public API where possible, + // but here we are manually inserting into storage which requires visibility. + // Ensure Contract fields are pub or accessible if this fails, but + // usually #[ink::contract] makes storage fields private by default unless + // explicitly pub. In the original code, they were inside the module. + // Since we moved tests out, we might need to rely on public methods + // like `create` or `mint` to set up state, OR make Contract fields pub. + // + // However, looking at the provided lib.rs, `balances` is NOT pub. + // We should use `mint` or `create` to set up state instead of manipulating + // private storage, which is a better testing practice anyway. + + // Let's use public API to replicate the setup: + // Original: + // erc.balances.insert((alice(), 1), &U256::from(10)); + // erc.balances.insert((alice(), 2), &U256::from(20)); + // erc.balances.insert((bob(), 1), &U256::from(10)); + + // New way using public API: + let _ = erc.create(10.into()); // Token ID 1 for Alice + let _ = erc.create(20.into()); // Token ID 2 for Alice + + // Setup Bob's balance: Alice transfers to Bob, or we temporarily switch caller + // Since create/mint assigns to caller. + set_sender(bob()); + // Bob needs Token ID 1. But Token ID 1 is already created by Alice. + // Bob can mint more if logic allows, or Alice transfers. + // The `mint` function allows minting existing tokens. + // But `mint` checks if token exists. Token 1 exists (nonce is incremented). + let _ = erc.mint(1, 10.into()); // Bob mints 10 of Token ID 1 + + // Reset sender to Alice + set_sender(alice()); + + erc +} + +#[ink::test] +fn can_get_correct_balance_of() { + let erc = init_contract(); + + assert_eq!(erc.balance_of(alice(), 1), U256::from(10)); + assert_eq!(erc.balance_of(alice(), 2), U256::from(20)); + assert_eq!(erc.balance_of(alice(), 3), U256::zero()); + assert_eq!(erc.balance_of(bob(), 1), U256::from(10)); // Bob has 10 of ID 1 + assert_eq!(erc.balance_of(bob(), 2), U256::zero()); +} + +#[ink::test] +fn can_get_correct_batch_balance_of() { + let erc = init_contract(); + + assert_eq!( + erc.balance_of_batch(vec![alice()], vec![1, 2, 3]), + vec![U256::from(10), 20.into(), 0.into()] + ); + // Modified expectation: Bob has 10 of Token 1 from init_contract() + assert_eq!( + erc.balance_of_batch(vec![alice(), bob()], vec![1]), + vec![U256::from(10), 10.into()] + ); + + assert_eq!( + erc.balance_of_batch(vec![alice(), bob(), charlie()], vec![1, 2]), + vec![ + U256::from(10), + 20.into(), + 10.into(), // Bob has 10 of Token 1 + 0.into(), + 0.into(), + 0.into() + ] + ); +} + +#[ink::test] +fn can_send_tokens_between_accounts() { + let mut erc = init_contract(); + + assert!( + erc.safe_transfer_from(alice(), bob(), 1, 5.into(), vec![]) + .is_ok() + ); + assert_eq!(erc.balance_of(alice(), 1), U256::from(5)); + // Bob started with 10, got 5 more + assert_eq!(erc.balance_of(bob(), 1), U256::from(15)); + + assert!( + erc.safe_transfer_from(alice(), bob(), 2, 5.into(), vec![]) + .is_ok() + ); + assert_eq!(erc.balance_of(alice(), 2), U256::from(15)); + assert_eq!(erc.balance_of(bob(), 2), U256::from(5)); +} + +#[ink::test] +fn sending_too_many_tokens_fails() { + let mut erc = init_contract(); + let res = erc.safe_transfer_from(alice(), bob(), 1, 99.into(), vec![]); + assert_eq!(res.unwrap_err(), Error::InsufficientU256); +} + +#[ink::test] +fn sending_tokens_to_zero_address_fails() { + let burn: Address = [0; 20].into(); + + let mut erc = init_contract(); + let res = erc.safe_transfer_from(alice(), burn, 1, 10.into(), vec![]); + assert_eq!(res.unwrap_err(), Error::ZeroAddressTransfer); +} + +#[ink::test] +fn can_send_batch_tokens() { + let mut erc = init_contract(); + assert!( + erc.safe_batch_transfer_from( + alice(), + bob(), + vec![1, 2], + vec![U256::from(5), U256::from(10)], + vec![] + ) + .is_ok() + ); + + let balances = erc.balance_of_batch(vec![alice(), bob()], vec![1, 2]); + assert_eq!( + balances, + // Alice: 10-5=5, 20-10=10 + // Bob: 10+5=15, 0+10=10 + vec![U256::from(5), 10.into(), 15.into(), 10.into()] + ); +} + +#[ink::test] +fn rejects_batch_if_lengths_dont_match() { + let mut erc = init_contract(); + let res = erc.safe_batch_transfer_from( + alice(), + bob(), + vec![1, 2, 3], + vec![U256::from(5)], + vec![], + ); + assert_eq!(res.unwrap_err(), Error::BatchTransferMismatch); +} + +#[ink::test] +fn batch_transfers_fail_if_len_is_zero() { + let mut erc = init_contract(); + let res = + erc.safe_batch_transfer_from(alice(), bob(), vec![], vec![], vec![]); + assert_eq!(res.unwrap_err(), Error::BatchTransferMismatch); +} + +#[ink::test] +fn operator_can_send_tokens() { + let mut erc = init_contract(); + + let owner = alice(); + let operator = bob(); + + set_sender(owner); + assert!(erc.set_approval_for_all(operator, true).is_ok()); + + set_sender(operator); + assert!( + erc.safe_transfer_from(owner, charlie(), 1, 5.into(), vec![]) + .is_ok() + ); + assert_eq!(erc.balance_of(alice(), 1), U256::from(5)); + assert_eq!(erc.balance_of(charlie(), 1), U256::from(5)); +} + +#[ink::test] +fn approvals_work() { + let mut erc = init_contract(); + let owner = alice(); + let operator = bob(); + let another_operator = charlie(); + + // Note: All of these tests are from the context of the owner who is either + // allowing or disallowing an operator to control their funds. + set_sender(owner); + assert!(!erc.is_approved_for_all(owner, operator)); + + assert!(erc.set_approval_for_all(operator, true).is_ok()); + assert!(erc.is_approved_for_all(owner, operator)); + + assert!(erc.set_approval_for_all(another_operator, true).is_ok()); + assert!(erc.is_approved_for_all(owner, another_operator)); + + assert!(erc.set_approval_for_all(operator, false).is_ok()); + assert!(!erc.is_approved_for_all(owner, operator)); +} + +#[ink::test] +fn minting_tokens_works() { + let mut erc = Contract::new(); + + set_sender(alice()); + assert_eq!(erc.create(0.into()), 1); + assert_eq!(erc.balance_of(alice(), 1), U256::zero()); + + assert!(erc.mint(1, 123.into()).is_ok()); + assert_eq!(erc.balance_of(alice(), 1), U256::from(123)); +} + +#[ink::test] +fn minting_not_allowed_for_nonexistent_tokens() { + let mut erc = Contract::new(); + + let res = erc.mint(1, 123.into()); + assert_eq!(res.unwrap_err(), Error::UnexistentToken); +} \ No newline at end of file diff --git a/integration-tests/public/erc20/Cargo.toml b/integration-tests/public/tokens/erc20/Cargo.toml similarity index 100% rename from integration-tests/public/erc20/Cargo.toml rename to integration-tests/public/tokens/erc20/Cargo.toml diff --git a/integration-tests/public/tokens/erc20/lib.rs b/integration-tests/public/tokens/erc20/lib.rs new file mode 100644 index 00000000000..e2d20c6ff44 --- /dev/null +++ b/integration-tests/public/tokens/erc20/lib.rs @@ -0,0 +1,226 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +pub mod erc20 { + use ink::{ + U256, + storage::Mapping, + }; + + /// A simple ERC-20 contract. + #[ink(storage)] + #[derive(Default)] + pub struct Erc20 { + /// Total token supply. + total_supply: U256, + /// Mapping from owner to number of owned token. + balances: Mapping, + /// Mapping of the token amount which an account is allowed to withdraw + /// from another account. + allowances: Mapping<(Address, Address), U256>, + } + + /// Event emitted when a token transfer occurs. + #[ink(event)] + pub struct Transfer { + #[ink(topic)] + from: Option
, + #[ink(topic)] + to: Option
, + value: U256, + } + + /// Event emitted when an approval occurs that `spender` is allowed to withdraw + /// up to the amount of `value` tokens from `owner`. + #[ink(event)] + pub struct Approval { + #[ink(topic)] + owner: Address, + #[ink(topic)] + spender: Address, + value: U256, + } + + /// The ERC-20 error types. + #[derive(Debug, PartialEq, Eq)] + #[ink::error] + pub enum Error { + /// Returned if not enough balance to fulfill a request is available. + InsufficientBalance, + /// Returned if not enough allowance to fulfill a request is available. + InsufficientAllowance, + } + + /// The ERC-20 result type. + pub type Result = core::result::Result; + + impl Erc20 { + /// Creates a new ERC-20 contract with the specified initial supply. + #[ink(constructor)] + pub fn new(total_supply: U256) -> Self { + let mut balances = Mapping::default(); + let caller = Self::env().caller(); + balances.insert(caller, &total_supply); + Self::env().emit_event(Transfer { + from: None, + to: Some(caller), + value: total_supply, + }); + Self { + total_supply, + balances, + allowances: Default::default(), + } + } + + /// Returns the total token supply. + #[ink(message)] + pub fn total_supply(&self) -> U256 { + self.total_supply + } + + /// Returns the account balance for the specified `owner`. + /// + /// Returns `0` if the account is non-existent. + #[ink(message)] + pub fn balance_of(&self, owner: Address) -> U256 { + self.balance_of_impl(&owner) + } + + /// Returns the account balance for the specified `owner`. + /// + /// Returns `0` if the account is non-existent. + /// + /// # Note + /// + /// Prefer to call this method over `balance_of` since this + /// works using references which are more efficient. + #[inline] + fn balance_of_impl(&self, owner: &Address) -> U256 { + self.balances.get(owner).unwrap_or_default() + } + + /// Returns the amount which `spender` is still allowed to withdraw from `owner`. + /// + /// Returns `0` if no allowance has been set. + #[ink(message)] + pub fn allowance(&self, owner: Address, spender: Address) -> U256 { + self.allowance_impl(&owner, &spender) + } + + /// Returns the amount which `spender` is still allowed to withdraw from `owner`. + /// + /// Returns `0` if no allowance has been set. + /// + /// # Note + /// + /// Prefer to call this method over `allowance` since this + /// works using references which are more efficient. + #[inline] + fn allowance_impl(&self, owner: &Address, spender: &Address) -> U256 { + self.allowances.get((owner, spender)).unwrap_or_default() + } + + /// Transfers `value` amount of tokens from the caller's account to account `to`. + /// + /// On success a `Transfer` event is emitted. + /// + /// # Errors + /// + /// Returns `InsufficientBalance` error if there are not enough tokens on + /// the caller's account balance. + #[ink(message)] + pub fn transfer(&mut self, to: Address, value: U256) -> Result<()> { + let from = self.env().caller(); + self.transfer_from_to(&from, &to, value) + } + + /// Allows `spender` to withdraw from the caller's account multiple times, up to + /// the `value` amount. + /// + /// If this function is called again it overwrites the current allowance with + /// `value`. + /// + /// An `Approval` event is emitted. + #[ink(message)] + pub fn approve(&mut self, spender: Address, value: U256) -> Result<()> { + let owner = self.env().caller(); + self.allowances.insert((&owner, &spender), &value); + self.env().emit_event(Approval { + owner, + spender, + value, + }); + Ok(()) + } + + /// Transfers `value` tokens on the behalf of `from` to the account `to`. + /// + /// This can be used to allow a contract to transfer tokens on ones behalf and/or + /// to charge fees in sub-currencies, for example. + /// + /// On success a `Transfer` event is emitted. + /// + /// # Errors + /// + /// Returns `InsufficientAllowance` error if there are not enough tokens allowed + /// for the caller to withdraw from `from`. + /// + /// Returns `InsufficientBalance` error if there are not enough tokens on + /// the account balance of `from`. + #[ink(message)] + pub fn transfer_from( + &mut self, + from: Address, + to: Address, + value: U256, + ) -> Result<()> { + let caller = self.env().caller(); + let allowance = self.allowance_impl(&from, &caller); + if allowance < value { + return Err(Error::InsufficientAllowance) + } + self.transfer_from_to(&from, &to, value)?; + // We checked that allowance >= value + #[allow(clippy::arithmetic_side_effects)] + self.allowances + .insert((&from, &caller), &(allowance - value)); + Ok(()) + } + + /// Transfers `value` amount of tokens from the caller's account to account `to`. + /// + /// On success a `Transfer` event is emitted. + /// + /// # Errors + /// + /// Returns `InsufficientBalance` error if there are not enough tokens on + /// the caller's account balance. + fn transfer_from_to( + &mut self, + from: &Address, + to: &Address, + value: U256, + ) -> Result<()> { + let from_balance = self.balance_of_impl(from); + if from_balance < value { + return Err(Error::InsufficientBalance) + } + // We checked that from_balance >= value + #[allow(clippy::arithmetic_side_effects)] + self.balances.insert(from, &(from_balance - value)); + let to_balance = self.balance_of_impl(to); + self.balances + .insert(to, &(to_balance.checked_add(value).unwrap())); + self.env().emit_event(Transfer { + from: Some(*from), + to: Some(*to), + value, + }); + Ok(()) + } + } +} + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/tokens/erc20/tests.rs b/integration-tests/public/tokens/erc20/tests.rs new file mode 100644 index 00000000000..92de1f4cff6 --- /dev/null +++ b/integration-tests/public/tokens/erc20/tests.rs @@ -0,0 +1,447 @@ +use crate::erc20::*; +use ink::primitives::{ + Address, + Clear, + Hash, +}; +use ink::U256; + +// --- Helper Functions for Unit Tests --- + +fn set_caller(sender: Address) { + ink::env::test::set_caller(sender); +} + +fn encoded_into_hash(entity: T) -> Hash +where + T: ink::scale::Encode, +{ + use ink::{ + env::hash::{ + Blake2x256, + CryptoHash, + HashOutput, + }, + primitives::Clear, + }; + + let mut result = Hash::CLEAR_HASH; + let len_result = result.as_ref().len(); + let encoded = entity.encode(); + let len_encoded = encoded.len(); + if len_encoded <= len_result { + result.as_mut()[..len_encoded].copy_from_slice(&encoded); + return result + } + let mut hash_output = + <::Type as Default>::default(); + ::hash(&encoded, &mut hash_output); + let copy_len = core::cmp::min(hash_output.len(), len_result); + result.as_mut()[0..copy_len].copy_from_slice(&hash_output[0..copy_len]); + result +} + +fn assert_transfer_event( + event: &ink::env::test::EmittedEvent, + expected_from: Option
, + expected_to: Option
, + expected_value: U256, +) { + let decoded_event = + ::decode(&mut &event.data[..]) + .expect("encountered invalid contract event data buffer"); + let Transfer { from, to, value } = decoded_event; + assert_eq!(from, expected_from, "encountered invalid Transfer.from"); + assert_eq!(to, expected_to, "encountered invalid Transfer.to"); + assert_eq!(value, expected_value, "encountered invalid Transfer.value"); + + let mut expected_topics = Vec::new(); + expected_topics.push( + ink::blake2x256!("Transfer(Option
,Option
,U256)").into(), + ); + if let Some(from) = expected_from { + expected_topics.push(encoded_into_hash(from)); + } else { + expected_topics.push(Hash::CLEAR_HASH); + } + if let Some(to) = expected_to { + expected_topics.push(encoded_into_hash(to)); + } else { + expected_topics.push(Hash::CLEAR_HASH); + } + expected_topics.push(encoded_into_hash(value)); + + let topics = event.topics.clone(); + for (n, (actual_topic, expected_topic)) in + topics.iter().zip(expected_topics).enumerate() + { + let mut topic_hash = Hash::CLEAR_HASH; + let len = actual_topic.len(); + topic_hash.as_mut()[0..len].copy_from_slice(&actual_topic[0..len]); + + assert_eq!( + topic_hash, expected_topic, + "encountered invalid topic at {n}" + ); + } +} + +// --- Unit Tests --- + +/// The default constructor does its job. +#[ink::test] +fn new_works() { + // Constructor works. + set_caller(Address::from([0x01; 20])); + let _erc20 = Erc20::new(100.into()); + + // Transfer event triggered during initial construction. + let emitted_events = ink::env::test::recorded_events(); + assert_eq!(1, emitted_events.len()); + + assert_transfer_event( + &emitted_events[0], + None, + Some(Address::from([0x01; 20])), + 100.into(), + ); +} + +/// The total supply was applied. +#[ink::test] +fn total_supply_works() { + // Constructor works. + set_caller(Address::from([0x01; 20])); + let erc20 = Erc20::new(100.into()); + // Transfer event triggered during initial construction. + let emitted_events = ink::env::test::recorded_events(); + assert_transfer_event( + &emitted_events[0], + None, + Some(Address::from([0x01; 20])), + 100.into(), + ); + // Get the token total supply. + assert_eq!(erc20.total_supply(), U256::from(100)); +} + +/// Get the actual balance of an account. +#[ink::test] +fn balance_of_works() { + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + + // Constructor works + let erc20 = Erc20::new(100.into()); + // Transfer event triggered during initial construction + let emitted_events = ink::env::test::recorded_events(); + assert_transfer_event( + &emitted_events[0], + None, + Some(accounts.alice), + 100.into(), + ); + let accounts = ink::env::test::default_accounts(); + // Alice owns all the tokens on contract instantiation + assert_eq!(erc20.balance_of(accounts.alice), U256::from(100)); + // Bob does not owns tokens + assert_eq!(erc20.balance_of(accounts.bob), U256::zero()); +} + +#[ink::test] +fn transfer_works() { + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + + // Constructor works. + let mut erc20 = Erc20::new(100.into()); + // Transfer event triggered during initial construction. + assert_eq!(erc20.balance_of(accounts.bob), U256::zero()); + // Alice transfers 10 tokens to Bob. + assert_eq!(erc20.transfer(accounts.bob, U256::from(10)), Ok(())); + // Bob owns 10 tokens. + assert_eq!(erc20.balance_of(accounts.bob), U256::from(10)); + + let emitted_events = ink::env::test::recorded_events(); + assert_eq!(emitted_events.len(), 2); + // Check first transfer event related to ERC-20 instantiation. + assert_transfer_event( + &emitted_events[0], + None, + Some(accounts.alice), + 100.into(), + ); + // Check the second transfer event relating to the actual transfer. + assert_transfer_event( + &emitted_events[1], + Some(accounts.alice), + Some(accounts.bob), + 10.into(), + ); +} + +#[ink::test] +fn invalid_transfer_should_fail() { + // Constructor works. + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + + let initial_supply = 100.into(); + let mut erc20 = Erc20::new(initial_supply); + + assert_eq!(erc20.balance_of(accounts.bob), U256::zero()); + + // Set the contract as callee and Bob as caller. + let contract = ink::env::address(); + ink::env::test::set_callee(contract); + set_caller(accounts.bob); + + // Bob fails to transfer 10 tokens to Eve. + assert_eq!( + erc20.transfer(accounts.eve, 10.into()), + Err(Error::InsufficientBalance) + ); + // Alice owns all the tokens. + assert_eq!(erc20.balance_of(accounts.alice), U256::from(100)); + assert_eq!(erc20.balance_of(accounts.bob), U256::zero()); + assert_eq!(erc20.balance_of(accounts.eve), U256::zero()); + + // Transfer event triggered during initial construction. + let emitted_events = ink::env::test::recorded_events(); + assert_eq!(emitted_events.len(), 1); + assert_transfer_event( + &emitted_events[0], + None, + Some(accounts.alice), + 100.into(), + ); +} + +#[ink::test] +fn transfer_from_works() { + // Constructor works. + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + + let mut erc20 = Erc20::new(100.into()); + + // Bob fails to transfer tokens owned by Alice. + assert_eq!( + erc20.transfer_from(accounts.alice, accounts.eve, 10.into()), + Err(Error::InsufficientAllowance) + ); + // Alice approves Bob for token transfers on her behalf. + assert_eq!(erc20.approve(accounts.bob, 10.into()), Ok(())); + + // The approve event takes place. + assert_eq!(ink::env::test::recorded_events().len(), 2); + + // Set the contract as callee and Bob as caller. + let contract = ink::env::address(); + ink::env::test::set_callee(contract); + ink::env::test::set_caller(accounts.bob); + + // Bob transfers tokens from Alice to Eve. + assert_eq!( + erc20.transfer_from(accounts.alice, accounts.eve, 10.into()), + Ok(()) + ); + // Eve owns tokens. + assert_eq!(erc20.balance_of(accounts.eve), U256::from(10)); + + // Check all transfer events that happened during the previous calls: + let emitted_events = ink::env::test::recorded_events(); + assert_eq!(emitted_events.len(), 3); + assert_transfer_event( + &emitted_events[0], + None, + Some(accounts.alice), + 100.into(), + ); + // The second event `emitted_events[1]` is an Approve event that we skip + // checking. + assert_transfer_event( + &emitted_events[2], + Some(accounts.alice), + Some(accounts.eve), + 10.into(), + ); +} + +#[ink::test] +fn allowance_must_not_change_on_failed_transfer() { + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + let mut erc20 = Erc20::new(100.into()); + + // Alice approves Bob for token transfers on her behalf. + let alice_balance = erc20.balance_of(accounts.alice); + let initial_allowance = alice_balance + 2; + assert_eq!(erc20.approve(accounts.bob, initial_allowance), Ok(())); + + // Get contract address. + let callee = ink::env::address(); + ink::env::test::set_callee(callee); + ink::env::test::set_caller(accounts.bob); + + // Bob tries to transfer tokens from Alice to Eve. + let emitted_events_before = ink::env::test::recorded_events().len(); + assert_eq!( + erc20.transfer_from( + accounts.alice, + accounts.eve, + alice_balance + U256::from(1) + ), + Err(Error::InsufficientBalance) + ); + // Allowance must have stayed the same + assert_eq!( + erc20.allowance(accounts.alice, accounts.bob), + initial_allowance + ); + // No more events must have been emitted + assert_eq!( + emitted_events_before, + ink::env::test::recorded_events().len() + ) +} + +// --- E2E Tests --- + +#[cfg(feature = "e2e-tests")] +mod e2e_tests { + use super::*; + use ink_e2e::ContractsBackend; + + type E2EResult = std::result::Result>; + + #[ink_e2e::test] + async fn e2e_transfer(mut client: Client) -> E2EResult<()> { + // given + let total_supply = U256::from(1_000_000_000); + let mut constructor = Erc20Ref::new(total_supply); + let erc20 = client + .instantiate("erc20", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = erc20.call_builder::(); + + // when + let total_supply_msg = call_builder.total_supply(); + let total_supply_res = client + .call(&ink_e2e::bob(), &total_supply_msg) + .dry_run() + .await?; + + let bob_account = ink_e2e::address::( + ink_e2e::Sr25519Keyring::Bob, + ); + let transfer_to_bob = U256::from(500_000_000); + let transfer = call_builder.transfer(bob_account, transfer_to_bob); + let _transfer_res = client + .call(&ink_e2e::alice(), &transfer) + .submit() + .await + .expect("transfer failed"); + + let balance_of = call_builder.balance_of(bob_account); + let balance_of_res = client + .call(&ink_e2e::alice(), &balance_of) + .dry_run() + .await?; + + // then + assert_eq!( + total_supply, + total_supply_res.return_value(), + "total_supply" + ); + assert_eq!(transfer_to_bob, balance_of_res.return_value(), "balance_of"); + + Ok(()) + } + + #[ink_e2e::test] + async fn e2e_allowances(mut client: Client) -> E2EResult<()> { + // given + let total_supply = U256::from(1_000_000_000); + let mut constructor = Erc20Ref::new(total_supply); + let erc20 = client + .instantiate("erc20", &ink_e2e::bob(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = erc20.call_builder::(); + + // when + + let bob_account = ink_e2e::address::( + ink_e2e::Sr25519Keyring::Bob, + ); + let charlie_account = ink_e2e::address::( + ink_e2e::Sr25519Keyring::Charlie, + ); + + let amount = U256::from(500_000_000); + // tx + let transfer_from = + call_builder.transfer_from(bob_account, charlie_account, amount); + let transfer_from_result = client + .call(&ink_e2e::charlie(), &transfer_from) + .submit() + .await; + + assert!( + transfer_from_result.is_err(), + "unapproved transfer_from should fail" + ); + + // Bob approves Charlie to transfer up to amount on his behalf + let approved_value = U256::from(1_000); + let approve_call = call_builder.approve(charlie_account, approved_value); + client + .call(&ink_e2e::bob(), &approve_call) + .submit() + .await + .expect("approve failed"); + + // `transfer_from` the approved amount + let transfer_from = + call_builder.transfer_from(bob_account, charlie_account, approved_value); + let transfer_from_result = client + .call(&ink_e2e::charlie(), &transfer_from) + .submit() + .await; + assert!( + transfer_from_result.is_ok(), + "approved transfer_from should succeed" + ); + + let balance_of = call_builder.balance_of(bob_account); + let balance_of_res = client + .call(&ink_e2e::alice(), &balance_of) + .dry_run() + .await?; + + // `transfer_from` again, this time exceeding the approved amount + let transfer_from = + call_builder.transfer_from(bob_account, charlie_account, 1.into()); + let transfer_from_result = client + .call(&ink_e2e::charlie(), &transfer_from) + .submit() + .await; + assert!( + transfer_from_result.is_err(), + "transfer_from exceeding the approved amount should fail" + ); + + assert_eq!( + total_supply - approved_value, + balance_of_res.return_value(), + "balance_of" + ); + + Ok(()) + } +} \ No newline at end of file diff --git a/integration-tests/public/erc721/Cargo.toml b/integration-tests/public/tokens/erc721/Cargo.toml similarity index 100% rename from integration-tests/public/erc721/Cargo.toml rename to integration-tests/public/tokens/erc721/Cargo.toml diff --git a/integration-tests/public/tokens/erc721/lib.rs b/integration-tests/public/tokens/erc721/lib.rs new file mode 100644 index 00000000000..1123c480bef --- /dev/null +++ b/integration-tests/public/tokens/erc721/lib.rs @@ -0,0 +1,395 @@ +//! # ERC-721 +//! +//! This is an ERC-721 Token implementation. +//! +//! ## Warning +//! +//! This contract is an *example*. It is neither audited nor endorsed for production use. +//! Do **not** rely on it to keep anything of value secure. +//! +//! ## Overview +//! +//! This contract demonstrates how to build non-fungible or unique tokens using ink!. +//! +//! ## Error Handling +//! +//! Any function that modifies the state returns a `Result` type and does not changes the +//! state if the `Error` occurs. +//! The errors are defined as an `enum` type. Any other error or invariant violation +//! triggers a panic and therefore rolls back the transaction. +//! +//! ## Token Management +//! +//! After creating a new token, the function caller becomes the owner. +//! A token can be created, transferred, or destroyed. +//! +//! Token owners can assign other accounts for transferring specific tokens on their +//! behalf. It is also possible to authorize an operator (higher rights) for another +//! account to handle tokens. +//! +//! ### Token Creation +//! +//! Token creation start by calling the `mint(&mut self, id: u32)` function. +//! The token owner becomes the function caller. The Token ID needs to be specified +//! as the argument on this function call. +//! +//! ### Token Transfer +//! +//! Transfers may be initiated by: +//! - The owner of a token +//! - The approved address of a token +//! - An authorized operator of the current owner of a token +//! +//! The token owner can transfer a token by calling the `transfer` or `transfer_from` +//! functions. An approved address can make a token transfer by calling the +//! `transfer_from` function. Operators can transfer tokens on another account's behalf or +//! can approve a token transfer for a different account. +//! +//! ### Token Removal +//! +//! Tokens can be destroyed by burning them. Only the token owner is allowed to burn a +//! token. + +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +pub mod erc721 { + use ink::storage::Mapping; + + /// A token ID. + pub type TokenId = u32; + + #[ink(storage)] + #[derive(Default)] + pub struct Erc721 { + /// Mapping from token to owner. + token_owner: Mapping, + /// Mapping from token to approvals users. + token_approvals: Mapping, + /// Mapping from owner to number of owned token. + owned_tokens_count: Mapping, + /// Mapping from owner to operator approvals. + operator_approvals: Mapping<(Address, Address), ()>, + } + + #[derive(Debug, PartialEq, Eq, Copy, Clone)] + #[ink::error] + pub enum Error { + NotOwner, + NotApproved, + TokenExists, + TokenNotFound, + CannotInsert, + CannotFetchValue, + NotAllowed, + } + + /// Event emitted when a token transfer occurs. + #[ink(event)] + pub struct Transfer { + #[ink(topic)] + from: Option
, + #[ink(topic)] + to: Option
, + #[ink(topic)] + id: TokenId, + } + + /// Event emitted when a token approve occurs. + #[ink(event)] + pub struct Approval { + #[ink(topic)] + from: Address, + #[ink(topic)] + to: Address, + #[ink(topic)] + id: TokenId, + } + + /// Event emitted when an operator is enabled or disabled for an owner. + /// The operator can manage all NFTs of the owner. + #[ink(event)] + pub struct ApprovalForAll { + #[ink(topic)] + owner: Address, + #[ink(topic)] + operator: Address, + approved: bool, + } + + impl Erc721 { + /// Creates a new ERC-721 token contract. + #[ink(constructor)] + pub fn new() -> Self { + Default::default() + } + + /// Returns the balance of the owner. + /// + /// This represents the amount of unique tokens the owner has. + #[ink(message)] + pub fn balance_of(&self, owner: Address) -> u32 { + self.balance_of_or_zero(&owner) + } + + /// Returns the owner of the token. + #[ink(message)] + pub fn owner_of(&self, id: TokenId) -> Option
{ + self.token_owner.get(id) + } + + /// Returns the approved account ID for this token if any. + #[ink(message)] + pub fn get_approved(&self, id: TokenId) -> Option
{ + self.token_approvals.get(id) + } + + /// Returns `true` if the operator is approved by the owner. + #[ink(message)] + pub fn is_approved_for_all(&self, owner: Address, operator: Address) -> bool { + self.approved_for_all(owner, operator) + } + + /// Approves or disapproves the operator for all tokens of the caller. + #[ink(message)] + pub fn set_approval_for_all( + &mut self, + to: Address, + approved: bool, + ) -> Result<(), Error> { + self.approve_for_all(to, approved)?; + Ok(()) + } + + /// Approves the account to transfer the specified token on behalf of the caller. + #[ink(message)] + pub fn approve(&mut self, to: Address, id: TokenId) -> Result<(), Error> { + self.approve_for(&to, id)?; + Ok(()) + } + + /// Transfers the token from the caller to the given destination. + #[ink(message)] + pub fn transfer( + &mut self, + destination: Address, + id: TokenId, + ) -> Result<(), Error> { + let caller = self.env().caller(); + self.transfer_token_from(&caller, &destination, id)?; + Ok(()) + } + + /// Transfer approved or owned token. + #[ink(message)] + pub fn transfer_from( + &mut self, + from: Address, + to: Address, + id: TokenId, + ) -> Result<(), Error> { + self.transfer_token_from(&from, &to, id)?; + Ok(()) + } + + /// Creates a new token. + #[ink(message)] + pub fn mint(&mut self, id: TokenId) -> Result<(), Error> { + let caller = self.env().caller(); + self.add_token_to(&caller, id)?; + self.env().emit_event(Transfer { + from: Some(Address::from([0x0; 20])), + to: Some(caller), + id, + }); + Ok(()) + } + + /// Deletes an existing token. Only the owner can burn the token. + #[ink(message)] + pub fn burn(&mut self, id: TokenId) -> Result<(), Error> { + let caller = self.env().caller(); + let Self { + token_owner, + owned_tokens_count, + .. + } = self; + + let owner = token_owner.get(id).ok_or(Error::TokenNotFound)?; + if owner != caller { + return Err(Error::NotOwner); + }; + + let count = owned_tokens_count + .get(caller) + .map(|c| c.checked_sub(1).unwrap()) + .ok_or(Error::CannotFetchValue)?; + owned_tokens_count.insert(caller, &count); + token_owner.remove(id); + self.clear_approval(id); + + self.env().emit_event(Transfer { + from: Some(caller), + to: Some(Address::from([0x0; 20])), + id, + }); + + Ok(()) + } + + /// Transfers token `id` `from` the sender to the `to` `Address`. + fn transfer_token_from( + &mut self, + from: &Address, + to: &Address, + id: TokenId, + ) -> Result<(), Error> { + let caller = self.env().caller(); + let owner = self.owner_of(id).ok_or(Error::TokenNotFound)?; + if !self.approved_or_owner(caller, id, owner) { + return Err(Error::NotApproved); + }; + if owner != *from { + return Err(Error::NotOwner); + }; + self.clear_approval(id); + self.remove_token_from(from, id)?; + self.add_token_to(to, id)?; + self.env().emit_event(Transfer { + from: Some(*from), + to: Some(*to), + id, + }); + Ok(()) + } + + /// Removes token `id` from the owner. + fn remove_token_from( + &mut self, + from: &Address, + id: TokenId, + ) -> Result<(), Error> { + let Self { + token_owner, + owned_tokens_count, + .. + } = self; + + if !token_owner.contains(id) { + return Err(Error::TokenNotFound); + } + + let count = owned_tokens_count + .get(from) + .map(|c| c.checked_sub(1).unwrap()) + .ok_or(Error::CannotFetchValue)?; + owned_tokens_count.insert(from, &count); + token_owner.remove(id); + + Ok(()) + } + + /// Adds the token `id` to the `to` AccountID. + fn add_token_to(&mut self, to: &Address, id: TokenId) -> Result<(), Error> { + let Self { + token_owner, + owned_tokens_count, + .. + } = self; + + if token_owner.contains(id) { + return Err(Error::TokenExists); + } + + if *to == Address::from([0x0; 20]) { + return Err(Error::NotAllowed); + }; + + let count = owned_tokens_count + .get(to) + .map(|c| c.checked_add(1).unwrap()) + .unwrap_or(1); + + owned_tokens_count.insert(to, &count); + token_owner.insert(id, to); + + Ok(()) + } + + /// Approves or disapproves the operator to transfer all tokens of the caller. + fn approve_for_all(&mut self, to: Address, approved: bool) -> Result<(), Error> { + let caller = self.env().caller(); + if to == caller { + return Err(Error::NotAllowed); + } + self.env().emit_event(ApprovalForAll { + owner: caller, + operator: to, + approved, + }); + + if approved { + self.operator_approvals.insert((&caller, &to), &()); + } else { + self.operator_approvals.remove((&caller, &to)); + } + + Ok(()) + } + + /// Approve the passed `Address` to transfer the specified token on behalf of + /// the message's sender. + fn approve_for(&mut self, to: &Address, id: TokenId) -> Result<(), Error> { + let caller = self.env().caller(); + let owner = self.owner_of(id).ok_or(Error::TokenNotFound)?; + if !(owner == caller || self.approved_for_all(owner, caller)) { + return Err(Error::NotAllowed); + }; + + if *to == Address::from([0x0; 20]) { + return Err(Error::NotAllowed); + }; + + if self.token_approvals.contains(id) { + return Err(Error::CannotInsert); + } else { + self.token_approvals.insert(id, to); + } + + self.env().emit_event(Approval { + from: caller, + to: *to, + id, + }); + + Ok(()) + } + + /// Removes existing approval from token `id`. + fn clear_approval(&mut self, id: TokenId) { + self.token_approvals.remove(id); + } + + // Returns the total number of tokens from an account. + fn balance_of_or_zero(&self, of: &Address) -> u32 { + self.owned_tokens_count.get(of).unwrap_or(0) + } + + /// Gets an operator on other Account's behalf. + fn approved_for_all(&self, owner: Address, operator: Address) -> bool { + self.operator_approvals.contains((&owner, &operator)) + } + + /// Returns true if the `Address` `from` is the owner of token `id` + /// or it has been approved on behalf of the token `id` owner. + fn approved_or_owner(&self, from: Address, id: TokenId, owner: Address) -> bool { + from != Address::from([0x0; 20]) + && (from == owner + || self.token_approvals.get(id) == Some(from) + || self.approved_for_all(owner, from)) + } + } +} + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/tokens/erc721/tests.rs b/integration-tests/public/tokens/erc721/tests.rs new file mode 100644 index 00000000000..0991f6bbc73 --- /dev/null +++ b/integration-tests/public/tokens/erc721/tests.rs @@ -0,0 +1,301 @@ +use crate::erc721::*; +use ink::primitives::Address; + +fn set_caller(sender: Address) { + ink::env::test::set_caller(sender); +} + +#[ink::test] +fn mint_works() { + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + // Create a new contract instance. + let mut erc721 = Erc721::new(); + // Token 1 does not exists. + assert_eq!(erc721.owner_of(1), None); + // Alice does not owns tokens. + assert_eq!(erc721.balance_of(accounts.alice), 0); + // Create token Id 1. + assert_eq!(erc721.mint(1), Ok(())); + // Alice owns 1 token. + assert_eq!(erc721.balance_of(accounts.alice), 1); +} + +#[ink::test] +fn mint_existing_should_fail() { + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + // Create a new contract instance. + let mut erc721 = Erc721::new(); + // Create token Id 1. + assert_eq!(erc721.mint(1), Ok(())); + // The first Transfer event takes place + assert_eq!(1, ink::env::test::recorded_events().len()); + // Alice owns 1 token. + assert_eq!(erc721.balance_of(accounts.alice), 1); + // Alice owns token Id 1. + assert_eq!(erc721.owner_of(1), Some(accounts.alice)); + // Cannot create token Id if it exists. + // Bob cannot own token Id 1. + assert_eq!(erc721.mint(1), Err(Error::TokenExists)); +} + +#[ink::test] +fn transfer_works() { + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + // Create a new contract instance. + let mut erc721 = Erc721::new(); + // Create token Id 1 for Alice + assert_eq!(erc721.mint(1), Ok(())); + // Alice owns token 1 + assert_eq!(erc721.balance_of(accounts.alice), 1); + // Bob does not owns any token + assert_eq!(erc721.balance_of(accounts.bob), 0); + // The first Transfer event takes place + assert_eq!(1, ink::env::test::recorded_events().len()); + // Alice transfers token 1 to Bob + assert_eq!(erc721.transfer(accounts.bob, 1), Ok(())); + // The second Transfer event takes place + assert_eq!(2, ink::env::test::recorded_events().len()); + // Bob owns token 1 + assert_eq!(erc721.balance_of(accounts.bob), 1); +} + +#[ink::test] +fn invalid_transfer_should_fail() { + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + // Create a new contract instance. + let mut erc721 = Erc721::new(); + // Transfer token fails if it does not exists. + assert_eq!(erc721.transfer(accounts.bob, 2), Err(Error::TokenNotFound)); + // Token Id 2 does not exists. + assert_eq!(erc721.owner_of(2), None); + // Create token Id 2. + assert_eq!(erc721.mint(2), Ok(())); + // Alice owns 1 token. + assert_eq!(erc721.balance_of(accounts.alice), 1); + // Token Id 2 is owned by Alice. + assert_eq!(erc721.owner_of(2), Some(accounts.alice)); + // Set Bob as caller + set_caller(accounts.bob); + // Bob cannot transfer not owned tokens. + assert_eq!(erc721.transfer(accounts.eve, 2), Err(Error::NotApproved)); +} + +#[ink::test] +fn approved_transfer_works() { + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + // Create a new contract instance. + let mut erc721 = Erc721::new(); + // Create token Id 1. + assert_eq!(erc721.mint(1), Ok(())); + // Token Id 1 is owned by Alice. + assert_eq!(erc721.owner_of(1), Some(accounts.alice)); + // Approve token Id 1 transfer for Bob on behalf of Alice. + assert_eq!(erc721.approve(accounts.bob, 1), Ok(())); + // Set Bob as caller + set_caller(accounts.bob); + // Bob transfers token Id 1 from Alice to Eve. + assert_eq!( + erc721.transfer_from(accounts.alice, accounts.eve, 1), + Ok(()) + ); + // TokenId 3 is owned by Eve. + assert_eq!(erc721.owner_of(1), Some(accounts.eve)); + // Alice does not owns tokens. + assert_eq!(erc721.balance_of(accounts.alice), 0); + // Bob does not owns tokens. + assert_eq!(erc721.balance_of(accounts.bob), 0); + // Eve owns 1 token. + assert_eq!(erc721.balance_of(accounts.eve), 1); +} + +#[ink::test] +fn approved_for_all_works() { + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + // Create a new contract instance. + let mut erc721 = Erc721::new(); + // Create token Id 1. + assert_eq!(erc721.mint(1), Ok(())); + // Create token Id 2. + assert_eq!(erc721.mint(2), Ok(())); + // Alice owns 2 tokens. + assert_eq!(erc721.balance_of(accounts.alice), 2); + // Approve token Id 1 transfer for Bob on behalf of Alice. + assert_eq!(erc721.set_approval_for_all(accounts.bob, true), Ok(())); + // Bob is an approved operator for Alice + assert!(erc721.is_approved_for_all(accounts.alice, accounts.bob)); + // Set Bob as caller + set_caller(accounts.bob); + // Bob transfers token Id 1 from Alice to Eve. + assert_eq!( + erc721.transfer_from(accounts.alice, accounts.eve, 1), + Ok(()) + ); + // TokenId 1 is owned by Eve. + assert_eq!(erc721.owner_of(1), Some(accounts.eve)); + // Alice owns 1 token. + assert_eq!(erc721.balance_of(accounts.alice), 1); + // Bob transfers token Id 2 from Alice to Eve. + assert_eq!( + erc721.transfer_from(accounts.alice, accounts.eve, 2), + Ok(()) + ); + // Bob does not own tokens. + assert_eq!(erc721.balance_of(accounts.bob), 0); + // Eve owns 2 tokens. + assert_eq!(erc721.balance_of(accounts.eve), 2); + // Remove operator approval for Bob on behalf of Alice. + set_caller(accounts.alice); + assert_eq!(erc721.set_approval_for_all(accounts.bob, false), Ok(())); + // Bob is not an approved operator for Alice. + assert!(!erc721.is_approved_for_all(accounts.alice, accounts.bob)); +} + +#[ink::test] +fn approve_nonexistent_token_should_fail() { + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + // Create a new contract instance. + let mut erc721 = Erc721::new(); + // Approve transfer of nonexistent token id 1 + assert_eq!(erc721.approve(accounts.bob, 1), Err(Error::TokenNotFound)); +} + +#[ink::test] +fn not_approved_transfer_should_fail() { + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + // Create a new contract instance. + let mut erc721 = Erc721::new(); + // Create token Id 1. + assert_eq!(erc721.mint(1), Ok(())); + // Alice owns 1 token. + assert_eq!(erc721.balance_of(accounts.alice), 1); + // Bob does not owns tokens. + assert_eq!(erc721.balance_of(accounts.bob), 0); + // Eve does not owns tokens. + assert_eq!(erc721.balance_of(accounts.eve), 0); + // Set Eve as caller + set_caller(accounts.eve); + // Eve is not an approved operator by Alice. + assert_eq!( + erc721.transfer_from(accounts.alice, accounts.frank, 1), + Err(Error::NotApproved) + ); + // Alice owns 1 token. + assert_eq!(erc721.balance_of(accounts.alice), 1); + // Bob does not owns tokens. + assert_eq!(erc721.balance_of(accounts.bob), 0); + // Eve does not owns tokens. + assert_eq!(erc721.balance_of(accounts.eve), 0); +} + +#[ink::test] +fn burn_works() { + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + // Create a new contract instance. + let mut erc721 = Erc721::new(); + // Create token Id 1 for Alice + assert_eq!(erc721.mint(1), Ok(())); + // Alice owns 1 token. + assert_eq!(erc721.balance_of(accounts.alice), 1); + // Alice owns token Id 1. + assert_eq!(erc721.owner_of(1), Some(accounts.alice)); + // Destroy token Id 1. + assert_eq!(erc721.burn(1), Ok(())); + // Alice does not owns tokens. + assert_eq!(erc721.balance_of(accounts.alice), 0); + // Token Id 1 does not exists + assert_eq!(erc721.owner_of(1), None); +} + +#[ink::test] +fn burn_fails_token_not_found() { + // Create a new contract instance. + let mut erc721 = Erc721::new(); + // Try burning a non existent token + assert_eq!(erc721.burn(1), Err(Error::TokenNotFound)); +} + +#[ink::test] +fn burn_fails_not_owner() { + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + // Create a new contract instance. + let mut erc721 = Erc721::new(); + // Create token Id 1 for Alice + assert_eq!(erc721.mint(1), Ok(())); + // Try burning this token with a different account + set_caller(accounts.eve); + assert_eq!(erc721.burn(1), Err(Error::NotOwner)); +} + +#[ink::test] +fn burn_clears_approval() { + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + // Create a new contract instance. + let mut erc721 = Erc721::new(); + // Create token Id 1 for Alice + assert_eq!(erc721.mint(1), Ok(())); + // Alice gives approval to Bob to transfer token Id 1 + assert_eq!(erc721.approve(accounts.bob, 1), Ok(())); + // Alice burns token + assert_eq!(erc721.burn(1), Ok(())); + // Set caller to Frank + set_caller(accounts.frank); + // Frank mints token Id 1 + assert_eq!(erc721.mint(1), Ok(())); + // Set caller to Bob + set_caller(accounts.bob); + // Bob tries to transfer token Id 1 from Frank to himself + assert_eq!( + erc721.transfer_from(accounts.frank, accounts.bob, 1), + Err(Error::NotApproved) + ); +} + +#[ink::test] +fn transfer_from_fails_not_owner() { + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + // Create a new contract instance. + let mut erc721 = Erc721::new(); + // Create token Id 1 for Alice + assert_eq!(erc721.mint(1), Ok(())); + // Bob can transfer alice's tokens + assert_eq!(erc721.set_approval_for_all(accounts.bob, true), Ok(())); + // Set caller to Frank + set_caller(accounts.frank); + // Create token Id 2 for Frank + assert_eq!(erc721.mint(2), Ok(())); + // Set caller to Bob + set_caller(accounts.bob); + // Bob makes invalid call to transfer_from (Alice is token owner, not Frank) + assert_eq!( + erc721.transfer_from(accounts.frank, accounts.bob, 1), + Err(Error::NotOwner) + ); +} + +#[ink::test] +fn transfer_fails_not_owner() { + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + // Create a new contract instance. + let mut erc721 = Erc721::new(); + // Create token Id 1 for Alice + assert_eq!(erc721.mint(1), Ok(())); + // Bob can transfer alice's tokens + assert_eq!(erc721.set_approval_for_all(accounts.bob, true), Ok(())); + // Set caller to bob + set_caller(accounts.bob); + // Bob makes invalid call to transfer (he is not token owner, Alice is) + assert_eq!(erc721.transfer(accounts.bob, 1), Err(Error::NotOwner)); +} \ No newline at end of file diff --git a/integration-tests/public/trait-dyn-cross-contract-calls/lib.rs b/integration-tests/public/trait-dyn-cross-contract-calls/lib.rs deleted file mode 100644 index 441828494b0..00000000000 --- a/integration-tests/public/trait-dyn-cross-contract-calls/lib.rs +++ /dev/null @@ -1,133 +0,0 @@ -//! This crate contains the `Caller` contract with no functionality except forwarding -//! all calls to the `trait_incrementer::Incrementer` contract. -//! -//! The `Caller` doesn't use the `trait_incrementer::IncrementerRef`. Instead, -//! all interactions with the `Incrementer` is done through the wrapper from -//! `ink::contract_ref_from_path!` and the trait `dyn_traits::Increment`. -#![cfg_attr(not(feature = "std"), no_std, no_main)] -#![allow(clippy::new_without_default)] - -#[ink::contract] -pub mod caller { - use dyn_traits::Increment; - - /// The caller of the incrementer smart contract. - #[ink(storage)] - pub struct Caller { - /// Here we accept a type which implements the `Incrementer` ink! trait. - incrementer: ink::contract_ref_from_path!(Increment), - } - - impl Caller { - /// Creates a new caller smart contract around the `incrementer` account id. - #[ink(constructor)] - pub fn new(incrementer: Address) -> Self { - Self { - incrementer: incrementer.into(), - } - } - } - - impl Increment for Caller { - #[ink(message)] - fn inc(&mut self) { - self.incrementer.inc() - } - - #[ink(message)] - fn get(&self) -> u64 { - self.incrementer.get() - } - } -} - -#[cfg(all(test, feature = "e2e-tests"))] -mod e2e_tests { - use super::caller::{ - Caller, - CallerRef, - }; - use dyn_traits::Increment; - use ink_e2e::ContractsBackend; - use trait_incrementer::incrementer::{ - Incrementer, - IncrementerRef, - }; - - type E2EResult = Result>; - - /// A test deploys and instantiates the `trait_incrementer::Incrementer` and - /// `trait_incrementer_caller::Caller` contracts, where the `Caller` uses the account - /// id of the `Incrementer` for instantiation. - /// - /// The test verifies that we can increment the value of the `Incrementer` contract - /// through the `Caller` contract. - #[ink_e2e::test] - async fn e2e_cross_contract_calls(mut client: Client) -> E2EResult<()> { - let _ = client - .upload("trait-incrementer", &ink_e2e::alice()) - .submit() - .await - .expect("uploading `trait-incrementer` failed") - .code_hash; - - let _ = client - .upload("trait-incrementer-caller", &ink_e2e::alice()) - .submit() - .await - .expect("uploading `trait-incrementer-caller` failed") - .code_hash; - - let mut constructor = IncrementerRef::new(); - - let incrementer = client - .instantiate("trait-incrementer", &ink_e2e::alice(), &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let incrementer_call = incrementer.call_builder::(); - - let mut constructor = CallerRef::new(incrementer.addr); - - let caller = client - .instantiate( - "trait-incrementer-caller", - &ink_e2e::alice(), - &mut constructor, - ) - .submit() - .await - .expect("instantiate failed"); - let mut caller_call = caller.call_builder::(); - - // Check through the caller that the value of the incrementer is zero - let get = caller_call.get(); - let value = client - .call(&ink_e2e::alice(), &get) - .dry_run() - .await? - .return_value(); - assert_eq!(value, 0); - - // Increment the value of the incrementer via the caller - let inc = caller_call.inc(); - let _ = client - .call(&ink_e2e::alice(), &inc) - .submit() - .await - .expect("calling `inc` failed"); - - // Ask the `trait-increment` about a value. It should be updated by the caller. - // Also use `contract_ref_from_path!(Increment)` instead of `IncrementerRef` - // to check that it also works with e2e testing. - let get = incrementer_call.get(); - let value = client - .call(&ink_e2e::alice(), &get) - .dry_run() - .await? - .return_value(); - assert_eq!(value, 1); - - Ok(()) - } -} diff --git a/integration-tests/public/trait-erc20/lib.rs b/integration-tests/public/trait-erc20/lib.rs deleted file mode 100644 index 55e823ba4b3..00000000000 --- a/integration-tests/public/trait-erc20/lib.rs +++ /dev/null @@ -1,562 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -#[ink::contract] -mod erc20 { - use ink::{ - U256, - storage::Mapping, - }; - - /// The ERC-20 error types. - #[derive(Debug, PartialEq, Eq)] - #[ink::error] - pub enum Error { - /// Returned if not enough balance to fulfill a request is available. - InsufficientBalance, - /// Returned if not enough allowance to fulfill a request is available. - InsufficientAllowance, - } - - /// The ERC-20 result type. - pub type Result = core::result::Result; - - /// Trait implemented by all ERC-20 respecting smart contracts. - #[ink::trait_definition] - pub trait BaseErc20 { - /// Returns the total token supply. - #[ink(message)] - fn total_supply(&self) -> U256; - - /// Returns the account balance for the specified `owner`. - #[ink(message)] - fn balance_of(&self, owner: Address) -> U256; - - /// Returns the amount which `spender` is still allowed to withdraw from `owner`. - #[ink(message)] - fn allowance(&self, owner: Address, spender: Address) -> U256; - - /// Transfers `value` amount of tokens from the caller's account to account `to`. - #[ink(message)] - fn transfer(&mut self, to: Address, value: U256) -> Result<()>; - - /// Allows `spender` to withdraw from the caller's account multiple times, up to - /// the `value` amount. - #[ink(message)] - fn approve(&mut self, spender: Address, value: U256) -> Result<()>; - - /// Transfers `value` tokens on the behalf of `from` to the account `to`. - #[ink(message)] - fn transfer_from( - &mut self, - from: Address, - to: Address, - value: U256, - ) -> Result<()>; - } - - /// A simple ERC-20 contract. - #[ink(storage)] - #[derive(Default)] - pub struct Erc20 { - /// Total token supply. - total_supply: U256, - /// Mapping from owner to number of owned token. - balances: Mapping, - /// Mapping of the token amount which an account is allowed to withdraw - /// from another account. - allowances: Mapping<(Address, Address), U256>, - } - - /// Event emitted when a token transfer occurs. - #[ink(event)] - pub struct Transfer { - #[ink(topic)] - from: Option
, - #[ink(topic)] - to: Option
, - #[ink(topic)] - value: U256, - } - - /// Event emitted when an approval occurs that `spender` is allowed to withdraw - /// up to the amount of `value` tokens from `owner`. - #[ink(event)] - pub struct Approval { - #[ink(topic)] - owner: Address, - #[ink(topic)] - spender: Address, - #[ink(topic)] - value: U256, - } - - impl Erc20 { - /// Creates a new ERC-20 contract with the specified initial supply. - #[ink(constructor)] - pub fn new(total_supply: U256) -> Self { - let mut balances = Mapping::default(); - let caller = Self::env().caller(); - balances.insert(caller, &total_supply); - Self::env().emit_event(Transfer { - from: None, - to: Some(caller), - value: total_supply, - }); - Self { - total_supply, - balances, - allowances: Default::default(), - } - } - } - - impl BaseErc20 for Erc20 { - /// Returns the total token supply. - #[ink(message)] - fn total_supply(&self) -> U256 { - self.total_supply - } - - /// Returns the account balance for the specified `owner`. - /// - /// Returns `0` if the account is non-existent. - #[ink(message)] - fn balance_of(&self, owner: Address) -> U256 { - self.balance_of_impl(&owner) - } - - /// Returns the amount which `spender` is still allowed to withdraw from `owner`. - /// - /// Returns `0` if no allowance has been set. - #[ink(message)] - fn allowance(&self, owner: Address, spender: Address) -> U256 { - self.allowance_impl(&owner, &spender) - } - - /// Transfers `value` amount of tokens from the caller's account to account `to`. - /// - /// On success a `Transfer` event is emitted. - /// - /// # Errors - /// - /// Returns `InsufficientBalance` error if there are not enough tokens on - /// the caller's account balance. - #[ink(message)] - fn transfer(&mut self, to: Address, value: U256) -> Result<()> { - let from = self.env().caller(); - self.transfer_from_to(&from, &to, value) - } - - /// Allows `spender` to withdraw from the caller's account multiple times, up to - /// the `value` amount. - /// - /// If this function is called again it overwrites the current allowance with - /// `value`. - /// - /// An `Approval` event is emitted. - #[ink(message)] - fn approve(&mut self, spender: Address, value: U256) -> Result<()> { - let owner = self.env().caller(); - self.allowances.insert((&owner, &spender), &value); - self.env().emit_event(Approval { - owner, - spender, - value, - }); - Ok(()) - } - - /// Transfers `value` tokens on the behalf of `from` to the account `to`. - /// - /// This can be used to allow a contract to transfer tokens on ones behalf and/or - /// to charge fees in sub-currencies, for example. - /// - /// On success a `Transfer` event is emitted. - /// - /// # Errors - /// - /// Returns `InsufficientAllowance` error if there are not enough tokens allowed - /// for the caller to withdraw from `from`. - /// - /// Returns `InsufficientBalance` error if there are not enough tokens on - /// the account balance of `from`. - #[ink(message)] - fn transfer_from( - &mut self, - from: Address, - to: Address, - value: U256, - ) -> Result<()> { - let caller = self.env().caller(); - let allowance = self.allowance_impl(&from, &caller); - if allowance < value { - return Err(Error::InsufficientAllowance) - } - self.transfer_from_to(&from, &to, value)?; - // We checked that allowance >= value - #[allow(clippy::arithmetic_side_effects)] - self.allowances - .insert((&from, &caller), &(allowance - value)); - Ok(()) - } - } - - #[ink(impl)] - impl Erc20 { - /// Returns the account balance for the specified `owner`. - /// - /// Returns `0` if the account is non-existent. - /// - /// # Note - /// - /// Prefer to call this method over `balance_of` since this - /// works using references which are more efficient. - #[inline] - fn balance_of_impl(&self, owner: &Address) -> U256 { - self.balances.get(owner).unwrap_or_default() - } - - /// Returns the amount which `spender` is still allowed to withdraw from `owner`. - /// - /// Returns `0` if no allowance has been set. - /// - /// # Note - /// - /// Prefer to call this method over `allowance` since this - /// works using references which are more efficient. - #[inline] - fn allowance_impl(&self, owner: &Address, spender: &Address) -> U256 { - self.allowances.get((owner, spender)).unwrap_or_default() - } - - /// Transfers `value` amount of tokens from the caller's account to account `to`. - /// - /// On success a `Transfer` event is emitted. - /// - /// # Errors - /// - /// Returns `InsufficientBalance` error if there are not enough tokens on - /// the caller's account balance. - fn transfer_from_to( - &mut self, - from: &Address, - to: &Address, - value: U256, - ) -> Result<()> { - let from_balance = self.balance_of_impl(from); - if from_balance < value { - return Err(Error::InsufficientBalance) - } - // We checked that from_balance >= value - #[allow(clippy::arithmetic_side_effects)] - self.balances.insert(from, &(from_balance - value)); - let to_balance = self.balance_of_impl(to); - self.balances - .insert(to, &(to_balance.checked_add(value).unwrap())); - self.env().emit_event(Transfer { - from: Some(*from), - to: Some(*to), - value, - }); - Ok(()) - } - } - - /// Unit tests. - #[cfg(test)] - mod tests { - /// Imports all the definitions from the outer scope so we can use them here. - use super::*; - use ink::{ - env::hash::{ - Blake2x256, - CryptoHash, - HashOutput, - }, - primitives::Clear, - }; - - fn assert_transfer_event( - event: &ink::env::test::EmittedEvent, - expected_from: Option
, - expected_to: Option
, - expected_value: U256, - ) { - let decoded_event = - ::decode(&mut &event.data[..]) - .expect("encountered invalid contract event data buffer"); - let Transfer { from, to, value } = decoded_event; - assert_eq!(from, expected_from, "encountered invalid Transfer.from"); - assert_eq!(to, expected_to, "encountered invalid Transfer.to"); - assert_eq!(value, expected_value, "encountered invalid Transfer.value"); - - fn encoded_into_hash(entity: T) -> Hash - where - T: ink::scale::Encode, - { - let mut result = Hash::CLEAR_HASH; - let len_result = result.as_ref().len(); - let encoded = entity.encode(); - let len_encoded = encoded.len(); - if len_encoded <= len_result { - result.as_mut()[..len_encoded].copy_from_slice(&encoded); - return result - } - let mut hash_output = - <::Type as Default>::default(); - ::hash(&encoded, &mut hash_output); - let copy_len = core::cmp::min(hash_output.len(), len_result); - result.as_mut()[0..copy_len].copy_from_slice(&hash_output[0..copy_len]); - result - } - - let mut expected_topics = Vec::new(); - expected_topics.push( - ink::blake2x256!("Transfer(Option
,Option
,U256)").into(), - ); - if let Some(from) = expected_from { - expected_topics.push(encoded_into_hash(from)); - } else { - expected_topics.push(Hash::CLEAR_HASH); - } - if let Some(to) = expected_to { - expected_topics.push(encoded_into_hash(to)); - } else { - expected_topics.push(Hash::CLEAR_HASH); - } - expected_topics.push(encoded_into_hash(value)); - - for (n, (actual_topic, expected_topic)) in - event.topics.iter().zip(expected_topics).enumerate() - { - let topic = ::decode(&mut &actual_topic[..]) - .expect("encountered invalid topic encoding"); - assert_eq!(topic, expected_topic, "encountered invalid topic at {n}"); - } - } - - /// The default constructor does its job. - #[ink::test] - fn new_works() { - // Constructor works. - set_caller(Address::from([0x01; 20])); - let initial_supply = 100.into(); - let erc20 = Erc20::new(initial_supply); - - // The `BaseErc20` trait has indeed been implemented. - assert_eq!(::total_supply(&erc20), initial_supply); - - // Transfer event triggered during initial construction. - let emitted_events = ink::env::test::recorded_events(); - assert_eq!(1, emitted_events.len()); - - assert_transfer_event( - &emitted_events[0], - None, - Some(Address::from([0x01; 20])), - 100.into(), - ); - } - - /// The total supply was applied. - #[ink::test] - fn total_supply_works() { - // Constructor works. - set_caller(Address::from([0x01; 20])); - let initial_supply = 100.into(); - let erc20 = Erc20::new(initial_supply); - // Transfer event triggered during initial construction. - let emitted_events = ink::env::test::recorded_events(); - assert_transfer_event( - &emitted_events[0], - None, - Some(Address::from([0x01; 20])), - 100.into(), - ); - // Get the token total supply. - assert_eq!(erc20.total_supply(), 100.into()); - } - - /// Get the actual balance of an account. - #[ink::test] - fn balance_of_works() { - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - - // Constructor works - let initial_supply = 100.into(); - let erc20 = Erc20::new(initial_supply); - // Transfer event triggered during initial construction - let emitted_events = ink::env::test::recorded_events(); - assert_transfer_event( - &emitted_events[0], - None, - Some(accounts.alice), - 100.into(), - ); - // Alice owns all the tokens on contract instantiation - assert_eq!(erc20.balance_of(accounts.alice), 100.into()); - // Bob does not owns tokens - assert_eq!(erc20.balance_of(accounts.bob), U256::zero()); - } - - #[ink::test] - fn transfer_works() { - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - - // Constructor works. - let initial_supply = 100.into(); - let mut erc20 = Erc20::new(initial_supply); - // Transfer event triggered during initial construction. - assert_eq!(erc20.balance_of(accounts.bob), U256::zero()); - // Alice transfers 10 tokens to Bob. - assert_eq!(erc20.transfer(accounts.bob, U256::from(10)), Ok(())); - // Bob owns 10 tokens. - assert_eq!(erc20.balance_of(accounts.bob), U256::from(10)); - - let emitted_events = ink::env::test::recorded_events(); - assert_eq!(emitted_events.len(), 2); - // Check first transfer event related to ERC-20 instantiation. - assert_transfer_event( - &emitted_events[0], - None, - Some(accounts.alice), - 100.into(), - ); - // Check the second transfer event relating to the actual transfer. - assert_transfer_event( - &emitted_events[1], - Some(accounts.alice), - Some(accounts.bob), - 10.into(), - ); - } - - #[ink::test] - fn invalid_transfer_should_fail() { - // Constructor works. - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - - let initial_supply = 100.into(); - let mut erc20 = Erc20::new(initial_supply); - - assert_eq!(erc20.balance_of(accounts.bob), U256::zero()); - // Set Bob as caller - set_caller(accounts.bob); - - // Bob fails to transfer 10 tokens to Eve. - assert_eq!( - erc20.transfer(accounts.eve, 10.into()), - Err(Error::InsufficientBalance) - ); - // Alice owns all the tokens. - assert_eq!(erc20.balance_of(accounts.alice), 100.into()); - assert_eq!(erc20.balance_of(accounts.bob), U256::zero()); - assert_eq!(erc20.balance_of(accounts.eve), U256::zero()); - - // Transfer event triggered during initial construction. - let emitted_events = ink::env::test::recorded_events(); - assert_eq!(emitted_events.len(), 1); - assert_transfer_event( - &emitted_events[0], - None, - Some(accounts.alice), - 100.into(), - ); - } - - #[ink::test] - fn transfer_from_works() { - // Constructor works. - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - - let initial_supply = 100.into(); - let mut erc20 = Erc20::new(initial_supply); - - // Transfer event triggered during initial construction. - let accounts = ink::env::test::default_accounts(); - - // Bob fails to transfer tokens owned by Alice. - assert_eq!( - erc20.transfer_from(accounts.alice, accounts.eve, 10.into()), - Err(Error::InsufficientAllowance) - ); - // Alice approves Bob for token transfers on her behalf. - assert_eq!(erc20.approve(accounts.bob, U256::from(10)), Ok(())); - - // The approve event takes place. - assert_eq!(ink::env::test::recorded_events().len(), 2); - - // Set Bob as caller. - set_caller(accounts.bob); - - // Bob transfers tokens from Alice to Eve. - assert_eq!( - erc20.transfer_from(accounts.alice, accounts.eve, 10.into()), - Ok(()) - ); - // Eve owns tokens. - assert_eq!(erc20.balance_of(accounts.eve), U256::from(10)); - - // Check all transfer events that happened during the previous calls: - let emitted_events = ink::env::test::recorded_events(); - assert_eq!(emitted_events.len(), 3); - assert_transfer_event( - &emitted_events[0], - None, - Some(accounts.alice), - 100.into(), - ); - // The second event `emitted_events[1]` is an Approve event that we skip - // checking. - assert_transfer_event( - &emitted_events[2], - Some(accounts.alice), - Some(accounts.eve), - 10.into(), - ); - } - - #[ink::test] - fn allowance_must_not_change_on_failed_transfer() { - let accounts = ink::env::test::default_accounts(); - set_caller(accounts.alice); - let initial_supply = 100.into(); - let mut erc20 = Erc20::new(initial_supply); - - // Alice approves Bob for token transfers on her behalf. - let alice_balance = erc20.balance_of(accounts.alice); - let initial_allowance = alice_balance + U256::from(2); - assert_eq!(erc20.approve(accounts.bob, initial_allowance), Ok(())); - - // Set Bob as caller. - set_caller(accounts.bob); - - // Bob tries to transfer tokens from Alice to Eve. - let emitted_events_before = ink::env::test::recorded_events(); - assert_eq!( - erc20.transfer_from( - accounts.alice, - accounts.eve, - alice_balance + U256::from(1) - ), - Err(Error::InsufficientBalance) - ); - // Allowance must have stayed the same - assert_eq!( - erc20.allowance(accounts.alice, accounts.bob), - initial_allowance - ); - // No more events must have been emitted - let emitted_events_after = ink::env::test::recorded_events(); - assert_eq!(emitted_events_before.len(), emitted_events_after.len()); - } - - fn set_caller(sender: Address) { - ink::env::test::set_caller(sender); - } - } -} diff --git a/integration-tests/public/trait-dyn-cross-contract-calls/Cargo.toml b/integration-tests/public/traits/dyn-cross-contract/Cargo.toml similarity index 100% rename from integration-tests/public/trait-dyn-cross-contract-calls/Cargo.toml rename to integration-tests/public/traits/dyn-cross-contract/Cargo.toml diff --git a/integration-tests/public/trait-dyn-cross-contract-calls/contracts/incrementer/Cargo.toml b/integration-tests/public/traits/dyn-cross-contract/contracts/incrementer/Cargo.toml similarity index 100% rename from integration-tests/public/trait-dyn-cross-contract-calls/contracts/incrementer/Cargo.toml rename to integration-tests/public/traits/dyn-cross-contract/contracts/incrementer/Cargo.toml diff --git a/integration-tests/public/trait-dyn-cross-contract-calls/contracts/incrementer/lib.rs b/integration-tests/public/traits/dyn-cross-contract/contracts/incrementer/lib.rs similarity index 100% rename from integration-tests/public/trait-dyn-cross-contract-calls/contracts/incrementer/lib.rs rename to integration-tests/public/traits/dyn-cross-contract/contracts/incrementer/lib.rs diff --git a/integration-tests/public/traits/dyn-cross-contract/lib.rs b/integration-tests/public/traits/dyn-cross-contract/lib.rs new file mode 100644 index 00000000000..d7df6217c70 --- /dev/null +++ b/integration-tests/public/traits/dyn-cross-contract/lib.rs @@ -0,0 +1,45 @@ +//! This crate contains the `Caller` contract with no functionality except forwarding +//! all calls to the `trait_incrementer::Incrementer` contract. +//! +//! The `Caller` doesn't use the `trait_incrementer::IncrementerRef`. Instead, +//! all interactions with the `Incrementer` is done through the wrapper from +//! `ink::contract_ref_from_path!` and the trait `dyn_traits::Increment`. +#![cfg_attr(not(feature = "std"), no_std, no_main)] +#![allow(clippy::new_without_default)] + +#[ink::contract] +pub mod caller { + use dyn_traits::Increment; + + /// The caller of the incrementer smart contract. + #[ink(storage)] + pub struct Caller { + /// Here we accept a type which implements the `Incrementer` ink! trait. + incrementer: ink::contract_ref_from_path!(Increment), + } + + impl Caller { + /// Creates a new caller smart contract around the `incrementer` account id. + #[ink(constructor)] + pub fn new(incrementer: Address) -> Self { + Self { + incrementer: incrementer.into(), + } + } + } + + impl Increment for Caller { + #[ink(message)] + fn inc(&mut self) { + self.incrementer.inc() + } + + #[ink(message)] + fn get(&self) -> u64 { + self.incrementer.get() + } + } +} + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/traits/dyn-cross-contract/tests.rs b/integration-tests/public/traits/dyn-cross-contract/tests.rs new file mode 100644 index 00000000000..c97860cd5a8 --- /dev/null +++ b/integration-tests/public/traits/dyn-cross-contract/tests.rs @@ -0,0 +1,87 @@ +use super::caller::{ + Caller, + CallerRef, +}; +use dyn_traits::Increment; +use ink_e2e::ContractsBackend; +use trait_incrementer::incrementer::{ + Incrementer, + IncrementerRef, +}; + +type E2EResult = Result>; + +/// A test deploys and instantiates the `trait_incrementer::Incrementer` and +/// `trait_incrementer_caller::Caller` contracts, where the `Caller` uses the account +/// id of the `Incrementer` for instantiation. +/// +/// The test verifies that we can increment the value of the `Incrementer` contract +/// through the `Caller` contract. +#[ink_e2e::test] +async fn e2e_cross_contract_calls(mut client: Client) -> E2EResult<()> { + let _ = client + .upload("trait-incrementer", &ink_e2e::alice()) + .submit() + .await + .expect("uploading `trait-incrementer` failed") + .code_hash; + + let _ = client + .upload("trait-incrementer-caller", &ink_e2e::alice()) + .submit() + .await + .expect("uploading `trait-incrementer-caller` failed") + .code_hash; + + let mut constructor = IncrementerRef::new(); + + let incrementer = client + .instantiate("trait-incrementer", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let incrementer_call = incrementer.call_builder::(); + + let mut constructor = CallerRef::new(incrementer.addr); + + let caller = client + .instantiate( + "trait-incrementer-caller", + &ink_e2e::alice(), + &mut constructor, + ) + .submit() + .await + .expect("instantiate failed"); + let mut caller_call = caller.call_builder::(); + + // Check through the caller that the value of the incrementer is zero + let get = caller_call.get(); + let value = client + .call(&ink_e2e::alice(), &get) + .dry_run() + .await? + .return_value(); + assert_eq!(value, 0); + + // Increment the value of the incrementer via the caller + let inc = caller_call.inc(); + let _ = client + .call(&ink_e2e::alice(), &inc) + .submit() + .await + .expect("calling `inc` failed"); + + // Ask the `trait-increment` about a value. It should be updated by the caller. + // Also use `contract_ref_from_path!(Increment)` instead of `IncrementerRef` + // to check that it also works with e2e testing. + let get = incrementer_call.get(); + let value = client + .call(&ink_e2e::alice(), &get) + .dry_run() + .await? + .return_value(); + assert_eq!(value, 1); + + Ok(()) +} \ No newline at end of file diff --git a/integration-tests/public/trait-dyn-cross-contract-calls/traits/Cargo.toml b/integration-tests/public/traits/dyn-cross-contract/traits/Cargo.toml similarity index 100% rename from integration-tests/public/trait-dyn-cross-contract-calls/traits/Cargo.toml rename to integration-tests/public/traits/dyn-cross-contract/traits/Cargo.toml diff --git a/integration-tests/public/trait-dyn-cross-contract-calls/traits/lib.rs b/integration-tests/public/traits/dyn-cross-contract/traits/lib.rs similarity index 100% rename from integration-tests/public/trait-dyn-cross-contract-calls/traits/lib.rs rename to integration-tests/public/traits/dyn-cross-contract/traits/lib.rs diff --git a/integration-tests/public/trait-erc20/Cargo.toml b/integration-tests/public/traits/erc20/Cargo.toml similarity index 100% rename from integration-tests/public/trait-erc20/Cargo.toml rename to integration-tests/public/traits/erc20/Cargo.toml diff --git a/integration-tests/public/traits/erc20/lib.rs b/integration-tests/public/traits/erc20/lib.rs new file mode 100644 index 00000000000..dd6542d6ecd --- /dev/null +++ b/integration-tests/public/traits/erc20/lib.rs @@ -0,0 +1,267 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +pub mod erc20 { + use ink::{ + U256, + storage::Mapping, + }; + + /// The ERC-20 error types. + #[derive(Debug, PartialEq, Eq)] + #[ink::error] + pub enum Error { + /// Returned if not enough balance to fulfill a request is available. + InsufficientBalance, + /// Returned if not enough allowance to fulfill a request is available. + InsufficientAllowance, + } + + /// The ERC-20 result type. + pub type Result = core::result::Result; + + /// Trait implemented by all ERC-20 respecting smart contracts. + #[ink::trait_definition] + pub trait BaseErc20 { + /// Returns the total token supply. + #[ink(message)] + fn total_supply(&self) -> U256; + + /// Returns the account balance for the specified `owner`. + #[ink(message)] + fn balance_of(&self, owner: Address) -> U256; + + /// Returns the amount which `spender` is still allowed to withdraw from `owner`. + #[ink(message)] + fn allowance(&self, owner: Address, spender: Address) -> U256; + + /// Transfers `value` amount of tokens from the caller's account to account `to`. + #[ink(message)] + fn transfer(&mut self, to: Address, value: U256) -> Result<()>; + + /// Allows `spender` to withdraw from the caller's account multiple times, up to + /// the `value` amount. + #[ink(message)] + fn approve(&mut self, spender: Address, value: U256) -> Result<()>; + + /// Transfers `value` tokens on the behalf of `from` to the account `to`. + #[ink(message)] + fn transfer_from( + &mut self, + from: Address, + to: Address, + value: U256, + ) -> Result<()>; + } + + /// A simple ERC-20 contract. + #[ink(storage)] + #[derive(Default)] + pub struct Erc20 { + /// Total token supply. + total_supply: U256, + /// Mapping from owner to number of owned token. + balances: Mapping, + /// Mapping of the token amount which an account is allowed to withdraw + /// from another account. + allowances: Mapping<(Address, Address), U256>, + } + + /// Event emitted when a token transfer occurs. + #[ink(event)] + pub struct Transfer { + #[ink(topic)] + pub from: Option
, + #[ink(topic)] + pub to: Option
, + #[ink(topic)] + pub value: U256, + } + + /// Event emitted when an approval occurs that `spender` is allowed to withdraw + /// up to the amount of `value` tokens from `owner`. + #[ink(event)] + pub struct Approval { + #[ink(topic)] + pub owner: Address, + #[ink(topic)] + pub spender: Address, + #[ink(topic)] + pub value: U256, + } + + impl Erc20 { + /// Creates a new ERC-20 contract with the specified initial supply. + #[ink(constructor)] + pub fn new(total_supply: U256) -> Self { + let mut balances = Mapping::default(); + let caller = Self::env().caller(); + balances.insert(caller, &total_supply); + Self::env().emit_event(Transfer { + from: None, + to: Some(caller), + value: total_supply, + }); + Self { + total_supply, + balances, + allowances: Default::default(), + } + } + } + + impl BaseErc20 for Erc20 { + /// Returns the total token supply. + #[ink(message)] + fn total_supply(&self) -> U256 { + self.total_supply + } + + /// Returns the account balance for the specified `owner`. + /// + /// Returns `0` if the account is non-existent. + #[ink(message)] + fn balance_of(&self, owner: Address) -> U256 { + self.balance_of_impl(&owner) + } + + /// Returns the amount which `spender` is still allowed to withdraw from `owner`. + /// + /// Returns `0` if no allowance has been set. + #[ink(message)] + fn allowance(&self, owner: Address, spender: Address) -> U256 { + self.allowance_impl(&owner, &spender) + } + + /// Transfers `value` amount of tokens from the caller's account to account `to`. + /// + /// On success a `Transfer` event is emitted. + /// + /// # Errors + /// + /// Returns `InsufficientBalance` error if there are not enough tokens on + /// the caller's account balance. + #[ink(message)] + fn transfer(&mut self, to: Address, value: U256) -> Result<()> { + let from = self.env().caller(); + self.transfer_from_to(&from, &to, value) + } + + /// Allows `spender` to withdraw from the caller's account multiple times, up to + /// the `value` amount. + /// + /// If this function is called again it overwrites the current allowance with + /// `value`. + /// + /// An `Approval` event is emitted. + #[ink(message)] + fn approve(&mut self, spender: Address, value: U256) -> Result<()> { + let owner = self.env().caller(); + self.allowances.insert((&owner, &spender), &value); + self.env().emit_event(Approval { + owner, + spender, + value, + }); + Ok(()) + } + + /// Transfers `value` tokens on the behalf of `from` to the account `to`. + /// + /// This can be used to allow a contract to transfer tokens on ones behalf and/or + /// to charge fees in sub-currencies, for example. + /// + /// On success a `Transfer` event is emitted. + /// + /// # Errors + /// + /// Returns `InsufficientAllowance` error if there are not enough tokens allowed + /// for the caller to withdraw from `from`. + /// + /// Returns `InsufficientBalance` error if there are not enough tokens on + /// the account balance of `from`. + #[ink(message)] + fn transfer_from( + &mut self, + from: Address, + to: Address, + value: U256, + ) -> Result<()> { + let caller = self.env().caller(); + let allowance = self.allowance_impl(&from, &caller); + if allowance < value { + return Err(Error::InsufficientAllowance) + } + self.transfer_from_to(&from, &to, value)?; + // We checked that allowance >= value + #[allow(clippy::arithmetic_side_effects)] + self.allowances + .insert((&from, &caller), &(allowance - value)); + Ok(()) + } + } + + #[ink(impl)] + impl Erc20 { + /// Returns the account balance for the specified `owner`. + /// + /// Returns `0` if the account is non-existent. + /// + /// # Note + /// + /// Prefer to call this method over `balance_of` since this + /// works using references which are more efficient. + #[inline] + fn balance_of_impl(&self, owner: &Address) -> U256 { + self.balances.get(owner).unwrap_or_default() + } + + /// Returns the amount which `spender` is still allowed to withdraw from `owner`. + /// + /// Returns `0` if no allowance has been set. + /// + /// # Note + /// + /// Prefer to call this method over `allowance` since this + /// works using references which are more efficient. + #[inline] + fn allowance_impl(&self, owner: &Address, spender: &Address) -> U256 { + self.allowances.get((owner, spender)).unwrap_or_default() + } + + /// Transfers `value` amount of tokens from the caller's account to account `to`. + /// + /// On success a `Transfer` event is emitted. + /// + /// # Errors + /// + /// Returns `InsufficientBalance` error if there are not enough tokens on + /// the caller's account balance. + fn transfer_from_to( + &mut self, + from: &Address, + to: &Address, + value: U256, + ) -> Result<()> { + let from_balance = self.balance_of_impl(from); + if from_balance < value { + return Err(Error::InsufficientBalance) + } + // We checked that from_balance >= value + #[allow(clippy::arithmetic_side_effects)] + self.balances.insert(from, &(from_balance - value)); + let to_balance = self.balance_of_impl(to); + self.balances + .insert(to, &(to_balance.checked_add(value).unwrap())); + self.env().emit_event(Transfer { + from: Some(*from), + to: Some(*to), + value, + }); + Ok(()) + } + } +} + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/traits/erc20/tests.rs b/integration-tests/public/traits/erc20/tests.rs new file mode 100644 index 00000000000..2c3183e2e1d --- /dev/null +++ b/integration-tests/public/traits/erc20/tests.rs @@ -0,0 +1,294 @@ +use super::erc20::*; +use ink::{ + env::hash::{ + Blake2x256, + CryptoHash, + HashOutput, + }, + primitives::{ + Clear, + Hash, + }, + U256, +}; + +fn assert_transfer_event( + event: &ink::env::test::EmittedEvent, + expected_from: Option
, + expected_to: Option
, + expected_value: U256, +) { + let decoded_event = ::decode(&mut &event.data[..]) + .expect("encountered invalid contract event data buffer"); + let Transfer { from, to, value } = decoded_event; + assert_eq!(from, expected_from, "encountered invalid Transfer.from"); + assert_eq!(to, expected_to, "encountered invalid Transfer.to"); + assert_eq!(value, expected_value, "encountered invalid Transfer.value"); + + fn encoded_into_hash(entity: T) -> Hash + where + T: ink::scale::Encode, + { + let mut result = Hash::CLEAR_HASH; + let len_result = result.as_ref().len(); + let encoded = entity.encode(); + let len_encoded = encoded.len(); + if len_encoded <= len_result { + result.as_mut()[..len_encoded].copy_from_slice(&encoded); + return result + } + let mut hash_output = <::Type as Default>::default(); + ::hash(&encoded, &mut hash_output); + let copy_len = core::cmp::min(hash_output.len(), len_result); + result.as_mut()[0..copy_len].copy_from_slice(&hash_output[0..copy_len]); + result + } + + let mut expected_topics = Vec::new(); + expected_topics.push( + ink::blake2x256!("Transfer(Option
,Option
,U256)").into(), + ); + if let Some(from) = expected_from { + expected_topics.push(encoded_into_hash(from)); + } else { + expected_topics.push(Hash::CLEAR_HASH); + } + if let Some(to) = expected_to { + expected_topics.push(encoded_into_hash(to)); + } else { + expected_topics.push(Hash::CLEAR_HASH); + } + expected_topics.push(encoded_into_hash(value)); + + for (n, (actual_topic, expected_topic)) in + event.topics.iter().zip(expected_topics).enumerate() + { + let topic = ::decode(&mut &actual_topic[..]) + .expect("encountered invalid topic encoding"); + assert_eq!(topic, expected_topic, "encountered invalid topic at {n}"); + } +} + +/// The default constructor does its job. +#[ink::test] +fn new_works() { + // Constructor works. + set_caller(Address::from([0x01; 20])); + let initial_supply = 100.into(); + let erc20 = Erc20::new(initial_supply); + + // The `BaseErc20` trait has indeed been implemented. + assert_eq!(::total_supply(&erc20), initial_supply); + + // Transfer event triggered during initial construction. + let emitted_events = ink::env::test::recorded_events(); + assert_eq!(1, emitted_events.len()); + + assert_transfer_event( + &emitted_events[0], + None, + Some(Address::from([0x01; 20])), + 100.into(), + ); +} + +/// The total supply was applied. +#[ink::test] +fn total_supply_works() { + // Constructor works. + set_caller(Address::from([0x01; 20])); + let initial_supply = 100.into(); + let erc20 = Erc20::new(initial_supply); + // Transfer event triggered during initial construction. + let emitted_events = ink::env::test::recorded_events(); + assert_transfer_event( + &emitted_events[0], + None, + Some(Address::from([0x01; 20])), + 100.into(), + ); + // Get the token total supply. + assert_eq!(erc20.total_supply(), 100.into()); +} + +/// Get the actual balance of an account. +#[ink::test] +fn balance_of_works() { + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + + // Constructor works + let initial_supply = 100.into(); + let erc20 = Erc20::new(initial_supply); + // Transfer event triggered during initial construction + let emitted_events = ink::env::test::recorded_events(); + assert_transfer_event( + &emitted_events[0], + None, + Some(accounts.alice), + 100.into(), + ); + // Alice owns all the tokens on contract instantiation + assert_eq!(erc20.balance_of(accounts.alice), 100.into()); + // Bob does not owns tokens + assert_eq!(erc20.balance_of(accounts.bob), U256::zero()); +} + +#[ink::test] +fn transfer_works() { + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + + // Constructor works. + let initial_supply = 100.into(); + let mut erc20 = Erc20::new(initial_supply); + // Transfer event triggered during initial construction. + assert_eq!(erc20.balance_of(accounts.bob), U256::zero()); + // Alice transfers 10 tokens to Bob. + assert_eq!(erc20.transfer(accounts.bob, U256::from(10)), Ok(())); + // Bob owns 10 tokens. + assert_eq!(erc20.balance_of(accounts.bob), U256::from(10)); + + let emitted_events = ink::env::test::recorded_events(); + assert_eq!(emitted_events.len(), 2); + // Check first transfer event related to ERC-20 instantiation. + assert_transfer_event( + &emitted_events[0], + None, + Some(accounts.alice), + 100.into(), + ); + // Check the second transfer event relating to the actual transfer. + assert_transfer_event( + &emitted_events[1], + Some(accounts.alice), + Some(accounts.bob), + 10.into(), + ); +} + +#[ink::test] +fn invalid_transfer_should_fail() { + // Constructor works. + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + + let initial_supply = 100.into(); + let mut erc20 = Erc20::new(initial_supply); + + assert_eq!(erc20.balance_of(accounts.bob), U256::zero()); + // Set Bob as caller + set_caller(accounts.bob); + + // Bob fails to transfer 10 tokens to Eve. + assert_eq!( + erc20.transfer(accounts.eve, 10.into()), + Err(Error::InsufficientBalance) + ); + // Alice owns all the tokens. + assert_eq!(erc20.balance_of(accounts.alice), 100.into()); + assert_eq!(erc20.balance_of(accounts.bob), U256::zero()); + assert_eq!(erc20.balance_of(accounts.eve), U256::zero()); + + // Transfer event triggered during initial construction. + let emitted_events = ink::env::test::recorded_events(); + assert_eq!(emitted_events.len(), 1); + assert_transfer_event( + &emitted_events[0], + None, + Some(accounts.alice), + 100.into(), + ); +} + +#[ink::test] +fn transfer_from_works() { + // Constructor works. + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + + let initial_supply = 100.into(); + let mut erc20 = Erc20::new(initial_supply); + + // Transfer event triggered during initial construction. + let accounts = ink::env::test::default_accounts(); + + // Bob fails to transfer tokens owned by Alice. + assert_eq!( + erc20.transfer_from(accounts.alice, accounts.eve, 10.into()), + Err(Error::InsufficientAllowance) + ); + // Alice approves Bob for token transfers on her behalf. + assert_eq!(erc20.approve(accounts.bob, U256::from(10)), Ok(())); + + // The approve event takes place. + assert_eq!(ink::env::test::recorded_events().len(), 2); + + // Set Bob as caller. + set_caller(accounts.bob); + + // Bob transfers tokens from Alice to Eve. + assert_eq!( + erc20.transfer_from(accounts.alice, accounts.eve, 10.into()), + Ok(()) + ); + // Eve owns tokens. + assert_eq!(erc20.balance_of(accounts.eve), U256::from(10)); + + // Check all transfer events that happened during the previous calls: + let emitted_events = ink::env::test::recorded_events(); + assert_eq!(emitted_events.len(), 3); + assert_transfer_event( + &emitted_events[0], + None, + Some(accounts.alice), + 100.into(), + ); + // The second event `emitted_events[1]` is an Approve event that we skip + // checking. + assert_transfer_event( + &emitted_events[2], + Some(accounts.alice), + Some(accounts.eve), + 10.into(), + ); +} + +#[ink::test] +fn allowance_must_not_change_on_failed_transfer() { + let accounts = ink::env::test::default_accounts(); + set_caller(accounts.alice); + let initial_supply = 100.into(); + let mut erc20 = Erc20::new(initial_supply); + + // Alice approves Bob for token transfers on her behalf. + let alice_balance = erc20.balance_of(accounts.alice); + let initial_allowance = alice_balance + U256::from(2); + assert_eq!(erc20.approve(accounts.bob, initial_allowance), Ok(())); + + // Set Bob as caller. + set_caller(accounts.bob); + + // Bob tries to transfer tokens from Alice to Eve. + let emitted_events_before = ink::env::test::recorded_events(); + assert_eq!( + erc20.transfer_from( + accounts.alice, + accounts.eve, + alice_balance + U256::from(1) + ), + Err(Error::InsufficientBalance) + ); + // Allowance must have stayed the same + assert_eq!( + erc20.allowance(accounts.alice, accounts.bob), + initial_allowance + ); + // No more events must have been emitted + let emitted_events_after = ink::env::test::recorded_events(); + assert_eq!(emitted_events_before.len(), emitted_events_after.len()); +} + +fn set_caller(sender: Address) { + ink::env::test::set_caller(sender); +} \ No newline at end of file diff --git a/integration-tests/public/trait-flipper/Cargo.toml b/integration-tests/public/traits/flipper/Cargo.toml similarity index 100% rename from integration-tests/public/trait-flipper/Cargo.toml rename to integration-tests/public/traits/flipper/Cargo.toml diff --git a/integration-tests/public/trait-flipper/lib.rs b/integration-tests/public/traits/flipper/lib.rs similarity index 60% rename from integration-tests/public/trait-flipper/lib.rs rename to integration-tests/public/traits/flipper/lib.rs index b465ea382e8..87616caefcd 100644 --- a/integration-tests/public/trait-flipper/lib.rs +++ b/integration-tests/public/traits/flipper/lib.rs @@ -40,25 +40,7 @@ pub mod flipper { self.value } } - - #[cfg(test)] - mod tests { - use super::*; - - #[::ink::test] - fn default_works() { - let flipper = Flipper::new(); - assert!(flipper.get()); - } - - #[::ink::test] - fn it_works() { - let mut flipper = Flipper::new(); - // Can call using universal call syntax using the trait. - assert!(::get(&flipper)); - ::flip(&mut flipper); - // Normal call syntax possible to as long as the trait is in scope. - assert!(!flipper.get()); - } - } } + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/traits/flipper/tests.rs b/integration-tests/public/traits/flipper/tests.rs new file mode 100644 index 00000000000..50be53f2eb9 --- /dev/null +++ b/integration-tests/public/traits/flipper/tests.rs @@ -0,0 +1,28 @@ +use super::Flip; +use super::flipper::Flipper; + +#[::ink::test] +fn default_works() { + // Given + let flipper = Flipper::new(); + + // Then + assert!(flipper.get()); +} + +#[::ink::test] +fn it_works() { + // Given + let mut flipper = Flipper::new(); + + // Then + // Can call using universal call syntax using the trait. + assert!(::get(&flipper)); + + // When + ::flip(&mut flipper); + + // Then + // Normal call syntax possible to as long as the trait is in scope. + assert!(!flipper.get()); +} \ No newline at end of file diff --git a/integration-tests/public/trait-incrementer/Cargo.toml b/integration-tests/public/traits/incrementer/Cargo.toml similarity index 100% rename from integration-tests/public/trait-incrementer/Cargo.toml rename to integration-tests/public/traits/incrementer/Cargo.toml diff --git a/integration-tests/public/trait-incrementer/lib.rs b/integration-tests/public/traits/incrementer/lib.rs similarity index 63% rename from integration-tests/public/trait-incrementer/lib.rs rename to integration-tests/public/traits/incrementer/lib.rs index 65edd6f9d16..fd3eff95acb 100644 --- a/integration-tests/public/trait-incrementer/lib.rs +++ b/integration-tests/public/traits/incrementer/lib.rs @@ -46,25 +46,7 @@ pub mod incrementer { self.value = 0; } } - - #[cfg(test)] - mod tests { - use super::*; - - #[test] - fn default_works() { - let incrementer = Incrementer::new(0); - assert_eq!(incrementer.get(), 0); - } - - #[test] - fn it_works() { - let mut incrementer = Incrementer::new(0); - // Can call using universal call syntax using the trait. - assert_eq!(::get(&incrementer), 0); - ::inc(&mut incrementer); - // Normal call syntax possible to as long as the trait is in scope. - assert_eq!(incrementer.get(), 1); - } - } } + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/integration-tests/public/traits/incrementer/tests.rs b/integration-tests/public/traits/incrementer/tests.rs new file mode 100644 index 00000000000..5bbd07a12f2 --- /dev/null +++ b/integration-tests/public/traits/incrementer/tests.rs @@ -0,0 +1,28 @@ +use super::incrementer::Incrementer; +use traits::Increment; + +#[test] +fn default_works() { + // Given + let incrementer = Incrementer::new(0); + + // Then + assert_eq!(incrementer.get(), 0); +} + +#[test] +fn it_works() { + // Given + let mut incrementer = Incrementer::new(0); + + // Then + // Can call using universal call syntax using the trait. + assert_eq!(::get(&incrementer), 0); + + // When + ::inc(&mut incrementer); + + // Then + // Normal call syntax possible to as long as the trait is in scope. + assert_eq!(incrementer.get(), 1); +} \ No newline at end of file diff --git a/integration-tests/public/trait-incrementer/traits/Cargo.toml b/integration-tests/public/traits/incrementer/traits/Cargo.toml similarity index 100% rename from integration-tests/public/trait-incrementer/traits/Cargo.toml rename to integration-tests/public/traits/incrementer/traits/Cargo.toml diff --git a/integration-tests/public/trait-incrementer/traits/lib.rs b/integration-tests/public/traits/incrementer/traits/lib.rs similarity index 100% rename from integration-tests/public/trait-incrementer/traits/lib.rs rename to integration-tests/public/traits/incrementer/traits/lib.rs diff --git a/integration-tests/public/wildcard-selector/lib.rs b/integration-tests/public/wildcard-selector/lib.rs deleted file mode 100644 index 5bcdce5c493..00000000000 --- a/integration-tests/public/wildcard-selector/lib.rs +++ /dev/null @@ -1,206 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -#[ink::contract] -pub mod wildcard_selector { - #[cfg(feature = "emit-event")] - use ink::prelude::format; - use ink::prelude::string::String; - - #[cfg(feature = "emit-event")] - #[ink::event] - pub struct Event { - msg: String, - } - - #[ink(storage)] - pub struct WildcardSelector {} - - struct MessageInput([u8; 4], String); - impl ink::env::DecodeDispatch for MessageInput { - fn decode_dispatch(input: &mut &[u8]) -> Result { - // todo improve code here - let mut selector: [u8; 4] = [0u8; 4]; - selector.copy_from_slice(&input[..4]); - let arg: String = ink::scale::Decode::decode(&mut &input[4..]).unwrap(); - Ok(MessageInput(selector, arg)) - } - } - - impl WildcardSelector { - /// Creates a new wildcard selector smart contract. - #[ink(constructor)] - #[allow(clippy::new_without_default)] - pub fn new() -> Self { - Self {} - } - - /// Wildcard selector handles messages with any selector. - #[ink(message, selector = _)] - pub fn wildcard(&mut self) { - let MessageInput(_selector, _message) = - ink::env::decode_input::().unwrap(); - - #[cfg(feature = "emit-event")] - self.env().emit_event(Event { - msg: format!("Wildcard selector: {_selector:?}, message: {_message}"), - }) - } - - /// Wildcard complement handles messages with a well-known reserved selector. - #[ink(message, selector = @)] - pub fn wildcard_complement(&mut self, _message: String) { - #[cfg(feature = "emit-event")] - self.env().emit_event(Event { - msg: format!("Wildcard complement message: {_message}"), - }); - } - } - - #[cfg(all(test, feature = "e2e-tests"))] - mod e2e_tests { - use super::*; - use ink_e2e::ContractsBackend; - - use ink::{ - env::call::utils::{ - Argument, - ArgumentList, - EmptyArgumentList, - }, - primitives::abi::Ink, - }; - - type E2EResult = std::result::Result>; - type Environment = ::Env; - - fn build_message( - addr: Address, - selector: [u8; 4], - message: String, - ) -> ink_e2e::CallBuilderFinal< - Environment, - ArgumentList, EmptyArgumentList, Ink>, - (), - Ink, - > { - ink::env::call::build_call::() - .call(addr) - .exec_input( - ink::env::call::ExecutionInput::new(ink::env::call::Selector::new( - selector, - )) - .push_arg(message), - ) - .returns::<()>() - } - - #[ink_e2e::test(features = ["emit-event"])] - async fn arbitrary_selectors_handled_by_wildcard( - mut client: Client, - ) -> E2EResult<()> { - // given - let mut constructor = WildcardSelectorRef::new(); - let contract_acc_id = client - .instantiate("wildcard_selector", &ink_e2e::alice(), &mut constructor) - .submit() - .await - .expect("instantiate failed") - .addr; - - // when - const ARBITRARY_SELECTOR: [u8; 4] = [0xF9, 0xF9, 0xF9, 0xF9]; - let wildcard_message = "WILDCARD_MESSAGE 1".to_string(); - let wildcard = build_message( - contract_acc_id, - ARBITRARY_SELECTOR, - wildcard_message.clone(), - ); - - let _result = client - .call(&ink_e2e::bob(), &wildcard) - .submit() - .await - .expect("wildcard failed"); - - const ARBITRARY_SELECTOR_2: [u8; 4] = [0x01, 0x23, 0x45, 0x67]; - let wildcard_message2 = "WILDCARD_MESSAGE 2".to_string(); - let wildcard2 = build_message( - contract_acc_id, - ARBITRARY_SELECTOR_2, - wildcard_message2.clone(), - ); - - let _result2 = client - .call(&ink_e2e::bob(), &wildcard2) - .submit() - .await - .expect("wildcard failed"); - - // then - let contract_events = _result.contract_emitted_events()?; - assert_eq!(1, contract_events.len()); - let contract_event = contract_events.first().expect("first event must exist"); - let event: Event = - ink::scale::Decode::decode(&mut &contract_event.event.data[..]) - .expect("encountered invalid contract event data buffer"); - assert_eq!( - event.msg, - format!( - "Wildcard selector: {ARBITRARY_SELECTOR:?}, message: {wildcard_message}" - ) - ); - - /* - // todo - assert!(result.debug_message().contains(&format!( - "Wildcard selector: {:?}, message: {}", - ARBITRARY_SELECTOR, wildcard_message - ))); - - assert!(result2.debug_message().contains(&format!( - "Wildcard selector: {:?}, message: {}", - ARBITRARY_SELECTOR_2, wildcard_message2 - ))); - */ - - Ok(()) - } - - #[ink_e2e::test] - async fn wildcard_complement_works(mut client: Client) -> E2EResult<()> { - // given - let mut constructor = WildcardSelectorRef::new(); - let contract_acc_id = client - .instantiate("wildcard_selector", &ink_e2e::alice(), &mut constructor) - .submit() - .await - .expect("instantiate failed") - .addr; - - // when - let wildcard_complement_message = "WILDCARD COMPLEMENT MESSAGE".to_string(); - let wildcard = build_message( - contract_acc_id, - ink::IIP2_WILDCARD_COMPLEMENT_SELECTOR, - wildcard_complement_message.clone(), - ); - - let _result = client - .call(&ink_e2e::bob(), &wildcard) - .submit() - .await - .expect("wildcard failed"); - - // then - /* - // todo - assert!(result.debug_message().contains(&format!( - "Wildcard complement message: {}", - wildcard_complement_message - ))); - */ - - Ok(()) - } - } -}