diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 9ca93687..220a92f3 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -13,6 +13,7 @@ name: Clippy & fmt env: RUST_FMT: nightly-2023-04-01 RUST_VERSION: "1.66" + CARGO_CONCORDIUM_VERSION: "3.0.0" jobs: rustfmt: @@ -130,16 +131,22 @@ jobs: with: toolchain: ${{ env.RUST_VERSION }} + - name: Install Wasm target + run: rustup target install wasm32-unknown-unknown + # we need to move the generated project to a temp folder, away from the template project # otherwise `cargo` runs would fail # see https://github.com/rust-lang/cargo/issues/9922 # Run all tests, including doc tests. - name: Run cargo test run: | - # TEMPLATE_DIR=`pwd` + CARGO_CCD=cargo-concordium_${{ env.CARGO_CONCORDIUM_VERSION }} + wget https://distribution.concordium.software/tools/linux/$CARGO_CCD + chmod +x $CARGO_CCD + sudo mv $CARGO_CCD /usr/bin/cargo-concordium mv $PROJECT_NAME ${{ runner.temp }}/ cd ${{ runner.temp }}/$PROJECT_NAME - cargo test + cargo concordium test --out "./concordium-out/module.wasm.v1" # The credential registry template is used to generate code for all combinations of parameters # with the `cargo-generate` and it is checked that the 'cargo test' command can be executed @@ -174,15 +181,22 @@ jobs: with: toolchain: ${{ env.RUST_VERSION }} + - name: Install Wasm target + run: rustup target install wasm32-unknown-unknown + # we need to move the generated project to a temp folder, away from the template project # otherwise `cargo` runs would fail # see https://github.com/rust-lang/cargo/issues/9922 # Run all tests, including doc tests. - name: Run cargo test run: | + CARGO_CCD=cargo-concordium_${{ env.CARGO_CONCORDIUM_VERSION }} + wget https://distribution.concordium.software/tools/linux/$CARGO_CCD + chmod +x $CARGO_CCD + sudo mv $CARGO_CCD /usr/bin/cargo-concordium mv $PROJECT_NAME ${{ runner.temp }}/ cd ${{ runner.temp }}/$PROJECT_NAME - cargo test + cargo concordium test --out "./concordium-out/module.wasm.v1" # All templates are generated with the `cargo-generate` command # and it is checked that the schemas can be built as part of the 'clippy' command. @@ -330,10 +344,12 @@ jobs: run: | mv $PROJECT_NAME ${{ runner.temp }}/ sed -i "s/root/Concordium /g" ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml - sed -i "s/{version = \"8.0\", default-features = false}/{path = \"..\/..\/concordium-std\", default-features = false}/g" ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml - sed -i "s/{version = \"5.0\", default-features = false}/{path = \"..\/..\/concordium-cis2\", default-features = false}/g" ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml + sed -i "s/{version = \"8.1\", default-features = false}/{path = \"..\/..\/concordium-std\", default-features = false}/g" ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml + sed -i "s/{version = \"5.1\", default-features = false}/{path = \"..\/..\/concordium-cis2\", default-features = false}/g" ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml + sed -i "s/concordium-smart-contract-testing = \"3.0\"/concordium-smart-contract-testing = {path = \"..\/..\/contract-testing\"}/g" ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml diff ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml examples/cis2-nft/Cargo.toml diff ${{ runner.temp }}/$PROJECT_NAME/src/lib.rs examples/cis2-nft/src/lib.rs + diff ${{ runner.temp }}/$PROJECT_NAME/tests/tests.rs examples/cis2-nft/tests/tests.rs # The credential-registry template is generated with the `cargo-generate` command # and it is checked that the code is equivalent to the credential-registry smart contract in the example folder. @@ -376,10 +392,12 @@ jobs: run: | mv $PROJECT_NAME ${{ runner.temp }}/ sed -i "s/root/Concordium /g" ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml - sed -i "s/version = \"8.0\", default-features = false/path = \"..\/..\/concordium-std\", version = \"8.0\", default-features = false/g" ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml - sed -i "s/version = \"5.0\", default-features = false/path = \"..\/..\/concordium-cis2\", version = \"5.0\", default-features = false/g" ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml + sed -i "s/version = \"8.1\", default-features = false/path = \"..\/..\/concordium-std\", version = \"8.1\", default-features = false/g" ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml + sed -i "s/version = \"5.1\", default-features = false/path = \"..\/..\/concordium-cis2\", version = \"5.1\", default-features = false/g" ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml + sed -i "s/concordium-smart-contract-testing = \"3.1\"/concordium-smart-contract-testing = {path = \"..\/..\/contract-testing\"}/g" ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml diff ${{ runner.temp }}/$PROJECT_NAME/Cargo.toml examples/credential-registry/Cargo.toml diff ${{ runner.temp }}/$PROJECT_NAME/src/lib.rs examples/credential-registry/src/lib.rs + diff ${{ runner.temp }}/$PROJECT_NAME/tests/tests.rs examples/credential-registry/tests/tests.rs diff ${{ runner.temp }}/$PROJECT_NAME/README.md examples/credential-registry/README.md clippy-cis2: @@ -623,9 +641,6 @@ jobs: - crypto-primitives crates: - - examples/nametoken/Cargo.toml - - examples/signature-verifier/Cargo.toml - - examples/cis3-nft-sponsored-txs/Cargo.toml - examples/credential-registry/Cargo.toml steps: @@ -787,30 +802,30 @@ jobs: - x86_64-unknown-linux-gnu crates: - - examples/voting/Cargo.toml - - examples/eSealing/Cargo.toml - - examples/auction/Cargo.toml - - examples/cis2-multi/Cargo.toml - - examples/cis2-multi-royalties/Cargo.toml - - examples/cis2-nft/Cargo.toml - - examples/cis3-nft-sponsored-txs/Cargo.toml - - examples/cis2-wccd/Cargo.toml - - examples/credential-registry/Cargo.toml - - examples/fib/Cargo.toml - - examples/icecream/Cargo.toml - - examples/memo/Cargo.toml - - examples/nametoken/Cargo.toml - - examples/piggy-bank/part1/Cargo.toml - - examples/piggy-bank/part2/Cargo.toml - - examples/proxy/Cargo.toml - - examples/recorder/Cargo.toml - - examples/signature-verifier/Cargo.toml - - examples/transfer-policy-check/Cargo.toml - - examples/two-step-transfer/Cargo.toml - - examples/smart-contract-upgrade/contract-version1/Cargo.toml - - examples/smart-contract-upgrade/contract-version2/Cargo.toml - - examples/offchain-transfers/Cargo.toml - - examples/account-signature-checks/Cargo.toml + - examples/voting + - examples/eSealing + - examples/auction + - examples/cis2-multi + - examples/cis2-multi-royalties + - examples/cis2-nft + - examples/cis3-nft-sponsored-txs + - examples/cis2-wccd + - examples/credential-registry + - examples/fib + - examples/icecream + - examples/memo + - examples/nametoken + - examples/piggy-bank/part1 + - examples/piggy-bank/part2 + - examples/proxy + - examples/recorder + - examples/signature-verifier + - examples/transfer-policy-check + - examples/two-step-transfer + - examples/smart-contract-upgrade/contract-version1 + - examples/smart-contract-upgrade/contract-version2 + - examples/offchain-transfers + - examples/account-signature-checks steps: - name: Checkout sources @@ -826,9 +841,20 @@ jobs: target: ${{ matrix.target }} override: true - - name: Run cargo test - uses: actions-rs/cargo@v1 - with: - command: test - # only run the library tests, no doc tests - args: --manifest-path ${{ matrix.crates }} --target=${{ matrix.target }} --lib + - name: Install Wasm target + run: rustup target install wasm32-unknown-unknown + + - name: Download and install Cargo Concordium + run: | + CARGO_CCD=cargo-concordium_${{ env.CARGO_CONCORDIUM_VERSION }} + wget https://distribution.concordium.software/tools/linux/$CARGO_CCD + chmod +x $CARGO_CCD + sudo mv $CARGO_CCD /usr/bin/cargo-concordium + + # The 'smart-contract-upgrade' example has a v1 and v2 contract and the tests in v1 needs the wasm module from v2 for upgrading. + - name: Build contract-upgrade version 2 module if needed + if: ${{ matrix.crates == 'examples/smart-contract-upgrade/contract-version1' }} + run: cargo concordium build --out "examples/smart-contract-upgrade/contract-version2/concordium-out/module.wasm.v1" -- --manifest-path "examples/smart-contract-upgrade/contract-version2/Cargo.toml" + + - name: Run cargo concordium test + run: cargo concordium test --out "${{ matrix.crates }}/concordium-out/module.wasm.v1" -- --manifest-path "${{ matrix.crates }}/Cargo.toml" diff --git a/concordium-cis2/CHANGELOG.md b/concordium-cis2/CHANGELOG.md index b1447263..a3866a8f 100644 --- a/concordium-cis2/CHANGELOG.md +++ b/concordium-cis2/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased changes +## concordium-cis2 5.1.0 (2023-10-18) + +- Derive `PartialEq` and `Eq` for `Cis2Event`, `BalanceOfQueryResponse`, and `OperatorOfQueryResponse`. + ## concordium-cis2 5.0.0 (2023-08-21) - Derive `PartialEq` and `Eq` for the `TransferEvent`, `MintEvent`, `BurnEvent`, `UpdateOperatorEvent`, `TokenMetadataEvent`, `OperatorUpdate`, and `UpdateOperator` types. diff --git a/concordium-cis2/Cargo.toml b/concordium-cis2/Cargo.toml index 2be06c7c..3d001501 100644 --- a/concordium-cis2/Cargo.toml +++ b/concordium-cis2/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "concordium-cis2" -version = "5.0.0" +version = "5.1.0" authors = ["Concordium "] edition = "2021" license = "MPL-2.0" diff --git a/concordium-cis2/src/lib.rs b/concordium-cis2/src/lib.rs index 9f81703c..7816a25e 100644 --- a/concordium-cis2/src/lib.rs +++ b/concordium-cis2/src/lib.rs @@ -783,7 +783,7 @@ pub struct TokenMetadataEvent { } /// Tagged CIS2 event to be serialized for the event log. -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, PartialEq, Eq)] #[concordium(repr(u8))] pub enum Cis2Event { /// A transfer between two addresses of some amount of tokens. @@ -1161,7 +1161,7 @@ pub struct BalanceOfQueryParams { /// The response which is sent back when calling the contract function /// `balanceOf`. /// It consists of the list of results corresponding to the list of queries. -#[derive(Debug, Serialize, SchemaType)] +#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] #[concordium(transparent)] pub struct BalanceOfQueryResponse(#[concordium(size_length = 2)] pub Vec); @@ -1197,7 +1197,7 @@ pub struct OperatorOfQueryParams { /// `operatorOf`. /// It consists of the list of result in the same order and length as the /// queries in the parameter. -#[derive(Debug, Serialize, SchemaType)] +#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] #[concordium(transparent)] pub struct OperatorOfQueryResponse(#[concordium(size_length = 2)] pub Vec); diff --git a/concordium-rust-sdk b/concordium-rust-sdk index 0a2f899d..c2bcf2b2 160000 --- a/concordium-rust-sdk +++ b/concordium-rust-sdk @@ -1 +1 @@ -Subproject commit 0a2f899de9996825ec2447f21d2b57f46516421a +Subproject commit c2bcf2b2cb048b3d19072cbd1bb19e2829328e1f diff --git a/concordium-std/CHANGELOG.md b/concordium-std/CHANGELOG.md index e4397537..d5d7f40f 100644 --- a/concordium-std/CHANGELOG.md +++ b/concordium-std/CHANGELOG.md @@ -2,9 +2,16 @@ ## Unreleased changes +## concordium-std 8.1.0 (2023-10-18) + - Set minimum Rust version to 1.66. - Fix bug in `StateMap::get_mut`, which allowed multiple mutable references to the state to coexist. - The signature has changed using `&self` to using `&mut self`. +- Deprecate the `test_infrastructure` module in favour of [concordium-smart-contract-testing](https://docs.rs/concordium-smart-contract-testing). + - Several traits are also deprecated as they are only needed when using the `test_infrastructure` for testing. + - Among the traits are `HasHost`, `HasStateApi`, `HasInitContext`, `HasReceiveContext`. + - They are replaced by concrete types, e.g. `Host`, `StateApi`, etc. in the documentation and nearly all example contracts. + - Add a section in the library documentation about how to migrate your contracts and tests. ## concordium-std 8.0.0 (2023-08-21) diff --git a/concordium-std/Cargo.toml b/concordium-std/Cargo.toml index b40020b3..c8aa53bc 100644 --- a/concordium-std/Cargo.toml +++ b/concordium-std/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "concordium-std" -version = "8.0.0" +version = "8.1.0" authors = ["Concordium "] edition = "2021" rust-version = "1.66" @@ -23,7 +23,7 @@ getrandom = { version = "0.2", features = ["custom"], optional = true } [dependencies.concordium-contracts-common] path = "../concordium-rust-sdk/concordium-base/smart-contracts/contracts-common/concordium-contracts-common" -version = "8.0" +version = "8.1" default-features = false features = ["smart-contract"] diff --git a/concordium-std/src/lib.rs b/concordium-std/src/lib.rs index c104dffb..5a0c1173 100644 --- a/concordium-std/src/lib.rs +++ b/concordium-std/src/lib.rs @@ -17,6 +17,12 @@ //! Also note that `concordium-std` version 4 only works with `cargo-concordium` //! version 2.1+. //! +//! Version 8.1 deprecates the module [`test_infrastructure`] in favor of the +//! library [concordium_smart_contract_testing], which should be used instead. +//! For more details including how to migrate your contract, see the +//! [Deprecating the +//! `test_infrastructure`](#deprecating-the-test_infrastructure) section. +//! //! # Panic handler //! When compiled without the `std` feature this crate sets the panic handler //! so that it terminates the process immediately, without any unwinding or @@ -92,6 +98,9 @@ //! testing and for most cases this feature should not be set manually. //! //! ## `crypto-primitives`: For testing crypto with actual implementations +//! This features is only relevant when using the **deprecated** +//! [test_infrastructure]. +//! //! Build with this feature if you want to run smart contract tests with actual //! (i.e., not mock) implementations of the cryptographic primitives from //! [`HasCryptoPrimitives`]. @@ -130,30 +139,39 @@ //! documentation, however note that there can only be one allocator set. //! See Rust [allocator](https://doc.rust-lang.org/std/alloc/index.html#the-global_allocator-attribute) documentation for more context and details. //! -//! # Traits -//! To support testing of smart contracts most of the functionality is -//! accessible via traits. This library generally provides two implementations -//! of most traits. The first one is supported by **host** functions, and this -//! is the implementation that is used when contracts are executed by nodes. The -//! second set of implementations supports testing of contracts and it is -//! defined in the [test_infrastructure](./test_infrastructure/index.html) -//! module. -//! -//! - [HasParameter] for accessing the contract parameter -//! - [HasCommonData] for accessing the data that is common to both init and -//! receive methods -//! - [HasInitContext] for all the context data available to the init functions -//! (note that this includes all the common data) -//! - [HasReceiveContext] for accessing all the context data available to the -//! receive functions (note that this includes all the common data) -//! - [HasLogger] for logging data during smart contract execution -//! - [HasPolicy] for accessing the policy of the sender, either of the init or +//! # Essential types +//! This crate has a number of essential types that are used when writing smart +//! contracts. The structure of these are, at present, a bit odd without the +//! historic context, which is explained below. +//! +//! Prior to version 8.1, a number of traits and generics were used when writing +//! smart contracts, e.g. [`HasHost`], to support the usage of +//! [`crate::test_infrastructure`] for testing, where two primary +//! implementations of each trait existed. The first one is supported by +//! **host** functions, and this is the implementation that is used when +//! contracts are executed by notes. The second set of implementations supports +//! testing contracts with [`crate::test_infrastructure`], but since the +//! deprecation of this module, the preferred way of writing contracts is to use +//! the concrete types. +//! +//! The essential concrete types are: +//! - [`StateApi`] for operations possible on the contract state +//! - [`Host`] for invoking operations on the host and accessing the state +//! - [`InitContext`] for all the context data available to the init functions +//! - [`ReceiveContext`] for accessing all the context data available to the +//! receive functions +//! - [`ExternParameter`] for accessing the contract parameter +//! - [`Logger`] for logging data during smart contract execution +//! - [`Policy`] for accessing the policy of the sender, either of the init or //! receive method -//! - [HasStateApi] for operations possible on the contract state -//! - [HasHost] for invoking operations on the host and accessing the state -//! - [HasCryptoPrimitives] for using cryptographic primitives such as hashing +//! - [`CryptoPrimitives`] for using cryptographic primitives such as hashing //! and signature verification. //! +//! Most of these are type aliases for similarly named structs prefixed with +//! `Extern`. The extern prefix made sense when two different implementations of +//! the traits were in play. Since that is no longer the case, we decided to +//! simplify the names with aliases. +//! //! # Signalling errors //! On the Wasm level contracts can signal errors by returning a negative i32 //! value as a result of either initialization or invocation of the receive @@ -207,9 +225,92 @@ //! | [QueryAccountBalanceError] | `-2147483623` | //! | [QueryContractBalanceError] | `-2147483622` | //! -//! [1]: https://doc.rust-lang.org/std/primitive.unit.html //! Other error codes may be added in the future and custom error codes should //! not use the range `i32::MIN` to `i32::MIN + 100`. +//! +//! # Deprecating the `test_infrastructure` +//! Version 8.1 deprecates the [test_infrastructure] in favor of the library +//! [concordium_smart_contract_testing]. A number of traits are also +//! deprecated at the same time since they only exist to support the +//! [test_infrastructure] and are not needed in the new testing library. +//! The primary of these traits are [`HasHost`], [`HasStateApi`], +//! [`HasInitContext`], and [`HasReceiveContext`]. +//! +//! ## Migration guide +//! To migrate your contract and its tests to the new testing library, you need +//! to do the following two steps: +//! +//! 1. Replace the usage of deprecated traits with their concrete alternatives +//! and remove generics. +//! +//! For init methods: +//! ```no_run +//! # use concordium_std::*; +//! # +//! # type State = (); +//! # +//! /// Before +//! #[init(contract = "contract_before")] +//! fn init_before( +//! ctx: &impl HasInitContext, +//! state_builder: &mut StateBuilder, +//! ) -> InitResult { todo!() } +//! +//! /// After +//! #[init(contract = "contract_after")] +//! fn init_after( // `` removed +//! ctx: &InitContext, // `impl` and `Has` removed +//! state_builder: &mut StateBuilder, // `` removed +//! ) -> InitResult { todo!() } +//! ``` +//! For receive methods: +//! ```no_run +//! # use concordium_std::*; +//! # +//! # type State = (); +//! # type MyReturnValue = (); +//! # +//! # #[init(contract = "my_contract")] +//! # fn contract_init( // `` removed +//! # ctx: &InitContext, // `impl` and `Has` removed +//! # state_builder: &mut StateBuilder, // `` removed +//! # ) -> InitResult { todo!() } +//! /// Before +//! #[receive(contract = "my_contract", name = "my_receive")] +//! fn receive_before( +//! ctx: &impl HasReceiveContext, +//! host: &impl HasHost, +//! ) -> ReceiveResult { todo!() } +//! +//! /// After +//! #[receive(contract = "my_contract", name = "my_receive")] +//! fn receive_after( // `` removed +//! ctx: &ReceiveContext, // `impl` and `Has` removed +//! host: &Host, // `impl Has` and `, StateApiType = S removed +//! ) -> ReceiveResult { todo!() } +//! ``` +//! +//! If you use logging, crypto-primitives, or similar, you must also +//! replace those uses of traits with concrete types. E.g. replacing `&mut impl +//! HasLogger` with `&mut Logger`. +//! +//! 2. Migrate your tests to use the new testing library. +//! +//! For an introduction to the library, see our [guide](https://developer.concordium.software/en/mainnet/smart-contracts/guides/integration-test-contract.html). +//! +//! If you follow our [recommended structure](https://developer.concordium.software/en/mainnet/smart-contracts/best-practices/development.html#recommended-structure) in your contract, +//! then you have a mix of unit and integrations tests: +//! - Unit tests that call methods directly on your state struct (without any +//! init/receive calls) +//! - Integration tests that call the init and receive methods +//! +//! If you do not want to migrate your contract and tests yet, then you can add +//! the `#[allow(deprecated)]` attribute to your test modules to avoid the +//! deprecation warnings. +//! +//! [1]: https://doc.rust-lang.org/std/primitive.unit.html +//! [test_infrastructure]: ./test_infrastructure/index.html +//! [concordium_smart_contract_testing]: https://docs.rs/concordium-smart-contract-testing #![cfg_attr(not(feature = "std"), no_std, feature(core_intrinsics))] #[cfg(not(feature = "std"))] @@ -286,4 +387,8 @@ pub use types::*; #[cfg_attr(feature = "wee_alloc", global_allocator)] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; +#[deprecated( + since = "8.1.0", + note = "Deprecated in favor of [concordium-smart-contract-testing](https://docs.rs/concordium-smart-contract-testing)." +)] pub mod test_infrastructure; diff --git a/concordium-std/src/traits.rs b/concordium-std/src/traits.rs index db970a60..f20156d0 100644 --- a/concordium-std/src/traits.rs +++ b/concordium-std/src/traits.rs @@ -21,6 +21,16 @@ use concordium_contracts_common::*; /// /// The reuse of `Read` methods is the reason for the slightly strange choice of /// methods of this trait. +/// +/// # Deprecation notice +/// **This trait is deprecated along with +/// [`crate::test_infrastructure`].** +/// +/// Use [`ExternParameter`](crate::types::ExternParameter) instead unless you +/// intend to use the deprecated test infrastructure. +/// +/// See the [crate](../concordium_std/#deprecating-the-test_infrastructure) +/// documentation for more details. pub trait HasParameter: Read + Seek + HasSize {} /// Objects which can access call responses from contract invocations. @@ -31,12 +41,32 @@ pub trait HasParameter: Read + Seek + HasSize {} /// /// The reuse of `Read` methods is the reason for the slightly strange choice of /// methods of this trait. +/// +/// # Deprecation notice +/// **This trait is deprecated along with +/// [`crate::test_infrastructure`].** +/// +/// Use [`CallResponse`](crate::types::CallResponse) instead unless you intend +/// to use the deprecated test infrastructure. +/// +/// See the [crate](../concordium_std/#deprecating-the-test_infrastructure) +/// documentation for more details. pub trait HasCallResponse: Read { /// Get the size of the call response to the contract invocation. fn size(&self) -> u32; } /// Objects which can access chain metadata. +/// +/// # Deprecation notice +/// **This trait is deprecated along with +/// [`crate::test_infrastructure`].** +/// +/// Use [`ChainMetadata`] instead unless you intend to use +/// the deprecated test infrastructure. +/// +/// See the [crate](../concordium_std/#deprecating-the-test_infrastructure) +/// documentation for more details. pub trait HasChainMetadata { /// Get time in milliseconds at the beginning of this block. fn slot_time(&self) -> SlotTime; @@ -49,6 +79,16 @@ pub trait HasChainMetadata { /// Since policies can be large this is deliberately written in a relatively /// low-level style to enable efficient traversal of all the attributes without /// any allocations. +/// +/// # Deprecation notice +/// **This trait is deprecated along with +/// [`crate::test_infrastructure`].** +/// +/// Use [`Policy`] instead unless you intend to use the +/// deprecated test infrastructure. +/// +/// See the [crate](../concordium_std/#deprecating-the-test_infrastructure) +/// documentation for more details. pub trait HasPolicy: Sized { type Iterator: Iterator; /// Identity provider who signed the identity object the credential is @@ -76,6 +116,17 @@ pub trait HasPolicy: Sized { } /// Common data accessible to both init and receive methods. +/// +/// # Deprecation notice +/// **This trait is deprecated along with +/// [`crate::test_infrastructure`].** +/// +/// Use [`InitContext`](crate::types::InitContext) or +/// [`ReceiveContext`](crate::types::ReceiveContext) instead unless you intend +/// to use the deprecated test infrastructure. +/// +/// See the [crate](../concordium_std/#deprecating-the-test_infrastructure) +/// documentation for more details. pub trait HasCommonData { type PolicyType: HasPolicy; type MetadataType: HasChainMetadata; @@ -96,6 +147,16 @@ pub trait HasCommonData { } /// Types which can act as init contexts. +/// +/// # Deprecation notice +/// **This trait is deprecated along with +/// [`crate::test_infrastructure`].** +/// +/// Use [`InitContext`](crate::types::InitContext) instead unless you intend to +/// use the deprecated test infrastructure. +/// +/// See the [crate](../concordium_std/#deprecating-the-test_infrastructure) +/// documentation for more details. pub trait HasInitContext: HasCommonData { /// Data needed to open the context. type InitData; @@ -106,6 +167,16 @@ pub trait HasInitContext: HasCommonData { } /// Types which can act as receive contexts. +/// +/// # Deprecation notice +/// **This trait is deprecated along with +/// [`crate::test_infrastructure`].** +/// +/// Use [`ReceiveContext`](crate::types::ReceiveContext) instead unless you +/// intend to use the deprecated test infrastructure. +/// +/// See the [crate](../concordium_std/#deprecating-the-test_infrastructure) +/// documentation for more details. pub trait HasReceiveContext: HasCommonData { type ReceiveData; @@ -128,6 +199,16 @@ pub trait HasReceiveContext: HasCommonData { } /// A type that can serve as the contract state entry type. +/// +/// # Deprecation notice +/// **This trait is deprecated along with +/// [`crate::test_infrastructure`].** +/// +/// Use [`StateEntry`](crate::types::StateEntry) instead unless you intend to +/// use the deprecated test infrastructure. +/// +/// See the [crate](../concordium_std/#deprecating-the-test_infrastructure) +/// documentation for more details. pub trait HasStateEntry where Self: Read, @@ -172,6 +253,16 @@ where } /// Types which can serve as the contract state. +/// +/// # Deprecation notice +/// **This trait is deprecated along with +/// [`crate::test_infrastructure`].** +/// +/// Use [`StateApi`](crate::types::StateApi) instead unless you intend to use +/// the deprecated test infrastructure. +/// +/// See the [crate](../concordium_std/#deprecating-the-test_infrastructure) +/// documentation for more details. pub trait HasStateApi: Clone { type EntryType: HasStateEntry; type IterType: Iterator; @@ -250,6 +341,16 @@ pub trait HasStateApi: Clone { /// /// The trait is parameterized by the `State` type. This is the type of the /// contract state that the particular contract operates on. +/// +/// # Deprecation notice +/// **This trait is deprecated along with +/// [`crate::test_infrastructure`].** +/// +/// Use [`Host`](crate::types::Host) instead unless you intend to use +/// the deprecated test infrastructure. +/// +/// See the [crate](../concordium_std/#deprecating-the-test_infrastructure) +/// documentation for more details. pub trait HasHost: Sized { /// The type of low-level state that is associated with the host. /// This provides access to low-level state operations. @@ -477,6 +578,16 @@ pub trait Deletable { /// [`invoke_contract`](HasHost::invoke_contract) or /// [`invoke_transfer`](HasHost::invoke_transfer) calls. In each section at most /// `64` items may be logged. +/// +/// # Deprecation notice +/// **This trait is deprecated along with +/// [`crate::test_infrastructure`].** +/// +/// Use [`Logger`](crate::types::Logger) instead unless you intend to use +/// the deprecated test infrastructure. +/// +/// See the [crate](../concordium_std/#deprecating-the-test_infrastructure) +/// documentation for more details. pub trait HasLogger { /// Initialize a logger. fn init() -> Self; @@ -498,6 +609,16 @@ pub trait HasLogger { } /// Objects which provide cryptographic primitives. +/// +/// # Deprecation notice +/// **This trait is deprecated along with +/// [`crate::test_infrastructure`].** +/// +/// Use [`CryptoPrimitives`](crate::types::CryptoPrimitives) instead unless you +/// intend to use the deprecated test infrastructure. +/// +/// See the [crate](../concordium_std/#deprecating-the-test_infrastructure) +/// documentation for more details. pub trait HasCryptoPrimitives { /// Verify an ed25519 signature. fn verify_ed25519_signature( diff --git a/concordium-std/src/types.rs b/concordium-std/src/types.rs index 43603451..32522b5b 100644 --- a/concordium-std/src/types.rs +++ b/concordium-std/src/types.rs @@ -3,8 +3,6 @@ use crate::{ }; use concordium_contracts_common::{AccountBalance, Amount, ParseError}; use core::{fmt, str::FromStr}; -// Re-export for backward compatibility. -pub use concordium_contracts_common::ExchangeRates; #[derive(Debug)] /// A high-level map based on the low-level key-value store, which is the @@ -64,38 +62,36 @@ pub use concordium_contracts_common::ExchangeRates; /// The type parameter `S` is extra compared to usual Rust collections. As /// mentioned above it specifies the [low-level state /// implementation](crate::HasStateApi). This library provides two such -/// implementations. The "external" one, which is the implementation supported -/// by external host functions provided by the chain, and a -/// [test](crate::test_infrastructure::TestStateApi) one. The latter one is -/// useful for testing since it provides an implementation that is easier to -/// construct, execute, and inspect during unit testing. +/// implementations. The "external" one ([`StateApi`]), which is the +/// implementation supported by external host functions provided by the chain, +/// and a [test](crate::test_infrastructure::TestStateApi) one. The latter one +/// is only useful for testing with the deprecated +/// [`test_infrastructure`](crate::test_infrastructure) module. /// /// In user code this type parameter should generally be treated as boilerplate, /// and contract entrypoints should always be stated in terms of a generic type -/// `S` that implements [HasStateApi](crate::HasStateApi) +/// `S` that implements [HasStateApi](crate::HasStateApi) and defaults to +/// `StateApi`, unless you intend to use the deprecated testing library. /// /// #### Example /// ```rust /// # use concordium_std::*; /// #[derive(Serial, DeserialWithState)] /// #[concordium(state_parameter = "S")] -/// struct MyState { +/// struct MyState { /// inner: StateMap, /// } /// #[init(contract = "mycontract")] -/// fn contract_init( -/// _ctx: &impl HasInitContext, -/// state_builder: &mut StateBuilder, -/// ) -> InitResult> { +/// fn contract_init(_ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { /// Ok(MyState { /// inner: state_builder.new_map(), /// }) /// } /// /// #[receive(contract = "mycontract", name = "receive", return_value = "Option")] -/// fn contract_receive( -/// _ctx: &impl HasReceiveContext, -/// host: &impl HasHost, StateApiType = S>, // the same low-level state must be propagated throughout +/// fn contract_receive( +/// _ctx: &ReceiveContext, +/// host: &Host, // the same low-level state must be propagated throughout /// ) -> ReceiveResult> { /// let state = host.state(); /// Ok(state.inner.get(&0).map(|v| *v)) @@ -109,13 +105,10 @@ pub use concordium_contracts_common::ExchangeRates; /// /// ```no_run /// # use concordium_std::*; -/// struct MyState { +/// struct MyState { /// inner: StateMap, /// } -/// fn incorrect_replace( -/// state_builder: &mut StateBuilder, -/// state: &mut MyState, -/// ) { +/// fn incorrect_replace(state_builder: &mut StateBuilder, state: &mut MyState) { /// // The following is incorrect. The old value of `inner` is not properly deleted. /// // from the state. /// state.inner = state_builder.new_map(); // ⚠️ @@ -126,26 +119,20 @@ pub use concordium_contracts_common::ExchangeRates; /// /// ```no_run /// # use concordium_std::*; -/// # struct MyState { +/// # struct MyState { /// # inner: StateMap /// # } -/// fn correct_replace( -/// state_builder: &mut StateBuilder, -/// state: &mut MyState, -/// ) { +/// fn correct_replace(state_builder: &mut StateBuilder, state: &mut MyState) { /// state.inner.clear_flat(); /// } /// ``` /// Or alternatively /// ```no_run /// # use concordium_std::*; -/// # struct MyState { +/// # struct MyState { /// # inner: StateMap /// # } -/// fn correct_replace( -/// state_builder: &mut StateBuilder, -/// state: &mut MyState, -/// ) { +/// fn correct_replace(state_builder: &mut StateBuilder, state: &mut MyState) { /// let old_map = mem::replace(&mut state.inner, state_builder.new_map()); /// old_map.delete() /// } @@ -203,6 +190,20 @@ pub struct StateMapIterMut<'a, K, V, S: HasStateApi> { /// New sets can be constructed using the /// [`new_set`][StateBuilder::new_set] method on the [`StateBuilder`]. /// +/// ``` +/// # use concordium_std::*; +/// # use concordium_std::test_infrastructure::*; +/// # let mut state_builder = TestStateBuilder::new(); +/// /// In an init method: +/// let mut set1 = state_builder.new_set(); +/// # set1.insert(0u8); // Specifies type of set. +/// +/// # let mut host = TestHost::new((), state_builder); +/// /// In a receive method: +/// let mut set2 = host.state_builder().new_set(); +/// # set2.insert(0u16); +/// ``` +/// /// ## Type parameters /// /// The set `StateSet` is parametrized by the type of _values_ `T`, and @@ -222,39 +223,34 @@ pub struct StateMapIterMut<'a, K, V, S: HasStateApi> { /// The type parameter `S` is extra compared to usual Rust collections. As /// mentioned above it specifies the [low-level state /// implementation](crate::HasStateApi). This library provides two such -/// implementations. The "external" one, which is the implementation supported -/// by external host functions provided by the chain, and a -/// [test](crate::test_infrastructure::TestStateApi) one. The latter one is -/// useful for testing since it provides an implementation that is easier to -/// construct, execute, and inspect during unit testing. +/// implementations. The "external" one ([`StateApi`]), which is the +/// implementation supported by external host functions provided by the chain, +/// and a [test](crate::test_infrastructure::TestStateApi) one. The latter one +/// is only useful for testing with the deprecated +/// [`test_infrastructure`](crate::test_infrastructure) module. /// /// In user code this type parameter should generally be treated as boilerplate, /// and contract entrypoints should always be stated in terms of a generic type -/// `S` that implements [HasStateApi](crate::HasStateApi) +/// `S` that implements [HasStateApi](crate::HasStateApi) and defaults to +/// `StateApi`, unless you intend to use the deprecated testing library. /// /// #### Example /// ```rust /// # use concordium_std::*; /// #[derive(Serial, DeserialWithState)] /// #[concordium(state_parameter = "S")] -/// struct MyState { +/// struct MyState { /// inner: StateSet, /// } /// #[init(contract = "mycontract")] -/// fn contract_init( -/// _ctx: &impl HasInitContext, -/// state_builder: &mut StateBuilder, -/// ) -> InitResult> { +/// fn contract_init(_ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { /// Ok(MyState { /// inner: state_builder.new_set(), /// }) /// } /// /// #[receive(contract = "mycontract", name = "receive", return_value = "bool")] -/// fn contract_receive( -/// _ctx: &impl HasReceiveContext, -/// host: &impl HasHost, StateApiType = S>, // the same low-level state must be propagated throughout -/// ) -> ReceiveResult { +/// fn contract_receive(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { /// let state = host.state(); /// Ok(state.inner.contains(&0)) /// } @@ -267,13 +263,10 @@ pub struct StateMapIterMut<'a, K, V, S: HasStateApi> { /// /// ```no_run /// # use concordium_std::*; -/// struct MyState { +/// struct MyState { /// inner: StateSet, /// } -/// fn incorrect_replace( -/// state_builder: &mut StateBuilder, -/// state: &mut MyState, -/// ) { +/// fn incorrect_replace(state_builder: &mut StateBuilder, state: &mut MyState) { /// // The following is incorrect. The old value of `inner` is not properly deleted. /// // from the state. /// state.inner = state_builder.new_set(); // ⚠️ @@ -284,26 +277,20 @@ pub struct StateMapIterMut<'a, K, V, S: HasStateApi> { /// /// ```no_run /// # use concordium_std::*; -/// # struct MyState { +/// # struct MyState { /// # inner: StateSet /// # } -/// fn correct_replace( -/// state_builder: &mut StateBuilder, -/// state: &mut MyState, -/// ) { +/// fn correct_replace(state_builder: &mut StateBuilder, state: &mut MyState) { /// state.inner.clear(); /// } /// ``` /// Or alternatively /// ```no_run /// # use concordium_std::*; -/// # struct MyState { +/// # struct MyState { /// # inner: StateSet /// # } -/// fn correct_replace( -/// state_builder: &mut StateBuilder, -/// state: &mut MyState, -/// ) { +/// fn correct_replace(state_builder: &mut StateBuilder, state: &mut MyState) { /// let old_set = mem::replace(&mut state.inner, state_builder.new_set()); /// old_set.delete() /// } @@ -417,7 +404,8 @@ impl<'a, V: Serial, S: HasStateApi> StateRefMut<'a, V, S> { #[repr(transparent)] /// An iterator over a part of the state. Its implementation is supported by /// host calls. -#[doc(hidden)] +/// +/// **Typically referred to via the alias [`StateIter`].** pub struct ExternStateIter { pub(crate) iterator_id: StateIteratorId, } @@ -541,7 +529,6 @@ pub(crate) struct ExternParameterDataPlaceholder {} /// A type representing the parameter to init and receive methods. /// Its trait implementations are backed by host functions. -#[doc(hidden)] pub struct ExternParameter { pub(crate) cursor: Cursor, } @@ -557,8 +544,9 @@ pub struct ExternParameter { /// /// This type is designed to be used via its [Read](crate::Read) and /// [HasCallResponse](crate::HasCallResponse) traits. +/// +/// **Typically referred to via the alias [`CallResponse`].** #[derive(Debug)] -#[doc(hidden)] pub struct ExternCallResponse { /// The index of the call response. pub(crate) i: NonZeroU32, @@ -581,6 +569,8 @@ impl ExternCallResponse { /// The intention is that this type is manipulated using methods of the /// [Write](crate::Write) trait. In particular it can be used as a sink to /// serialize values into. +/// +/// **Typically referred to via the alias [`ReturnValue`].** pub struct ExternReturnValue { pub(crate) current_position: u32, } @@ -850,16 +840,18 @@ macro_rules! ensure_ne { }; } -// Macros for failing a test +// Macros for failing a test (in `concordium_std::test_infrastructure`). /// The `fail` macro is used for testing as a substitute for the panic macro. /// It reports back error information to the host. -/// Used only in testing. +/// Used only in testing with +/// [`test_infrastructure`](crate::test_infrastructure). #[cfg(feature = "std")] #[macro_export] macro_rules! fail { () => { { + #[allow(deprecated)] $crate::test_infrastructure::report_error("", file!(), line!(), column!()); panic!() } @@ -867,6 +859,7 @@ macro_rules! fail { ($($arg:tt),+) => { { let msg = format!($($arg),+); + #[allow(deprecated)] $crate::test_infrastructure::report_error(&msg, file!(), line!(), column!()); panic!("{}", msg) } @@ -875,12 +868,14 @@ macro_rules! fail { /// The `fail` macro is used for testing as a substitute for the panic macro. /// It reports back error information to the host. -/// Used only in testing. +/// Used only in testing with +/// [`test_infrastructure`](crate::test_infrastructure). #[cfg(not(feature = "std"))] #[macro_export] macro_rules! fail { () => { { + #[allow(deprecated)] $crate::test_infrastructure::report_error("", file!(), line!(), column!()); panic!() } @@ -888,6 +883,7 @@ macro_rules! fail { ($($arg:tt),+) => { { let msg = &$crate::alloc::format!($($arg),+); + #[allow(deprecated)] $crate::test_infrastructure::report_error(&msg, file!(), line!(), column!()); panic!("{}", msg) } @@ -896,7 +892,8 @@ macro_rules! fail { /// The `claim` macro is used for testing as a substitute for the assert macro. /// It checks the condition and if false it reports back an error. -/// Used only in testing. +/// Used only in testing with +/// [`test_infrastructure`](crate::test_infrastructure). #[macro_export] macro_rules! claim { ($cond:expr) => { @@ -917,7 +914,9 @@ macro_rules! claim { } /// Ensure the first two arguments are equal, just like `assert_eq!`, otherwise -/// reports an error. Used only in testing. +/// reports an error. +/// Used only in testing with +/// [`test_infrastructure`](crate::test_infrastructure). #[macro_export] macro_rules! claim_eq { ($left:expr, $right:expr $(,)?) => { @@ -938,7 +937,8 @@ macro_rules! claim_eq { /// Ensure the first two arguments are *not* equal, just like `assert_ne!`, /// otherwise reports an error. -/// Used only in testing. +/// Used only in testing with +/// [`test_infrastructure`](crate::test_infrastructure). #[macro_export] macro_rules! claim_ne { ($left:expr, $right:expr $(,)?) => { @@ -960,8 +960,7 @@ macro_rules! claim_ne { /// The expected return type of the receive method of a smart contract. /// /// Optionally, to define a custom type for error instead of using -/// Reject, allowing to track the reason for rejection, *but only in unit -/// tests*. +/// Reject, allowing to track the reason for rejection. /// /// See also the documentation for [bail!](macro.bail.html) for how to use /// custom error types. @@ -976,10 +975,7 @@ macro_rules! claim_ne { /// } /// /// #[receive(contract = "mycontract", name = "receive")] -/// fn contract_receive( -/// _ctx: &impl HasReceiveContext, -/// _host: &impl HasHost<(), StateApiType = S>, -/// ) -> Result<(), MyCustomError> { +/// fn contract_receive(_ctx: &ReceiveContext, _host: &Host<()>) -> Result<(), MyCustomError> { /// Err(MyCustomError::SomeError) /// } /// ``` @@ -989,7 +985,7 @@ pub type ReceiveResult = Result; /// parametrized by the state type of the smart contract. /// /// Optionally, to define a custom type for error instead of using Reject, -/// allowing the track the reason for rejection, *but only in unit tests*. +/// allowing the track the reason for rejection. /// /// See also the documentation for [bail!](macro.bail.html) for how to use /// custom error types. @@ -1004,32 +1000,112 @@ pub type ReceiveResult = Result; /// } /// /// #[init(contract = "mycontract")] -/// fn contract_init( -/// _ctx: &impl HasInitContext, -/// _state_builder: &mut StateBuilder, +/// fn contract_init( +/// _ctx: &InitContext, +/// _state_builder: &mut StateBuilder, /// ) -> Result<(), MyCustomError> { /// Err(MyCustomError::SomeError) /// } /// ``` pub type InitResult = Result; +/// Type alias for the context of init methods. +/// +/// See [`ExternContext`] for more details. +pub type InitContext = ExternContext; + +/// Type alias for the context of receive methods. +/// +/// See [`ExternContext`] for more details. +pub type ReceiveContext = ExternContext; + +/// The host, which supports interactions with the chain, such as querying +/// the balance of the contract, accessing its state, and invoking operations on +/// other contracts and accounts. +/// +/// The type is parameterized by the `State` type. This is the type of the +/// contract state that the particular contract operates on. +/// +/// See [`ExternHost`] for more details. +pub type Host = ExternHost; + +/// The contract state, which uses Wasm host functions to interact with the node +/// and use the state. +/// +/// See [`ExternStateApi`] for more details. +pub type StateApi = ExternStateApi; + +/// Host-backed cryptographic primitives. +/// +/// See [`ExternCryptoPrimitives`] for the methods implemented via +/// [`HasCryptoPrimitives`](crate::traits::HasCryptoPrimitives). +pub type CryptoPrimitives = ExternCryptoPrimitives; + +/// A low-level host, used by receive methods with the attribute `low_level`. +/// +/// See [`ExternLowLevelHost`] for the methods implemented via `HasHost`. +pub type LowLevelHost = ExternLowLevelHost; + +/// Host-backed access to chain metadata. +/// +/// See [`ExternChainMeta`] for the methods implemented via the +/// `HasChainMetadata` trait. +pub type ChainMeta = ExternChainMeta; + +/// A host-backed iterator over part of the state. +/// +/// See [`ExternStateIter`] for the methods implemented via primarily +/// [`Iterator`]. +pub type StateIter = ExternStateIter; + +/// A type representing the return value of contract init or receive method. +/// +/// The intention is that this type is manipulated using methods of the +/// [Write](crate::Write) trait. In particular it can be used as a sink to +/// serialize values into. +/// +/// See [`ExternReturnValue`] for more details. +pub type ReturnValue = ExternReturnValue; + +/// A type representing the return value of contract invocation. +/// +/// A contract invocation **may** return a value. It is returned in the +/// following cases +/// - an entrypoint of a V1 contract was invoked and the invocation succeeded +/// - an entrypoint of a V1 contract was invoked and the invocation failed due +/// to a [`CallContractError::LogicReject`] +/// +/// In all other cases there is no response. +/// +/// This type is designed to be used via its [Read](crate::Read) and +/// [`HasCallResponse`](crate::HasCallResponse) traits. +/// +/// See [`ExternCallResponse`] for more details. +pub type CallResponse = ExternCallResponse; + /// Operations backed by host functions for the high-level interface. -#[doc(hidden)] +/// +/// **Typically referred to via the alias [`Host`].** pub struct ExternHost { pub state: State, pub state_builder: StateBuilder, } #[derive(Default)] -/// An state builder that allows the creation of [`StateMap`], [`StateSet`], and -/// [`StateBox`]. It is parametrized by a parameter `S` that is assumed to -/// implement [`HasStateApi`]. +/// A state builder that allows the creation of [`StateMap`], [`StateSet`], and +/// [`StateBox`]. +/// +/// It is parametrized by a parameter `S` that is assumed to +/// implement [`HasStateApi`] to support testing with the deprecated +/// [`test_infrastructure`](crate::test_infrastructure). The `S` defaults to +/// `StateApi`, which is sufficient to test with the [concordium-smart-contract-testing](https://docs.rs/concordium-smart-contract-testing) +/// library. /// -/// The state_builder is designed to provide an abstraction over the contract +/// The StateBuilder is designed to provide an abstraction over the contract /// state, abstracting over the exact **keys** (keys in the sense of key-value /// store, which is the low-level semantics of contract state) that are used /// when storing specific values. -pub struct StateBuilder { +pub struct StateBuilder { pub(crate) state_api: S, } @@ -1040,7 +1116,8 @@ impl StateBuilder { /// A struct for which HasCryptoPrimitives is implemented via the crypto host /// functions. -#[doc(hidden)] +/// +/// **Typically referred to via the alias [`CryptoPrimitives`].** pub struct ExternCryptoPrimitives; /// Sha2 digest with 256 bits (32 bytes). @@ -1085,7 +1162,7 @@ pub struct HashSha3256(pub [u8; 32]); pub struct HashKeccak256(pub [u8; 32]); #[derive(Debug, Clone, Default)] -#[doc(hidden)] +/// Typicall referred to via the alias [`StateApi`]. pub struct ExternStateApi; impl ExternStateApi { @@ -1095,7 +1172,8 @@ impl ExternStateApi { } /// Operations backed by host functions for the low-level interface. -#[doc(hidden)] +/// +/// **Typically referred to via the alias [`LowLevelHost`].** #[derive(Default)] pub struct ExternLowLevelHost { pub(crate) state_api: ExternStateApi, @@ -1103,20 +1181,19 @@ pub struct ExternLowLevelHost { } /// Context backed by host functions. +/// +/// Usuaully referred to via aliases [`InitContext`] or [`ReceiveContext`]. #[derive(Default)] -#[doc(hidden)] pub struct ExternContext { marker: crate::marker::PhantomData, } -#[doc(hidden)] +/// **Typically referred to via the alias [`ChainMeta`].** pub struct ExternChainMeta {} #[derive(Default)] -#[doc(hidden)] pub struct ExternInitContext; #[derive(Default)] -#[doc(hidden)] pub struct ExternReceiveContext; pub(crate) mod sealed { diff --git a/contract-testing/CHANGELOG.md b/contract-testing/CHANGELOG.md index 7e2c529e..e0585af7 100644 --- a/contract-testing/CHANGELOG.md +++ b/contract-testing/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased changes +## 3.1.0 + - Add functionality for setting the exchange rates and block time of the chain based on queries from an external node. - Configured via a builder pattern, see `Chain::builder`. - Add methods to `Chain`: @@ -15,6 +17,7 @@ - `Endpoint` - Add methods to the `Chain` for adding external accounts and contracts and for invoking contracts on an external node. - See the `Chain` method `contract_invoke_external` for more details. +- Add helper method `parse_return_value` to `ContractInvokeError` and `ContractInvokeSuccess`. - Bump minimum supported Rust version to `1.66`. ## 3.0.0 diff --git a/contract-testing/Cargo.toml b/contract-testing/Cargo.toml index 1d39ca72..7c5952af 100644 --- a/contract-testing/Cargo.toml +++ b/contract-testing/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "concordium-smart-contract-testing" -version = "3.0.0" +version = "3.1.0" edition = "2021" rust-version = "1.66" license = "MPL-2.0" @@ -13,10 +13,10 @@ exclude = ["tests"] # Do not publish tests. # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -concordium_base = {version = "3.0", path = "../concordium-rust-sdk/concordium-base/rust-src/concordium_base"} +concordium_base = {version = "3.1", path = "../concordium-rust-sdk/concordium-base/rust-src/concordium_base"} concordium-smart-contract-engine = {version = "3.0", path = "../concordium-rust-sdk/concordium-base/smart-contracts/wasm-chain-integration"} concordium-wasm = {version = "3.0", path = "../concordium-rust-sdk/concordium-base/smart-contracts/wasm-transform"} -concordium-rust-sdk = {version = "3.0", path = "../concordium-rust-sdk"} +concordium-rust-sdk = {version = "3.1", path = "../concordium-rust-sdk"} tokio = { version = "1.28", features = ["rt-multi-thread", "time"] } sha2 = "0.10" anyhow = "1" diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 389e0285..43bff27e 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -9,7 +9,8 @@ use concordium_base::{ constants::MAX_WASM_MODULE_SIZE, contracts_common::{ self, AccountAddress, AccountBalance, Address, Amount, ChainMetadata, ContractAddress, - Duration, ExchangeRate, ExchangeRates, ModuleReference, OwnedPolicy, SlotTime, Timestamp, + Deserial, Duration, ExchangeRate, ExchangeRates, ModuleReference, OwnedPolicy, ParseResult, + SlotTime, Timestamp, }, hashes::BlockHash, smart_contracts::{ContractEvent, ModuleSource, WasmModule, WasmVersion}, @@ -1973,6 +1974,28 @@ impl ContractInvokeError { _ => None, } } + + /// Try to extract and parse the value returned into a type that implements + /// [`Deserial`]. + /// + /// Returns an error if the return value: + /// - isn't present + /// - see [`Self::return_value`] for details about when this happens + /// - is present + /// - but could not be parsed into `T` + /// - could parse into `T`, but there were leftover bytes + pub fn parse_return_value(&self) -> ParseResult { + use contracts_common::{Cursor, Get, ParseError}; + let return_value = self.return_value().ok_or_else(ParseError::default)?; + let mut cursor = Cursor::new(return_value); + let res = cursor.get()?; + // Check that all bytes have been read, as leftover bytes usually indicate + // errors. + if cursor.offset != return_value.len() { + return Err(ParseError::default()); + } + Ok(res) + } } impl From for ContractInitErrorKind { diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index df8cf3e0..003fe66f 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -3,8 +3,9 @@ use concordium_base::{ common::types::{CredentialIndex, KeyIndex, Signature}, constants::ED25519_SIGNATURE_LENGTH, contracts_common::{ - self, AccountAddress, AccountBalance, Address, Amount, ContractAddress, ExchangeRate, - ModuleReference, OwnedContractName, OwnedEntrypointName, OwnedPolicy, SlotTime, Timestamp, + self, AccountAddress, AccountBalance, Address, Amount, ContractAddress, Deserial, + ExchangeRate, ModuleReference, OwnedContractName, OwnedEntrypointName, OwnedPolicy, + ParseResult, SlotTime, Timestamp, }, hashes::BlockHash, id::types::SchemeId, @@ -455,15 +456,15 @@ impl ContractInvokeSuccess { /// Only events from effective trace elements are included. See /// [`Self::effective_trace_elements`] for more details. pub fn events(&self) -> impl Iterator { - self.effective_trace_elements().flat_map(|cte| { - if let ContractTraceElement::Updated { + self.effective_trace_elements().flat_map(|cte| match cte { + ContractTraceElement::Updated { data, - } = cte - { - Some((data.address, data.events.as_slice())) - } else { - None - } + } => Some((data.address, data.events.as_slice())), + ContractTraceElement::Interrupted { + address, + events, + } => Some((*address, events.as_slice())), + _ => None, }) } @@ -606,6 +607,21 @@ impl ContractInvokeSuccess { .iter() .any(|element| matches!(element, DebugTraceElement::WithFailures { .. })) } + + /// Try to parse the return value into a type that implements [`Deserial`]. + /// + /// Ensures that all bytes of the return value are read. + pub fn parse_return_value(&self) -> ParseResult { + use contracts_common::{Cursor, Get, ParseError}; + let mut cursor = Cursor::new(&self.return_value); + let res = cursor.get()?; + // Check that all bytes have been read, as leftover bytes usually indicate + // errors. + if cursor.offset != self.return_value.len() { + return Err(ParseError::default()); + } + Ok(res) + } } /// A wrapper for [`ContractTraceElement`], which provides additional diff --git a/examples/README.md b/examples/README.md index a38fd2e9..2f0c74bd 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,3 +1,5 @@ +# Example contracts + Example smart contracts illustrating the use of the tools for developing smart contracts in Rust. @@ -7,7 +9,10 @@ the logic of the contract is reasonable, or safe. **Do not use these contracts as-is for anything other then experimenting.** -The list of contracts is as follows +## The examples + +The list of contracts is as follows: + - [account-signature-checks](./account-signature-checks) A simple contract that demonstrates how account signature checks can be performed in smart contracts. - [two-step-transfer](./two-step-transfer) A contract that acts like an account (can send, store and accept CCD), @@ -40,3 +45,14 @@ The list of contracts is as follows - [sponsoredTransactions](./cis3-nft-sponsored-txs) A contract implementing the sponsored transaction mechanism (CIS3 standard). - [smartContractUpgrade](./smart-contract-upgrade) An example of how to upgrade a smart contract. The state is migrated during the upgrade. + +## Running the tests + +To run the tests for an example contract in the folder `EXAMPLE` open a terminal an run the following commands: +1. `cd EXAMPLE` +2. `cargo concordium test --out concordium-out/module.wasm.v1` + +The smart contract upgrade example has specific instructions for running the tests. See the module documentation in `./smart-contract-upgrade/contract-version1/tests/tests.rs`. + +To learn more about testing contracts, please refer to [our integration testing documentation](https://developer.concordium.software/en/mainnet/smart-contracts/guides/integration-test-contract.html). + diff --git a/examples/account-signature-checks/src/lib.rs b/examples/account-signature-checks/src/lib.rs index b4b1175d..2f4e2e66 100644 --- a/examples/account-signature-checks/src/lib.rs +++ b/examples/account-signature-checks/src/lib.rs @@ -37,12 +37,7 @@ struct State {} /// Init function that creates a new smart contract. #[init(contract = "account_signature_checks")] -fn init( - _ctx: &impl HasInitContext, - _state_builder: &mut StateBuilder, -) -> InitResult { - Ok(State {}) -} +fn init(_ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult { Ok(State {}) } #[derive(Deserial, SchemaType)] struct CheckParam { @@ -61,10 +56,7 @@ struct CheckParam { error = "Error", return_value = "bool" )] -fn check( - ctx: &impl HasReceiveContext, - host: &impl HasHost, -) -> Result { +fn check(ctx: &ReceiveContext, host: &Host) -> Result { let param: CheckParam = ctx.parameter_cursor().get()?; let r = host.check_account_signature(param.address, ¶m.sigs, ¶m.data)?; Ok(r) @@ -77,10 +69,7 @@ fn check( parameter = "AccountAddress", return_value = "AccountPublicKeys" )] -fn view_keys( - ctx: &impl HasReceiveContext, - host: &impl HasHost, -) -> Result { +fn view_keys(ctx: &ReceiveContext, host: &Host) -> Result { let param: AccountAddress = ctx.parameter_cursor().get()?; let pk = host.account_public_keys(param)?; Ok(pk) diff --git a/examples/auction/Cargo.toml b/examples/auction/Cargo.toml index c16a2b01..01f741aa 100644 --- a/examples/auction/Cargo.toml +++ b/examples/auction/Cargo.toml @@ -14,5 +14,8 @@ wee_alloc = ["concordium-std/wee_alloc"] [dependencies] concordium-std = {path = "../../concordium-std", default-features = false} +[dev-dependencies] +concordium-smart-contract-testing = { path = "../../contract-testing" } + [lib] crate-type=["cdylib", "rlib"] diff --git a/examples/auction/src/lib.rs b/examples/auction/src/lib.rs index 54e0692a..a0eddc21 100644 --- a/examples/auction/src/lib.rs +++ b/examples/auction/src/lib.rs @@ -62,18 +62,18 @@ pub struct State { /// Type of the parameter to the `init` function #[derive(Serialize, SchemaType)] -struct InitParameter { +pub struct InitParameter { /// The item to be sold - item: String, + pub item: String, /// Time when auction ends using the RFC 3339 format (https://tools.ietf.org/html/rfc3339) - end: Timestamp, + pub end: Timestamp, /// The minimum accepted raise to over bid the current bidder in Euro cent. - minimum_raise: u64, + pub minimum_raise: u64, } /// `bid` function errors -#[derive(Debug, PartialEq, Eq, Clone, Reject, Serial, SchemaType)] -enum BidError { +#[derive(Debug, PartialEq, Eq, Clone, Reject, Serialize, SchemaType)] +pub enum BidError { /// Raised when a contract tries to bid; Only accounts /// are allowed to bid. OnlyAccount, @@ -89,8 +89,8 @@ enum BidError { } /// `finalize` function errors -#[derive(Debug, PartialEq, Eq, Clone, Reject, Serial, SchemaType)] -enum FinalizeError { +#[derive(Debug, PartialEq, Eq, Clone, Reject, Serialize, SchemaType)] +pub enum FinalizeError { /// Raised when finalizing an auction before auction end time passed AuctionStillActive, /// Raised when finalizing an auction that is already finalized @@ -99,10 +99,7 @@ enum FinalizeError { /// Init function that creates a new auction #[init(contract = "auction", parameter = "InitParameter")] -fn auction_init( - ctx: &impl HasInitContext, - _state_builder: &mut StateBuilder, -) -> InitResult { +fn auction_init(ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult { // Getting input parameters let parameter: InitParameter = ctx.parameter_cursor().get()?; // Creating `State` @@ -118,9 +115,9 @@ fn auction_init( /// Receive function for accounts to place a bid in the auction #[receive(contract = "auction", name = "bid", payable, mutable, error = "BidError")] -fn auction_bid( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, +fn auction_bid( + ctx: &ReceiveContext, + host: &mut Host, amount: Amount, ) -> Result<(), BidError> { let state = host.state(); @@ -172,20 +169,14 @@ fn auction_bid( /// View function that returns the content of the state #[receive(contract = "auction", name = "view", return_value = "State")] -fn view<'a, 'b, S: HasStateApi>( - _ctx: &'a impl HasReceiveContext, - host: &'b impl HasHost, -) -> ReceiveResult<&'b State> { +fn view<'a, 'b>(_ctx: &'a ReceiveContext, host: &'b Host) -> ReceiveResult<&'b State> { Ok(host.state()) } /// ViewHighestBid function that returns the highest bid which is the balance of /// the contract #[receive(contract = "auction", name = "viewHighestBid", return_value = "Amount")] -fn view_highest_bid( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, -) -> ReceiveResult { +fn view_highest_bid(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { Ok(host.self_balance()) } @@ -193,10 +184,7 @@ fn view_highest_bid( /// current balance of this smart contract) to the owner of the smart contract /// instance. #[receive(contract = "auction", name = "finalize", mutable, error = "FinalizeError")] -fn auction_finalize( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, -) -> Result<(), FinalizeError> { +fn auction_finalize(ctx: &ReceiveContext, host: &mut Host) -> Result<(), FinalizeError> { let state = host.state(); // Ensure the auction has not been finalized yet ensure_eq!( @@ -224,265 +212,3 @@ fn auction_finalize( } Ok(()) } - -#[concordium_cfg_test] -mod tests { - use super::*; - use core::fmt::Debug; - use std::sync::atomic::{AtomicU8, Ordering}; - use test_infrastructure::*; - - // A counter for generating new accounts - static ADDRESS_COUNTER: AtomicU8 = AtomicU8::new(0); - const AUCTION_END: u64 = 1; - const ITEM: &str = "Starry night by Van Gogh"; - - fn expect_error(expr: Result, err: E, msg: &str) - where - E: Eq + Debug, - T: Debug, { - let actual = expr.expect_err_report(msg); - claim_eq!(actual, err); - } - - fn item_end_parameter() -> InitParameter { - InitParameter { - item: ITEM.into(), - end: Timestamp::from_timestamp_millis(AUCTION_END), - minimum_raise: 100, - } - } - - fn create_parameter_bytes(parameter: &InitParameter) -> Vec { to_bytes(parameter) } - - fn parametrized_init_ctx(parameter_bytes: &[u8]) -> TestInitContext { - let mut ctx = TestInitContext::empty(); - ctx.set_parameter(parameter_bytes); - ctx - } - - fn new_account() -> AccountAddress { - let account = AccountAddress([ADDRESS_COUNTER.load(Ordering::SeqCst); 32]); - ADDRESS_COUNTER.fetch_add(1, Ordering::SeqCst); - account - } - - fn new_account_ctx<'a>() -> (AccountAddress, TestReceiveContext<'a>) { - let account = new_account(); - let ctx = new_ctx(account, account, AUCTION_END); - (account, ctx) - } - - fn new_ctx<'a>( - owner: AccountAddress, - sender: AccountAddress, - slot_time: u64, - ) -> TestReceiveContext<'a> { - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(Address::Account(sender)); - ctx.set_owner(owner); - ctx.set_metadata_slot_time(Timestamp::from_timestamp_millis(slot_time)); - ctx - } - - fn bid( - host: &mut TestHost, - ctx: &TestContext, - amount: Amount, - current_smart_contract_balance: Amount, - ) { - // Setting the contract balance. - // This should be the sum of the contract’s initial balance and - // the amount you wish to invoke it with when using the TestHost. - // https://docs.rs/concordium-std/latest/concordium_std/test_infrastructure/struct.TestHost.html#method.set_self_balance - // This is because the `self_balance` function on-chain behaves as follows: - // https://docs.rs/concordium-std/latest/concordium_std/trait.HasHost.html#tymethod.self_balance - host.set_self_balance(amount + current_smart_contract_balance); - - // Invoking the bid function. - auction_bid(ctx, host, amount).expect_report("Bidding should pass."); - } - - #[concordium_test] - /// Test that the smart-contract initialization sets the state correctly - /// (no bids, active state, indicated auction-end time and item name). - fn test_init() { - let parameter_bytes = create_parameter_bytes(&item_end_parameter()); - let ctx = parametrized_init_ctx(¶meter_bytes); - - let mut state_builder = TestStateBuilder::new(); - - let state_result = auction_init(&ctx, &mut state_builder); - state_result.expect_report("Contract initialization results in error"); - } - - #[concordium_test] - /// Test a sequence of bids and finalizations: - /// 0. Auction is initialized. - /// 1. Alice successfully bids 0.1 CCD. - /// 2. Alice successfully bids 0.2 CCD, highest - /// bid becomes 0.2 CCD. Alice gets her 0.1 CCD refunded. - /// 3. Bob successfully bids 0.3 CCD, highest - /// bid becomes 0.3 CCD. Alice gets her 0.2 CCD refunded. - /// 4. Someone tries to finalize the auction before - /// its end time. Attempt fails. - /// 5. Dave successfully finalizes the auction after its end time. - /// Carol (the owner of the contract) collects the highest bid amount. - /// 6. Attempts to subsequently bid or finalize fail. - fn test_auction_bid_and_finalize() { - let parameter_bytes = create_parameter_bytes(&item_end_parameter()); - let ctx0 = parametrized_init_ctx(¶meter_bytes); - - let amount = Amount::from_micro_ccd(100); - let winning_amount = Amount::from_micro_ccd(300); - let big_amount = Amount::from_micro_ccd(500); - - let mut state_builder = TestStateBuilder::new(); - - // Initializing auction - let initial_state = - auction_init(&ctx0, &mut state_builder).expect("Initialization should pass"); - - let mut host = TestHost::new(initial_state, state_builder); - host.set_exchange_rates(ExchangeRates { - euro_per_energy: ExchangeRate::new_unchecked(1, 1), - micro_ccd_per_euro: ExchangeRate::new_unchecked(1, 1), - }); - - // 1st bid: Alice bids `amount`. - // The current_smart_contract_balance before the invoke is 0. - let (alice, alice_ctx) = new_account_ctx(); - bid(&mut host, &alice_ctx, amount, Amount::from_micro_ccd(0)); - - // 2nd bid: Alice bids `amount + amount`. - // Alice gets her initial bid refunded. - // The current_smart_contract_balance before the invoke is amount. - bid(&mut host, &alice_ctx, amount + amount, amount); - - // 3rd bid: Bob bids `winning_amount`. - // Alice gets refunded. - // The current_smart_contract_balance before the invoke is amount + amount. - let (bob, bob_ctx) = new_account_ctx(); - bid(&mut host, &bob_ctx, winning_amount, amount + amount); - - // Trying to finalize auction that is still active - // (specifically, the tx is submitted at the last moment, - // at the AUCTION_END time) - let mut ctx4 = TestReceiveContext::empty(); - ctx4.set_metadata_slot_time(Timestamp::from_timestamp_millis(AUCTION_END)); - let finres = auction_finalize(&ctx4, &mut host); - expect_error( - finres, - FinalizeError::AuctionStillActive, - "Finalizing the auction should fail when it's before auction end time", - ); - - // Finalizing auction - let carol = new_account(); - let dave = new_account(); - let ctx5 = new_ctx(carol, dave, AUCTION_END + 1); - - let finres2 = auction_finalize(&ctx5, &mut host); - finres2.expect_report("Finalizing the auction should work"); - let transfers = host.get_transfers(); - // The input arguments of all executed `host.invoke_transfer` - // functions are checked here. - claim_eq!( - &transfers[..], - &[(alice, amount), (alice, amount + amount), (carol, winning_amount),], - "Transferring CCD to Alice/Carol should work" - ); - claim_eq!( - host.state().auction_state, - AuctionState::Sold(bob), - "Finalizing the auction should change the auction state to `Sold(bob)`" - ); - claim_eq!( - host.state().highest_bidder, - Some(bob), - "Finalizing the auction should mark bob as highest bidder" - ); - - // Attempting to finalize auction again should fail. - let finres3 = auction_finalize(&ctx5, &mut host); - expect_error( - finres3, - FinalizeError::AuctionAlreadyFinalized, - "Finalizing the auction a second time should fail", - ); - - // Attempting to bid again should fail. - let res4 = auction_bid(&bob_ctx, &mut host, big_amount); - expect_error( - res4, - BidError::AuctionAlreadyFinalized, - "Bidding should fail because the auction is finalized", - ); - } - - #[concordium_test] - /// Bids for amounts lower or equal to the highest bid should be rejected. - fn test_auction_bid_repeated_bid() { - let ctx1 = new_account_ctx().1; - let ctx2 = new_account_ctx().1; - - let parameter_bytes = create_parameter_bytes(&item_end_parameter()); - let ctx0 = parametrized_init_ctx(¶meter_bytes); - - let amount = Amount::from_micro_ccd(100); - - let mut state_builder = TestStateBuilder::new(); - - // Initializing auction - let initial_state = - auction_init(&ctx0, &mut state_builder).expect("Initialization should succeed."); - - let mut host = TestHost::new(initial_state, state_builder); - host.set_exchange_rates(ExchangeRates { - euro_per_energy: ExchangeRate::new_unchecked(1, 1), - micro_ccd_per_euro: ExchangeRate::new_unchecked(1, 1), - }); - - // 1st bid: Account1 bids `amount`. - // The current_smart_contract_balance before the invoke is 0. - bid(&mut host, &ctx1, amount, Amount::from_micro_ccd(0)); - - // Setting the contract balance. - // This should be the sum of the contract’s initial balance and - // the amount you wish to invoke it with when using the TestHost. - // The current_smart_contract_balance before the invoke is `amount`. - // The balance we wish to invoke the next function with is `amount` as well. - // https://docs.rs/concordium-std/latest/concordium_std/test_infrastructure/struct.TestHost.html#method.set_self_balance - // This is because the `self_balance` function on-chain behaves as follows: - // https://docs.rs/concordium-std/latest/concordium_std/trait.HasHost.html#tymethod.self_balance - host.set_self_balance(amount + amount); - - // 2nd bid: Account2 bids `amount` (should fail - // because amount is equal to highest bid). - let res2 = auction_bid(&ctx2, &mut host, amount); - expect_error( - res2, - BidError::BidBelowCurrentBid, - "Bidding 2 should fail because bid amount must be higher than highest bid", - ); - } - - #[concordium_test] - /// Bids for 0 CCD should be rejected. - fn test_auction_bid_zero() { - let ctx1 = new_account_ctx().1; - let parameter_bytes = create_parameter_bytes(&item_end_parameter()); - let ctx = parametrized_init_ctx(¶meter_bytes); - - let mut state_builder = TestStateBuilder::new(); - - // initializing auction - let initial_state = - auction_init(&ctx, &mut state_builder).expect("Initialization should succeed."); - - let mut host = TestHost::new(initial_state, state_builder); - - let res = auction_bid(&ctx1, &mut host, Amount::zero()); - expect_error(res, BidError::BidBelowCurrentBid, "Bidding zero should fail"); - } -} diff --git a/examples/auction/tests/tests.rs b/examples/auction/tests/tests.rs new file mode 100644 index 00000000..1c716eb7 --- /dev/null +++ b/examples/auction/tests/tests.rs @@ -0,0 +1,283 @@ +//! Tests for the auction smart contract. +use auction_smart_contract::*; +use concordium_smart_contract_testing::*; + +/// The tests accounts. +const ALICE: AccountAddress = AccountAddress([0; 32]); +const BOB: AccountAddress = AccountAddress([1; 32]); +const CAROL: AccountAddress = AccountAddress([2; 32]); +const DAVE: AccountAddress = AccountAddress([3; 32]); + +const SIGNER: Signer = Signer::with_one_key(); +const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10000); + +/// Test a sequence of bids and finalizations: +/// 0. Auction is initialized. +/// 1. Alice successfully bids 1 CCD. +/// 2. Alice successfully bids 2 CCD, highest +/// bid becomes 2 CCD. Alice gets her 1 CCD refunded. +/// 3. Bob successfully bids 3 CCD, highest +/// bid becomes 3 CCD. Alice gets her 2 CCD refunded. +/// 4. Alice tries to bid 3 CCD, which matches the current highest bid, which +/// fails. +/// 5. Alice tries to bid 3.5 CCD, which is below the minimum raise +/// threshold of 1 CCD. +/// 6. Someone tries to finalize the auction before +/// its end time. Attempt fails. +/// 7. Someone tries to bid after the auction has ended (but before it has been +/// finalized), which fails. +/// 8. Dave successfully finalizes the auction after +/// its end time. Carol (the owner of the contract) collects the highest bid +/// amount. +/// 9. Attempts to subsequently bid or finalize fail. +#[test] +fn test_multiple_scenarios() { + let (mut chain, contract_address) = initialize_chain_and_auction(); + + // 1. Alice successfully bids 1 CCD. + let _update_1 = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(1), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("auction.bid".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect("Alice successfully bids 1 CCD"); + + // 2. Alice successfully bids 2 CCD, highest + // bid becomes 2 CCD. Alice gets her 1 CCD refunded. + let update_2 = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(2), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("auction.bid".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect("Alice successfully bids 2 CCD"); + // Check that 1 CCD is transferred back to ALICE. + assert_eq!(update_2.account_transfers().collect::>()[..], [( + contract_address, + Amount::from_ccd(1), + ALICE + )]); + + // 3. Bob successfully bids 3 CCD, highest + // bid becomes 3 CCD. Alice gets her 2 CCD refunded. + let update_3 = chain + .contract_update( + SIGNER, + BOB, + Address::Account(BOB), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(3), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("auction.bid".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect("Bob successfully bids 3 CCD"); + // Check that 2 CCD is transferred back to ALICE. + assert_eq!(update_3.account_transfers().collect::>()[..], [( + contract_address, + Amount::from_ccd(2), + ALICE + )]); + + // 4. Alice tries to bid 3 CCD, which matches the current highest bid, which + // fails. + let update_4 = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(3), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("auction.bid".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect_err("Alice tries to bid 3 CCD"); + // Check that the correct error is returned. + let rv: BidError = update_4.parse_return_value().expect("Return value is valid"); + assert_eq!(rv, BidError::BidBelowCurrentBid); + + // 5. Alice tries to bid 3.5 CCD, which is below the minimum raise threshold of + // 1 CCD. + let update_5 = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_micro_ccd(3_500_000), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("auction.bid".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect_err("Alice tries to bid 3.5 CCD"); + // Check that the correct error is returned. + let rv: BidError = update_5.parse_return_value().expect("Return value is valid"); + assert_eq!(rv, BidError::BidBelowMinimumRaise); + + // 6. Someone tries to finalize the auction before + // its end time. Attempt fails. + let update_6 = chain + .contract_update( + SIGNER, + DAVE, + Address::Account(DAVE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("auction.finalize".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect_err("Attempt to finalize auction before end time"); + // Check that the correct error is returned. + let rv: FinalizeError = update_6.parse_return_value().expect("Return value is valid"); + assert_eq!(rv, FinalizeError::AuctionStillActive); + + // Increment the chain time by 1001 milliseconds. + chain.tick_block_time(Duration::from_millis(1001)).expect("Increment chain time"); + + // 7. Someone tries to bid after the auction has ended (but before it has been + // finalized), which fails. + let update_7 = chain + .contract_update( + SIGNER, + DAVE, + Address::Account(DAVE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(10), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("auction.bid".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect_err("Attempt to bid after auction has reached the endtime"); + // Check that the return value is `BidTooLate`. + let rv: BidError = update_7.parse_return_value().expect("Return value is valid"); + assert_eq!(rv, BidError::BidTooLate); + + // 8. Dave successfully finalizes the auction after its end time. + let update_8 = chain + .contract_update( + SIGNER, + DAVE, + Address::Account(DAVE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("auction.finalize".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect("Dave successfully finalizes the auction after its end time"); + + // Check that the correct amount is transferred to Carol. + assert_eq!(update_8.account_transfers().collect::>()[..], [( + contract_address, + Amount::from_ccd(3), + CAROL + )]); + + // 9. Attempts to subsequently bid or finalize fail. + let update_9 = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::from_ccd(1), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("auction.bid".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect_err("Attempt to bid after auction has been finalized"); + // Check that the return value is `AuctionAlreadyFinalized`. + let rv: BidError = update_9.parse_return_value().expect("Return value is valid"); + assert_eq!(rv, BidError::AuctionAlreadyFinalized); + + let update_10 = chain + .contract_update( + SIGNER, + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("auction.finalize".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect_err("Attempt to finalize auction after it has been finalized"); + let rv: FinalizeError = update_10.parse_return_value().expect("Return value is valid"); + assert_eq!(rv, FinalizeError::AuctionAlreadyFinalized); +} + +/// Setup auction and chain. +/// +/// Carol is the owner of the auction, which ends at `1000` milliseconds after +/// the unix epoch. The 'microCCD per euro' exchange rate is set to `1_000_000`, +/// so 1 CCD = 1 euro. +fn initialize_chain_and_auction() -> (Chain, ContractAddress) { + let mut chain = Chain::builder() + .micro_ccd_per_euro( + ExchangeRate::new(1_000_000, 1).expect("Exchange rate is in valid range"), + ) + .build() + .expect("Exchange rate is in valid range"); + + // Create some accounts accounts on the chain. + chain.create_account(Account::new(ALICE, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(CAROL, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(DAVE, ACC_INITIAL_BALANCE)); + + // Load and deploy the module. + let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); + let deployment = chain.module_deploy_v1(SIGNER, CAROL, module).expect("Deploy valid module"); + + // Create the InitParameter. + let parameter = InitParameter { + item: "Auction item".to_string(), + end: Timestamp::from_timestamp_millis(1000), + minimum_raise: 100, // 100 eurocent = 1 euro + }; + + // Initialize the auction contract. + let init = chain + .contract_init(SIGNER, CAROL, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_auction".to_string()), + param: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), + }) + .expect("Initialize auction"); + + (chain, init.contract_address) +} diff --git a/examples/cis2-multi-royalties/Cargo.toml b/examples/cis2-multi-royalties/Cargo.toml index ded30dc9..74f8c8b2 100644 --- a/examples/cis2-multi-royalties/Cargo.toml +++ b/examples/cis2-multi-royalties/Cargo.toml @@ -14,6 +14,9 @@ wee_alloc = ["concordium-std/wee_alloc"] concordium-std = {path = "../../concordium-std", default-features = false} concordium-cis2 = {path = "../../concordium-cis2", default-features = false} +[dev-dependencies] +concordium-smart-contract-testing = { path = "../../contract-testing" } + [lib] crate-type=["cdylib", "rlib"] diff --git a/examples/cis2-multi-royalties/src/lib.rs b/examples/cis2-multi-royalties/src/lib.rs index da2bb136..7e8069ac 100644 --- a/examples/cis2-multi-royalties/src/lib.rs +++ b/examples/cis2-multi-royalties/src/lib.rs @@ -51,65 +51,65 @@ const SUPPORTS_STANDARDS: [StandardIdentifier<'static>; 2] = /// Contract token ID type. /// To save bytes we use a small token ID type, but is limited to be represented /// by a `u8`. -type ContractTokenId = TokenIdU8; +pub type ContractTokenId = TokenIdU8; /// Contract token amount type. -type ContractTokenAmount = TokenAmountU64; +pub type ContractTokenAmount = TokenAmountU64; /// The parameter for the contract function `init` which sets up the contract. #[derive(Serial, Deserial, SchemaType)] #[repr(transparent)] -struct InitParams { +pub struct InitParams { /// Specifies if this contract enforces the royalty payout whenever a token /// is transferred. - pay_royalty: bool, + pub pay_royalty: bool, } /// The parameter for the contract function `mint` which mints a number of /// token types and/or amounts of tokens to a given address. #[derive(Serial, Deserial, SchemaType)] -struct MintParams { +pub struct MintParams { /// Owner of the newly minted tokens. - owner: Address, + pub owner: Address, /// A collection of tokens to mint. - tokens: collections::BTreeMap, + pub tokens: collections::BTreeMap, /// Royalty payable to minter (in percentage) - royalty_percentage: u8, + pub royalty_percentage: u8, } /// The parameter type for the contract function `setImplementors`. /// Takes a standard identifier and a list of contract addresses providing /// implementations of this standard. #[derive(Debug, Serialize, SchemaType)] -struct SetImplementorsParams { +pub struct SetImplementorsParams { /// The identifier for the standard. - id: StandardIdentifierOwned, + pub id: StandardIdentifierOwned, /// The addresses of the implementors of the standard. - implementors: Vec, + pub implementors: Vec, } /// The parameter type for the contract function `check_royalty`. /// Takes a tokenID and a sale price. #[derive(Debug, Serialize, SchemaType)] -struct CheckRoyaltyParams { +pub struct CheckRoyaltyParams { /// The identifier of the token. - id: ContractTokenId, + pub id: ContractTokenId, /// The sale price. - sale_price: u64, + pub sale_price: u64, } #[derive(Debug, Serialize, SchemaType)] -struct CheckRoyaltyResult { +pub struct CheckRoyaltyResult { /// The Address of the payee. - royalty_receiver: Address, + pub royalty_receiver: Address, /// The royalties to pay. - payment: u64, + pub payment: u64, } /// The state for each address. #[derive(Serial, DeserialWithState, Deletable)] #[concordium(state_parameter = "S")] -struct AddressState { +struct AddressState { /// The amount of tokens owned by this address. balances: StateMap, /// The address which are currently enabled as operators for this address. @@ -118,15 +118,15 @@ struct AddressState { /// The details of each token. #[derive(Debug, Serialize, SchemaType)] -struct TokenDetails { +pub struct TokenDetails { /// Who minted this token. - minter: Address, + pub minter: Address, /// The percentage royalty. - royalty: u8, + pub royalty: u8, } -impl AddressState { - fn empty(state_builder: &mut StateBuilder) -> Self { +impl AddressState { + fn empty(state_builder: &mut StateBuilder) -> Self { AddressState { balances: state_builder.new_map(), operators: state_builder.new_set(), @@ -140,7 +140,7 @@ impl AddressState { /// and this could be structured in a more space efficient way. #[derive(Serial, DeserialWithState)] #[concordium(state_parameter = "S")] -struct State { +struct State { /// The state of addresses. state: StateMap, S>, /// All of the token IDs @@ -157,7 +157,7 @@ struct State { /// The different errors the contract can produce. #[derive(Serialize, Debug, PartialEq, Eq, Reject, SchemaType)] -enum CustomContractError { +pub enum CustomContractError { /// Failed parsing the parameter. #[from(ParseError)] ParseParams, @@ -185,9 +185,9 @@ enum CustomContractError { Overflow, } -type ContractError = Cis2Error; +pub type ContractError = Cis2Error; -type ContractResult = Result; +pub type ContractResult = Result; /// Mapping the logging errors to ContractError. impl From for CustomContractError { @@ -213,9 +213,9 @@ impl From for CustomContractError { fn from(_: NewReceiveNameError) -> Self { Self::InvalidContractName } } -impl State { +impl State { /// Construct a state with no tokens - fn empty(state_builder: &mut StateBuilder, pay_royalty: bool) -> Self { + fn empty(state_builder: &mut StateBuilder, pay_royalty: bool) -> Self { State { state: state_builder.new_map(), tokens: state_builder.new_set(), @@ -231,7 +231,7 @@ impl State { token_id: &ContractTokenId, amount: ContractTokenAmount, owner: &Address, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, royalty: u8, ) { self.tokens.insert(*token_id); @@ -281,7 +281,7 @@ impl State { mut amount: ContractTokenAmount, from: &Address, to: &Address, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, royalties: &Option, ) -> ContractResult<()> { ensure!(self.contains_token(token_id), ContractError::InvalidTokenId); @@ -331,7 +331,7 @@ impl State { &mut self, owner: &Address, operator: &Address, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) { let mut owner_state = self.state.entry(*owner).or_insert_with(|| AddressState::empty(state_builder)); @@ -401,36 +401,30 @@ fn build_token_metadata_url(token_id: &ContractTokenId) -> String { parameter = "InitParams", event = "Cis2Event" )] -fn contract_init( - ctx: &impl HasInitContext, - state_builder: &mut StateBuilder, -) -> InitResult> { +fn contract_init(ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { // Construct the initial contract state. let params: InitParams = ctx.parameter_cursor().get()?; let state = State::empty(state_builder, params.pay_royalty); Ok(state) } -#[derive(Serialize, SchemaType)] -struct ViewAddressState { - balances: Vec<(ContractTokenId, ContractTokenAmount)>, - operators: Vec
, +#[derive(Serialize, SchemaType, Debug, PartialEq, Eq)] +pub struct ViewAddressState { + pub balances: Vec<(ContractTokenId, ContractTokenAmount)>, + pub operators: Vec
, } #[derive(Serialize, SchemaType)] -struct ViewState { - state: Vec<(Address, ViewAddressState)>, - tokens: Vec, +pub struct ViewState { + pub state: Vec<(Address, ViewAddressState)>, + pub tokens: Vec, } /// View function for testing. This reports on the entire state of the contract /// for testing purposes. In a realistic example there `balance_of` and similar /// functions with a smaller response. #[receive(contract = "cis2_multi_royalties", name = "view", return_value = "ViewState")] -fn contract_view( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ReceiveResult { +fn contract_view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { let state = host.state(); let mut inner_state = Vec::new(); @@ -483,10 +477,10 @@ fn contract_view( enable_logger, mutable )] -fn contract_mint( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, - logger: &mut impl HasLogger, +fn contract_mint( + ctx: &ReceiveContext, + host: &mut Host, + logger: &mut Logger, ) -> ContractResult<()> { // Get the contract owner let owner = ctx.owner(); @@ -547,10 +541,10 @@ type TransferParameter = TransferParams; enable_logger, mutable )] -fn contract_transfer( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, - logger: &mut impl HasLogger, +fn contract_transfer( + ctx: &ReceiveContext, + host: &mut Host, + logger: &mut Logger, ) -> ContractResult<()> { // Parse the parameter. let TransferParams(transfers): TransferParameter = ctx.parameter_cursor().get()?; @@ -637,10 +631,10 @@ fn contract_transfer( enable_logger, mutable )] -fn contract_update_operator( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, - logger: &mut impl HasLogger, +fn contract_update_operator( + ctx: &ReceiveContext, + host: &mut Host, + logger: &mut Logger, ) -> ContractResult<()> { // Parse the parameter. let UpdateOperatorParams(params) = ctx.parameter_cursor().get()?; @@ -687,9 +681,9 @@ type ContractBalanceOfQueryResponse = BalanceOfQueryResponse( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_balance_of( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: ContractBalanceOfQueryParams = ctx.parameter_cursor().get()?; @@ -716,9 +710,9 @@ fn contract_balance_of( return_value = "OperatorOfQueryResponse", error = "ContractError" )] -fn contract_operator_of( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_operator_of( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: OperatorOfQueryParams = ctx.parameter_cursor().get()?; @@ -749,9 +743,9 @@ type ContractTokenMetadataQueryParams = TokenMetadataQueryParams( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_token_metadata( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: ContractTokenMetadataQueryParams = ctx.parameter_cursor().get()?; @@ -792,10 +786,7 @@ fn contract_token_metadata( /// - Contract name part of the parameter is invalid. /// - Calling back `transfer` to sender contract rejects. #[receive(contract = "cis2_multi_royalties", name = "onReceivingCIS2", error = "ContractError")] -fn contract_on_cis2_received( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ContractResult<()> { +fn contract_on_cis2_received(ctx: &ReceiveContext, host: &Host) -> ContractResult<()> { // Ensure the sender is a contract. let sender = if let Address::Contract(contract) = ctx.sender() { contract @@ -840,9 +831,9 @@ fn contract_on_cis2_received( return_value = "SupportsQueryResponse", error = "ContractError" )] -fn contract_supports( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_supports( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: SupportsQueryParams = ctx.parameter_cursor().get()?; @@ -873,10 +864,7 @@ fn contract_supports( error = "ContractError", mutable )] -fn contract_set_implementor( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, -) -> ContractResult<()> { +fn contract_set_implementor(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<()> { // Authorize the sender. ensure!(ctx.sender().matches_account(&ctx.owner()), ContractError::Unauthorized); // Parse the parameter. @@ -899,9 +887,9 @@ fn contract_set_implementor( error = "ContractError", mutable )] -fn contract_check_royalty( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_check_royalty( + ctx: &ReceiveContext, + host: &mut Host, ) -> ContractResult { ensure!(host.state().pay_royalty, CustomContractError::ContractDoesNotPayRoyalties.into()); @@ -921,620 +909,9 @@ fn contract_check_royalty( name = "contract_pays_royalties", return_value = "bool" )] -fn contract_pays_royalties( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ReceiveResult { +fn contract_pays_royalties(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { // At the moment the parameters are not used in this scope let state = host.state(); Ok(state.pay_royalty) } - -// Tests - -#[concordium_cfg_test] -mod tests { - use super::*; - use test_infrastructure::*; - - const ACCOUNT_0: AccountAddress = AccountAddress([0u8; 32]); - const ADDRESS_0: Address = Address::Account(ACCOUNT_0); - const ACCOUNT_1: AccountAddress = AccountAddress([1u8; 32]); - const ADDRESS_1: Address = Address::Account(ACCOUNT_1); - const ACCOUNT_2: AccountAddress = AccountAddress([2u8; 32]); - const ADDRESS_2: Address = Address::Account(ACCOUNT_2); - const TOKEN_0: ContractTokenId = TokenIdU8(2); - const TOKEN_1: ContractTokenId = TokenIdU8(42); - - /// Test helper function which creates a contract state with two tokens with - /// id `TOKEN_0` and id `TOKEN_1` owned by `ADDRESS_0` - fn initial_state(state_builder: &mut StateBuilder) -> State { - let mut state = State::empty(state_builder, false); - state.mint(&TOKEN_0, 400.into(), &ADDRESS_0, state_builder, 0); - state.mint(&TOKEN_1, 1.into(), &ADDRESS_0, state_builder, 0); - state - } - - /// Test helper function which creates a contract state with a token with a - /// royalty - fn initial_state_with_royalties( - state_builder: &mut StateBuilder, - ) -> State { - let mut state = State::empty(state_builder, true); - state.mint(&TOKEN_0, 400.into(), &ADDRESS_0, state_builder, 50); - state.mint(&TOKEN_1, 1.into(), &ADDRESS_0, state_builder, 0); - state - } - - /// Test initialization succeeds with a state with no tokens. - #[concordium_test] - fn test_init() { - // Setup the context - let mut ctx = TestInitContext::empty(); - let mut builder = TestStateBuilder::new(); - let royalty = false; - let init_parameter_bytes = to_bytes(&royalty); - ctx.set_parameter(&init_parameter_bytes); - - // Call the contract function. - let result = contract_init(&ctx, &mut builder); - - // Check the result - let state = result.expect_report("Contract initialization failed"); - - // Check the state - claim_eq!(state.tokens.iter().count(), 0, "Only one token is initialized"); - } - - /// Test minting succeeds and the tokens are owned by the given address and - /// the appropriate events are logged. - #[concordium_test] - fn test_mint() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - ctx.set_owner(ACCOUNT_0); - - // and parameter. - let mut tokens = collections::BTreeMap::new(); - tokens.insert(TOKEN_0, 400.into()); - tokens.insert(TOKEN_1, 1.into()); - let parameter = MintParams { - owner: ADDRESS_0, - tokens, - royalty_percentage: 0, - }; - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = State::empty(&mut state_builder, false); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_mint(&ctx, &mut host, &mut logger); - - // Check the result - claim!(result.is_ok(), "Results in rejection"); - - // Check the state - claim_eq!(host.state().tokens.iter().count(), 2, "Only one token is initialized"); - let balance0 = - host.state().balance(&TOKEN_0, &ADDRESS_0).expect_report("Token is expected to exist"); - claim_eq!(balance0, 400.into(), "Initial tokens are owned by the contract instantiater"); - - let balance1 = - host.state().balance(&TOKEN_1, &ADDRESS_0).expect_report("Token is expected to exist"); - claim_eq!(balance1, 1.into(), "Initial tokens are owned by the contract instantiater"); - - // Check the logs - claim_eq!(logger.logs.len(), 4, "Exactly four events should be logged"); - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::Mint(MintEvent { - owner: ADDRESS_0, - token_id: TOKEN_0, - amount: ContractTokenAmount::from(400), - }))), - "Expected an event for minting TOKEN_0" - ); - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::Mint(MintEvent { - owner: ADDRESS_0, - token_id: TOKEN_1, - amount: ContractTokenAmount::from(1), - }))), - "Expected an event for minting TOKEN_1" - ); - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::TokenMetadata::<_, ContractTokenAmount>( - TokenMetadataEvent { - token_id: TOKEN_0, - metadata_url: MetadataUrl { - url: "https://some.example/token/02".to_string(), - hash: None, - }, - } - ))), - "Expected an event for token metadata for TOKEN_0" - ); - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::TokenMetadata::<_, ContractTokenAmount>( - TokenMetadataEvent { - token_id: TOKEN_1, - metadata_url: MetadataUrl { - url: "https://some.example/token/2A".to_string(), - hash: None, - }, - } - ))), - "Expected an event for token metadata for TOKEN_1" - ); - } - - /// Test transfer succeeds, when `from` is the sender. - #[concordium_test] - fn test_transfer_account() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - - // and parameter. - let transfer = Transfer { - token_id: TOKEN_0, - amount: ContractTokenAmount::from(100), - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let balance0 = - host.state().balance(&TOKEN_0, &ADDRESS_0).expect_report("Token is expected to exist"); - let balance1 = - host.state().balance(&TOKEN_0, &ADDRESS_1).expect_report("Token is expected to exist"); - claim_eq!( - balance0, - 300.into(), - "Token owner balance should be decreased by the transferred amount." - ); - claim_eq!( - balance1, - 100.into(), - "Token receiver balance should be increased by the transferred amount" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "Only one event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::Transfer(TransferEvent { - from: ADDRESS_0, - to: ADDRESS_1, - token_id: TOKEN_0, - amount: ContractTokenAmount::from(100), - })), - "Incorrect event emitted" - ) - } - - /// Test transfer token fails, when sender is neither the owner or an - /// operator of the owner. - #[concordium_test] - fn test_transfer_not_authorized() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_1); - - // and parameter. - let transfer = Transfer { - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - token_id: TOKEN_0, - amount: ContractTokenAmount::from(100), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - // Check the result. - let err = result.expect_err_report("Expected to fail"); - claim_eq!(err, ContractError::Unauthorized, "Error is expected to be Unauthorized") - } - - /// Test transfer succeeds when sender is not the owner, but is an operator - /// of the owner. - #[concordium_test] - fn test_operator_transfer() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_1); - - // and parameter. - let transfer = Transfer { - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - token_id: TOKEN_0, - amount: ContractTokenAmount::from(100), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let mut state = initial_state(&mut state_builder); - state.add_operator(&ADDRESS_0, &ADDRESS_1, &mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let balance0 = - host.state().balance(&TOKEN_0, &ADDRESS_0).expect_report("Token is expected to exist"); - let balance1 = - host.state().balance(&TOKEN_0, &ADDRESS_1).expect_report("Token is expected to exist"); - claim_eq!( - balance0, - 300.into(), - "Token owner balance should be decreased by the transferred amount" - ); - claim_eq!( - balance1, - 100.into(), - "Token receiver balance should be increased by the transferred amount" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "Only one event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::Transfer(TransferEvent { - from: ADDRESS_0, - to: ADDRESS_1, - token_id: TOKEN_0, - amount: ContractTokenAmount::from(100), - })), - "Incorrect event emitted" - ) - } - - /// Test adding an operator succeeds and the appropriate event is logged. - #[concordium_test] - fn test_add_operator() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - - // and parameter. - let update = UpdateOperator { - operator: ADDRESS_1, - update: OperatorUpdate::Add, - }; - let parameter = UpdateOperatorParams(vec![update]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_update_operator(&ctx, &mut host, &mut logger); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let is_operator = host.state().is_operator(&ADDRESS_1, &ADDRESS_0); - claim!(is_operator, "Account should be an operator"); - - // Checking that `ADDRESS_1` is an operator in the query response of the - // `contract_operator_of` function as well. - // Setup parameter. - let operator_of_query = OperatorOfQuery { - address: ADDRESS_1, - owner: ADDRESS_0, - }; - - let operator_of_query_vector = OperatorOfQueryParams { - queries: vec![operator_of_query], - }; - let parameter_bytes = to_bytes(&operator_of_query_vector); - - ctx.set_parameter(¶meter_bytes); - - // Checking the return value of the `contract_operator_of` function - let result: ContractResult = contract_operator_of(&ctx, &host); - - claim_eq!( - result.expect_report("Failed getting result value").0, - [true], - "Account should be an operator in the query response" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "One event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::::UpdateOperator( - UpdateOperatorEvent { - owner: ADDRESS_0, - operator: ADDRESS_1, - update: OperatorUpdate::Add, - } - )), - "Incorrect event emitted" - ) - } - - // test royalties are initialised properly - #[concordium_test] - fn test_royalties_setup() { - // Setup the context - let ctx = TestReceiveContext::empty(); - let mut builder1 = TestStateBuilder::new(); - let state1 = initial_state(&mut builder1); - let host = TestHost::new(state1, builder1); - let royalty_paid = - contract_pays_royalties(&ctx, &host).expect_report("Invoke should succeed"); - claim_eq!(royalty_paid, false, "Royalty incorrectly initialised"); - - let mut builder2 = TestStateBuilder::new(); - let state2 = initial_state_with_royalties(&mut builder2); - let host = TestHost::new(state2, builder2); - let royalty_paid = - contract_pays_royalties(&ctx, &host).expect_report("Invoke should succeed"); - claim_eq!(royalty_paid, true, "Royalty incorrectly initialised"); - } - - // test royalties are paid correctly - #[concordium_test] - fn test_royalties_paid() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - - // and parameter. - let transfer = Transfer { - token_id: TOKEN_0, - amount: ContractTokenAmount::from(100), - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state_with_royalties(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let balance0 = - host.state().balance(&TOKEN_0, &ADDRESS_0).expect_report("Token is expected to exist"); - let balance1 = - host.state().balance(&TOKEN_0, &ADDRESS_1).expect_report("Token is expected to exist"); - claim_eq!( - balance0, - 350.into(), - "Token owner balance should be decreased by the transferred amount." - ); - claim_eq!( - balance1, - 50.into(), - "Token receiver balance should be increased by the transferred amount" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 2, "Only two events should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::Transfer(TransferEvent { - from: ADDRESS_0, - to: ADDRESS_0, - token_id: TOKEN_0, - amount: ContractTokenAmount::from(50), - })), - "Incorrect event emitted 1" - ); - claim_eq!( - logger.logs[1], - to_bytes(&Cis2Event::Transfer(TransferEvent { - from: ADDRESS_0, - to: ADDRESS_1, - token_id: TOKEN_0, - amount: ContractTokenAmount::from(50), - })), - "Incorrect event emitted 2" - ); - - // second transfer - let mut second_ctx = TestReceiveContext::empty(); - second_ctx.set_sender(ADDRESS_1); - - let second_transfer = Transfer { - token_id: TOKEN_0, - amount: ContractTokenAmount::from(10), - from: ADDRESS_1, - to: Receiver::from_account(ACCOUNT_2), - data: AdditionalData::empty(), - }; - let second_parameter = TransferParams::from(vec![second_transfer]); - let second_parameter_bytes = to_bytes(&second_parameter); - second_ctx.set_parameter(&second_parameter_bytes); - - let mut second_logger = TestLogger::init(); - - // Call the contract function. - let second_result: ContractResult<()> = - contract_transfer(&second_ctx, &mut host, &mut second_logger); - // Check the result. - claim!(second_result.is_ok(), "Results in rejection"); - - // Check the state. - let second_balance0 = - host.state().balance(&TOKEN_0, &ADDRESS_0).expect_report("Token is expected to exist"); - let second_balance1 = - host.state().balance(&TOKEN_0, &ADDRESS_1).expect_report("Token is expected to exist"); - let second_balance2 = - host.state().balance(&TOKEN_0, &ADDRESS_2).expect_report("Token is expected to exist"); - claim_eq!( - second_balance0, - 355.into(), - "Token owner balance should be increase by the royalty payment." - ); - claim_eq!( - second_balance1, - 40.into(), - "Token receiver balance should be decreased by the transferred amount" - ); - - claim_eq!( - second_balance2, - 5.into(), - "Token receiver balance should be increased by the transferred amount - the royalty \ - payment" - ); - - // Check the logs. - claim_eq!(second_logger.logs.len(), 2, "Only two events should be logged"); - claim_eq!( - second_logger.logs[0], - to_bytes(&Cis2Event::Transfer(TransferEvent { - from: ADDRESS_1, - to: ADDRESS_0, - token_id: TOKEN_0, - amount: ContractTokenAmount::from(5), - })), - "Incorrect event emitted 3" - ); - claim_eq!( - second_logger.logs[1], - to_bytes(&Cis2Event::Transfer(TransferEvent { - from: ADDRESS_1, - to: ADDRESS_2, - token_id: TOKEN_0, - amount: ContractTokenAmount::from(5), - })), - "Incorrect event emitted 4" - ); - } - - // test code sends correct error when price is 0 - #[concordium_test] - fn test_royalties_with_zero_price() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_owner(ACCOUNT_1); - let royalty = CheckRoyaltyParams { - id: TOKEN_0, - sale_price: 0, - }; - let parameter: CheckRoyaltyParams = royalty; - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut state_builder = TestStateBuilder::new(); - let mut state = initial_state(&mut state_builder); - state.pay_royalty = true; - let mut host = TestHost::new(state, state_builder); - - let result = contract_check_royalty(&ctx, &mut host); - // Check the result. - let err = result.expect_err_report("Expected to fail with 0 price"); - claim_eq!( - err, - CustomContractError::NoRoyaltyPayable.into(), - "Error should be no royalty payable" - ) - } - - // test code sends correct error when royalty percentage is 0 - #[concordium_test] - fn test_royalties_with_zero_royalty_percentage() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_owner(ACCOUNT_1); - let royalty = CheckRoyaltyParams { - id: TOKEN_0, - sale_price: 200, - }; - let parameter: CheckRoyaltyParams = royalty; - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut state_builder = TestStateBuilder::new(); - let mut state = initial_state(&mut state_builder); - state.pay_royalty = true; - let mut host = TestHost::new(state, state_builder); - - let result = contract_check_royalty(&ctx, &mut host); - // Check the result. - let err = result.expect_err_report("Expected to fail with 0 price"); - claim_eq!( - err, - CustomContractError::NoRoyaltyPayable.into(), - "Error should be no royalty payable" - ) - } - - // test code sends correct error when token id is invalid - #[concordium_test] - fn test_royalties_with_invalid_token() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_owner(ACCOUNT_1); - let royalty = CheckRoyaltyParams { - id: TokenIdU8(5), - sale_price: 200, - }; - let parameter: CheckRoyaltyParams = royalty; - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut state_builder = TestStateBuilder::new(); - let mut state = initial_state(&mut state_builder); - state.pay_royalty = true; - let mut host = TestHost::new(state, state_builder); - let result = contract_check_royalty(&ctx, &mut host); - // Check the result. - let err = result.expect_err_report("Expected to fail with invalid tokenID"); - claim_eq!(err, ContractError::InvalidTokenId, "Error should be invalid tokenID"); - } -} diff --git a/examples/cis2-multi-royalties/tests/tests.rs b/examples/cis2-multi-royalties/tests/tests.rs new file mode 100644 index 00000000..ddcea404 --- /dev/null +++ b/examples/cis2-multi-royalties/tests/tests.rs @@ -0,0 +1,508 @@ +//! Tests for the `cis2_multi_royalties` contract. +use cis2_multi_royalties::*; +use concordium_cis2::*; +use concordium_smart_contract_testing::*; +use concordium_std::collections::BTreeMap; + +/// The tests accounts. +const ALICE: AccountAddress = AccountAddress([0; 32]); +const ALICE_ADDR: Address = Address::Account(ALICE); +const BOB: AccountAddress = AccountAddress([1; 32]); +const BOB_ADDR: Address = Address::Account(BOB); +const CHARLIE: AccountAddress = AccountAddress([2; 32]); +const CHARLIE_ADDR: Address = Address::Account(CHARLIE); + +/// Token IDs. +const TOKEN_0: ContractTokenId = TokenIdU8(2); +const TOKEN_1: ContractTokenId = TokenIdU8(42); + +/// Initial balance of the accounts. +const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10000); + +/// A signer for all the transactions. +const SIGNER: Signer = Signer::with_one_key(); + +/// Test minting succeeds and the tokens are owned by the given address and +/// the appropriate events are logged. +#[test] +fn test_minting() { + let (chain, contract_address, update) = initialize_contract_with_alice_tokens(0); + + // Invoke the view entrypoint and check that the tokens are owned by Alice. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi_royalties.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + + // Check that the tokens are owned by Alice. + let rv: ViewState = invoke.parse_return_value().expect("ViewState return value"); + assert_eq!(rv.tokens[..], [TOKEN_0, TOKEN_1]); + assert_eq!(rv.state, vec![(ALICE_ADDR, ViewAddressState { + balances: vec![(TOKEN_0, 400.into()), (TOKEN_1, 1.into())], + operators: Vec::new(), + })]); + + // Check that the events are logged. + let events = update.events().flat_map(|(_addr, events)| events); + + let events: Vec> = + events.map(|e| e.parse().expect("Deserialize event")).collect(); + + assert_eq!(events, [ + Cis2Event::Mint(MintEvent { + token_id: TokenIdU8(2), + amount: TokenAmountU64(400), + owner: ALICE_ADDR, + }), + Cis2Event::TokenMetadata(TokenMetadataEvent { + token_id: TokenIdU8(2), + metadata_url: MetadataUrl { + url: "https://some.example/token/02".to_string(), + hash: None, + }, + }), + Cis2Event::Mint(MintEvent { + token_id: TokenIdU8(42), + amount: TokenAmountU64(1), + owner: ALICE_ADDR, + }), + Cis2Event::TokenMetadata(TokenMetadataEvent { + token_id: TokenIdU8(42), + metadata_url: MetadataUrl { + url: "https://some.example/token/2A".to_string(), + hash: None, + }, + }), + ]); +} + +/// Test regular transfer where sender is the owner. +#[test] +fn test_account_transfer() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(0); + + // Transfer one token from Alice to Bob. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_0, + amount: TokenAmountU64(1), + data: AdditionalData::empty(), + }]); + + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi_royalties.transfer".to_string(), + ), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect("Transfer tokens"); + + // Check that Bob has 1 `TOKEN_0` and Alice has 399. Also check that Alice still + // has 1 `TOKEN_1`. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi_royalties.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + let rv: ViewState = invoke.parse_return_value().expect("ViewState return value"); + assert_eq!(rv.state, vec![ + (ALICE_ADDR, ViewAddressState { + balances: vec![(TOKEN_0, 399.into()), (TOKEN_1, 1.into())], + operators: Vec::new(), + }), + (BOB_ADDR, ViewAddressState { + balances: vec![(TOKEN_0, 1.into())], + operators: Vec::new(), + }), + ]); + + // Check that the events are logged. + let events = update + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect::>>(); + + // Check that two events are produced, since royalties are enabled. + // One, which transfers 0% of the cost to the royalty receiver (i.e., the + // original minter, which is Alice). And another, which transfers the + // remaininng 100% to the receiver (i.e., Bob). + assert_eq!(events, [ + Cis2Event::Transfer(TransferEvent { + token_id: TOKEN_0, + amount: TokenAmountU64(0), // Royalties paid to Alice. + from: ALICE_ADDR, + to: ALICE_ADDR, + }), + Cis2Event::Transfer(TransferEvent { + token_id: TOKEN_0, + amount: TokenAmountU64(1), // Transfer made to Bob. + from: ALICE_ADDR, + to: BOB_ADDR, + }), + ]); +} + +/// Test that you can add an operator. +/// Initialize the contract with two tokens owned by Alice. +/// Then add Bob as an operator for Alice. +#[test] +fn test_add_operator() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(0); + + // Add Bob as an operator for Alice. + let params = UpdateOperatorParams(vec![UpdateOperator { + update: OperatorUpdate::Add, + operator: BOB_ADDR, + }]); + + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi_royalties.updateOperator".to_string(), + ), + address: contract_address, + message: OwnedParameter::from_serial(¶ms).expect("UpdateOperator params"), + }) + .expect("Update operator"); + + // Check that an operator event occurred. + let events = update + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect::>>(); + assert_eq!(events, [Cis2Event::UpdateOperator(UpdateOperatorEvent { + operator: BOB_ADDR, + owner: ALICE_ADDR, + update: OperatorUpdate::Add, + }),]); + + // Construct a query parameter to check whether Bob is an operator for Alice. + let query_params = OperatorOfQueryParams { + queries: vec![OperatorOfQuery { + owner: ALICE_ADDR, + address: BOB_ADDR, + }], + }; + + // Invoke the operatorOf view entrypoint and check that Bob is an operator for + // Alice. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi_royalties.operatorOf".to_string(), + ), + address: contract_address, + message: OwnedParameter::from_serial(&query_params).expect("OperatorOf params"), + }) + .expect("Invoke view"); + + let rv: OperatorOfQueryResponse = invoke.parse_return_value().expect("OperatorOf return value"); + assert_eq!(rv, OperatorOfQueryResponse(vec![true])); +} + +/// Test that a transfer fails when the sender is neither an operator or the +/// owner. In particular, Bob will attempt to transfer some of Alice's tokens to +/// himself. +#[test] +fn test_unauthorized_sender() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(0); + + // Construct a transfer of `TOKEN_0` from Alice to Bob, which will be submitted + // by Bob. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_0, + amount: TokenAmountU64(1), + data: AdditionalData::empty(), + }]); + + // Notice that Bob is the sender/invoker. + let update = chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi_royalties.transfer".to_string(), + ), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect_err("Transfer tokens"); + + // Check that the correct error is returned. + let rv: ContractError = update.parse_return_value().expect("ContractError return value"); + assert_eq!(rv, ContractError::Unauthorized); +} + +/// Test that an operator can make a transfer. +#[test] +fn test_operator_can_transfer() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(0); + + // Add Bob as an operator for Alice. + let params = UpdateOperatorParams(vec![UpdateOperator { + update: OperatorUpdate::Add, + operator: BOB_ADDR, + }]); + chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi_royalties.updateOperator".to_string(), + ), + address: contract_address, + message: OwnedParameter::from_serial(¶ms).expect("UpdateOperator params"), + }) + .expect("Update operator"); + + // Let Bob make a transfer to himself on behalf of Alice. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_0, + amount: TokenAmountU64(1), + data: AdditionalData::empty(), + }]); + + chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi_royalties.transfer".to_string(), + ), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect("Transfer tokens"); + + // Check that Bob now has 1 of `TOKEN_0` and Alice has 399. Also check that + // Alice still has 1 `TOKEN_1`. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi_royalties.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + let rv: ViewState = invoke.parse_return_value().expect("ViewState return value"); + assert_eq!(rv.state, vec![ + (ALICE_ADDR, ViewAddressState { + balances: vec![(TOKEN_0, 399.into()), (TOKEN_1, 1.into())], + operators: vec![BOB_ADDR], + }), + (BOB_ADDR, ViewAddressState { + balances: vec![(TOKEN_0, 1.into())], + operators: Vec::new(), + }), + ]); +} + +/// Test that royalties are paid out correctly. +/// Set the royalty percentage to 50%. +#[test] +fn test_royalty_payment() { + // Set the royalty percentage to 50%. + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(50); + + // Transfer 100 `TOKEN_0` from Alice to Bob. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_0, + amount: TokenAmountU64(100), + data: AdditionalData::empty(), + }]); + + let update_1 = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi_royalties.transfer".to_string(), + ), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect("Transfer tokens"); + + // Check that Alice now has 350 tokens and Bob has 50, since 50 of them were + // paid in royalties to Alice. + let invoke_1 = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi_royalties.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + let rv: ViewState = invoke_1.parse_return_value().expect("ViewState return value"); + assert_eq!(rv.state, vec![ + (ALICE_ADDR, ViewAddressState { + balances: vec![(TOKEN_0, 350.into()), (TOKEN_1, 1.into())], + operators: Vec::new(), + }), + (BOB_ADDR, ViewAddressState { + balances: vec![(TOKEN_0, 50.into())], + operators: Vec::new(), + }), + ]); + + // Check that two transfer events were logged. + let events = update_1 + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect::>>(); + assert_eq!(events, [ + Cis2Event::Transfer(TransferEvent { + token_id: TOKEN_0, + amount: TokenAmountU64(50), // Royalties paid to Alice. + from: ALICE_ADDR, + to: ALICE_ADDR, + }), + Cis2Event::Transfer(TransferEvent { + token_id: TOKEN_0, + amount: TokenAmountU64(50), // Transfer made to Bob. + from: ALICE_ADDR, + to: BOB_ADDR, + }), + ]); + + // Transfer 50 `TOKEN_0` from Bob to Charlie. + let transfer_params_2 = TransferParams::from(vec![concordium_cis2::Transfer { + from: BOB_ADDR, + to: Receiver::Account(CHARLIE), + token_id: TOKEN_0, + amount: TokenAmountU64(50), + data: AdditionalData::empty(), + }]); + let update_2 = chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi_royalties.transfer".to_string(), + ), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params_2).expect("Transfer params"), + }) + .expect("Transfer tokens"); + + // Check that Bob now has 0 tokens and Charlie has 25, since 25 of them were + // paid in royalties to Alice, who now has 375. + let invoke_2 = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi_royalties.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + let rv: ViewState = invoke_2.parse_return_value().expect("ViewState return value"); + assert_eq!(rv.state, vec![ + (ALICE_ADDR, ViewAddressState { + balances: vec![(TOKEN_0, 375.into()), (TOKEN_1, 1.into())], + operators: Vec::new(), + }), + (BOB_ADDR, ViewAddressState { + balances: vec![(TOKEN_0, 0.into())], + operators: Vec::new(), + }), + (CHARLIE_ADDR, ViewAddressState { + balances: vec![(TOKEN_0, 25.into())], + operators: Vec::new(), + }), + ]); + + // Check that two transfer events were logged. + let events = update_2 + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect::>>(); + assert_eq!(events, [ + Cis2Event::Transfer(TransferEvent { + token_id: TOKEN_0, + amount: TokenAmountU64(25), // Royalties paid to Alice. + from: BOB_ADDR, + to: ALICE_ADDR, + }), + Cis2Event::Transfer(TransferEvent { + token_id: TOKEN_0, + amount: TokenAmountU64(25), // Transfer made to Charlie. + from: BOB_ADDR, + to: CHARLIE_ADDR, + }), + ]); +} + +/// Helper function that sets up the contract with two types of tokens minted to +/// Alice. She has 400 of `TOKEN_0` and 1 of `TOKEN_1`. +fn initialize_contract_with_alice_tokens( + royalty_percentage: u8, +) -> (Chain, ContractAddress, ContractInvokeSuccess) { + let (mut chain, contract_address) = initialize_chain_and_contract(); + + let mint_params = MintParams { + owner: ALICE_ADDR, + tokens: BTreeMap::from_iter(vec![(TOKEN_0, 400.into()), (TOKEN_1, 1.into())]), + royalty_percentage, + }; + + // Mint two tokens to Alice. + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi_royalties.mint".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&mint_params).expect("Mint params"), + }) + .expect("Mint tokens"); + + (chain, contract_address, update) +} + +/// Setup chain and contract. +/// +/// Also creates the three accounts, Alice, Bob, and Charlie and initializes the +/// contract. +/// +/// Alice is the owner of the contract. +fn initialize_chain_and_contract() -> (Chain, ContractAddress) { + let mut chain = Chain::new(); + + // Create some accounts on the chain. + chain.create_account(Account::new(ALICE, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(CHARLIE, ACC_INITIAL_BALANCE)); + + // Load and deploy the module. + let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); + let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Deploy valid module"); + + let parameter = InitParams { + pay_royalty: true, + }; + + // Initialize the auction contract. + let init = chain + .contract_init(SIGNER, ALICE, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_cis2_multi_royalties".to_string()), + param: OwnedParameter::from_serial(¶meter).expect("Init params"), + }) + .expect("Initialize contract"); + + (chain, init.contract_address) +} diff --git a/examples/cis2-multi/Cargo.toml b/examples/cis2-multi/Cargo.toml index 8b47f9c5..789c13af 100644 --- a/examples/cis2-multi/Cargo.toml +++ b/examples/cis2-multi/Cargo.toml @@ -14,6 +14,9 @@ wee_alloc = ["concordium-std/wee_alloc"] concordium-std = {path = "../../concordium-std", default-features = false} concordium-cis2 = {path = "../../concordium-cis2", default-features = false} +[dev-dependencies] +concordium-smart-contract-testing = {path = "../../contract-testing"} + [lib] crate-type=["cdylib", "rlib"] diff --git a/examples/cis2-multi/src/lib.rs b/examples/cis2-multi/src/lib.rs index 4aa30bf4..ecd98c30 100644 --- a/examples/cis2-multi/src/lib.rs +++ b/examples/cis2-multi/src/lib.rs @@ -38,32 +38,32 @@ const SUPPORTS_STANDARDS: [StandardIdentifier<'static>; 2] = /// Contract token ID type. /// To save bytes we use a small token ID type, but is limited to be represented /// by a `u8`. -type ContractTokenId = TokenIdU8; +pub type ContractTokenId = TokenIdU8; /// Contract token amount type. -type ContractTokenAmount = TokenAmountU64; +pub type ContractTokenAmount = TokenAmountU64; -#[derive(Serial, Deserial, SchemaType)] -struct MintParam { - token_amount: ContractTokenAmount, - metadata_url: MetadataUrl, +#[derive(Serialize, SchemaType)] +pub struct MintParam { + pub token_amount: ContractTokenAmount, + pub metadata_url: MetadataUrl, } /// The parameter for the contract function `mint` which mints a number of /// token types and/or amounts of tokens to a given address. -#[derive(Serial, Deserial, SchemaType)] -struct MintParams { +#[derive(Serialize, SchemaType)] +pub struct MintParams { /// Owner of the newly minted tokens. - owner: Address, + pub owner: Address, /// A collection of tokens to mint. - tokens: collections::BTreeMap, + pub tokens: collections::BTreeMap, } /// The parameter type for the contract function `setImplementors`. /// Takes a standard identifier and a list of contract addresses providing /// implementations of this standard. #[derive(Debug, Serialize, SchemaType)] -struct SetImplementorsParams { +pub struct SetImplementorsParams { /// The identifier for the standard. id: StandardIdentifierOwned, /// The addresses of the implementors of the standard. @@ -73,15 +73,15 @@ struct SetImplementorsParams { /// The state for each address. #[derive(Serial, DeserialWithState, Deletable)] #[concordium(state_parameter = "S")] -struct AddressState { +struct AddressState { /// The amount of tokens owned by this address. balances: StateMap, /// The address which are currently enabled as operators for this address. operators: StateSet, } -impl AddressState { - fn empty(state_builder: &mut StateBuilder) -> Self { +impl AddressState { + fn empty(state_builder: &mut StateBuilder) -> Self { AddressState { balances: state_builder.new_map(), operators: state_builder.new_set(), @@ -95,7 +95,7 @@ impl AddressState { /// and this could be structured in a more space efficient way. #[derive(Serial, DeserialWithState)] #[concordium(state_parameter = "S")] -struct State { +struct State { /// The state of addresses. state: StateMap, S>, /// All of the token IDs @@ -107,7 +107,7 @@ struct State { /// The different errors the contract can produce. #[derive(Serialize, Debug, PartialEq, Eq, Reject, SchemaType)] -enum CustomContractError { +pub enum CustomContractError { /// Failed parsing the parameter. #[from(ParseError)] ParseParams, @@ -123,9 +123,9 @@ enum CustomContractError { InvokeContractError, } -type ContractError = Cis2Error; +pub type ContractError = Cis2Error; -type ContractResult = Result; +pub type ContractResult = Result; /// Mapping the logging errors to ContractError. impl From for CustomContractError { @@ -155,9 +155,9 @@ impl From for CustomContractError { fn from(_: NewContractNameError) -> Self { Self::InvalidContractName } } -impl State { +impl State { /// Construct a state with no tokens - fn empty(state_builder: &mut StateBuilder) -> Self { + fn empty(state_builder: &mut StateBuilder) -> Self { State { state: state_builder.new_map(), tokens: state_builder.new_map(), @@ -171,7 +171,7 @@ impl State { token_id: &ContractTokenId, mint_param: &MintParam, owner: &Address, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) { self.tokens.insert(*token_id, mint_param.metadata_url.to_owned()); let mut owner_state = @@ -221,7 +221,7 @@ impl State { amount: ContractTokenAmount, from: &Address, to: &Address, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) -> ContractResult<()> { ensure!(self.contains_token(token_id), ContractError::InvalidTokenId); // A zero transfer does not modify the state. @@ -258,7 +258,7 @@ impl State { &mut self, owner: &Address, operator: &Address, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) { let mut owner_state = self.state.entry(*owner).or_insert_with(|| AddressState::empty(state_builder)); @@ -296,34 +296,28 @@ impl State { /// Initialize contract instance with a no token types. #[init(contract = "cis2_multi", event = "Cis2Event")] -fn contract_init( - _ctx: &impl HasInitContext, - state_builder: &mut StateBuilder, -) -> InitResult> { +fn contract_init(_ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { // Construct the initial contract state. Ok(State::empty(state_builder)) } -#[derive(Serialize, SchemaType)] -struct ViewAddressState { - balances: Vec<(ContractTokenId, ContractTokenAmount)>, - operators: Vec
, +#[derive(Serialize, SchemaType, PartialEq, Eq, Debug)] +pub struct ViewAddressState { + pub balances: Vec<(ContractTokenId, ContractTokenAmount)>, + pub operators: Vec
, } -#[derive(Serialize, SchemaType)] -struct ViewState { - state: Vec<(Address, ViewAddressState)>, - tokens: Vec, +#[derive(Serialize, SchemaType, PartialEq, Eq)] +pub struct ViewState { + pub state: Vec<(Address, ViewAddressState)>, + pub tokens: Vec, } /// View function for testing. This reports on the entire state of the contract /// for testing purposes. In a realistic example there `balance_of` and similar /// functions with a smaller response. #[receive(contract = "cis2_multi", name = "view", return_value = "ViewState")] -fn contract_view( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ReceiveResult { +fn contract_view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { let state = host.state(); let mut inner_state = Vec::new(); @@ -376,9 +370,9 @@ fn contract_view( enable_logger, mutable )] -fn contract_mint( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_mint( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Get the contract owner @@ -436,9 +430,9 @@ type TransferParameter = TransferParams; enable_logger, mutable )] -fn contract_transfer( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_transfer( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Parse the parameter. @@ -502,9 +496,9 @@ fn contract_transfer( enable_logger, mutable )] -fn contract_update_operator( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_update_operator( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Parse the parameter. @@ -552,9 +546,9 @@ type ContractBalanceOfQueryResponse = BalanceOfQueryResponse( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_balance_of( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: ContractBalanceOfQueryParams = ctx.parameter_cursor().get()?; @@ -581,9 +575,9 @@ fn contract_balance_of( return_value = "OperatorOfQueryResponse", error = "ContractError" )] -fn contract_operator_of( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_operator_of( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: OperatorOfQueryParams = ctx.parameter_cursor().get()?; @@ -614,9 +608,9 @@ type ContractTokenMetadataQueryParams = TokenMetadataQueryParams( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_token_metadata( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: ContractTokenMetadataQueryParams = ctx.parameter_cursor().get()?; @@ -654,10 +648,7 @@ fn contract_token_metadata( /// - Contract name part of the parameter is invalid. /// - Calling back `transfer` to sender contract rejects. #[receive(contract = "cis2_multi", name = "onReceivingCIS2", error = "ContractError")] -fn contract_on_cis2_received( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ContractResult<()> { +fn contract_on_cis2_received(ctx: &ReceiveContext, host: &Host) -> ContractResult<()> { // Ensure the sender is a contract. let sender = if let Address::Contract(contract) = ctx.sender() { contract @@ -702,9 +693,9 @@ fn contract_on_cis2_received( return_value = "SupportsQueryResponse", error = "ContractError" )] -fn contract_supports( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_supports( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: SupportsQueryParams = ctx.parameter_cursor().get()?; @@ -735,10 +726,7 @@ fn contract_supports( error = "ContractError", mutable )] -fn contract_set_implementor( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, -) -> ContractResult<()> { +fn contract_set_implementor(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<()> { // Authorize the sender. ensure!(ctx.sender().matches_account(&ctx.owner()), ContractError::Unauthorized); // Parse the parameter. @@ -747,386 +735,3 @@ fn contract_set_implementor( host.state_mut().set_implementors(params.id, params.implementors); Ok(()) } - -// Tests - -#[concordium_cfg_test] -mod tests { - use super::*; - use test_infrastructure::*; - - const ACCOUNT_0: AccountAddress = AccountAddress([0u8; 32]); - const ADDRESS_0: Address = Address::Account(ACCOUNT_0); - const ACCOUNT_1: AccountAddress = AccountAddress([1u8; 32]); - const ADDRESS_1: Address = Address::Account(ACCOUNT_1); - const TOKEN_0: ContractTokenId = TokenIdU8(2); - const TOKEN_1: ContractTokenId = TokenIdU8(42); - - /// Test helper function which creates a contract state with two tokens with - /// id `TOKEN_0` and id `TOKEN_1` owned by `ADDRESS_0` - fn initial_state(state_builder: &mut StateBuilder) -> State { - let mut state = State::empty(state_builder); - state.mint( - &TOKEN_0, - &MintParam { - token_amount: 400.into(), - metadata_url: MetadataUrl { - url: format!("https://some.example/token/{TOKEN_0}"), - hash: None, - }, - }, - &ADDRESS_0, - state_builder, - ); - state.mint( - &TOKEN_1, - &MintParam { - token_amount: 1.into(), - metadata_url: MetadataUrl { - url: format!("https://some.example/token/{TOKEN_1}"), - hash: None, - }, - }, - &ADDRESS_0, - state_builder, - ); - state - } - - /// Test initialization succeeds with a state with no tokens. - #[concordium_test] - fn test_init() { - // Setup the context - let ctx = TestInitContext::empty(); - let mut builder = TestStateBuilder::new(); - - // Call the contract function. - let result = contract_init(&ctx, &mut builder); - - // Check the result - let state = result.expect_report("Contract initialization failed"); - - // Check the state - claim_eq!(state.tokens.iter().count(), 0, "Only one token is initialized"); - } - - /// Test minting succeeds and the tokens are owned by the given address and - /// the appropriate events are logged. - #[concordium_test] - fn test_mint() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - ctx.set_owner(ACCOUNT_0); - - // and parameter. - let mut tokens = collections::BTreeMap::new(); - tokens.insert(TOKEN_0, MintParam { - token_amount: 400.into(), - metadata_url: MetadataUrl { - url: "https://some.example/token/02".to_string(), - hash: None, - }, - }); - tokens.insert(TOKEN_1, MintParam { - token_amount: 1.into(), - metadata_url: MetadataUrl { - url: "https://some.example/token/2A".to_string(), - hash: None, - }, - }); - let parameter = MintParams { - owner: ADDRESS_0, - tokens, - }; - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = State::empty(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_mint(&ctx, &mut host, &mut logger); - - // Check the result - claim!(result.is_ok(), "Results in rejection"); - - // Check the state - claim_eq!(host.state().tokens.iter().count(), 2, "Only one token is initialized"); - let balance0 = - host.state().balance(&TOKEN_0, &ADDRESS_0).expect_report("Token is expected to exist"); - claim_eq!(balance0, 400.into(), "Initial tokens are owned by the contract instantiater"); - - let balance1 = - host.state().balance(&TOKEN_1, &ADDRESS_0).expect_report("Token is expected to exist"); - claim_eq!(balance1, 1.into(), "Initial tokens are owned by the contract instantiater"); - - // Check the logs - claim_eq!(logger.logs.len(), 4, "Exactly four events should be logged"); - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::Mint(MintEvent { - owner: ADDRESS_0, - token_id: TOKEN_0, - amount: ContractTokenAmount::from(400), - }))), - "Expected an event for minting TOKEN_0" - ); - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::Mint(MintEvent { - owner: ADDRESS_0, - token_id: TOKEN_1, - amount: ContractTokenAmount::from(1), - }))), - "Expected an event for minting TOKEN_1" - ); - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::TokenMetadata::<_, ContractTokenAmount>( - TokenMetadataEvent { - token_id: TOKEN_0, - metadata_url: MetadataUrl { - url: "https://some.example/token/02".to_string(), - hash: None, - }, - } - ))), - "Expected an event for token metadata for TOKEN_0" - ); - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::TokenMetadata::<_, ContractTokenAmount>( - TokenMetadataEvent { - token_id: TOKEN_1, - metadata_url: MetadataUrl { - url: "https://some.example/token/2A".to_string(), - hash: None, - }, - } - ))), - "Expected an event for token metadata for TOKEN_1" - ); - } - - /// Test transfer succeeds, when `from` is the sender. - #[concordium_test] - fn test_transfer_account() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - - // and parameter. - let transfer = Transfer { - token_id: TOKEN_0, - amount: ContractTokenAmount::from(100), - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let balance0 = - host.state().balance(&TOKEN_0, &ADDRESS_0).expect_report("Token is expected to exist"); - let balance1 = - host.state().balance(&TOKEN_0, &ADDRESS_1).expect_report("Token is expected to exist"); - claim_eq!( - balance0, - 300.into(), - "Token owner balance should be decreased by the transferred amount." - ); - claim_eq!( - balance1, - 100.into(), - "Token receiver balance should be increased by the transferred amount" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "Only one event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::Transfer(TransferEvent { - from: ADDRESS_0, - to: ADDRESS_1, - token_id: TOKEN_0, - amount: ContractTokenAmount::from(100), - })), - "Incorrect event emitted" - ) - } - - /// Test transfer token fails, when sender is neither the owner or an - /// operator of the owner. - #[concordium_test] - fn test_transfer_not_authorized() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_1); - - // and parameter. - let transfer = Transfer { - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - token_id: TOKEN_0, - amount: ContractTokenAmount::from(100), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - // Check the result. - let err = result.expect_err_report("Expected to fail"); - claim_eq!(err, ContractError::Unauthorized, "Error is expected to be Unauthorized") - } - - /// Test transfer succeeds when sender is not the owner, but is an operator - /// of the owner. - #[concordium_test] - fn test_operator_transfer() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_1); - - // and parameter. - let transfer = Transfer { - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - token_id: TOKEN_0, - amount: ContractTokenAmount::from(100), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let mut state = initial_state(&mut state_builder); - state.add_operator(&ADDRESS_0, &ADDRESS_1, &mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let balance0 = - host.state().balance(&TOKEN_0, &ADDRESS_0).expect_report("Token is expected to exist"); - let balance1 = - host.state().balance(&TOKEN_0, &ADDRESS_1).expect_report("Token is expected to exist"); - claim_eq!( - balance0, - 300.into(), - "Token owner balance should be decreased by the transferred amount" - ); - claim_eq!( - balance1, - 100.into(), - "Token receiver balance should be increased by the transferred amount" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "Only one event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::Transfer(TransferEvent { - from: ADDRESS_0, - to: ADDRESS_1, - token_id: TOKEN_0, - amount: ContractTokenAmount::from(100), - })), - "Incorrect event emitted" - ) - } - - /// Test adding an operator succeeds and the appropriate event is logged. - #[concordium_test] - fn test_add_operator() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - - // and parameter. - let update = UpdateOperator { - operator: ADDRESS_1, - update: OperatorUpdate::Add, - }; - let parameter = UpdateOperatorParams(vec![update]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_update_operator(&ctx, &mut host, &mut logger); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let is_operator = host.state().is_operator(&ADDRESS_1, &ADDRESS_0); - claim!(is_operator, "Account should be an operator"); - - // Checking that `ADDRESS_1` is an operator in the query response of the - // `contract_operator_of` function as well. - // Setup parameter. - let operator_of_query = OperatorOfQuery { - address: ADDRESS_1, - owner: ADDRESS_0, - }; - - let operator_of_query_vector = OperatorOfQueryParams { - queries: vec![operator_of_query], - }; - let parameter_bytes = to_bytes(&operator_of_query_vector); - - ctx.set_parameter(¶meter_bytes); - - // Checking the return value of the `contract_operator_of` function - let result: ContractResult = contract_operator_of(&ctx, &host); - - claim_eq!( - result.expect_report("Failed getting result value").0, - [true], - "Account should be an operator in the query response" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "One event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::::UpdateOperator( - UpdateOperatorEvent { - owner: ADDRESS_0, - operator: ADDRESS_1, - update: OperatorUpdate::Add, - } - )), - "Incorrect event emitted" - ) - } -} diff --git a/examples/cis2-multi/tests/tests.rs b/examples/cis2-multi/tests/tests.rs new file mode 100644 index 00000000..b1a8f918 --- /dev/null +++ b/examples/cis2-multi/tests/tests.rs @@ -0,0 +1,353 @@ +//! Tests for the `cis2_multi` contract. +use cis2_multi::*; +use concordium_cis2::*; +use concordium_smart_contract_testing::*; +use concordium_std::collections::BTreeMap; + +/// The tests accounts. +const ALICE: AccountAddress = AccountAddress([0; 32]); +const ALICE_ADDR: Address = Address::Account(ALICE); +const BOB: AccountAddress = AccountAddress([1; 32]); +const BOB_ADDR: Address = Address::Account(BOB); + +/// Token IDs. +const TOKEN_0: ContractTokenId = TokenIdU8(2); +const TOKEN_1: ContractTokenId = TokenIdU8(42); + +/// Initial balance of the accounts. +const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10000); + +/// A signer for all the transactions. +const SIGNER: Signer = Signer::with_one_key(); + +/// Test minting succeeds and the tokens are owned by the given address and +/// the appropriate events are logged. +#[test] +fn test_minting() { + let (chain, contract_address, update) = initialize_contract_with_alice_tokens(); + + // Invoke the view entrypoint and check that the tokens are owned by Alice. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + + // Check that the tokens are owned by Alice. + let rv: ViewState = invoke.parse_return_value().expect("ViewState return value"); + assert_eq!(rv.tokens[..], [TOKEN_0, TOKEN_1]); + assert_eq!(rv.state, vec![(ALICE_ADDR, ViewAddressState { + balances: vec![(TOKEN_0, 400.into()), (TOKEN_1, 1.into())], + operators: Vec::new(), + })]); + + // Check that the events are logged. + let events = update.events().flat_map(|(_addr, events)| events); + + let events: Vec> = + events.map(|e| e.parse().expect("Deserialize event")).collect(); + + assert_eq!(events, [ + Cis2Event::Mint(MintEvent { + token_id: TokenIdU8(2), + amount: TokenAmountU64(400), + owner: ALICE_ADDR, + }), + Cis2Event::TokenMetadata(TokenMetadataEvent { + token_id: TokenIdU8(2), + metadata_url: MetadataUrl { + url: "https://some.example/token/02".to_string(), + hash: None, + }, + }), + Cis2Event::Mint(MintEvent { + token_id: TokenIdU8(42), + amount: TokenAmountU64(1), + owner: ALICE_ADDR, + }), + Cis2Event::TokenMetadata(TokenMetadataEvent { + token_id: TokenIdU8(42), + metadata_url: MetadataUrl { + url: "https://some.example/token/2A".to_string(), + hash: None, + }, + }), + ]); +} + +/// Test regular transfer where sender is the owner. +#[test] +fn test_account_transfer() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Transfer one token from Alice to Bob. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_0, + amount: TokenAmountU64(1), + data: AdditionalData::empty(), + }]); + + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect("Transfer tokens"); + + // Check that Bob has 1 `TOKEN_0` and Alice has 399. Also check that Alice still + // has 1 `TOKEN_1`. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + let rv: ViewState = invoke.parse_return_value().expect("ViewState return value"); + assert_eq!(rv.state, vec![ + (ALICE_ADDR, ViewAddressState { + balances: vec![(TOKEN_0, 399.into()), (TOKEN_1, 1.into())], + operators: Vec::new(), + }), + (BOB_ADDR, ViewAddressState { + balances: vec![(TOKEN_0, 1.into())], + operators: Vec::new(), + }), + ]); + + // Check that the events are logged. + let events = update + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect::>>(); + + assert_eq!(events, [Cis2Event::Transfer(TransferEvent { + token_id: TOKEN_0, + amount: TokenAmountU64(1), + from: ALICE_ADDR, + to: BOB_ADDR, + }),]); +} + +/// Test that you can add an operator. +/// Initialize the contract with two tokens owned by Alice. +/// Then add Bob as an operator for Alice. +#[test] +fn test_add_operator() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Add Bob as an operator for Alice. + let params = UpdateOperatorParams(vec![UpdateOperator { + update: OperatorUpdate::Add, + operator: BOB_ADDR, + }]); + + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.updateOperator".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶ms).expect("UpdateOperator params"), + }) + .expect("Update operator"); + + // Check that an operator event occurred. + let events = update + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect::>>(); + assert_eq!(events, [Cis2Event::UpdateOperator(UpdateOperatorEvent { + operator: BOB_ADDR, + owner: ALICE_ADDR, + update: OperatorUpdate::Add, + }),]); + + // Construct a query parameter to check whether Bob is an operator for Alice. + let query_params = OperatorOfQueryParams { + queries: vec![OperatorOfQuery { + owner: ALICE_ADDR, + address: BOB_ADDR, + }], + }; + + // Invoke the operatorOf view entrypoint and check that Bob is an operator for + // Alice. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.operatorOf".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&query_params).expect("OperatorOf params"), + }) + .expect("Invoke view"); + + let rv: OperatorOfQueryResponse = invoke.parse_return_value().expect("OperatorOf return value"); + assert_eq!(rv, OperatorOfQueryResponse(vec![true])); +} + +/// Test that a transfer fails when the sender is neither an operator or the +/// owner. In particular, Bob will attempt to transfer some of Alice's tokens to +/// himself. +#[test] +fn test_unauthorized_sender() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Construct a transfer of `TOKEN_0` from Alice to Bob, which will be submitted + // by Bob. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_0, + amount: TokenAmountU64(1), + data: AdditionalData::empty(), + }]); + + // Notice that Bob is the sender/invoker. + let update = chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect_err("Transfer tokens"); + + // Check that the correct error is returned. + let rv: ContractError = update.parse_return_value().expect("ContractError return value"); + assert_eq!(rv, ContractError::Unauthorized); +} + +/// Test that an operator can make a transfer. +#[test] +fn test_operator_can_transfer() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Add Bob as an operator for Alice. + let params = UpdateOperatorParams(vec![UpdateOperator { + update: OperatorUpdate::Add, + operator: BOB_ADDR, + }]); + chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.updateOperator".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶ms).expect("UpdateOperator params"), + }) + .expect("Update operator"); + + // Let Bob make a transfer to himself on behalf of Alice. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_0, + amount: TokenAmountU64(1), + data: AdditionalData::empty(), + }]); + + chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect("Transfer tokens"); + + // Check that Bob now has 1 of `TOKEN_0` and Alice has 399. Also check that + // Alice still has 1 `TOKEN_1`. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + let rv: ViewState = invoke.parse_return_value().expect("ViewState return value"); + assert_eq!(rv.state, vec![ + (ALICE_ADDR, ViewAddressState { + balances: vec![(TOKEN_0, 399.into()), (TOKEN_1, 1.into())], + operators: vec![BOB_ADDR], + }), + (BOB_ADDR, ViewAddressState { + balances: vec![(TOKEN_0, 1.into())], + operators: Vec::new(), + }), + ]); +} + +/// Helper function that sets up the contract with two types of tokens minted to +/// Alice. She has 400 of `TOKEN_0` and 1 of `TOKEN_1`. +fn initialize_contract_with_alice_tokens() -> (Chain, ContractAddress, ContractInvokeSuccess) { + let (mut chain, contract_address) = initialize_chain_and_contract(); + + let mint_params = MintParams { + owner: ALICE_ADDR, + tokens: BTreeMap::from_iter(vec![ + (TOKEN_0, MintParam { + token_amount: 400.into(), + metadata_url: MetadataUrl { + url: "https://some.example/token/02".to_string(), + hash: None, + }, + }), + (TOKEN_1, MintParam { + token_amount: 1.into(), + metadata_url: MetadataUrl { + url: "https://some.example/token/2A".to_string(), + hash: None, + }, + }), + ]), + }; + + // Mint two tokens for which Alice is the owner. + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.mint".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&mint_params).expect("Mint params"), + }) + .expect("Mint tokens"); + + (chain, contract_address, update) +} + +/// Setup chain and contract. +/// +/// Also creates the two accounts, Alice and Bob. +/// +/// Alice is the owner of the contract. +fn initialize_chain_and_contract() -> (Chain, ContractAddress) { + let mut chain = Chain::new(); + + // Create some accounts accounts on the chain. + chain.create_account(Account::new(ALICE, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); + + // Load and deploy the module. + let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); + let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Deploy valid module"); + + // Initialize the auction contract. + let init = chain + .contract_init(SIGNER, ALICE, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_cis2_multi".to_string()), + param: OwnedParameter::empty(), + }) + .expect("Initialize contract"); + + (chain, init.contract_address) +} diff --git a/examples/cis2-nft/Cargo.toml b/examples/cis2-nft/Cargo.toml index 551ff3d4..f0639b24 100644 --- a/examples/cis2-nft/Cargo.toml +++ b/examples/cis2-nft/Cargo.toml @@ -15,6 +15,9 @@ wee_alloc = ["concordium-std/wee_alloc"] concordium-std = {path = "../../concordium-std", default-features = false} concordium-cis2 = {path = "../../concordium-cis2", default-features = false} +[dev-dependencies] +concordium-smart-contract-testing = {path = "../../contract-testing"} + [lib] crate-type=["cdylib", "rlib"] diff --git a/examples/cis2-nft/src/lib.rs b/examples/cis2-nft/src/lib.rs index 0c3ff526..4b352ef3 100644 --- a/examples/cis2-nft/src/lib.rs +++ b/examples/cis2-nft/src/lib.rs @@ -18,6 +18,8 @@ //! address to another address. An address can enable and disable one or more //! addresses as operators. An operator of some address is allowed to transfer //! any tokens owned by this address. +//! +//! Tests are located in `./tests/tests.rs`. #![cfg_attr(not(feature = "std"), no_std)] @@ -26,46 +28,46 @@ use concordium_std::*; /// The baseurl for the token metadata, gets appended with the token ID as hex /// encoding before emitted in the TokenMetadata event. -const TOKEN_METADATA_BASE_URL: &str = "https://some.example/token/"; +pub const TOKEN_METADATA_BASE_URL: &str = "https://some.example/token/"; /// List of supported standards by this contract address. -const SUPPORTS_STANDARDS: [StandardIdentifier<'static>; 2] = +pub const SUPPORTS_STANDARDS: [StandardIdentifier<'static>; 2] = [CIS0_STANDARD_IDENTIFIER, CIS2_STANDARD_IDENTIFIER]; // Types /// Contract token ID type. /// To save bytes we use a token ID type limited to a `u32`. -type ContractTokenId = TokenIdU32; +pub type ContractTokenId = TokenIdU32; /// Contract token amount. /// Since the tokens are non-fungible the total supply of any token will be at /// most 1 and it is fine to use a small type for representing token amounts. -type ContractTokenAmount = TokenAmountU8; +pub type ContractTokenAmount = TokenAmountU8; /// The parameter for the contract function `mint` which mints a number of /// tokens to a given address. #[derive(Serial, Deserial, SchemaType)] -struct MintParams { +pub struct MintParams { /// Owner of the newly minted tokens. - owner: Address, + pub owner: Address, /// A collection of tokens to mint. #[concordium(size_length = 1)] - tokens: collections::BTreeSet, + pub tokens: collections::BTreeSet, } /// The state for each address. #[derive(Serial, DeserialWithState, Deletable)] #[concordium(state_parameter = "S")] -struct AddressState { +pub struct AddressState { /// The tokens owned by this address. - owned_tokens: StateSet, + pub owned_tokens: StateSet, /// The address which are currently enabled as operators for this address. - operators: StateSet, + pub operators: StateSet, } -impl AddressState { - fn empty(state_builder: &mut StateBuilder) -> Self { +impl AddressState { + fn empty(state_builder: &mut StateBuilder) -> Self { AddressState { owned_tokens: state_builder.new_set(), operators: state_builder.new_set(), @@ -78,30 +80,30 @@ impl AddressState { // and this could be structured in a more space efficient way depending on the use case. #[derive(Serial, DeserialWithState)] #[concordium(state_parameter = "S")] -struct State { +pub struct State { /// The state for each address. - state: StateMap, S>, + pub state: StateMap, S>, /// All of the token IDs - all_tokens: StateSet, + pub all_tokens: StateSet, /// Map with contract addresses providing implementations of additional /// standards. - implementors: StateMap, S>, + pub implementors: StateMap, S>, } /// The parameter type for the contract function `setImplementors`. /// Takes a standard identifier and list of contract addresses providing /// implementations of this standard. #[derive(Debug, Serialize, SchemaType)] -struct SetImplementorsParams { +pub struct SetImplementorsParams { /// The identifier for the standard. - id: StandardIdentifierOwned, + pub id: StandardIdentifierOwned, /// The addresses of the implementors of the standard. - implementors: Vec, + pub implementors: Vec, } /// The custom errors the contract can produce. #[derive(Serialize, Debug, PartialEq, Eq, Reject, SchemaType)] -enum CustomContractError { +pub enum CustomContractError { /// Failed parsing the parameter. #[from(ParseError)] ParseParams, @@ -117,9 +119,9 @@ enum CustomContractError { } /// Wrapping the custom errors in a type with CIS2 errors. -type ContractError = Cis2Error; +pub type ContractError = Cis2Error; -type ContractResult = Result; +pub type ContractResult = Result; /// Mapping the logging errors to CustomContractError. impl From for CustomContractError { @@ -142,9 +144,9 @@ impl From for ContractError { } // Functions for creating, updating and querying the contract state. -impl State { +impl State { /// Creates a new state with no tokens. - fn empty(state_builder: &mut StateBuilder) -> Self { + fn empty(state_builder: &mut StateBuilder) -> Self { State { state: state_builder.new_map(), all_tokens: state_builder.new_set(), @@ -157,7 +159,7 @@ impl State { &mut self, token: ContractTokenId, owner: &Address, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) -> ContractResult<()> { ensure!(self.all_tokens.insert(token), CustomContractError::TokenIdAlreadyExists.into()); @@ -208,7 +210,7 @@ impl State { amount: ContractTokenAmount, from: &Address, to: &Address, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) -> ContractResult<()> { ensure!(self.contains_token(token_id), ContractError::InvalidTokenId); // A zero transfer does not modify the state. @@ -243,7 +245,7 @@ impl State { &mut self, owner: &Address, operator: &Address, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) { let mut owner_state = self.state.entry(*owner).or_insert_with(|| AddressState::empty(state_builder)); @@ -289,33 +291,27 @@ fn build_token_metadata_url(token_id: &ContractTokenId) -> String { /// Initialize contract instance with no token types initially. #[init(contract = "cis2_nft", event = "Cis2Event")] -fn contract_init( - _ctx: &impl HasInitContext, - state_builder: &mut StateBuilder, -) -> InitResult> { +fn contract_init(_ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { // Construct the initial contract state. Ok(State::empty(state_builder)) } -#[derive(Serialize, SchemaType)] -struct ViewAddressState { - owned_tokens: Vec, - operators: Vec
, +#[derive(Serialize, SchemaType, PartialEq, Eq, Debug)] +pub struct ViewAddressState { + pub owned_tokens: Vec, + pub operators: Vec
, } -#[derive(Serialize, SchemaType)] -struct ViewState { - state: Vec<(Address, ViewAddressState)>, - all_tokens: Vec, +#[derive(Serialize, SchemaType, PartialEq, Eq, Debug)] +pub struct ViewState { + pub state: Vec<(Address, ViewAddressState)>, + pub all_tokens: Vec, } /// View function that returns the entire contents of the state. Meant for /// testing. #[receive(contract = "cis2_nft", name = "view", return_value = "ViewState")] -fn contract_view( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ReceiveResult { +fn contract_view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { let state = host.state(); let mut inner_state = Vec::new(); @@ -359,9 +355,9 @@ fn contract_view( enable_logger, mutable )] -fn contract_mint( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_mint( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Get the contract owner @@ -423,9 +419,9 @@ type TransferParameter = TransferParams; enable_logger, mutable )] -fn contract_transfer( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_transfer( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Parse the parameter. @@ -489,9 +485,9 @@ fn contract_transfer( enable_logger, mutable )] -fn contract_update_operator( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_update_operator( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Parse the parameter. @@ -531,9 +527,9 @@ fn contract_update_operator( return_value = "OperatorOfQueryResponse", error = "ContractError" )] -fn contract_operator_of( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_operator_of( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: OperatorOfQueryParams = ctx.parameter_cursor().get()?; @@ -567,9 +563,9 @@ type ContractBalanceOfQueryResponse = BalanceOfQueryResponse( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_balance_of( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: ContractBalanceOfQueryParams = ctx.parameter_cursor().get()?; @@ -600,9 +596,9 @@ type ContractTokenMetadataQueryParams = TokenMetadataQueryParams( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_token_metadata( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: ContractTokenMetadataQueryParams = ctx.parameter_cursor().get()?; @@ -634,9 +630,9 @@ fn contract_token_metadata( return_value = "SupportsQueryResponse", error = "ContractError" )] -fn contract_supports( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_supports( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: SupportsQueryParams = ctx.parameter_cursor().get()?; @@ -667,10 +663,7 @@ fn contract_supports( error = "ContractError", mutable )] -fn contract_set_implementor( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, -) -> ContractResult<()> { +fn contract_set_implementor(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<()> { // Authorize the sender. ensure!(ctx.sender().matches_account(&ctx.owner()), ContractError::Unauthorized); // Parse the parameter. @@ -679,373 +672,3 @@ fn contract_set_implementor( host.state_mut().set_implementors(params.id, params.implementors); Ok(()) } - -// Tests - -#[concordium_cfg_test] -mod tests { - use super::*; - use test_infrastructure::*; - - const ACCOUNT_0: AccountAddress = AccountAddress([0u8; 32]); - const ADDRESS_0: Address = Address::Account(ACCOUNT_0); - const ACCOUNT_1: AccountAddress = AccountAddress([1u8; 32]); - const ADDRESS_1: Address = Address::Account(ACCOUNT_1); - const TOKEN_0: ContractTokenId = TokenIdU32(0); - const TOKEN_1: ContractTokenId = TokenIdU32(42); - const TOKEN_2: ContractTokenId = TokenIdU32(43); - - /// Test helper function which creates a contract state with two tokens with - /// id `TOKEN_0` and id `TOKEN_1` owned by `ADDRESS_0` - fn initial_state(state_builder: &mut StateBuilder) -> State { - let mut state = State::empty(state_builder); - state.mint(TOKEN_0, &ADDRESS_0, state_builder).expect_report("Failed to mint TOKEN_0"); - state.mint(TOKEN_1, &ADDRESS_0, state_builder).expect_report("Failed to mint TOKEN_1"); - state - } - - /// Test initialization succeeds. - #[concordium_test] - fn test_init() { - // Setup the context - let ctx = TestInitContext::empty(); - let mut builder = TestStateBuilder::new(); - - // Call the contract function. - let result = contract_init(&ctx, &mut builder); - - // Check the result - let state = result.expect_report("Contract initialization failed"); - - // Check the state - // Note. This is rather expensive as an iterator is created and then traversed - - // should be avoided when writing smart contracts. - claim_eq!(state.all_tokens.iter().count(), 0, "No token should be initialized"); - } - - /// Test minting, ensuring the new tokens are owned by the given address and - /// the appropriate events are logged. - #[concordium_test] - fn test_mint() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - ctx.set_owner(ACCOUNT_0); - - // and parameter. - let mut tokens = collections::BTreeSet::new(); - tokens.insert(TOKEN_0); - tokens.insert(TOKEN_1); - tokens.insert(TOKEN_2); - let parameter = MintParams { - tokens, - owner: ADDRESS_0, - }; - - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = State::empty(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_mint(&ctx, &mut host, &mut logger); - - // Check the result - claim!(result.is_ok(), "Results in rejection"); - - // Check the state - // Note. This is rather expensive as an iterator is created and then traversed - - // should be avoided when writing smart contracts. - claim_eq!(host.state().all_tokens.iter().count(), 3, "Expected three tokens in the state."); - - let balance0 = - host.state().balance(&TOKEN_0, &ADDRESS_0).expect_report("Token is expected to exist"); - claim_eq!(balance0, 1.into(), "Tokens should be owned by the given address 0"); - - let balance1 = - host.state().balance(&TOKEN_1, &ADDRESS_0).expect_report("Token is expected to exist"); - claim_eq!(balance1, 1.into(), "Tokens should be owned by the given address 0"); - - let balance2 = - host.state().balance(&TOKEN_2, &ADDRESS_0).expect_report("Token is expected to exist"); - claim_eq!(balance2, 1.into(), "Tokens should be owned by the given address 0"); - - // Check the logs - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::Mint(MintEvent { - owner: ADDRESS_0, - token_id: TOKEN_0, - amount: ContractTokenAmount::from(1), - }))), - "Expected an event for minting TOKEN_0" - ); - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::Mint(MintEvent { - owner: ADDRESS_0, - token_id: TOKEN_1, - amount: ContractTokenAmount::from(1), - }))), - "Expected an event for minting TOKEN_1" - ); - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::TokenMetadata::<_, ContractTokenAmount>( - TokenMetadataEvent { - token_id: TOKEN_0, - metadata_url: MetadataUrl { - url: format!("{}00000000", TOKEN_METADATA_BASE_URL), - hash: None, - }, - } - ))), - "Expected an event for token metadata for TOKEN_0" - ); - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::TokenMetadata::<_, ContractTokenAmount>( - TokenMetadataEvent { - token_id: TOKEN_1, - metadata_url: MetadataUrl { - url: format!("{}2A000000", TOKEN_METADATA_BASE_URL), - hash: None, - }, - } - ))), - "Expected an event for token metadata for TOKEN_1" - ); - } - - /// Test transfer succeeds, when `from` is the sender. - #[concordium_test] - fn test_transfer_account() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - - // and parameter. - let transfer = Transfer { - token_id: TOKEN_0, - amount: ContractTokenAmount::from(1), - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let balance0 = - host.state().balance(&TOKEN_0, &ADDRESS_0).expect_report("Token is expected to exist"); - let balance1 = - host.state().balance(&TOKEN_0, &ADDRESS_1).expect_report("Token is expected to exist"); - let balance2 = - host.state().balance(&TOKEN_1, &ADDRESS_0).expect_report("Token is expected to exist"); - claim_eq!( - balance0, - 0.into(), - "Token owner balance should be decreased by the transferred amount" - ); - claim_eq!( - balance1, - 1.into(), - "Token receiver balance should be increased by the transferred amount" - ); - claim_eq!( - balance2, - 1.into(), - "Token receiver balance for token 1 should be the same as before" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "Only one event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::Transfer(TransferEvent { - from: ADDRESS_0, - to: ADDRESS_1, - token_id: TOKEN_0, - amount: ContractTokenAmount::from(1), - })), - "Incorrect event emitted" - ) - } - - /// Test transfer token fails, when sender is neither the owner or an - /// operator of the owner. - #[concordium_test] - fn test_transfer_not_authorized() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_1); - - // and parameter. - let transfer = Transfer { - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - token_id: TOKEN_0, - amount: ContractTokenAmount::from(1), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - // Check the result. - let err = result.expect_err_report("Expected to fail"); - claim_eq!(err, ContractError::Unauthorized, "Error is expected to be Unauthorized") - } - - /// Test transfer succeeds when sender is not the owner, but is an operator - /// of the owner. - #[concordium_test] - fn test_operator_transfer() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_1); - - // and parameter. - let transfer = Transfer { - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - token_id: TOKEN_0, - amount: ContractTokenAmount::from(1), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - - let mut state_builder = TestStateBuilder::new(); - let mut state = initial_state(&mut state_builder); - state.add_operator(&ADDRESS_0, &ADDRESS_1, &mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let balance0 = - host.state().balance(&TOKEN_0, &ADDRESS_0).expect_report("Token is expected to exist"); - let balance1 = host - .state_mut() - .balance(&TOKEN_0, &ADDRESS_1) - .expect_report("Token is expected to exist"); - claim_eq!( - balance0, - 0.into(), - "Token owner balance should be decreased by the transferred amount" - ); - claim_eq!( - balance1, - 1.into(), - "Token receiver balance should be increased by the transferred amount" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "Only one event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::Transfer(TransferEvent { - from: ADDRESS_0, - to: ADDRESS_1, - token_id: TOKEN_0, - amount: ContractTokenAmount::from(1), - })), - "Incorrect event emitted" - ) - } - - /// Test adding an operator succeeds and the appropriate event is logged. - #[concordium_test] - fn test_add_operator() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - - // and parameter. - let update = UpdateOperator { - update: OperatorUpdate::Add, - operator: ADDRESS_1, - }; - let parameter = UpdateOperatorParams(vec![update]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_update_operator(&ctx, &mut host, &mut logger); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let is_operator = host.state().is_operator(&ADDRESS_1, &ADDRESS_0); - claim!(is_operator, "Account should be an operator"); - - // Checking that `ADDRESS_1` is an operator in the query response of the - // `contract_operator_of` function as well. - // Setup parameter. - let operator_of_query = OperatorOfQuery { - address: ADDRESS_1, - owner: ADDRESS_0, - }; - - let operator_of_query_vector = OperatorOfQueryParams { - queries: vec![operator_of_query], - }; - let parameter_bytes = to_bytes(&operator_of_query_vector); - - ctx.set_parameter(¶meter_bytes); - - // Checking the return value of the `contract_operator_of` function - let result: ContractResult = contract_operator_of(&ctx, &host); - - claim_eq!( - result.expect_report("Failed getting result value").0, - [true], - "Account should be an operator in the query response" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "One event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::::UpdateOperator( - UpdateOperatorEvent { - owner: ADDRESS_0, - operator: ADDRESS_1, - update: OperatorUpdate::Add, - } - )), - "Incorrect event emitted" - ) - } -} diff --git a/examples/cis2-nft/tests/tests.rs b/examples/cis2-nft/tests/tests.rs new file mode 100644 index 00000000..78b68cd9 --- /dev/null +++ b/examples/cis2-nft/tests/tests.rs @@ -0,0 +1,336 @@ +//! Tests for the `cis2_nft` contract. +use cis2_nft::*; +use concordium_cis2::*; +use concordium_smart_contract_testing::*; +use concordium_std::collections::BTreeSet; + +/// The tests accounts. +const ALICE: AccountAddress = AccountAddress([0; 32]); +const ALICE_ADDR: Address = Address::Account(ALICE); +const BOB: AccountAddress = AccountAddress([1; 32]); +const BOB_ADDR: Address = Address::Account(BOB); + +/// Token IDs. +const TOKEN_0: ContractTokenId = TokenIdU32(2); +const TOKEN_1: ContractTokenId = TokenIdU32(42); + +/// Initial balance of the accounts. +const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10000); + +/// A signer for all the transactions. +const SIGNER: Signer = Signer::with_one_key(); + +/// Test minting succeeds and the tokens are owned by the given address and +/// the appropriate events are logged. +#[test] +fn test_minting() { + let (chain, contract_address, update) = initialize_contract_with_alice_tokens(); + + // Invoke the view entrypoint and check that the tokens are owned by Alice. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_nft.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + + // Check that the tokens are owned by Alice. + let rv: ViewState = invoke.parse_return_value().expect("ViewState return value"); + assert_eq!(rv.all_tokens[..], [TOKEN_0, TOKEN_1]); + assert_eq!(rv.state, vec![(ALICE_ADDR, ViewAddressState { + owned_tokens: vec![TOKEN_0, TOKEN_1], + operators: Vec::new(), + })]); + + // Check that the events are logged. + let events = update.events().flat_map(|(_addr, events)| events); + + let events: Vec> = + events.map(|e| e.parse().expect("Deserialize event")).collect(); + + assert_eq!(events, [ + Cis2Event::Mint(MintEvent { + token_id: TokenIdU32(2), + amount: TokenAmountU8(1), + owner: ALICE_ADDR, + }), + Cis2Event::TokenMetadata(TokenMetadataEvent { + token_id: TokenIdU32(2), + metadata_url: MetadataUrl { + url: format!("{TOKEN_METADATA_BASE_URL}02000000"), + hash: None, + }, + }), + Cis2Event::Mint(MintEvent { + token_id: TokenIdU32(42), + amount: TokenAmountU8(1), + owner: ALICE_ADDR, + }), + Cis2Event::TokenMetadata(TokenMetadataEvent { + token_id: TokenIdU32(42), + metadata_url: MetadataUrl { + url: format!("{TOKEN_METADATA_BASE_URL}2A000000"), + hash: None, + }, + }), + ]); +} + +/// Test regular transfer where sender is the owner. +#[test] +fn test_account_transfer() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Transfer `TOKEN_0` from Alice to Bob. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_0, + amount: TokenAmountU8(1), + data: AdditionalData::empty(), + }]); + + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_nft.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect("Transfer tokens"); + + // Check that Bob now has `TOKEN_0` and that Alice still has `TOKEN_1`. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_nft.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + let rv: ViewState = invoke.parse_return_value().expect("ViewState return value"); + assert_eq!(rv.state, vec![ + (ALICE_ADDR, ViewAddressState { + owned_tokens: vec![TOKEN_1], + operators: Vec::new(), + }), + (BOB_ADDR, ViewAddressState { + owned_tokens: vec![TOKEN_0], + operators: Vec::new(), + }), + ]); + + // Check that the events are logged. + let events = update + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect::>>(); + + assert_eq!(events, [Cis2Event::Transfer(TransferEvent { + token_id: TOKEN_0, + amount: TokenAmountU8(1), + from: ALICE_ADDR, + to: BOB_ADDR, + }),]); +} + +/// Test that you can add an operator. +/// Initialize the contract with two tokens owned by Alice. +/// Then add Bob as an operator for Alice. +#[test] +fn test_add_operator() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Add Bob as an operator for Alice. + let params = UpdateOperatorParams(vec![UpdateOperator { + update: OperatorUpdate::Add, + operator: BOB_ADDR, + }]); + + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_nft.updateOperator".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶ms).expect("UpdateOperator params"), + }) + .expect("Update operator"); + + // Check that an operator event occurred. + let events = update + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect::>>(); + assert_eq!(events, [Cis2Event::UpdateOperator(UpdateOperatorEvent { + operator: BOB_ADDR, + owner: ALICE_ADDR, + update: OperatorUpdate::Add, + }),]); + + // Construct a query parameter to check whether Bob is an operator for Alice. + let query_params = OperatorOfQueryParams { + queries: vec![OperatorOfQuery { + owner: ALICE_ADDR, + address: BOB_ADDR, + }], + }; + + // Invoke the operatorOf view entrypoint and check that Bob is an operator for + // Alice. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_nft.operatorOf".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&query_params).expect("OperatorOf params"), + }) + .expect("Invoke view"); + + let rv: OperatorOfQueryResponse = invoke.parse_return_value().expect("OperatorOf return value"); + assert_eq!(rv, OperatorOfQueryResponse(vec![true])); +} + +/// Test that a transfer fails when the sender is neither an operator or the +/// owner. In particular, Bob will attempt to transfer one of Alice's tokens to +/// himself. +#[test] +fn test_unauthorized_sender() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Construct a transfer of `TOKEN_0` from Alice to Bob, which will be submitted + // by Bob. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_0, + amount: TokenAmountU8(1), + data: AdditionalData::empty(), + }]); + + // Notice that Bob is the sender/invoker. + let update = chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_nft.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect_err("Transfer tokens"); + + // Check that the correct error is returned. + let rv: ContractError = update.parse_return_value().expect("ContractError return value"); + assert_eq!(rv, ContractError::Unauthorized); +} + +/// Test that an operator can make a transfer. +#[test] +fn test_operator_can_transfer() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Add Bob as an operator for Alice. + let params = UpdateOperatorParams(vec![UpdateOperator { + update: OperatorUpdate::Add, + operator: BOB_ADDR, + }]); + chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_nft.updateOperator".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶ms).expect("UpdateOperator params"), + }) + .expect("Update operator"); + + // Let Bob make a transfer to himself on behalf of Alice. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_0, + amount: TokenAmountU8(1), + data: AdditionalData::empty(), + }]); + + chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_nft.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect("Transfer tokens"); + + // Check that Bob now has `TOKEN_0` and that Alice still has `TOKEN_1`. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_nft.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + let rv: ViewState = invoke.parse_return_value().expect("ViewState return value"); + assert_eq!(rv.state, vec![ + (ALICE_ADDR, ViewAddressState { + owned_tokens: vec![TOKEN_1], + operators: vec![BOB_ADDR], + }), + (BOB_ADDR, ViewAddressState { + owned_tokens: vec![TOKEN_0], + operators: Vec::new(), + }), + ]); +} + +/// Helper function that sets up the contract with two tokens minted to +/// Alice, `TOKEN_0` and `TOKEN_1`. +fn initialize_contract_with_alice_tokens() -> (Chain, ContractAddress, ContractInvokeSuccess) { + let (mut chain, contract_address) = initialize_chain_and_contract(); + + let mint_params = MintParams { + owner: ALICE_ADDR, + tokens: BTreeSet::from_iter(vec![TOKEN_0, TOKEN_1]), + }; + + // Mint two tokens to Alice. + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_nft.mint".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&mint_params).expect("Mint params"), + }) + .expect("Mint tokens"); + + (chain, contract_address, update) +} + +/// Setup chain and contract. +/// +/// Also creates the two accounts, Alice and Bob. +/// +/// Alice is the owner of the contract. +fn initialize_chain_and_contract() -> (Chain, ContractAddress) { + let mut chain = Chain::new(); + + // Create some accounts accounts on the chain. + chain.create_account(Account::new(ALICE, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); + + // Load and deploy the module. + let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); + let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Deploy valid module"); + + // Initialize the auction contract. + let init = chain + .contract_init(SIGNER, ALICE, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_cis2_nft".to_string()), + param: OwnedParameter::empty(), + }) + .expect("Initialize contract"); + + (chain, init.contract_address) +} diff --git a/examples/cis2-wccd/Cargo.toml b/examples/cis2-wccd/Cargo.toml index a62bf425..0a992fb3 100644 --- a/examples/cis2-wccd/Cargo.toml +++ b/examples/cis2-wccd/Cargo.toml @@ -6,8 +6,7 @@ edition = "2021" license = "MPL-2.0" [features] -default = ["std", "crypto-primitives", "wee_alloc"] -crypto-primitives = ["concordium-std/crypto-primitives"] +default = ["std", "wee_alloc"] std = ["concordium-std/std", "concordium-cis2/std"] wee_alloc = ["concordium-std/wee_alloc"] @@ -15,6 +14,9 @@ wee_alloc = ["concordium-std/wee_alloc"] concordium-std = {path = "../../concordium-std", default-features = false} concordium-cis2 = {path = "../../concordium-cis2", default-features = false} +[dev-dependencies] +concordium-smart-contract-testing = { path = "../../contract-testing" } + [lib] crate-type=["cdylib", "rlib"] diff --git a/examples/cis2-wccd/src/lib.rs b/examples/cis2-wccd/src/lib.rs index 5746a291..19fb8110 100644 --- a/examples/cis2-wccd/src/lib.rs +++ b/examples/cis2-wccd/src/lib.rs @@ -37,13 +37,13 @@ use concordium_cis2::{Cis2Event, *}; use concordium_std::*; /// The id of the wCCD token in this contract. -const TOKEN_ID_WCCD: ContractTokenId = TokenIdUnit(); +pub const TOKEN_ID_WCCD: ContractTokenId = TokenIdUnit(); /// Tag for the NewAdmin event. pub const NEW_ADMIN_EVENT_TAG: u8 = 0; /// List of supported standards by this contract address. -const SUPPORTS_STANDARDS: [StandardIdentifier<'static>; 2] = +pub const SUPPORTS_STANDARDS: [StandardIdentifier<'static>; 2] = [CIS0_STANDARD_IDENTIFIER, CIS2_STANDARD_IDENTIFIER]; /// Sha256 digest @@ -54,29 +54,29 @@ pub type Sha256 = [u8; 32]; /// Contract token ID type. /// Since this contract will only ever contain this one token type, we use the /// smallest possible token ID. -type ContractTokenId = TokenIdUnit; +pub type ContractTokenId = TokenIdUnit; /// Contract token amount type. /// Since this contract is wrapping the CCD and the CCD can be represented as a /// u64, we can specialize the token amount to u64 reducing module size and cost /// of arithmetics. -type ContractTokenAmount = TokenAmountU64; +pub type ContractTokenAmount = TokenAmountU64; /// The state tracked for each address. #[derive(Serial, DeserialWithState, Deletable)] #[concordium(state_parameter = "S")] -struct AddressState { +pub struct AddressState { /// The number of tokens owned by this address. - balance: ContractTokenAmount, + pub balance: ContractTokenAmount, /// The address which are currently enabled as operators for this token and /// this address. - operators: StateSet, + pub operators: StateSet, } /// The contract state, #[derive(Serial, DeserialWithState)] #[concordium(state_parameter = "S")] -struct State { +struct State { /// The admin address can upgrade the contract, pause and unpause the /// contract, transfer the admin address to a new address, set /// implementors, and update the metadata URL in the contract. @@ -99,27 +99,27 @@ struct State { /// The parameter type for the contract function `unwrap`. /// Takes an amount of tokens and unwraps the CCD and sends it to a receiver. #[derive(Serialize, SchemaType)] -struct UnwrapParams { +pub struct UnwrapParams { /// The amount of tokens to unwrap. - amount: ContractTokenAmount, + pub amount: ContractTokenAmount, /// The owner of the tokens. - owner: Address, + pub owner: Address, /// The address to receive these unwrapped CCD. - receiver: Receiver, + pub receiver: Receiver, /// If the `Receiver` is a contract the unwrapped CCD together with these /// additional data bytes are sent to the function entrypoint specified in /// the `Receiver`. - data: AdditionalData, + pub data: AdditionalData, } /// The parameter type for the contract function `wrap`. /// It includes a receiver for receiving the wrapped CCD tokens. -#[derive(Serialize, SchemaType)] -struct WrapParams { +#[derive(Serialize, SchemaType, Debug)] +pub struct WrapParams { /// The address to receive these tokens. /// If the receiver is the sender of the message wrapping the tokens, it /// will not log a transfer event. - to: Receiver, + pub to: Receiver, /// Some additional data bytes are used in the `OnReceivingCis2` hook. Only /// if the `Receiver` is a contract and the `Receiver` is not /// the invoker of the wrap function the receive hook function is @@ -127,18 +127,18 @@ struct WrapParams { /// specified in the `Receiver` with these additional data bytes as /// part of the input parameters. This action allows the receiving smart /// contract to react to the credited wCCD amount. - data: AdditionalData, + pub data: AdditionalData, } /// The parameter type for the contract function `setImplementors`. /// Takes a standard identifier and list of contract addresses providing /// implementations of this standard. -#[derive(Debug, Serialize, SchemaType)] -struct SetImplementorsParams { +#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] +pub struct SetImplementorsParams { /// The identifier for the standard. - id: StandardIdentifierOwned, + pub id: StandardIdentifierOwned, /// The addresses of the implementors of the standard. - implementors: Vec, + pub implementors: Vec, } /// The parameter type for the contract function `upgrade`. @@ -146,57 +146,57 @@ struct SetImplementorsParams { /// after triggering the upgrade. The upgrade is reverted if the entrypoint /// fails. This is useful for doing migration in the same transaction triggering /// the upgrade. -#[derive(Debug, Serialize, SchemaType)] -struct UpgradeParams { +#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] +pub struct UpgradeParams { /// The new module reference. - module: ModuleReference, + pub module: ModuleReference, /// Optional entrypoint to call in the new module after upgrade. - migrate: Option<(OwnedEntrypointName, OwnedParameter)>, + pub migrate: Option<(OwnedEntrypointName, OwnedParameter)>, } /// The return type for the contract function `view`. -#[derive(Serialize, SchemaType)] -struct ReturnBasicState { +#[derive(Serialize, SchemaType, PartialEq, Eq, Debug)] +pub struct ReturnBasicState { /// The admin address can upgrade the contract, pause and unpause the /// contract, transfer the admin address to a new address, set /// implementors, and update the metadata URL in the contract. - admin: Address, + pub admin: Address, /// Contract is paused if `paused = true` and unpaused if `paused = false`. - paused: bool, + pub paused: bool, /// The metadata URL of the token. - metadata_url: MetadataUrl, + pub metadata_url: MetadataUrl, } /// The parameter type for the contract function `setMetadataUrl`. -#[derive(Serialize, SchemaType, Clone)] -struct SetMetadataUrlParams { +#[derive(Serialize, SchemaType, Clone, PartialEq, Eq, Debug)] +pub struct SetMetadataUrlParams { /// The URL following the specification RFC1738. - url: String, + pub url: String, /// The hash of the document stored at the above URL. - hash: Option, + pub hash: Option, } /// The parameter type for the contract function `setPaused`. -#[derive(Serialize, SchemaType)] +#[derive(Serialize, SchemaType, PartialEq, Eq, Debug)] #[repr(transparent)] -struct SetPausedParams { +pub struct SetPausedParams { /// Contract is paused if `paused = true` and unpaused if `paused = false`. - paused: bool, + pub paused: bool, } /// A NewAdminEvent introduced by this smart contract. -#[derive(Serial, SchemaType)] +#[derive(Serialize, SchemaType, PartialEq, Eq, Debug)] #[repr(transparent)] #[concordium(transparent)] -struct NewAdminEvent { +pub struct NewAdminEvent { /// New admin address. - new_admin: Address, + pub new_admin: Address, } /// Tagged events to be serialized for the event log. -#[derive(SchemaType, Serial)] +#[derive(SchemaType, Serialize, PartialEq, Eq, Debug)] #[concordium(repr(u8))] -enum WccdEvent { +pub enum WccdEvent { NewAdmin { new_admin: NewAdminEvent, }, @@ -206,7 +206,7 @@ enum WccdEvent { /// The different errors the contract can produce. #[derive(Serialize, Debug, PartialEq, Eq, Reject, SchemaType)] -enum CustomContractError { +pub enum CustomContractError { /// Failed parsing the parameter. #[from(ParseError)] ParseParams, @@ -230,9 +230,9 @@ enum CustomContractError { FailedUpgradeUnsupportedModuleVersion, } -type ContractError = Cis2Error; +pub type ContractError = Cis2Error; -type ContractResult = Result; +pub type ContractResult = Result; /// Mapping the logging errors to ContractError. impl From for CustomContractError { @@ -271,9 +271,9 @@ impl From for ContractError { fn from(c: CustomContractError) -> Self { Cis2Error::Custom(c) } } -impl State { +impl State { /// Creates a new state with no one owning any tokens by default. - fn new(state_builder: &mut StateBuilder, admin: Address, metadata_url: MetadataUrl) -> Self { + fn new(state_builder: &mut StateBuilder, admin: Address, metadata_url: MetadataUrl) -> Self { State { admin, paused: false, @@ -311,7 +311,7 @@ impl State { amount: ContractTokenAmount, from: &Address, to: &Address, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) -> ContractResult<()> { ensure_eq!(token_id, &TOKEN_ID_WCCD, ContractError::InvalidTokenId); if amount == 0u64.into() { @@ -339,7 +339,7 @@ impl State { &mut self, owner: &Address, operator: &Address, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) { let mut owner_state = self.token.entry(*owner).or_insert_with(|| AddressState { balance: 0u64.into(), @@ -364,7 +364,7 @@ impl State { token_id: &ContractTokenId, amount: ContractTokenAmount, owner: &Address, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) -> ContractResult<()> { ensure_eq!(token_id, &TOKEN_ID_WCCD, ContractError::InvalidTokenId); let mut owner_state = self.token.entry(*owner).or_insert_with(|| AddressState { @@ -428,11 +428,11 @@ impl State { parameter = "SetMetadataUrlParams", event = "WccdEvent" )] -fn contract_init( - ctx: &impl HasInitContext, - state_builder: &mut StateBuilder, +fn contract_init( + ctx: &InitContext, + state_builder: &mut StateBuilder, logger: &mut impl HasLogger, -) -> InitResult> { +) -> InitResult { // Parse the parameter. let params: SetMetadataUrlParams = ctx.parameter_cursor().get()?; // Get the instantiator of this contract instance to be used as the initial @@ -483,9 +483,9 @@ fn contract_init( mutable, payable )] -fn contract_wrap( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_wrap( + ctx: &ReceiveContext, + host: &mut Host, amount: Amount, logger: &mut impl HasLogger, ) -> ContractResult<()> { @@ -549,9 +549,9 @@ fn contract_wrap( enable_logger, mutable )] -fn contract_unwrap( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_unwrap( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Check that contract is not paused. @@ -611,9 +611,9 @@ fn contract_unwrap( enable_logger, mutable )] -fn contract_update_admin( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_update_admin( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Check that only the current admin is authorized to update the admin address. @@ -649,10 +649,7 @@ fn contract_update_admin( error = "ContractError", mutable )] -fn contract_set_paused( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, -) -> ContractResult<()> { +fn contract_set_paused(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<()> { // Check that only the admin is authorized to pause/unpause the contract. ensure_eq!(ctx.sender(), host.state().admin, ContractError::Unauthorized); @@ -678,9 +675,9 @@ fn contract_set_paused( enable_logger, mutable )] -fn contract_state_set_metadata_url( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_state_set_metadata_url( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Check that only the admin is authorized to update the URL. @@ -735,9 +732,9 @@ type TransferParameter = TransferParams; enable_logger, mutable )] -fn contract_transfer( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_transfer( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Check that contract is not paused. @@ -804,9 +801,9 @@ fn contract_transfer( enable_logger, mutable )] -fn contract_update_operator( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_update_operator( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Check that contract is not paused. @@ -842,9 +839,9 @@ fn contract_update_operator( /// Parameter type for the CIS-2 function `balanceOf` specialized to the subset /// of TokenIDs used by this contract. -type ContractBalanceOfQueryParams = BalanceOfQueryParams; +pub type ContractBalanceOfQueryParams = BalanceOfQueryParams; -type ContractBalanceOfQueryResponse = BalanceOfQueryResponse; +pub type ContractBalanceOfQueryResponse = BalanceOfQueryResponse; /// Get the balance of given token IDs and addresses. /// @@ -858,9 +855,9 @@ type ContractBalanceOfQueryResponse = BalanceOfQueryResponse( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_balance_of( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: ContractBalanceOfQueryParams = ctx.parameter_cursor().get()?; @@ -887,9 +884,9 @@ fn contract_balance_of( return_value = "OperatorOfQueryResponse", error = "ContractError" )] -fn contract_operator_of( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_operator_of( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: OperatorOfQueryParams = ctx.parameter_cursor().get()?; @@ -922,9 +919,9 @@ pub type ContractTokenMetadataQueryParams = TokenMetadataQueryParams( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_token_metadata( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: ContractTokenMetadataQueryParams = ctx.parameter_cursor().get()?; @@ -948,10 +945,7 @@ fn contract_token_metadata( return_value = "ReturnBasicState", error = "ContractError" )] -fn contract_view( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ContractResult { +fn contract_view(_ctx: &ReceiveContext, host: &Host) -> ContractResult { let state = ReturnBasicState { admin: host.state().admin, paused: host.state().paused, @@ -972,9 +966,9 @@ fn contract_view( return_value = "SupportsQueryResponse", error = "ContractError" )] -fn contract_supports( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_supports( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: SupportsQueryParams = ctx.parameter_cursor().get()?; @@ -1005,10 +999,7 @@ fn contract_supports( error = "ContractError", mutable )] -fn contract_set_implementor( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, -) -> ContractResult<()> { +fn contract_set_implementor(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<()> { // Check that only the admin is authorized to set implementors. ensure_eq!(ctx.sender(), host.state().admin, ContractError::Unauthorized); // Parse the parameter. @@ -1041,7 +1032,7 @@ fn contract_set_implementor( low_level )] fn contract_upgrade( - ctx: &impl HasReceiveContext, + ctx: &ReceiveContext, host: &mut impl HasHost, ) -> ContractResult<()> { // Read the top-level contract state. @@ -1064,806 +1055,3 @@ fn contract_upgrade( } Ok(()) } - -// Tests - -#[concordium_cfg_test] -mod tests { - use super::*; - use test_infrastructure::*; - - const ACCOUNT_0: AccountAddress = AccountAddress([0u8; 32]); - const ADDRESS_0: Address = Address::Account(ACCOUNT_0); - const ACCOUNT_1: AccountAddress = AccountAddress([1u8; 32]); - const ADDRESS_1: Address = Address::Account(ACCOUNT_1); - const ADMIN_ACCOUNT: AccountAddress = AccountAddress([2u8; 32]); - const ADMIN_ADDRESS: Address = Address::Account(ADMIN_ACCOUNT); - const NEW_ADMIN_ACCOUNT: AccountAddress = AccountAddress([3u8; 32]); - const NEW_ADMIN_ADDRESS: Address = Address::Account(NEW_ADMIN_ACCOUNT); - - // The metadata url for the wCCD token. - const INITIAL_TOKEN_METADATA_URL: &str = "https://some.example/token/wccd"; - - /// Test helper function which creates a contract state where ADDRESS_0 owns - /// 400 tokens. - fn initial_state(state_builder: &mut StateBuilder) -> State { - // Set up crypto primitives to hash the document. - let crypto_primitives = TestCryptoPrimitives::new(); - // The hash of the document stored at the above URL. - let initial_metadata_hash: Sha256 = - crypto_primitives.hash_sha2_256("document".as_bytes()).0; - - let metadata_url = MetadataUrl { - url: INITIAL_TOKEN_METADATA_URL.to_string(), - hash: Some(initial_metadata_hash), - }; - - let mut state = State::new(state_builder, ADMIN_ADDRESS, metadata_url); - state - .mint(&TOKEN_ID_WCCD, 400u64.into(), &ADDRESS_0, state_builder) - .expect_report("Failed to setup state"); - state - } - - /// Test initialization succeeds and the tokens are owned by the contract - /// instantiator and the appropriate events are logged. - #[concordium_test] - fn test_init() { - // Set up the context - let mut ctx = TestInitContext::empty(); - ctx.set_init_origin(ACCOUNT_0); - - let mut logger = TestLogger::init(); - let mut builder = TestStateBuilder::new(); - - // Set up crypto primitives to hash the document. - let crypto_primitives = TestCryptoPrimitives::new(); - // The hash of the document stored at the above URL. - let initial_metadata_hash: Sha256 = - crypto_primitives.hash_sha2_256("document".as_bytes()).0; - - // Set up the parameter. - let parameter = SetMetadataUrlParams { - url: String::from(INITIAL_TOKEN_METADATA_URL), - hash: Some(initial_metadata_hash), - }; - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - // Call the contract function. - let result = contract_init(&ctx, &mut builder, &mut logger); - - // Check the result - let state = result.expect_report("Contract initialization failed"); - - // Check the state - claim_eq!(state.token.iter().count(), 0, "Only one token is initialized"); - let balance0 = - state.balance(&TOKEN_ID_WCCD, &ADDRESS_0).expect_report("Token is expected to exist"); - claim_eq!( - balance0, - 0u64.into(), - "No initial tokens are owned by the contract instantiator" - ); - - // Check the logs - claim_eq!(logger.logs.len(), 3, "Exactly three events should be logged"); - claim!( - logger.logs.contains(&to_bytes(&WccdEvent::Cis2Event(Cis2Event::Mint(MintEvent { - owner: ADDRESS_0, - token_id: TOKEN_ID_WCCD, - amount: ContractTokenAmount::from(0), - })))), - "Missing event for minting the token" - ); - claim!( - logger.logs.contains(&to_bytes(&WccdEvent::Cis2Event(Cis2Event::TokenMetadata::< - _, - ContractTokenAmount, - >( - TokenMetadataEvent { - token_id: TOKEN_ID_WCCD, - metadata_url: MetadataUrl { - url: String::from(INITIAL_TOKEN_METADATA_URL), - hash: Some(initial_metadata_hash), - }, - } - )))), - "Missing event with metadata for the token" - ); - claim!( - logger.logs.contains(&to_bytes(&WccdEvent::NewAdmin { - new_admin: NewAdminEvent { - new_admin: ADDRESS_0, - }, - })), - "Missing event for the new admin" - ); - } - - /// Test only admin can setMetadataUrl - #[concordium_test] - #[cfg(feature = "crypto-primitives")] - fn test_set_metadata_url() { - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - - // Set up crypto primitives to hash the document. - let crypto_primitives = TestCryptoPrimitives::new(); - // The hash of the document stored at the above URL. - let initial_metadata_hash: Sha256 = - crypto_primitives.hash_sha2_256("document".as_bytes()).0; - - let metadata_url = MetadataUrl { - url: INITIAL_TOKEN_METADATA_URL.to_string(), - hash: Some(initial_metadata_hash), - }; - - let state = State::new(&mut state_builder, ADMIN_ADDRESS, metadata_url); - let mut host = TestHost::new(state, state_builder); - - // Set up the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADMIN_ADDRESS); - - // Create a new_url and a new_hash - let new_url = "https://some.example/token/wccd/updated".to_string(); - let new_hash = crypto_primitives.hash_sha2_256("document2".as_bytes()).0; - - // Set up the parameter. - let parameter = SetMetadataUrlParams { - url: new_url.clone(), - hash: Some(new_hash), - }; - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - // Call the contract function. - let result = contract_state_set_metadata_url(&ctx, &mut host, &mut logger); - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the logs - claim_eq!(logger.logs.len(), 1, "Exactly one event should be logged"); - claim!( - logger.logs.contains(&to_bytes(&WccdEvent::Cis2Event(Cis2Event::TokenMetadata::< - _, - ContractTokenAmount, - >( - TokenMetadataEvent { - token_id: TOKEN_ID_WCCD, - metadata_url: MetadataUrl { - url: new_url.clone(), - hash: Some(new_hash), - }, - } - )))), - "Missing event with updated metadata for the token" - ); - - // Check the state. - let url = host.state().metadata_url.url.clone(); - let hash = host.state().metadata_url.hash; - claim_eq!(url, new_url, "Expected url being updated"); - claim_eq!(hash, Some(new_hash), "Expected hash being updated"); - - // Check only the admin can update the metadata URL - ctx.set_sender(ADDRESS_0); - - // Call the contract function. - let err = contract_state_set_metadata_url(&ctx, &mut host, &mut logger); - - // Check that ADDRESS_0 was not successful in updating the metadata url. - claim_eq!(err, Err(ContractError::Unauthorized), "Error is expected to be Unauthorized") - } - - /// Test transfer succeeds, when `from` is the sender. - #[concordium_test] - fn test_transfer_account() { - // Set up the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - - // Set up the parameter. - let transfer = Transfer { - token_id: TOKEN_ID_WCCD, - amount: ContractTokenAmount::from(100), - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - - // Set up crypto primitives to hash the document. - let crypto_primitives = TestCryptoPrimitives::new(); - // The hash of the document stored at the above URL. - let initial_metadata_hash: Sha256 = - crypto_primitives.hash_sha2_256("document".as_bytes()).0; - - let metadata_url = MetadataUrl { - url: INITIAL_TOKEN_METADATA_URL.to_string(), - hash: Some(initial_metadata_hash), - }; - - let mut state = State::new(&mut state_builder, ADMIN_ADDRESS, metadata_url); - state - .mint(&TOKEN_ID_WCCD, 400.into(), &ADDRESS_0, &mut state_builder) - .expect_report("Failed to setup state"); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let balance0 = host - .state() - .balance(&TOKEN_ID_WCCD, &ADDRESS_0) - .expect_report("Token is expected to exist"); - let balance1 = host - .state() - .balance(&TOKEN_ID_WCCD, &ADDRESS_1) - .expect_report("Token is expected to exist"); - claim_eq!( - balance0, - 300.into(), - "Token owner balance should be decreased by the transferred amount" - ); - claim_eq!( - balance1, - 100.into(), - "Token receiver balance should be increased by the transferred amount" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "Only one event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&WccdEvent::Cis2Event(Cis2Event::Transfer(TransferEvent { - from: ADDRESS_0, - to: ADDRESS_1, - token_id: TOKEN_ID_WCCD, - amount: ContractTokenAmount::from(100), - }))), - "Incorrect event emitted" - ) - } - - /// Test transfer token fails, when sender is neither the owner or an - /// operator of the owner. - #[concordium_test] - fn test_transfer_not_authorized() { - // Set up the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_1); - - // Set up the parameter. - let transfer = Transfer { - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - token_id: TOKEN_ID_WCCD, - amount: ContractTokenAmount::from(100), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - // Check the result. - let err = result.expect_err_report("Expected to fail"); - claim_eq!(err, ContractError::Unauthorized, "Error is expected to be Unauthorized") - } - - /// Test transfer succeeds when sender is not the owner, but is an operator - /// of the owner. - #[concordium_test] - fn test_operator_transfer() { - // Set up the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_1); - - // Set up the parameter. - let transfer = Transfer { - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - token_id: TOKEN_ID_WCCD, - amount: ContractTokenAmount::from(100), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let mut state = initial_state(&mut state_builder); - state.add_operator(&ADDRESS_0, &ADDRESS_1, &mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let balance0 = host - .state() - .balance(&TOKEN_ID_WCCD, &ADDRESS_0) - .expect_report("Token is expected to exist"); - let balance1 = host - .state() - .balance(&TOKEN_ID_WCCD, &ADDRESS_1) - .expect_report("Token is expected to exist"); - claim_eq!( - balance0, - 300.into(), - "Token owner balance should be decreased by the transferred amount" - ); - claim_eq!( - balance1, - 100.into(), - "Token receiver balance should be increased by the transferred amount" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "Only one event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&WccdEvent::Cis2Event(Cis2Event::Transfer(TransferEvent { - from: ADDRESS_0, - to: ADDRESS_1, - token_id: TOKEN_ID_WCCD, - amount: ContractTokenAmount::from(100), - }))), - "Incorrect event emitted" - ) - } - - /// Test adding an operator succeeds and the appropriate event is logged. - #[concordium_test] - fn test_add_operator() { - // Set up the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - - // Set up the parameter. - let update = UpdateOperator { - operator: ADDRESS_1, - update: OperatorUpdate::Add, - }; - let parameter = UpdateOperatorParams(vec![update]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_update_operator(&ctx, &mut host, &mut logger); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - claim!(host.state().is_operator(&ADDRESS_1, &ADDRESS_0), "Account should be an operator"); - - // Checking that `ADDRESS_1` is an operator in the query response of the - // `contract_operator_of` function as well. - // Set up the parameter. - let operator_of_query = OperatorOfQuery { - address: ADDRESS_1, - owner: ADDRESS_0, - }; - - let operator_of_query_vector = OperatorOfQueryParams { - queries: vec![operator_of_query], - }; - let parameter_bytes = to_bytes(&operator_of_query_vector); - - ctx.set_parameter(¶meter_bytes); - - // Checking the return value of the `contract_operator_of` function - let result: ContractResult = contract_operator_of(&ctx, &host); - - claim_eq!( - result.expect_report("Failed getting result value").0, - [true], - "Account should be an operator in the query response" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "One event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&WccdEvent::Cis2Event( - Cis2Event::::UpdateOperator( - UpdateOperatorEvent { - owner: ADDRESS_0, - operator: ADDRESS_1, - update: OperatorUpdate::Add, - } - ) - )), - "Incorrect event emitted" - ) - } - - /// Test wrap and unwrap functions. - #[concordium_test] - fn test_wrap_and_unwrap() { - // Set up the context. - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_1); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Set up the parameter. - let wrap_params = WrapParams { - to: Receiver::from_account(ACCOUNT_1), - data: AdditionalData::empty(), - }; - let parameter_bytes = to_bytes(&wrap_params); - ctx.set_parameter(¶meter_bytes); - - let amount = 100; - - // Testing the `wrap` function - - // ADDRESS_1 wraps some CCD. - let result: ContractResult<()> = - contract_wrap(&ctx, &mut host, Amount::from_micro_ccd(amount), &mut logger); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the wCCD balance of ADDRESS_1. - let balance0 = host - .state() - .balance(&TOKEN_ID_WCCD, &ADDRESS_1) - .expect_report("Token is expected to exist"); - claim_eq!( - balance0, - ContractTokenAmount::from(amount), - "ADDRESS_1 should have received wCCD tokens" - ); - - // Testing the `unwrap` function - - // Set up the parameter. - let unwrap_params = UnwrapParams { - amount: ContractTokenAmount::from(amount), - owner: ADDRESS_1, - receiver: Receiver::from_account(ACCOUNT_1), - data: AdditionalData::empty(), - }; - let parameter_bytes = to_bytes(&unwrap_params); - ctx.set_parameter(¶meter_bytes); - - host.set_self_balance(Amount::from_micro_ccd(amount)); - - // ADDRESS_1 unwraps some wCCD. - let result: ContractResult<()> = contract_unwrap(&ctx, &mut host, &mut logger); - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the wCCD balance of ADDRESS_1. - let balance0 = host - .state() - .balance(&TOKEN_ID_WCCD, &ADDRESS_1) - .expect_report("Token is expected to exist"); - - claim_eq!( - balance0, - ContractTokenAmount::from(0), - "ADDRESS_1 should have no WCCD tokens anymore" - ); - } - - /// Test unwrapping to a receiver account that doesn't exist. - /// - /// This test also showcases the use of [`TestHost::with_rollback`], - /// which handles rolling back the state if a receive function rejects. - #[concordium_test] - fn test_unwrap_to_missing_account() { - // Set up the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - - // Set up the parameter. - let parameter = UnwrapParams { - amount: ContractTokenAmount::from(100), - owner: ADDRESS_0, - receiver: Receiver::from_account(ACCOUNT_1), - data: AdditionalData::empty(), - }; - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Make ACCOUNT_1 missing such that transfers to it will fail. - host.make_account_missing(ACCOUNT_1); - - // Call the contract function. Note the use of `with_rollback`. - let result: ContractResult<()> = - host.with_rollback(|host| contract_unwrap(&ctx, host, &mut logger)); - - claim_eq!( - result, - Err(ContractError::Custom(CustomContractError::InvokeTransferError)), - "InvokeTransferError should have occurred" - ); - - // The balance should still be 400 due to the rollback after rejecting. - claim_eq!( - host.state().balance(&TOKEN_ID_WCCD, &ADDRESS_0), - Ok(400u64.into()), - "ADDRESS_0 balance should still be 400" - ); - claim!(host.get_transfers().is_empty(), "No transfers should have happened"); - } - - /// Test admin can update to a new admin address. - #[concordium_test] - fn test_update_admin() { - // Set up the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADMIN_ADDRESS); - let mut logger = TestLogger::init(); - - // Set up the parameter. - let parameter_bytes = to_bytes(&[NEW_ADMIN_ADDRESS]); - ctx.set_parameter(¶meter_bytes); - - // Set up the state and host. - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Check the admin state. - claim_eq!(host.state().admin, ADMIN_ADDRESS, "Admin should be the old admin address"); - - // Call the contract function. - let result: ContractResult<()> = contract_update_admin(&ctx, &mut host, &mut logger); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the admin state. - claim_eq!(host.state().admin, NEW_ADMIN_ADDRESS, "Admin should be the new admin address"); - - // Check the logs - claim_eq!(logger.logs.len(), 1, "Exactly one event should be logged"); - - // Check the event - claim!( - logger.logs.contains(&to_bytes(&WccdEvent::NewAdmin { - new_admin: NewAdminEvent { - new_admin: NEW_ADMIN_ADDRESS, - }, - })), - "Missing event for the new admin" - ); - } - - /// Test that only the current admin can update the admin address. - #[concordium_test] - fn test_update_admin_not_authorized() { - // Set up the context. - let mut ctx = TestReceiveContext::empty(); - // NEW_ADMIN is not the current admin but tries to update the admin variable to - // its own address. - ctx.set_sender(NEW_ADMIN_ADDRESS); - let mut logger = TestLogger::init(); - - // Set up the parameter. - let parameter_bytes = to_bytes(&[NEW_ADMIN_ADDRESS]); - ctx.set_parameter(¶meter_bytes); - - // Set up the state and host. - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Check the admin state. - claim_eq!(host.state().admin, ADMIN_ADDRESS, "Admin should be the old admin address"); - - // Call the contract function. - let result: ContractResult<()> = contract_update_admin(&ctx, &mut host, &mut logger); - - // Check that invoke failed. - claim_eq!( - result, - Err(ContractError::Unauthorized), - "Update admin should fail because not the current admin tries to update" - ); - - // Check the admin state. - claim_eq!(host.state().admin, ADMIN_ADDRESS, "Admin should be still the old admin address"); - } - - /// Test pausing the contract. - #[concordium_test] - fn test_pause() { - // Set up the context. - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADMIN_ADDRESS); - - // Set up the parameter to pause the contract. - let parameter_bytes = to_bytes(&true); - ctx.set_parameter(¶meter_bytes); - - // Set up the state and host. - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_set_paused(&ctx, &mut host); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check contract is paused. - claim_eq!(host.state().paused, true, "Smart contract should be paused"); - } - - /// Test unpausing the contract. - #[concordium_test] - fn test_unpause() { - // Set up the context. - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADMIN_ADDRESS); - - // Set up the parameter to pause the contract. - let parameter_bytes = to_bytes(&true); - ctx.set_parameter(¶meter_bytes); - - // Set up the state and host. - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_set_paused(&ctx, &mut host); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check contract is paused. - claim_eq!(host.state().paused, true, "Smart contract should be paused"); - - // Set up the parameter to unpause the contract. - let parameter_bytes = to_bytes(&false); - ctx.set_parameter(¶meter_bytes); - - // Call the contract function. - let result: ContractResult<()> = contract_set_paused(&ctx, &mut host); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check contract is unpaused. - claim_eq!(host.state().paused, false, "Smart contract should be unpaused"); - } - - /// Test that only the current admin can pause/unpause the contract. - #[concordium_test] - fn test_pause_not_authorized() { - // Set up the context. - let mut ctx = TestReceiveContext::empty(); - // NEW_ADMIN is not the current admin but tries to pause/unpause the contract. - ctx.set_sender(NEW_ADMIN_ADDRESS); - - // Set up the parameter to pause the contract. - let parameter_bytes = to_bytes(&true); - ctx.set_parameter(¶meter_bytes); - - // Set up the state and host. - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_set_paused(&ctx, &mut host); - - // Check that invoke failed. - claim_eq!( - result, - Err(ContractError::Unauthorized), - "Pause should fail because not the current admin tries to invoke it" - ); - } - - /// Test that one can NOT call non-admin state-mutative functions (wrap, - /// unwrap, transfer, updateOperator) when the contract is paused. - #[concordium_test] - fn test_no_execution_of_state_mutative_functions_when_paused() { - // Set up the context. - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADMIN_ADDRESS); - - // Set up the parameter to pause the contract. - let parameter_bytes = to_bytes(&true); - ctx.set_parameter(¶meter_bytes); - - // Set up the state and host. - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_set_paused(&ctx, &mut host); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check contract is paused. - claim_eq!(host.state().paused, true, "Smart contract should be paused"); - - let mut logger = TestLogger::init(); - - // Call the `transfer` function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - - // Check that invoke failed. - claim_eq!( - result, - Err(ContractError::Custom(CustomContractError::ContractPaused)), - "Transfer should fail because contract is paused" - ); - - // Call the `updateOperator` function. - let result: ContractResult<()> = contract_update_operator(&ctx, &mut host, &mut logger); - - // Check that invoke failed. - claim_eq!( - result, - Err(ContractError::Custom(CustomContractError::ContractPaused)), - "Update operator should fail because contract is paused" - ); - - // Call the `wrap` function. - let result: ContractResult<()> = - contract_wrap(&ctx, &mut host, Amount::from_micro_ccd(100), &mut logger); - - // Check that invoke failed. - claim_eq!( - result, - Err(ContractError::Custom(CustomContractError::ContractPaused)), - "Wrap should fail because contract is paused" - ); - - // Call the`unwrap` function. - let result: ContractResult<()> = contract_unwrap(&ctx, &mut host, &mut logger); - - // Check that invoke failed. - claim_eq!( - result, - Err(ContractError::Custom(CustomContractError::ContractPaused)), - "Unwrap should fail because contract is paused" - ); - } -} diff --git a/examples/cis2-wccd/tests/tests.rs b/examples/cis2-wccd/tests/tests.rs new file mode 100644 index 00000000..1d73a26b --- /dev/null +++ b/examples/cis2-wccd/tests/tests.rs @@ -0,0 +1,652 @@ +//! Tests for the `cis2_wCCD` contract. +use cis2_wccd::*; +use concordium_cis2::*; +use concordium_smart_contract_testing::*; + +/// The tests accounts. +const ALICE: AccountAddress = AccountAddress([0; 32]); +const ALICE_ADDR: Address = Address::Account(ALICE); +const BOB: AccountAddress = AccountAddress([1; 32]); +const BOB_ADDR: Address = Address::Account(BOB); +const CHARLIE: AccountAddress = AccountAddress([2; 32]); + +/// Initial balance of the accounts. +const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10000); + +/// A signer for all the transactions. +const SIGNER: Signer = Signer::with_one_key(); + +/// The metadata url for testing. +const METADATA_URL: &str = "https://example.com"; + +/// Test that init produces the correct logs. +#[test] +fn test_init() { + let (_chain, init) = initialize_chain_and_contract(); + // Check that the logs are correct. + + let events = init + .events + .iter() + .map(|e| e.parse().expect("Parsing WccdEvent.")) + .collect::>(); + + assert_eq!(events, [ + WccdEvent::Cis2Event(Cis2Event::Mint(MintEvent { + token_id: TOKEN_ID_WCCD, + amount: ContractTokenAmount::from(0u64), + owner: ALICE_ADDR, + })), + WccdEvent::Cis2Event(Cis2Event::TokenMetadata(TokenMetadataEvent { + token_id: TOKEN_ID_WCCD, + metadata_url: MetadataUrl { + url: METADATA_URL.to_string(), + hash: None, + }, + })), + WccdEvent::NewAdmin { + new_admin: NewAdminEvent { + new_admin: ALICE_ADDR, + }, + } + ]); +} + +/// Test that only the admin can set the metadata URL. +#[test] +fn test_set_metadata_url() { + let (mut chain, contract_address, _) = initialize_contract_with_alice_tokens(); + + let new_metadata_url = "https://new-url.com".to_string(); + + // Construct the parameters. + let params = SetMetadataUrlParams { + url: new_metadata_url.clone(), + hash: None, + }; + + // Try to set the metadata URL from Bob's account, who is not the admin. + let update = chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.setMetadataUrl".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶ms).expect("SetMetadataUrl params"), + }) + .expect_err("SetMetadataUrl"); + + // Check that the return value is correct. + let rv: ContractError = update.parse_return_value().expect("Parsing ContractError"); + assert_eq!(rv, ContractError::Unauthorized); + + // Set the metadata URL from Alice's account, who is the admin. + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.setMetadataUrl".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶ms).expect("SetMetadataUrl params"), + }) + .expect("SetMetadataUrl"); + + // Check that the logs are correct. + let events = deserialize_update_events(&update); + + assert_eq!(events, [WccdEvent::Cis2Event(Cis2Event::TokenMetadata(TokenMetadataEvent { + token_id: TOKEN_ID_WCCD, + metadata_url: MetadataUrl { + url: new_metadata_url, + hash: None, + }, + })),]); +} + +/// Test regular transfer where sender is the owner. +#[test] +fn test_account_transfer() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Transfer one token from Alice to Bob. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_ID_WCCD, + amount: TokenAmountU64(1), + data: AdditionalData::empty(), + }]); + + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect("Transfer tokens"); + + // Check that Alice now has 99 wCCD and Bob has 1. + let balances = get_balances(&chain, contract_address); + assert_eq!(balances.0, [99.into(), 1.into()]); + + // Check that a single transfer event occurred. + let events = deserialize_update_events(&update); + assert_eq!(events, [WccdEvent::Cis2Event(Cis2Event::Transfer(TransferEvent { + token_id: TOKEN_ID_WCCD, + amount: TokenAmountU64(1), + from: ALICE_ADDR, + to: BOB_ADDR, + })),]); +} + +/// Test that you can add an operator. +/// Initialize the contract with one token owned by Alice. +/// Then add Bob as an operator for Alice. +#[test] +fn test_add_operator() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Add Bob as an operator for Alice. + let params = UpdateOperatorParams(vec![UpdateOperator { + update: OperatorUpdate::Add, + operator: BOB_ADDR, + }]); + + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.updateOperator".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶ms).expect("UpdateOperator params"), + }) + .expect("Update operator"); + + // Check that an operator event occurred. + let events = deserialize_update_events(&update); + assert_eq!(events, [WccdEvent::Cis2Event(Cis2Event::UpdateOperator(UpdateOperatorEvent { + operator: BOB_ADDR, + owner: ALICE_ADDR, + update: OperatorUpdate::Add, + })),]); + + // Construct a query parameter to check whether Bob is an operator for Alice. + let query_params = OperatorOfQueryParams { + queries: vec![OperatorOfQuery { + owner: ALICE_ADDR, + address: BOB_ADDR, + }], + }; + + // Invoke the operatorOf view entrypoint and check that Bob is an operator for + // Alice. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.operatorOf".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&query_params).expect("OperatorOf params"), + }) + .expect("Invoke view"); + + let rv: OperatorOfQueryResponse = invoke.parse_return_value().expect("OperatorOf return value"); + assert_eq!(rv, OperatorOfQueryResponse(vec![true])); +} + +/// Test that a transfer fails when the sender is neither an operator or the +/// owner. In particular, Bob will attempt to transfer some of Alice's tokens to +/// himself. +#[test] +fn test_unauthorized_sender() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Construct a transfer of `TOKEN_ID_WCCD` from Alice to Bob, which will be + // submitted by Bob. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_ID_WCCD, + amount: TokenAmountU64(1), + data: AdditionalData::empty(), + }]); + + // Notice that Bob is the sender/invoker. + let update = chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect_err("Transfer tokens"); + + // Check that the correct error is returned. + let rv: ContractError = update.parse_return_value().expect("ContractError return value"); + assert_eq!(rv, ContractError::Unauthorized); +} + +/// Test that an operator can make a transfer. +#[test] +fn test_operator_can_transfer() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Add Bob as an operator for Alice. + let params = UpdateOperatorParams(vec![UpdateOperator { + update: OperatorUpdate::Add, + operator: BOB_ADDR, + }]); + chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.updateOperator".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶ms).expect("UpdateOperator params"), + }) + .expect("Update operator"); + + // Let Bob make a transfer to himself on behalf of Alice. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_ID_WCCD, + amount: TokenAmountU64(1), + data: AdditionalData::empty(), + }]); + + chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect("Transfer tokens"); + + // Check that Bob now has 1 wCCD and Alice has 99. + let balances = get_balances(&chain, contract_address); + assert_eq!(balances.0, [99.into(), 1.into()]); +} + +/// Test wrap and unwrap functions. +#[test] +fn test_wrap_unwrap() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Wrap 100 microCCD into wCCD for Bob. + let wrap_params = WrapParams { + to: Receiver::Account(BOB), + data: AdditionalData::empty(), + }; + chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::from_micro_ccd(100), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.wrap".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&wrap_params).expect("Wrap params"), + }) + .expect("Wrap CCD"); + // Check that Bob now has 100 wCCD. Alice also has 100wCCD. + let balances = get_balances(&chain, contract_address); + assert_eq!(balances.0, [100.into(), 100.into()]); + + // Unwrap 100 wCCD for Bob. + let unwrap_params = UnwrapParams { + amount: 100.into(), + owner: BOB_ADDR, + receiver: Receiver::Account(BOB), + data: AdditionalData::empty(), + }; + let update_unwrap = chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.unwrap".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&unwrap_params).expect("Unwrap params"), + }) + .expect("Unwrap wCCD"); + // Check that Bob now has 0 wCCD. Alice still has 100 wCCD. + let balances = get_balances(&chain, contract_address); + assert_eq!(balances.0, [100.into(), 0.into()]); + + // Also check that the burn event is produced. + let events = deserialize_update_events(&update_unwrap); + assert_eq!(events, [WccdEvent::Cis2Event(Cis2Event::Burn(BurnEvent { + token_id: TOKEN_ID_WCCD, + amount: 100.into(), + owner: BOB_ADDR, + })),]); +} + +/// Test unwrapping to a missing account. +#[test] +fn test_unwrap_to_missing_account() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Unwrap 100 wCCD for Bob. + let unwrap_params = UnwrapParams { + amount: 100.into(), + owner: ALICE_ADDR, + receiver: Receiver::Account(CHARLIE), // The Charlie account has not been created. + data: AdditionalData::empty(), + }; + let update_unwrap = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.unwrap".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&unwrap_params).expect("Unwrap params"), + }) + .expect_err("Unwrap wCCD"); + + // Check that the correct error is returned. + let rv: ContractError = update_unwrap.parse_return_value().expect("ContractError return value"); + assert_eq!(rv, ContractError::Custom(CustomContractError::InvokeTransferError)); +} + +/// Test that you can update the admin account. +#[test] +fn test_update_admin() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Update the admin account to Bob. + let params = BOB_ADDR; + + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.updateAdmin".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶ms).expect("UpdateAdmin params"), + }) + .expect("Update admin"); + + // Invoke the view function to check that the new admin is Bob. + assert_eq!(invoke_view(&mut chain, contract_address).admin, BOB_ADDR); + + // Check that the logs are correct. + let events = deserialize_update_events(&update); + assert_eq!(events, [WccdEvent::NewAdmin { + new_admin: NewAdminEvent { + new_admin: BOB_ADDR, + }, + },]); +} + +/// Test that only the current admin can update the admin account. +#[test] +fn test_update_admin_unauthorized() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Update the admin account to Bob. + let params = BOB_ADDR; + + let update = chain + .contract_update( + SIGNER, + BOB, + BOB_ADDR, // Bob is not the admin. + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.updateAdmin".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶ms).expect("UpdateAdmin params"), + }, + ) + .expect_err("Update admin"); + + // Check that the correct error is returned. + let rv: ContractError = update.parse_return_value().expect("ContractError return value"); + assert_eq!(rv, ContractError::Unauthorized); +} + +/// Test that the pause/unpause entrypoints correctly sets the pause value in +/// the state. +#[test] +fn test_pause_functionality() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Pause the contract. + chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.setPaused".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&true).expect("Pause params"), + }) + .expect("Pause"); + + // Check that the contract is now paused. + assert_eq!(invoke_view(&mut chain, contract_address).paused, true); + + // Unpause the contract. + chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.setPaused".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&false).expect("Unpause params"), + }) + .expect("Unpause"); + // Check that the contract is now unpaused. + assert_eq!(invoke_view(&mut chain, contract_address).paused, false); +} + +/// Test that only the admin can pause/unpause the contract. +#[test] +fn test_pause_unpause_unauthorized() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Pause the contract as Bob, who is not the admin. + let update = chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.setPaused".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&true).expect("Pause params"), + }) + .expect_err("Pause"); + + // Check that the correct error is returned. + let rv: ContractError = update.parse_return_value().expect("ContractError return value"); + assert_eq!(rv, ContractError::Unauthorized); +} + +/// Test that one can NOT call non-admin state-mutative functions (wrap, +/// unwrap, transfer, updateOperator) when the contract is paused. +#[test] +fn test_no_execution_of_state_mutative_functions_when_paused() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Pause the contract. + chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.setPaused".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&true).expect("Pause params"), + }) + .expect("Pause"); + + // Try to wrap 100 microCCD into wCCD for Bob. + let wrap_params = WrapParams { + to: Receiver::Account(BOB), + data: AdditionalData::empty(), + }; + let update_wrap = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::from_micro_ccd(100), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.wrap".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&wrap_params).expect("Wrap params"), + }) + .expect_err("Wrap CCD"); + + // Check that the correct error is returned. + assert_contract_paused_error(&update_wrap); + + // Try to unwrap 1 wCCD for Alice. + let unwrap_params = UnwrapParams { + amount: 1.into(), + owner: ALICE_ADDR, + receiver: Receiver::Account(ALICE), + data: AdditionalData::empty(), + }; + let update_unwrap = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.unwrap".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&unwrap_params).expect("Unwrap params"), + }) + .expect_err("Unwrap wCCD"); + assert_contract_paused_error(&update_unwrap); + + // Try to transfer 1 wCCD from Alice to Bob. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_ID_WCCD, + amount: TokenAmountU64(1), + data: AdditionalData::empty(), + }]); + let update_transfer = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect_err("Transfer tokens"); + assert_contract_paused_error(&update_transfer); + + // Try to add Bob as an operator for Alice. + let params = UpdateOperatorParams(vec![UpdateOperator { + update: OperatorUpdate::Add, + operator: BOB_ADDR, + }]); + let update_operator = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.updateOperator".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶ms).expect("UpdateOperator params"), + }) + .expect_err("Update operator"); + assert_contract_paused_error(&update_operator); +} + +// Helpers: + +/// Helper function that initializes the contract and wraps 100 microCCD into +/// wCCD for Alice. +fn initialize_contract_with_alice_tokens() -> (Chain, ContractAddress, ContractInvokeSuccess) { + let (mut chain, init) = initialize_chain_and_contract(); + + let wrap_params = WrapParams { + to: Receiver::Account(ALICE), + data: AdditionalData::empty(), + }; + + // Wrap 100 CCD into wCCD for Alice. + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::from_micro_ccd(100), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.wrap".to_string()), + address: init.contract_address, + message: OwnedParameter::from_serial(&wrap_params).expect("Wrap params"), + }) + .expect("Wrap CCD"); + (chain, init.contract_address, update) +} + +/// Setup chain and contract. +/// +/// Also creates the two accounts, Alice and Bob. +/// +/// Alice is the owner of the contract. +fn initialize_chain_and_contract() -> (Chain, ContractInitSuccess) { + let mut chain = Chain::new(); + + // Create some accounts accounts on the chain. + chain.create_account(Account::new(ALICE, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); + + // Load and deploy the module. + let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); + let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Deploy valid module"); + + // Construct the initial parameters. + let params = SetMetadataUrlParams { + url: METADATA_URL.to_string(), + hash: None, + }; + + // Initialize the auction contract. + let init = chain + .contract_init(SIGNER, ALICE, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_cis2_wCCD".to_string()), + param: OwnedParameter::from_serial(¶ms).expect("Init params"), + }) + .expect("Initialize contract"); + + (chain, init) +} + +/// Get the result of the view entrypoint. +fn invoke_view(chain: &mut Chain, contract_address: ContractAddress) -> ReturnBasicState { + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + invoke.parse_return_value().expect("Return value") +} + +/// Get the balances for Alice and Bob. +fn get_balances( + chain: &Chain, + contract_address: ContractAddress, +) -> ContractBalanceOfQueryResponse { + let balance_of_params = ContractBalanceOfQueryParams { + queries: vec![ + BalanceOfQuery { + token_id: TOKEN_ID_WCCD, + address: ALICE_ADDR, + }, + BalanceOfQuery { + token_id: TOKEN_ID_WCCD, + address: BOB_ADDR, + }, + ], + }; + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_wCCD.balanceOf".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&balance_of_params) + .expect("BalanceOf params"), + }) + .expect("Invoke balanceOf"); + let rv: ContractBalanceOfQueryResponse = + invoke.parse_return_value().expect("BalanceOf return value"); + rv +} + +/// Deserialize the events from an update. +fn deserialize_update_events(update: &ContractInvokeSuccess) -> Vec { + update + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect() +} + +/// Check that the returned error is `ContractPaused`. +fn assert_contract_paused_error(update: &ContractInvokeError) { + let rv: ContractError = update.parse_return_value().expect("ContractError return value"); + assert_eq!(rv, ContractError::Custom(CustomContractError::ContractPaused)); +} diff --git a/examples/cis3-nft-sponsored-txs/Cargo.toml b/examples/cis3-nft-sponsored-txs/Cargo.toml index fa6d0a09..95d7794d 100644 --- a/examples/cis3-nft-sponsored-txs/Cargo.toml +++ b/examples/cis3-nft-sponsored-txs/Cargo.toml @@ -6,15 +6,17 @@ edition = "2021" license = "MPL-2.0" [features] -default = ["std", "wee_alloc", "crypto-primitives"] +default = ["std", "wee_alloc"] std = ["concordium-std/std", "concordium-cis2/std"] wee_alloc = ["concordium-std/wee_alloc"] -crypto-primitives = ["concordium-std/crypto-primitives"] [dependencies] concordium-std = {path = "../../concordium-std", default-features = false} concordium-cis2 = {path = "../../concordium-cis2", default-features = false} +[dev-dependencies] +concordium-smart-contract-testing = { path = "../../contract-testing" } + [lib] crate-type=["cdylib", "rlib"] diff --git a/examples/cis3-nft-sponsored-txs/src/lib.rs b/examples/cis3-nft-sponsored-txs/src/lib.rs index 586b9a7d..e3b9eb1b 100644 --- a/examples/cis3-nft-sponsored-txs/src/lib.rs +++ b/examples/cis3-nft-sponsored-txs/src/lib.rs @@ -61,7 +61,7 @@ use concordium_std::{collections::BTreeMap, EntrypointName, *}; /// The url for the token metadata. Every `token_id` in this contract has the /// same metadata url for simplicity. -const TOKEN_METADATA_URL: &str = "https://gist.githubusercontent.com/abizjak/ab5b6fc0afb78acf23ee24d979eb7639/raw/7c03f174d628df1d2fd0dc8cffb319c89e770708/metadata.json"; +pub const TOKEN_METADATA_URL: &str = "https://gist.githubusercontent.com/abizjak/ab5b6fc0afb78acf23ee24d979eb7639/raw/7c03f174d628df1d2fd0dc8cffb319c89e770708/metadata.json"; /// The standard identifier for the CIS-3: Concordium Token Standard 3. pub const CIS3_STANDARD_IDENTIFIER: StandardIdentifier<'static> = @@ -81,7 +81,7 @@ pub const NONCE_EVENT_TAG: u8 = u8::MAX - 5; /// Tagged events to be serialized for the event log. #[derive(Debug, Serial)] #[concordium(repr(u8))] -enum Event { +pub enum Event { /// The event tracks the nonce used by the signer of the `PermitMessage` /// whenever the `permit` function is invoked. #[concordium(tag = 250)] @@ -194,15 +194,15 @@ pub struct MintParams { /// The state for each address. #[derive(Serial, DeserialWithState, Deletable)] #[concordium(state_parameter = "S")] -struct AddressState { +struct AddressState { /// The tokens owned by this address. owned_tokens: StateSet, /// The address which are currently enabled as operators for this address. operators: StateSet, } -impl AddressState { - fn empty(state_builder: &mut StateBuilder) -> Self { +impl AddressState { + fn empty(state_builder: &mut StateBuilder) -> Self { AddressState { owned_tokens: state_builder.new_set(), operators: state_builder.new_set(), @@ -215,7 +215,7 @@ impl AddressState { // and this could be structured in a more space efficient way depending on the use case. #[derive(Serial, DeserialWithState)] #[concordium(state_parameter = "S")] -struct State { +struct State { /// Counter to increase the `token_id` at every mint function invoke. token_id_counter: u32, /// The state for each address. @@ -293,7 +293,7 @@ pub struct PermitParamPartial { /// The custom errors the contract can produce. #[derive(Serialize, Debug, PartialEq, Eq, Reject, SchemaType)] -enum CustomContractError { +pub enum CustomContractError { /// Failed parsing the parameter. #[from(ParseError)] ParseParams, @@ -331,9 +331,9 @@ enum CustomContractError { } /// Wrapping the custom errors in a type with CIS2 errors. -type ContractError = Cis2Error; +pub type ContractError = Cis2Error; -type ContractResult = Result; +pub type ContractResult = Result; /// Mapping account signature error to CustomContractError impl From for CustomContractError { @@ -366,9 +366,9 @@ impl From for ContractError { } // Functions for creating, updating and querying the contract state. -impl State { +impl State { /// Creates a new state with no tokens. - fn empty(state_builder: &mut StateBuilder) -> Self { + fn empty(state_builder: &mut StateBuilder) -> Self { State { token_id_counter: 0, state: state_builder.new_map(), @@ -383,7 +383,7 @@ impl State { &mut self, token: ContractTokenId, owner: &Address, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) -> ContractResult<()> { ensure!(self.all_tokens.insert(token), CustomContractError::TokenIdAlreadyExists.into()); @@ -434,7 +434,7 @@ impl State { amount: ContractTokenAmount, from: &Address, to: &Address, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) -> ContractResult<()> { ensure!(self.contains_token(token_id), ContractError::InvalidTokenId); // A zero transfer does not modify the state. @@ -469,7 +469,7 @@ impl State { &mut self, owner: &Address, operator: &Address, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) { let mut owner_state = self.state.entry(*owner).or_insert_with(|| AddressState::empty(state_builder)); @@ -507,36 +507,30 @@ impl State { /// Initialize contract instance with no token types initially. #[init(contract = "cis3_nft", event = "Event")] -fn contract_init( - _ctx: &impl HasInitContext, - state_builder: &mut StateBuilder, -) -> InitResult> { +fn contract_init(_ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { // Construct the initial contract state. Ok(State::empty(state_builder)) } /// Part of the return paramter of the `view` function. #[derive(Serialize, SchemaType, Debug, PartialEq)] -struct ViewAddressState { - owned_tokens: Vec, - operators: Vec
, +pub struct ViewAddressState { + pub owned_tokens: Vec, + pub operators: Vec
, } /// Return paramter of the `view` function. #[derive(Serialize, SchemaType, Debug)] -struct ViewState { - state: Vec<(Address, ViewAddressState)>, - all_tokens: Vec, - all_nonces: Vec<(AccountAddress, u64)>, +pub struct ViewState { + pub state: Vec<(Address, ViewAddressState)>, + pub all_tokens: Vec, + pub all_nonces: Vec<(AccountAddress, u64)>, } /// View function that returns the entire contents of the state. Meant for /// testing. #[receive(contract = "cis3_nft", name = "view", return_value = "ViewState")] -fn contract_view( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ReceiveResult { +fn contract_view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { let state = host.state(); let mut inner_state = Vec::new(); @@ -582,9 +576,9 @@ fn contract_view( enable_logger, mutable )] -fn contract_mint( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_mint( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Parse the parameter. @@ -625,14 +619,14 @@ fn contract_mint( Ok(()) } -type TransferParameter = TransferParams; +pub type TransferParameter = TransferParams; /// Internal `transfer/permit` helper function. Invokes the `transfer` /// function of the state. Logs a `Transfer` event and invokes a receive hook /// function. The function assumes that the transfer is authorized. -fn transfer( +fn transfer( transfer: concordium_cis2::Transfer, - host: &mut impl HasHost, StateApiType = S>, + host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { let (state, builder) = host.state_and_builder(); @@ -685,9 +679,9 @@ fn transfer( enable_logger, mutable )] -fn contract_transfer( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_transfer( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Parse the parameter. @@ -710,10 +704,7 @@ fn contract_transfer( /// Helper function that can be invoked at the front-end to serialize the /// `PermitMessage` before signing it in the wallet. #[receive(contract = "cis3_nft", name = "serializationHelper", parameter = "PermitMessage")] -fn contract_serialization_helper( - _ctx: &impl HasReceiveContext, - _host: &impl HasHost, StateApiType = S>, -) -> ContractResult<()> { +fn contract_serialization_helper(_ctx: &ReceiveContext, _host: &Host) -> ContractResult<()> { Ok(()) } @@ -726,9 +717,9 @@ fn contract_serialization_helper( crypto_primitives, mutable )] -fn contract_view_message_hash( - ctx: &impl HasReceiveContext, - _host: &mut impl HasHost, StateApiType = S>, +fn contract_view_message_hash( + ctx: &ReceiveContext, + _host: &mut Host, crypto_primitives: &impl HasCryptoPrimitives, ) -> ContractResult<[u8; 32]> { // Parse the parameter. @@ -796,9 +787,9 @@ fn contract_view_message_hash( mutable, enable_logger )] -fn contract_permit( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_permit( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, crypto_primitives: &impl HasCryptoPrimitives, ) -> ContractResult<()> { @@ -884,12 +875,12 @@ fn contract_permit( /// `add_operator/remove_operator` function of the state. /// Logs a `UpdateOperator` event. The function assumes that the sender is /// authorized to do the `updateOperator` action. -fn update_operator( +fn update_operator( update: OperatorUpdate, sender: Address, operator: Address, - state: &mut State, - builder: &mut StateBuilder, + state: &mut State, + builder: &mut StateBuilder, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Update the operator in the state. @@ -924,9 +915,9 @@ fn update_operator( enable_logger, mutable )] -fn contract_update_operator( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_update_operator( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Parse the parameter. @@ -952,9 +943,9 @@ fn contract_update_operator( return_value = "OperatorOfQueryResponse", error = "ContractError" )] -fn contract_operator_of( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_operator_of( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: OperatorOfQueryParams = ctx.parameter_cursor().get()?; @@ -988,9 +979,9 @@ type ContractBalanceOfQueryResponse = BalanceOfQueryResponse( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_balance_of( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: ContractBalanceOfQueryParams = ctx.parameter_cursor().get()?; @@ -1039,9 +1030,9 @@ pub struct VecOfAccountAddresses { return_value = "PublicKeyOfQueryResponse", error = "ContractError" )] -fn contract_public_key_of( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_public_key_of( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: VecOfAccountAddresses = ctx.parameter_cursor().get()?; @@ -1077,9 +1068,9 @@ impl From> for NonceOfQueryResponse { return_value = "NonceOfQueryResponse", error = "ContractError" )] -fn contract_nonce_of( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_nonce_of( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: VecOfAccountAddresses = ctx.parameter_cursor().get()?; @@ -1110,9 +1101,9 @@ type ContractTokenMetadataQueryParams = TokenMetadataQueryParams( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_token_metadata( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: ContractTokenMetadataQueryParams = ctx.parameter_cursor().get()?; @@ -1144,9 +1135,9 @@ fn contract_token_metadata( return_value = "SupportsQueryResponse", error = "ContractError" )] -fn contract_supports( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_supports( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: SupportsQueryParams = ctx.parameter_cursor().get()?; @@ -1176,9 +1167,9 @@ fn contract_supports( return_value = "SupportsQueryResponse", error = "ContractError" )] -fn contract_supports_permit( - ctx: &impl HasReceiveContext, - _host: &impl HasHost, StateApiType = S>, +fn contract_supports_permit( + ctx: &ReceiveContext, + _host: &Host, ) -> ContractResult { // Parse the parameter. let params: SupportsPermitQueryParams = ctx.parameter_cursor().get()?; @@ -1209,10 +1200,7 @@ fn contract_supports_permit( error = "ContractError", mutable )] -fn contract_set_implementor( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, -) -> ContractResult<()> { +fn contract_set_implementor(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<()> { // Authorize the sender. ensure!(ctx.sender().matches_account(&ctx.owner()), ContractError::Unauthorized); // Parse the parameter. @@ -1221,415 +1209,3 @@ fn contract_set_implementor( host.state_mut().set_implementors(params.id, params.implementors); Ok(()) } - -// Tests - -#[concordium_cfg_test] -mod tests { - use super::*; - use test_infrastructure::*; - - const ACCOUNT_0: AccountAddress = AccountAddress([0u8; 32]); - const ADDRESS_0: Address = Address::Account(ACCOUNT_0); - const ACCOUNT_1: AccountAddress = AccountAddress([1u8; 32]); - const ADDRESS_1: Address = Address::Account(ACCOUNT_1); - const ACCOUNT_2: AccountAddress = AccountAddress([2u8; 32]); - const ADDRESS_2: Address = Address::Account(ACCOUNT_2); - const TOKEN_1: ContractTokenId = TokenIdU32(1); - const TOKEN_2: ContractTokenId = TokenIdU32(2); - - /// Test helper function which creates a contract state with two tokens with - /// id `TOKEN_1` owned by `ADDRESS_0` and id `TOKEN_2` owned by `ADDRESS_1`. - fn initial_state(state_builder: &mut StateBuilder) -> State { - let mut state = State::empty(state_builder); - state.mint(TOKEN_1, &ADDRESS_0, state_builder).expect_report("Failed to mint TOKEN_1"); - state.mint(TOKEN_2, &ADDRESS_1, state_builder).expect_report("Failed to mint TOKEN_2"); - state.token_id_counter = 2; - state - } - - /// Test initialization succeeds. - #[concordium_test] - fn test_init() { - // Setup the context - let ctx = TestInitContext::empty(); - let mut builder = TestStateBuilder::new(); - - // Call the contract function. - let result = contract_init(&ctx, &mut builder); - - // Check the result - let state = result.expect_report("Contract initialization failed"); - - // Check the state - // Note. This is rather expensive as an iterator is created and then traversed - - // should be avoided when writing smart contracts. - claim_eq!(state.all_tokens.iter().count(), 0, "No token should be initialized"); - } - - /// Test minting, ensuring the new tokens are owned by the given address and - /// the appropriate events are logged. - #[concordium_test] - fn test_mint() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - ctx.set_owner(ACCOUNT_0); - - // and parameter. - let parameter = MintParams { - owner: ADDRESS_0, - }; - - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = State::empty(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_mint(&ctx, &mut host, &mut logger); - - // Check the result - claim!(result.is_ok(), "Results in rejection"); - - // Check the state - // Note. This is rather expensive as an iterator is created and then traversed - - // should be avoided when writing smart contracts. - claim_eq!(host.state().all_tokens.iter().count(), 1, "Expected one token in the state."); - - let balance0 = - host.state().balance(&TOKEN_1, &ADDRESS_0).expect_report("Token is expected to exist"); - claim_eq!(balance0, 1.into(), "Tokens should be owned by the given address 0"); - - // Check the logs - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::Mint(MintEvent { - owner: ADDRESS_0, - token_id: TOKEN_1, - amount: ContractTokenAmount::from(1), - }))), - "Expected an event for minting TOKEN_1" - ); - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::TokenMetadata::<_, ContractTokenAmount>( - TokenMetadataEvent { - token_id: TOKEN_1, - metadata_url: MetadataUrl { - url: TOKEN_METADATA_URL.to_string(), - hash: None, - }, - } - ))), - "Expected an event for token metadata for TOKEN_1" - ); - } - - /// Test transfer succeeds, when `from` is the sender. - #[concordium_test] - fn test_transfer_account() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - - // and parameter. - let transfer = Transfer { - token_id: TOKEN_1, - amount: ContractTokenAmount::from(1), - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let balance0 = - host.state().balance(&TOKEN_1, &ADDRESS_0).expect_report("Token is expected to exist"); - let balance1 = - host.state().balance(&TOKEN_1, &ADDRESS_1).expect_report("Token is expected to exist"); - let balance2 = - host.state().balance(&TOKEN_2, &ADDRESS_1).expect_report("Token is expected to exist"); - claim_eq!( - balance0, - 0.into(), - "Token owner balance should be decreased by the transferred amount" - ); - claim_eq!( - balance1, - 1.into(), - "Token receiver balance should be increased by the transferred amount" - ); - claim_eq!( - balance2, - 1.into(), - "Token receiver balance for token 1 should be the same as before" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "Only one event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::Transfer(TransferEvent { - from: ADDRESS_0, - to: ADDRESS_1, - token_id: TOKEN_1, - amount: ContractTokenAmount::from(1), - })), - "Incorrect event emitted" - ) - } - - /// Test transfer token fails, when sender is neither the owner or an - /// operator of the owner. - #[concordium_test] - fn test_transfer_not_authorized() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_1); - - // and parameter. - let transfer = Transfer { - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - token_id: TOKEN_1, - amount: ContractTokenAmount::from(1), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - // Check the result. - let err = result.expect_err_report("Expected to fail"); - claim_eq!(err, ContractError::Unauthorized, "Error is expected to be Unauthorized") - } - - /// Test transfer succeeds when sender is not the owner, but is an operator - /// of the owner. - #[concordium_test] - fn test_operator_transfer() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_1); - - // and parameter. - let transfer = Transfer { - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - token_id: TOKEN_1, - amount: ContractTokenAmount::from(1), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - - let mut state_builder = TestStateBuilder::new(); - let mut state = initial_state(&mut state_builder); - state.add_operator(&ADDRESS_0, &ADDRESS_1, &mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let balance0 = - host.state().balance(&TOKEN_1, &ADDRESS_0).expect_report("Token is expected to exist"); - let balance1 = host - .state_mut() - .balance(&TOKEN_1, &ADDRESS_1) - .expect_report("Token is expected to exist"); - claim_eq!( - balance0, - 0.into(), - "Token owner balance should be decreased by the transferred amount" - ); - claim_eq!( - balance1, - 1.into(), - "Token receiver balance should be increased by the transferred amount" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "Only one event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::Transfer(TransferEvent { - from: ADDRESS_0, - to: ADDRESS_1, - token_id: TOKEN_1, - amount: ContractTokenAmount::from(1), - })), - "Incorrect event emitted" - ) - } - - /// Test adding an operator succeeds and the appropriate event is logged. - #[concordium_test] - fn test_add_operator() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - - // and parameter. - let update = UpdateOperator { - update: OperatorUpdate::Add, - operator: ADDRESS_1, - }; - let parameter = UpdateOperatorParams(vec![update]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_update_operator(&ctx, &mut host, &mut logger); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let is_operator = host.state().is_operator(&ADDRESS_1, &ADDRESS_0); - claim!(is_operator, "Account should be an operator"); - - // Checking that `ADDRESS_1` is an operator in the query response of the - // `contract_operator_of` function as well. - // Setup parameter. - let operator_of_query = OperatorOfQuery { - address: ADDRESS_1, - owner: ADDRESS_0, - }; - - let operator_of_query_vector = OperatorOfQueryParams { - queries: vec![operator_of_query], - }; - let parameter_bytes = to_bytes(&operator_of_query_vector); - - ctx.set_parameter(¶meter_bytes); - - // Checking the return value of the `contract_operator_of` function - let result: ContractResult = contract_operator_of(&ctx, &host); - - claim_eq!( - result.expect_report("Failed getting result value").0, - [true], - "Account should be an operator in the query response" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "One event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::::UpdateOperator( - UpdateOperatorEvent { - owner: ADDRESS_0, - operator: ADDRESS_1, - update: OperatorUpdate::Add, - } - )), - "Incorrect event emitted" - ) - } - - /// Test `view` function. - #[concordium_test] - fn test_view() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - ctx.set_owner(ACCOUNT_0); - - let mut state_builder = TestStateBuilder::new(); - - // Create the state values - let mut nonces_registry = state_builder.new_map(); - nonces_registry.insert(ACCOUNT_1, 0); - nonces_registry.insert(ACCOUNT_2, 1); - - let mut all_tokens = state_builder.new_set(); - all_tokens.insert(TOKEN_1); - all_tokens.insert(TOKEN_2); - - let mut state = state_builder.new_map(); - let mut operators = state_builder.new_set(); - operators.insert(ADDRESS_2); - let mut owned_tokens = state_builder.new_set(); - owned_tokens.insert(TOKEN_1); - owned_tokens.insert(TOKEN_2); - state.insert(ADDRESS_1, AddressState { - owned_tokens, - operators, - }); - - let state = State { - token_id_counter: 2, - state, - all_tokens, - implementors: state_builder.new_map(), - nonces_registry, - }; - - let host = TestHost::new(state, state_builder); - - // Call the contract function. - let result = contract_view(&ctx, &host); - - let returned_view_state = result.expect_report("Failed getting contract_view result value"); - - claim_eq!( - returned_view_state.all_nonces, - [(ACCOUNT_1, 0), (ACCOUNT_2, 1)], - "Correct public keys should be returned by the view function" - ); - - claim_eq!( - returned_view_state.all_tokens, - vec![TOKEN_1, TOKEN_2], - "Correct tokens should be returned by the view function" - ); - - claim_eq!( - returned_view_state.state[0].0, - ADDRESS_1, - "Correct address in state should be returned by the view function" - ); - - claim_eq!( - returned_view_state.state[0].1, - ViewAddressState { - owned_tokens: vec![TOKEN_1, TOKEN_2], - operators: vec![concordium_std::Address::Account(ACCOUNT_2)], - }, - "Correct ViewAddressState should be returned by the view function" - ); - } -} diff --git a/examples/cis3-nft-sponsored-txs/tests/tests.rs b/examples/cis3-nft-sponsored-txs/tests/tests.rs new file mode 100644 index 00000000..87b68faa --- /dev/null +++ b/examples/cis3-nft-sponsored-txs/tests/tests.rs @@ -0,0 +1,337 @@ +//! Tests for the `cis3_nft_sponsored_txs` contract. +use cis3_nft_sponsored_txs::*; +use concordium_cis2::*; +use concordium_smart_contract_testing::*; + +/// The tests accounts. +const ALICE: AccountAddress = AccountAddress([0; 32]); +const ALICE_ADDR: Address = Address::Account(ALICE); +const BOB: AccountAddress = AccountAddress([1; 32]); +const BOB_ADDR: Address = Address::Account(BOB); + +/// Token IDs. +const TOKEN_0: ContractTokenId = TokenIdU32(1); +const TOKEN_1: ContractTokenId = TokenIdU32(2); + +/// Initial balance of the accounts. +const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10000); + +/// A signer for all the transactions. +const SIGNER: Signer = Signer::with_one_key(); + +/// Test minting succeeds and the tokens are owned by the given address and +/// the appropriate events are logged. +#[test] +fn test_minting() { + let (chain, contract_address, update) = initialize_contract_with_alice_tokens(); + + // Invoke the view entrypoint and check that the tokens are owned by Alice. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + + // Check that the tokens are owned by Alice. + let rv: ViewState = invoke.parse_return_value().expect("ViewState return value"); + assert_eq!(rv.all_tokens[..], [TOKEN_0, TOKEN_1]); + assert_eq!(rv.state, vec![(ALICE_ADDR, ViewAddressState { + owned_tokens: vec![TOKEN_0, TOKEN_1], + operators: Vec::new(), + })]); + + // Check that the events are logged. + let events = update.events().flat_map(|(_addr, events)| events); + + let events: Vec> = + events.map(|e| e.parse().expect("Deserialize event")).collect(); + + // Check that the correct events are logged. + // Note: this only looks at the second update event, which is why it only shows + // TOKEN_1. + assert_eq!(events, [ + Cis2Event::Mint(MintEvent { + token_id: TOKEN_1, + amount: TokenAmountU8(1), + owner: ALICE_ADDR, + }), + Cis2Event::TokenMetadata(TokenMetadataEvent { + token_id: TOKEN_1, + metadata_url: MetadataUrl { + url: TOKEN_METADATA_URL.to_string(), + hash: None, + }, + }), + ]); +} + +/// Test regular transfer where sender is the owner. +#[test] +fn test_account_transfer() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Transfer `TOKEN_0` from Alice to Bob. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_0, + amount: TokenAmountU8(1), + data: AdditionalData::empty(), + }]); + + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect("Transfer tokens"); + + // Check that Bob now has `TOKEN_0` and that Alice still has `TOKEN_1`. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + let rv: ViewState = invoke.parse_return_value().expect("ViewState return value"); + assert_eq!(rv.state, vec![ + (ALICE_ADDR, ViewAddressState { + owned_tokens: vec![TOKEN_1], + operators: Vec::new(), + }), + (BOB_ADDR, ViewAddressState { + owned_tokens: vec![TOKEN_0], + operators: Vec::new(), + }), + ]); + + // Check that a single transfer event occurred. + let events = update + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect::>>(); + assert_eq!(events, [Cis2Event::Transfer(TransferEvent { + token_id: TOKEN_0, + amount: TokenAmountU8(1), + from: ALICE_ADDR, + to: BOB_ADDR, + }),]); +} + +/// Test that you can add an operator. +/// Initialize the contract with two tokens owned by Alice. +/// Then add Bob as an operator for Alice. +#[test] +fn test_add_operator() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Add Bob as an operator for Alice. + let params = UpdateOperatorParams(vec![UpdateOperator { + update: OperatorUpdate::Add, + operator: BOB_ADDR, + }]); + + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.updateOperator".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶ms).expect("UpdateOperator params"), + }) + .expect("Update operator"); + + // Check that an operator event occurred. + let events = update + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect::>>(); + assert_eq!(events, [Cis2Event::UpdateOperator(UpdateOperatorEvent { + operator: BOB_ADDR, + owner: ALICE_ADDR, + update: OperatorUpdate::Add, + }),]); + + // Construct a query parameter to check whether Bob is an operator for Alice. + let query_params = OperatorOfQueryParams { + queries: vec![OperatorOfQuery { + owner: ALICE_ADDR, + address: BOB_ADDR, + }], + }; + + // Invoke the operatorOf view entrypoint and check that Bob is an operator for + // Alice. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.operatorOf".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&query_params).expect("OperatorOf params"), + }) + .expect("Invoke view"); + + let rv: OperatorOfQueryResponse = invoke.parse_return_value().expect("OperatorOf return value"); + assert_eq!(rv, OperatorOfQueryResponse(vec![true])); +} + +/// Test that a transfer fails when the sender is neither an operator or the +/// owner. In particular, Bob will attempt to transfer one of Alice's tokens to +/// himself. +#[test] +fn test_unauthorized_sender() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Construct a transfer of `TOKEN_0` from Alice to Bob, which will be submitted + // by Bob. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_0, + amount: TokenAmountU8(1), + data: AdditionalData::empty(), + }]); + + // Notice that Bob is the sender/invoker. + let update = chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect_err("Transfer tokens"); + + // Check that the correct error is returned. + let rv: ContractError = update.parse_return_value().expect("ContractError return value"); + assert_eq!(rv, ContractError::Unauthorized); +} + +/// Test that an operator can make a transfer. +#[test] +fn test_operator_can_transfer() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Add Bob as an operator for Alice. + let params = UpdateOperatorParams(vec![UpdateOperator { + update: OperatorUpdate::Add, + operator: BOB_ADDR, + }]); + chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.updateOperator".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶ms).expect("UpdateOperator params"), + }) + .expect("Update operator"); + + // Let Bob make a transfer to himself on behalf of Alice. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_0, + amount: TokenAmountU8(1), + data: AdditionalData::empty(), + }]); + + chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect("Transfer tokens"); + + // Check that Bob now has `TOKEN_0` and that Alice still has `TOKEN_1`. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + let rv: ViewState = invoke.parse_return_value().expect("ViewState return value"); + assert_eq!(rv.state, vec![ + (ALICE_ADDR, ViewAddressState { + owned_tokens: vec![TOKEN_1], + operators: vec![BOB_ADDR], + }), + (BOB_ADDR, ViewAddressState { + owned_tokens: vec![TOKEN_0], + operators: Vec::new(), + }), + ]); +} + +/// Helper function that sets up the contract with two tokens minted to +/// Alice, `TOKEN_0` and `TOKEN_1`. +/// +/// Only one token can be minted per update, so two updates are made. +/// This function returns the second mint update. +fn initialize_contract_with_alice_tokens() -> (Chain, ContractAddress, ContractInvokeSuccess) { + let (mut chain, contract_address) = initialize_chain_and_contract(); + + let mint_params = MintParams { + owner: ALICE_ADDR, + }; + + // Mint `TOKEN_0` to Alice. + chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.mint".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&mint_params).expect("Mint params"), + }) + .expect("Mint tokens"); + + // Mint `TOKEN_1` to Alice. + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis3_nft.mint".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&mint_params).expect("Mint params"), + }) + .expect("Mint tokens"); + + (chain, contract_address, update) +} + +/// Setup chain and contract. +/// +/// Also creates the two accounts, Alice and Bob. +/// +/// Alice is the owner of the contract. +fn initialize_chain_and_contract() -> (Chain, ContractAddress) { + let mut chain = Chain::new(); + + // Create some accounts accounts on the chain. + chain.create_account(Account::new(ALICE, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); + + // Load and deploy the module. + let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); + let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Deploy valid module"); + + // Initialize the auction contract. + let init = chain + .contract_init(SIGNER, ALICE, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_cis3_nft".to_string()), + param: OwnedParameter::empty(), + }) + .expect("Initialize contract"); + + (chain, init.contract_address) +} diff --git a/examples/counter-notify/Cargo.toml b/examples/counter-notify/Cargo.toml index 5f9ff705..2efbc854 100644 --- a/examples/counter-notify/Cargo.toml +++ b/examples/counter-notify/Cargo.toml @@ -15,5 +15,8 @@ wee_alloc = ["concordium-std/wee_alloc"] [dependencies] concordium-std = {path = "../../concordium-std", default-features = false} +[dev-dependencies] +concordium-smart-contract-testing = { path = "../../contract-testing" } + [lib] crate-type=["cdylib", "rlib"] diff --git a/examples/counter-notify/src/lib.rs b/examples/counter-notify/src/lib.rs index 0c0b6f53..24e63859 100644 --- a/examples/counter-notify/src/lib.rs +++ b/examples/counter-notify/src/lib.rs @@ -1,3 +1,13 @@ +//! An example of reentrancy attacks. +//! +//! Consists of two contracts: +//! - `counter-notify` +//! - A counter contract that also notifies some contract about increments. +//! - After the notification call it checks to see that its counter hasn't +//! been altered. +//! - `reentrancy-attacker` +//! - A contract that tries to make an reentrancy attack on the +//! `counter-notify` contract. #![cfg_attr(not(feature = "std"), no_std)] use concordium_std::*; @@ -5,27 +15,23 @@ type State = u64; #[init(contract = "counter-notify")] #[inline(always)] -fn contract_init( - _ctx: &impl HasInitContext, - _state_builder: &mut StateBuilder, -) -> InitResult { +fn contract_init(_ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult { Ok(0u64) } #[receive(contract = "counter-notify", name = "just-increment", mutable)] -fn just_increment( - _ctx: &impl HasReceiveContext, - host: &mut impl HasHost, -) -> ReceiveResult<()> { +fn just_increment(_ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { *host.state_mut() += 1; Ok(()) } -#[receive(contract = "counter-notify", name = "increment-and-notify", mutable)] -fn increment_and_notify( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, -) -> ReceiveResult { +#[receive( + contract = "counter-notify", + name = "increment-and-notify", + mutable, + parameter = "(ContractAddress, OwnedEntrypointName)" +)] +fn increment_and_notify(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult { let (contract, entrypoint): (ContractAddress, OwnedEntrypointName) = ctx.parameter_cursor().get()?; @@ -46,39 +52,29 @@ fn increment_and_notify( Ok(preinvoke_count == *host.state()) } -#[concordium_cfg_test] -mod tests { - use super::*; - use concordium_std::claim_eq; - use test_infrastructure::*; +//////////////////////////////////////////////////////////////////////////////////////////////// - #[concordium_test] - fn state_can_change_due_to_reentrancy() { - let mut ctx = TestReceiveContext::empty(); - let self_address = ContractAddress { - index: 0, - subindex: 0, - }; - let entrypoint_just_increment = OwnedEntrypointName::new_unchecked("just-increment".into()); - let parameter_bytes = to_bytes(&(self_address, entrypoint_just_increment.clone())); - ctx.set_parameter(¶meter_bytes); - ctx.set_self_address(self_address); - - let mut host = TestHost::new(0u64, TestStateBuilder::new()); - - // We are simulating reentrancy with this mock because we mutate the state. - host.setup_mock_entrypoint( - self_address, - entrypoint_just_increment, - MockFn::new_v1(|_parameter, _amount, _balance, state: &mut State| { - *state += 1; - Ok((true, ())) - }), - ); - - let res = increment_and_notify(&ctx, &mut host).expect_report("Calling receive failed."); +#[init(contract = "reentrancy-attacker")] +fn reentrancy_init(_ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult<()> { + Ok(()) +} - // The count - claim_eq!(res, false); +/// Tries to call the entrypoint `just-increment` on the sender iff it is a +/// contract. Fails if the sender is an account or the `just-increment` call +/// fails. +#[receive(contract = "reentrancy-attacker", name = "call-just-increment", mutable)] +fn reentrancy_receive(ctx: &ReceiveContext, host: &mut Host<()>) -> ReceiveResult<()> { + match ctx.sender() { + Address::Account(_) => fail!(), + Address::Contract(contract) => { + host.invoke_contract( + &contract, + &(), + EntrypointName::new_unchecked("just-increment"), + Amount::zero(), + ) + .unwrap(); + Ok(()) + } } } diff --git a/examples/counter-notify/tests/tests.rs b/examples/counter-notify/tests/tests.rs new file mode 100644 index 00000000..d100b87a --- /dev/null +++ b/examples/counter-notify/tests/tests.rs @@ -0,0 +1,115 @@ +use concordium_smart_contract_testing::*; + +const ACC_0: AccountAddress = AccountAddress([0u8; 32]); +const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(1000); +const SIGNER: Signer = Signer::with_one_key(); + +/// Test that the reentrancy attack occurs and is caught by the `counter-notify` +/// contract. +#[test] +fn tests() { + // Create the test chain. + let mut chain = Chain::new(); + + // Create one account on the chain. + chain.create_account(Account::new(ACC_0, ACC_INITIAL_BALANCE)); + + // Load and deploy the module. + let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); + let deployment = chain.module_deploy_v1(SIGNER, ACC_0, module).expect("Deploy valid module"); + + // Initialize the contract. + let init_counter = chain + .contract_init(SIGNER, ACC_0, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_counter-notify".to_string()), + param: OwnedParameter::empty(), + }) + .expect("Init of counter-notify contract should succeed"); + let init_reentrancy = chain + .contract_init(SIGNER, ACC_0, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_reentrancy-attacker".to_string()), + param: OwnedParameter::empty(), + }) + .expect("Init of reentrancy-attacker should succeed"); + + let update = chain + .contract_update( + SIGNER, + ACC_0, + Address::Account(ACC_0), + Energy::from(5000), + UpdateContractPayload { + amount: Amount::zero(), + address: init_counter.contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "counter-notify.increment-and-notify".to_string(), + ), + message: OwnedParameter::from_serial(&( + init_reentrancy.contract_address, + EntrypointName::new_unchecked("call-just-increment"), + )) + .expect("Serialize account address."), + }, + ) + .expect("Updating contract"); + + use concordium_smart_contract_testing::ContractTraceElement::*; + + // Check that the correct calls and events occured. + assert!(matches!(update.effective_trace_elements().collect::>()[..], [ + Interrupted { + address: ContractAddress { + index: 0, + subindex: 0, + }, + .. + }, + Interrupted { + address: ContractAddress { + index: 1, + subindex: 0, + }, + .. + }, + Updated { + data: InstanceUpdatedEvent { + receive_name: rcv_name_1, + .. + }, + }, + Resumed { + address: ContractAddress { + index: 1, + subindex: 0, + }, + success: true, + }, + Updated { + data: InstanceUpdatedEvent { + receive_name: rcv_name_2, + .. + }, + }, + Resumed { + address: ContractAddress { + index: 0, + subindex: 0, + }, + success: true, + }, + Updated { + data: InstanceUpdatedEvent { + receive_name: rcv_name_3, + .. + }, + }, + ] if rcv_name_1 == "counter-notify.just-increment" && rcv_name_2 == "reentrancy-attacker.call-just-increment" && rcv_name_3 == "counter-notify.increment-and-notify")); + + // Check that the contract observed the reentrancy attack. + let rv: bool = update.parse_return_value().unwrap(); + assert!(rv, "Re-entrancy attack not observed."); +} diff --git a/examples/credential-registry/Cargo.toml b/examples/credential-registry/Cargo.toml index 230e55af..c290c00d 100644 --- a/examples/credential-registry/Cargo.toml +++ b/examples/credential-registry/Cargo.toml @@ -15,10 +15,13 @@ wee_alloc = ["concordium-std/wee_alloc"] crypto-primitives = ["concordium-std/crypto-primitives"] [dependencies] -concordium-std = {path = "../../concordium-std", version = "8.0", default-features = false, features = ["concordium-quickcheck"]} -concordium-cis2 = {path = "../../concordium-cis2", version = "5.0", default-features = false} +concordium-std = {path = "../../concordium-std", version = "8.1", default-features = false, features = ["concordium-quickcheck"]} +concordium-cis2 = {path = "../../concordium-cis2", version = "5.1", default-features = false} quickcheck = {version = "1"} +[dev-dependencies] +concordium-smart-contract-testing = {path = "../../contract-testing"} + [lib] crate-type=["cdylib", "rlib"] diff --git a/examples/credential-registry/README.md b/examples/credential-registry/README.md index 025b317a..c5021899 100644 --- a/examples/credential-registry/README.md +++ b/examples/credential-registry/README.md @@ -5,7 +5,7 @@ part of verifiable credentials (VCs). The contract follows CIS-4: Credential Registry Standard. The contract keeps track of credentials' public data, allows managing the -VC life cycle. and querying VCs data and status. The intended users are +VC life cycle, and querying VCs data and status. The intended users are issuers of VCs, holders of VCs, revocation authorities, and verifiers. When initializing a contract, the issuer provides a type and a schema @@ -44,4 +44,3 @@ private key. - view credential status to verify VC validity; - view credential data to verify proofs (verifiable presentations) requested from holders. - \ No newline at end of file diff --git a/examples/credential-registry/src/lib.rs b/examples/credential-registry/src/lib.rs index eab8c7e1..d7fcbe6a 100644 --- a/examples/credential-registry/src/lib.rs +++ b/examples/credential-registry/src/lib.rs @@ -7,22 +7,22 @@ pub const CIS4_STANDARD_IDENTIFIER: StandardIdentifier<'static> = StandardIdentifier::new_unchecked("CIS-4"); /// List of supported standards by this contract address. -const SUPPORTS_STANDARDS: [StandardIdentifier<'static>; 2] = +pub const SUPPORTS_STANDARDS: [StandardIdentifier<'static>; 2] = [CIS0_STANDARD_IDENTIFIER, CIS4_STANDARD_IDENTIFIER]; /// Credential type is a string that corresponds to the value of the "name" /// attribute of the JSON credential schema. #[derive(Serialize, SchemaType, PartialEq, Eq, Clone, Debug)] -struct CredentialType { +pub struct CredentialType { #[concordium(size_length = 1)] - credential_type: String, + pub credential_type: String, } /// A schema reference is a schema URL pointing to the JSON /// schema for a verifiable credential. #[derive(Serialize, SchemaType, PartialEq, Eq, Clone, Debug)] -struct SchemaRef { - schema_ref: MetadataUrl, +pub struct SchemaRef { + pub schema_ref: MetadataUrl, } impl From for SchemaRef { @@ -47,22 +47,22 @@ pub enum CredentialStatus { pub struct CredentialEntry { /// If this flag is set to `true` the holder can send a signed message to /// revoke their credential. - holder_revocable: bool, + pub holder_revocable: bool, /// The date from which the credential is considered valid. - valid_from: Timestamp, + pub valid_from: Timestamp, /// After this date, the credential becomes expired. `None` corresponds to a /// credential that cannot expire. - valid_until: Option, + pub valid_until: Option, /// The nonce is used to avoid replay attacks when checking the holder's /// signature on a revocation message. - revocation_nonce: u64, + pub revocation_nonce: u64, /// Revocation flag - revoked: bool, + pub revoked: bool, /// Metadata URL of the credential (not to be confused with the metadata URL /// of the **issuer**). /// This data is only needed when credential info is requested. In other /// operations, `StateBox` defers loading the metadata url. - metadata_url: StateBox, + pub metadata_url: StateBox, } impl CredentialEntry { @@ -126,7 +126,7 @@ pub struct State { /// Contract Errors. #[derive(Debug, PartialEq, Eq, Reject, Serial, SchemaType)] -enum ContractError { +pub enum ContractError { #[from(ParseError)] ParseParamsError, CredentialNotFound, @@ -164,11 +164,11 @@ impl From for ContractError { } } -type ContractResult = Result; +pub type ContractResult = Result; /// Credentials are identified by a holder's public key. /// Each time a credential is issued, a fresh key pair is generated. -type CredentialHolderId = PublicKeyEd25519; +pub type CredentialHolderId = PublicKeyEd25519; /// Functions for creating, updating and querying the contract state. impl State { @@ -320,22 +320,22 @@ impl State { /// Data for events of registering and updating a credential. /// Used by the tagged event `CredentialEvent`. -#[derive(Serialize, SchemaType)] -struct CredentialEventData { +#[derive(Debug, PartialEq, Eq, Serialize, SchemaType)] +pub struct CredentialEventData { /// A public key of the credential's holder. - holder_id: PublicKeyEd25519, + pub holder_id: PublicKeyEd25519, /// A reference to the credential JSON schema. - schema_ref: SchemaRef, + pub schema_ref: SchemaRef, /// Type of the credential. - credential_type: CredentialType, + pub credential_type: CredentialType, /// The original credential's metadata. - metadata_url: MetadataUrl, + pub metadata_url: MetadataUrl, } /// A type for specifying who is revoking a credential, when registering a /// revocation event. -#[derive(Serialize, SchemaType)] -enum Revoker { +#[derive(Debug, PartialEq, Eq, Serialize, SchemaType)] +pub enum Revoker { Issuer, Holder, /// `Other` is used for the cases when the revoker is not the issuer or @@ -347,10 +347,10 @@ enum Revoker { /// A short comment on a reason of revoking or restoring a credential. /// The string is of a limited size of 256 bytes in order to fit into a single /// log entry along with other data. -#[derive(Serialize, SchemaType, Clone)] -struct Reason { +#[derive(PartialEq, Eq, Debug, Serialize, SchemaType, Clone)] +pub struct Reason { #[concordium(size_length = 1)] - reason: String, + pub reason: String, } impl From for Reason { @@ -363,45 +363,45 @@ impl From for Reason { /// An untagged revocation event. /// For a tagged version use `CredentialEvent`. -#[derive(Serialize, SchemaType)] -struct RevokeCredentialEvent { +#[derive(Debug, PartialEq, Eq, Serialize, SchemaType)] +pub struct RevokeCredentialEvent { /// A public key of the credential's holder. - holder_id: CredentialHolderId, + pub holder_id: CredentialHolderId, /// Who revokes the credential. - revoker: Revoker, + pub revoker: Revoker, /// An optional text clarifying the revocation reasons. /// The issuer can use this field to comment on the revocation, so the /// holder can observe it in the wallet. - reason: Option, + pub reason: Option, } /// An untagged restoration event. /// For a tagged version use `CredentialEvent`. -#[derive(Serialize, SchemaType)] -struct RestoreCredentialEvent { +#[derive(Debug, PartialEq, Eq, Serialize, SchemaType)] +pub struct RestoreCredentialEvent { /// A public key of the credential's holder. - holder_id: CredentialHolderId, + pub holder_id: CredentialHolderId, /// An optional text clarifying the restoring reasons. - reason: Option, + pub reason: Option, } /// An untagged credential metadata event. Emitted when updating the credential /// metadata. For a tagged version use `CredentialEvent`. -#[derive(Serialize, SchemaType)] -struct CredentialMetadataEvent { - credential_id: CredentialHolderId, - metadata_url: MetadataUrl, +#[derive(Debug, PartialEq, Eq, Serialize, SchemaType)] +pub struct CredentialMetadataEvent { + pub credential_id: CredentialHolderId, + pub metadata_url: MetadataUrl, } /// The schema reference has been updated for the credential type. -#[derive(Serialize, SchemaType)] -struct CredentialSchemaRefEvent { - credential_type: CredentialType, - schema_ref: SchemaRef, +#[derive(Debug, PartialEq, Eq, Serialize, SchemaType)] +pub struct CredentialSchemaRefEvent { + pub credential_type: CredentialType, + pub schema_ref: SchemaRef, } -#[derive(Serialize, SchemaType)] -enum RevocationKeyAction { +#[derive(Debug, PartialEq, Eq, Serialize, SchemaType)] +pub enum RevocationKeyAction { Register, Remove, } @@ -409,17 +409,18 @@ enum RevocationKeyAction { /// An untagged revocation key event. /// Emitted when keys are registered and removed. /// For a tagged version use `CredentialEvent`. -#[derive(Serialize, SchemaType)] -struct RevocationKeyEvent { +#[derive(Debug, PartialEq, Eq, Serialize, SchemaType)] +pub struct RevocationKeyEvent { /// The public key that is registered/removed - key: PublicKeyEd25519, + pub key: PublicKeyEd25519, /// A register/remove action. - action: RevocationKeyAction, + pub action: RevocationKeyAction, } /// Tagged credential registry event. /// This version should be used for logging the events. -enum CredentialEvent { +#[derive(Debug, PartialEq, Eq)] +pub enum CredentialEvent { /// Credential registration event. Logged when an entry in the registry is /// created for the first time. Register(CredentialEventData), @@ -573,19 +574,19 @@ impl Deserial for CredentialEvent { #[derive(Serialize, SchemaType)] pub struct InitParams { /// The issuer's metadata. - issuer_metadata: MetadataUrl, + pub issuer_metadata: MetadataUrl, /// The type of credentials for this registry. - credential_type: CredentialType, + pub credential_type: CredentialType, /// The credential schema for this registry. - schema: SchemaRef, + pub schema: SchemaRef, /// The issuer for the registry. If `None`, the `init_origin` is used as /// `issuer`. - issuer_account: Option, + pub issuer_account: Option, /// The issuer's public key. - issuer_key: PublicKeyEd25519, + pub issuer_key: PublicKeyEd25519, /// Revocation keys available right after initialization. #[concordium(size_length = 1)] - revocation_keys: Vec, + pub revocation_keys: Vec, } /// Init function that creates a fresh registry state given the required @@ -645,42 +646,42 @@ fn sender_is_issuer(ctx: &impl HasReceiveContext, state: &State< #[derive(Serialize, SchemaType, PartialEq, Eq, Clone, Debug)] pub struct CredentialInfo { /// The holder's identifier is a public key. - holder_id: CredentialHolderId, + pub holder_id: CredentialHolderId, /// If this flag is set to `true` the holder can send a signed message to /// revoke their credential. - holder_revocable: bool, + pub holder_revocable: bool, /// The date from which the credential is considered valid. - valid_from: Timestamp, + pub valid_from: Timestamp, /// After this date, the credential becomes expired. `None` corresponds to a /// credential that cannot expire. - valid_until: Option, + pub valid_until: Option, /// Link to the metadata of this credential. - metadata_url: MetadataUrl, + pub metadata_url: MetadataUrl, } /// Parameters for registering a credential #[derive(Serialize, SchemaType, Clone, Debug)] pub struct RegisterCredentialParam { /// Public credential data. - credential_info: CredentialInfo, + pub credential_info: CredentialInfo, /// Any additional data required by the issuer in the registration process. /// This data is not used in this contract. However, it is part of the CIS-4 /// standard that this contract implements; `auxiliary_data` can be /// used, for example, to implement signature-based authentication. #[concordium(size_length = 2)] - auxiliary_data: Vec, + pub auxiliary_data: Vec, } /// Response to a credential data query. #[derive(Serialize, SchemaType, Clone, Debug)] pub struct CredentialQueryResponse { - credential_info: CredentialInfo, + pub credential_info: CredentialInfo, /// A schema URL pointing to the JSON schema for a verifiable /// credential. - schema_ref: SchemaRef, + pub schema_ref: SchemaRef, /// The nonce is used to avoid replay attacks when checking the holder's /// signature on a revocation message. - revocation_nonce: u64, + pub revocation_nonce: u64, } /// A view entrypoint for looking up an entry in the registry by id. @@ -766,26 +767,26 @@ fn contract_register_credential( /// Metadata of the signature. #[derive(Serialize, SchemaType, Clone)] -struct SigningData { +pub struct SigningData { /// The contract_address that the signature is intended for. - contract_address: ContractAddress, + pub contract_address: ContractAddress, /// The entry_point that the signature is intended for. - entry_point: OwnedEntrypointName, + pub entry_point: OwnedEntrypointName, /// A nonce to prevent replay attacks. - nonce: u64, + pub nonce: u64, /// A timestamp to make signatures expire. - timestamp: Timestamp, + pub timestamp: Timestamp, } /// A message prefix for revocation requests by holders and revocation /// authorities. -const SIGNARUTE_DOMAIN: &str = "WEB3ID:REVOKE"; +pub const SIGNARUTE_DOMAIN: &str = "WEB3ID:REVOKE"; /// A parameter type for revoking a credential by the holder. #[derive(Serialize, SchemaType)] pub struct RevokeCredentialHolderParam { - signature: SignatureEd25519, - data: RevocationDataHolder, + pub signature: SignatureEd25519, + pub data: RevocationDataHolder, } /// Prepare the message bytes for the holder @@ -800,11 +801,11 @@ impl RevokeCredentialHolderParam { #[derive(Serialize, SchemaType)] pub struct RevocationDataHolder { /// Id of the credential to revoke. - credential_id: CredentialHolderId, + pub credential_id: CredentialHolderId, /// Info about the signature. - signing_data: SigningData, + pub signing_data: SigningData, /// (Optional) reason for revoking the credential. - reason: Option, + pub reason: Option, } /// Helper function that can be invoked at the front end to serialize @@ -830,22 +831,22 @@ fn contract_serialization_helper_holder_revoke( #[derive(Serialize, SchemaType)] pub struct RevokeCredentialIssuerParam { /// Id of the credential to revoke. - credential_id: CredentialHolderId, + pub credential_id: CredentialHolderId, /// (Optional) reason for revoking the credential. - reason: Option, + pub reason: Option, /// Any additional data required by the issuer in the registration process. /// This data is not used in this contract. However, it is part of the CIS-4 /// standard that this contract implements; `auxiliary_data` can be /// used, for example, to implement signature-based authentication. #[concordium(size_length = 2)] - auxiliary_data: Vec, + pub auxiliary_data: Vec, } /// A parameter type for revoking a credential by a revocation authority. #[derive(Serialize, SchemaType)] pub struct RevokeCredentialOtherParam { - signature: SignatureEd25519, - data: RevocationDataOther, + pub signature: SignatureEd25519, + pub data: RevocationDataOther, } impl RevokeCredentialOtherParam { @@ -858,13 +859,13 @@ impl RevokeCredentialOtherParam { #[derive(Serialize, SchemaType)] pub struct RevocationDataOther { /// Id of the credential to revoke. - credential_id: CredentialHolderId, + pub credential_id: CredentialHolderId, /// Info about the signature. - signing_data: SigningData, + pub signing_data: SigningData, /// The key with which the revocation payload is signed. - revocation_key: PublicKeyEd25519, + pub revocation_key: PublicKeyEd25519, /// (Optional) reason for revoking the credential. - reason: Option, + pub reason: Option, } /// Helper function that can be invoked at the front end to serialize @@ -1276,13 +1277,13 @@ fn contract_revocation_keys( /// A response type for the registry metadata request. #[derive(Serialize, SchemaType)] -struct MetadataResponse { +pub struct MetadataResponse { /// A reference to the issuer's metadata. - issuer_metadata: MetadataUrl, + pub issuer_metadata: MetadataUrl, /// The type of credentials used. - credential_type: CredentialType, + pub credential_type: CredentialType, /// A reference to the JSON schema corresponding to this type. - credential_schema: SchemaRef, + pub credential_schema: SchemaRef, } /// A view entrypoint to get the registry metadata. @@ -1385,11 +1386,11 @@ fn contract_update_credential_schema( } #[derive(Serialize, SchemaType)] -struct CredentialMetadataParam { +pub struct CredentialMetadataParam { /// The id of the credential to update. - credential_id: CredentialHolderId, + pub credential_id: CredentialHolderId, /// The new metadata URL. - metadata_url: MetadataUrl, + pub metadata_url: MetadataUrl, } /// Update existing credential metadata URL. @@ -1436,9 +1437,9 @@ fn contract_update_credential_metadata( #[derive(Serialize, SchemaType)] pub struct RestoreCredentialIssuerParam { /// Id of the credential to restore. - credential_id: CredentialHolderId, + pub credential_id: CredentialHolderId, /// (Optional) reason for restoring the credential. - reason: Option, + pub reason: Option, } /// Restore credential by the issuer. @@ -1525,11 +1526,11 @@ fn contract_supports( /// Takes a standard identifier and list of contract addresses providing /// implementations of this standard. #[derive(Debug, Serialize, SchemaType)] -struct SetImplementorsParams { +pub struct SetImplementorsParams { /// The identifier for the standard. - id: StandardIdentifierOwned, + pub id: StandardIdentifierOwned, /// The addresses of the implementors of the standard. - implementors: Vec, + pub implementors: Vec, } /// Set the addresses for an implementation given a standard identifier and a @@ -1564,11 +1565,11 @@ fn contract_set_implementor( /// fails. This is useful for doing migration in the same transaction triggering /// the upgrade. #[derive(Debug, Serialize, SchemaType)] -struct UpgradeParams { +pub struct UpgradeParams { /// The new module reference. - module: ModuleReference, + pub module: ModuleReference, /// Optional entrypoint to call in the new module after upgrade. - migrate: Option<(OwnedEntrypointName, OwnedParameter)>, + pub migrate: Option<(OwnedEntrypointName, OwnedParameter)>, } /// Mapping errors related to contract invocations to ContractError. @@ -1636,6 +1637,7 @@ fn contract_upgrade( } #[concordium_cfg_test] +#[allow(deprecated)] mod tests { use super::*; @@ -1697,24 +1699,13 @@ mod tests { const ISSUER_ACCOUNT: AccountAddress = AccountAddress([0u8; 32]); const ISSUER_METADATA_URL: &str = "https://example-university.com/university.json"; - const CREDANIAL_METADATA_URL: &str = + const CREDENTIAL_METADATA_URL: &str = "https://example-university.com/diplomas/university-vc-metadata.json"; - const CREDENTIAL_TYPE: &str = "UniversityDegreeCredential"; - const CREDENTIAL_SCHEMA_URL: &str = - "https://credentials-schemas.com/JsonSchema2023-education-certificate.json"; - const ACCOUNT_0: AccountAddress = AccountAddress([0u8; 32]); - const ADDRESS_0: Address = Address::Account(ACCOUNT_0); // Seed: 2FEE333FAD122A45AAB7BEB3228FA7858C48B551EA8EBC49D2D56E2BA22049FF const PUBLIC_KEY: PublicKeyEd25519 = PublicKeyEd25519([ 172, 5, 96, 236, 139, 208, 146, 88, 124, 42, 62, 124, 86, 108, 35, 242, 32, 11, 7, 48, 193, 61, 177, 220, 104, 169, 145, 4, 8, 1, 236, 112, ]); - const SIGNATURE: SignatureEd25519 = SignatureEd25519([ - 254, 138, 58, 131, 209, 45, 191, 52, 98, 228, 26, 234, 155, 245, 244, 226, 0, 153, 104, - 111, 201, 136, 243, 167, 251, 116, 110, 206, 172, 223, 41, 180, 90, 22, 63, 43, 157, 129, - 226, 75, 49, 33, 155, 76, 160, 133, 127, 146, 150, 80, 199, 201, 80, 98, 179, 43, 46, 46, - 211, 222, 185, 216, 12, 4, - ]); /// A helper that returns a credential that is not revoked, cannot expire /// and is immediately activated. It is also possible to revoke it by the @@ -1722,7 +1713,7 @@ mod tests { fn credential_entry(state_builder: &mut StateBuilder) -> CredentialEntry { CredentialEntry { metadata_url: state_builder.new_box(MetadataUrl { - url: CREDANIAL_METADATA_URL.into(), + url: CREDENTIAL_METADATA_URL.into(), hash: None, }), valid_from: Timestamp::from_timestamp_millis(0), @@ -1740,82 +1731,6 @@ mod tests { } } - fn get_credential_schema() -> (CredentialType, SchemaRef) { - ( - CredentialType { - credential_type: CREDENTIAL_TYPE.to_string(), - }, - SchemaRef { - schema_ref: MetadataUrl { - url: CREDENTIAL_SCHEMA_URL.to_string(), - hash: None, - }, - }, - ) - } - - #[concordium_test] - /// Test that initializing the contract succeeds with some state. - fn test_init() { - let mut ctx = TestInitContext::empty(); - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - - ctx.set_init_origin(ISSUER_ACCOUNT); - - let schema = get_credential_schema(); - - let parameter_bytes = to_bytes(&InitParams { - issuer_metadata: issuer_metadata(), - issuer_account: ISSUER_ACCOUNT.into(), - issuer_key: PUBLIC_KEY, - revocation_keys: vec![PUBLIC_KEY], - credential_type: schema.0.clone(), - schema: schema.1.clone(), - }); - ctx.set_parameter(¶meter_bytes); - - let state_result = init(&ctx, &mut state_builder, &mut logger); - let state = state_result.expect_report("Contract initialization results in an error"); - - // Check that the initial parameters are in the state. - claim_eq!(state.credential_schema, schema.1, "Incorrect schema in the state"); - claim_eq!(state.issuer_account, ISSUER_ACCOUNT, "Incorrect issuer in the state"); - claim_eq!( - state.issuer_metadata, - issuer_metadata(), - "Incorrect issuer metadata in the state" - ); - - // Check that the correct events were logged. - - claim_eq!(logger.logs.len(), 3, "Incorrect number of logged events"); - - claim_eq!( - logger.logs[0], - to_bytes(&CredentialEvent::IssuerMetadata(issuer_metadata())), - "Incorrect issuer metadata event logged" - ); - - claim_eq!( - logger.logs[1], - to_bytes(&CredentialEvent::RevocationKey(RevocationKeyEvent { - key: PUBLIC_KEY, - action: RevocationKeyAction::Register, - })), - "Incorrect revocation key event logged" - ); - - claim_eq!( - logger.logs[2], - to_bytes(&CredentialEvent::Schema(CredentialSchemaRefEvent { - credential_type: schema.0, - schema_ref: schema.1, - })), - "Incorrect schema event logged" - ); - } - /// Not expired and not revoked credential is `Active` #[concordium_test] fn test_get_status_active() { @@ -2009,266 +1924,4 @@ mod tests { false } } - - /// Test the credential registration entrypoint. - #[concordium_test] - fn test_contract_register_credential() { - let now = Timestamp::from_timestamp_millis(0); - let contract = ContractAddress { - index: 0, - subindex: 0, - }; - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - ctx.set_owner(ACCOUNT_0); - ctx.set_self_address(contract); - ctx.set_metadata_slot_time(now); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let (credential_type, schema_ref) = get_credential_schema(); - let state = State::new( - &mut state_builder, - ISSUER_ACCOUNT, - PUBLIC_KEY, - issuer_metadata(), - credential_type.clone(), - schema_ref.clone(), - ); - let mut host = TestHost::new(state, state_builder); - - let entry = credential_entry(host.state_builder()); - - // Create input parameters. - - let param = RegisterCredentialParam { - credential_info: entry.info(PUBLIC_KEY), - auxiliary_data: Vec::new(), - }; - let parameter_bytes = to_bytes(¶m); - ctx.set_parameter(¶meter_bytes); - - // Create a credential - let res = contract_register_credential(&ctx, &mut host, &mut logger); - - // Check that it was registered successfully - claim!(res.is_ok(), "Credential registration failed: {:?}", res); - let fetched: CredentialQueryResponse = host - .state() - .view_credential_info(PUBLIC_KEY) - .expect_report("Credential is expected to exist"); - claim_eq!( - fetched.credential_info, - entry.info(PUBLIC_KEY), - "Credential info expected to be equal" - ); - claim_eq!(fetched.revocation_nonce, 0, "Revocation nonce expected to be 0"); - - let status = host - .state() - .view_credential_status(now, PUBLIC_KEY) - .expect_report("Status query expected to succeed"); - claim_eq!(status, CredentialStatus::Active); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "One event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&CredentialEvent::Register(CredentialEventData { - holder_id: PUBLIC_KEY, - schema_ref, - credential_type, - metadata_url: MetadataUrl { - url: CREDANIAL_METADATA_URL.into(), - hash: None, - }, - })), - "Incorrect register credential event logged" - ); - } - - /// Test the revoke credential entrypoint, when the holder revokes the - /// credential. - #[concordium_test] - fn test_revoke_by_holder() { - let now = Timestamp::from_timestamp_millis(0); - let contract = ContractAddress { - index: 0, - subindex: 0, - }; - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - ctx.set_owner(ISSUER_ACCOUNT); - ctx.set_invoker(ISSUER_ACCOUNT); - ctx.set_self_address(contract); - ctx.set_named_entrypoint(OwnedEntrypointName::new_unchecked( - "revokeCredentialHolder".into(), - )); - ctx.set_metadata_slot_time(now); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let (credential_type, schema_ref) = get_credential_schema(); - let state = State::new( - &mut state_builder, - ISSUER_ACCOUNT, - PUBLIC_KEY, - issuer_metadata(), - credential_type, - schema_ref, - ); - let mut host = TestHost::new(state, state_builder); - - let (state, state_builder) = host.state_and_builder(); - let entry = credential_entry(state_builder); - let credential_info = entry.info(PUBLIC_KEY); - - claim!( - credential_info.holder_revocable, - "Initial credential expected to be holder-revocable" - ); - - // Create a credential the holder is going to revoke - let res = state.register_credential(&credential_info, state_builder); - - // Check that it was registered successfully - claim!(res.is_ok(), "Credential registration failed"); - - // Create singing data - let signing_data = SigningData { - contract_address: contract, - entry_point: OwnedEntrypointName::new_unchecked("revokeCredentialHolder".into()), - nonce: 0, - timestamp: Timestamp::from_timestamp_millis(10000000000), - }; - - // Create input parameters for revocation. - - let revocation_reason = "Just because"; - - let revoke_param = RevokeCredentialHolderParam { - signature: SIGNATURE, - data: RevocationDataHolder { - credential_id: PUBLIC_KEY, - signing_data, - reason: Some(revocation_reason.to_string().into()), - }, - }; - - let parameter_bytes = to_bytes(&revoke_param); - ctx.set_parameter(¶meter_bytes); - - let crypto_primitives = TestCryptoPrimitives::new(); - // Inovke `permit` function. - let result: ContractResult<()> = - contract_revoke_credential_holder(&ctx, &mut host, &mut logger, &crypto_primitives); - - // Check the result. - claim!(result.is_ok(), "Results in rejection: {:?}", result); - - // Check the status. - let status = host - .state() - .view_credential_status(now, PUBLIC_KEY) - .expect_report("Credential is expected to exist"); - claim_eq!(status, CredentialStatus::Revoked); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "One event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&CredentialEvent::Revoke(RevokeCredentialEvent { - holder_id: PUBLIC_KEY, - revoker: Revoker::Holder, - reason: Some(revocation_reason.to_string().into()), - })), - "Incorrect revoke credential event logged" - ); - } - - /// Test the restore credential entrypoint. - #[concordium_test] - fn test_contract_restore_credential() { - let now = Timestamp::from_timestamp_millis(0); - let contract = ContractAddress { - index: 0, - subindex: 0, - }; - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - ctx.set_owner(ACCOUNT_0); - ctx.set_self_address(contract); - ctx.set_metadata_slot_time(now); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let (credential_type, schema_ref) = get_credential_schema(); - let state = State::new( - &mut state_builder, - ISSUER_ACCOUNT, - PUBLIC_KEY, - issuer_metadata(), - credential_type, - schema_ref, - ); - let mut host = TestHost::new(state, state_builder); - - let (state, state_builder) = host.state_and_builder(); - let entry = credential_entry(state_builder); - let credential_info = entry.info(PUBLIC_KEY); - - // Create a credential the issuer is going to restore - let res = state.register_credential(&credential_info, state_builder); - - // Check that it was registered successfully - claim!(res.is_ok(), "Credential registration failed"); - - // Make sure the credential has the `Revoked` status - let revoke_res = state.revoke_credential(now, PUBLIC_KEY); - - // Check that the credential was revoked successfully. - claim!(revoke_res.is_ok(), "Credential revocation failed"); - - // Create input parameters. - - let param = RestoreCredentialIssuerParam { - credential_id: PUBLIC_KEY, - reason: None, - }; - let parameter_bytes = to_bytes(¶m); - ctx.set_parameter(¶meter_bytes); - - // Check the status before restoring. - let status = host - .state() - .view_credential_status(now, PUBLIC_KEY) - .expect_report("Credential is expected to exist"); - claim_eq!(status, CredentialStatus::Revoked, "Expected Revoked"); - - // Call the restore credential entrypoint - let res = contract_restore_credential(&ctx, &mut host, &mut logger); - - // Check that it was restored succesfully - claim!(res.is_ok(), "Credential restoring failed"); - // Check the status after restoring. - let status = host - .state() - .view_credential_status(now, PUBLIC_KEY) - .expect_report("Credential is expected to exist"); - claim_eq!(status, CredentialStatus::Active, "Expected Active"); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "One event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&CredentialEvent::Restore(RestoreCredentialEvent { - holder_id: PUBLIC_KEY, - reason: None, - })), - "Incorrect revoke credential event logged" - ); - } } diff --git a/examples/credential-registry/tests/tests.rs b/examples/credential-registry/tests/tests.rs new file mode 100644 index 00000000..12b0ab98 --- /dev/null +++ b/examples/credential-registry/tests/tests.rs @@ -0,0 +1,343 @@ +//! Tests for the credential registry contract. +use concordium_cis2::*; +use concordium_smart_contract_testing::*; +use concordium_std::{PublicKeyEd25519, SignatureEd25519, Timestamp}; +use credential_registry::*; + +/// Constants for tests +const SIGNER: Signer = Signer::with_one_key(); +pub const ISSUER_ACCOUNT: AccountAddress = AccountAddress([0u8; 32]); +pub const ISSUER_ADDRESS: Address = Address::Account(ISSUER_ACCOUNT); +pub const ISSUER_METADATA_URL: &str = "https://example-university.com/university.json"; +pub const CREDENTIAL_METADATA_URL: &str = + "https://example-university.com/diplomas/university-vc-metadata.json"; +pub const CREDENTIAL_TYPE: &str = "UniversityDegreeCredential"; +pub const CREDENTIAL_SCHEMA_URL: &str = + "https://credentials-schemas.com/JsonSchema2023-education-certificate.json"; +// Seed: 2FEE333FAD122A45AAB7BEB3228FA7858C48B551EA8EBC49D2D56E2BA22049FF +pub const PUBLIC_KEY: PublicKeyEd25519 = PublicKeyEd25519([ + 172, 5, 96, 236, 139, 208, 146, 88, 124, 42, 62, 124, 86, 108, 35, 242, 32, 11, 7, 48, 193, 61, + 177, 220, 104, 169, 145, 4, 8, 1, 236, 112, +]); +pub const SIGNATURE: SignatureEd25519 = SignatureEd25519([ + 254, 138, 58, 131, 209, 45, 191, 52, 98, 228, 26, 234, 155, 245, 244, 226, 0, 153, 104, 111, + 201, 136, 243, 167, 251, 116, 110, 206, 172, 223, 41, 180, 90, 22, 63, 43, 157, 129, 226, 75, + 49, 33, 155, 76, 160, 133, 127, 146, 150, 80, 199, 201, 80, 98, 179, 43, 46, 46, 211, 222, 185, + 216, 12, 4, +]); + +/// Test initialization of the contract. +#[test] +fn test_init() { + let (_chain, init) = setup(); + + let schema = get_credential_schema(); + + let events = init + .events + .iter() + .map(|e| e.parse().expect("Parse event")) + .collect::>(); + + assert_eq!(events.len(), 3); + assert_eq!( + events[0], + CredentialEvent::IssuerMetadata(issuer_metadata()), + "Incorrect issuer metadata event logged" + ); + assert_eq!( + events[1], + CredentialEvent::RevocationKey(RevocationKeyEvent { + key: PUBLIC_KEY, + action: RevocationKeyAction::Register, + }), + "Incorrect revocation key event logged" + ); + assert_eq!( + events[2], + CredentialEvent::Schema(CredentialSchemaRefEvent { + credential_type: schema.0, + schema_ref: schema.1, + }), + "Incorrect schema event logged" + ); +} + +/// Test register credential. +#[test] +fn test_register_credential() { + let (mut chain, init) = setup(); + + let update = register_credential(&mut chain, init.contract_address); + + let credential_status = get_credential_status(&mut chain, init.contract_address, PUBLIC_KEY); + assert_eq!(credential_status, CredentialStatus::Active, "Credential is not active"); + + // Check that the correct register event was produced. + let events = update + .events() + .flat_map(|(_contract, events)| events.iter().map(|e| e.parse().expect("Parsing event"))) + .collect::>(); + + assert_eq!(events, [CredentialEvent::Register(CredentialEventData { + holder_id: PUBLIC_KEY, + schema_ref: SchemaRef { + schema_ref: MetadataUrl { + url: CREDENTIAL_SCHEMA_URL.to_string(), + hash: None, + }, + }, + credential_type: CredentialType { + credential_type: CREDENTIAL_TYPE.to_string(), + }, + metadata_url: MetadataUrl { + url: CREDENTIAL_METADATA_URL.into(), + hash: None, + }, + })]); +} + +/// Test the revoke credential entrypoint, when the holder revokes the +/// credential. +#[test] +fn test_revoke_by_holder() { + let (mut chain, init) = setup(); + // Register a credential that is revocable by the holder. + register_credential(&mut chain, init.contract_address); + + let revocation_reason: Reason = "Just because".to_string().into(); + + let update = revoke_credential(&mut chain, init.contract_address, &revocation_reason); + + // Check the credential status. + let credential_status = get_credential_status(&mut chain, init.contract_address, PUBLIC_KEY); + assert_eq!(credential_status, CredentialStatus::Revoked, "Credential is not revoked"); + + // Check that the correct revoke event was produced. + let events = update + .events() + .flat_map(|(_contract, events)| events.iter().map(|e| e.parse().expect("Parsing event"))) + .collect::>(); + assert_eq!(events, [CredentialEvent::Revoke(RevokeCredentialEvent { + holder_id: PUBLIC_KEY, + revoker: Revoker::Holder, + reason: Some(revocation_reason), + })]); +} + +/// Test the restore credential entrypoint. +#[test] +fn test_contract_restore_credential() { + let (mut chain, init) = setup(); + + // Register a credential. + register_credential(&mut chain, init.contract_address); + + // Revoke the credential. + let revocation_reason: Reason = "Just because".to_string().into(); + revoke_credential(&mut chain, init.contract_address, &revocation_reason); + + // Check that the credential status is revoked. + let credential_status = get_credential_status(&mut chain, init.contract_address, PUBLIC_KEY); + assert_eq!(credential_status, CredentialStatus::Revoked, "Credential is not revoked"); + + // Restore the credential. + let parameter = RestoreCredentialIssuerParam { + credential_id: PUBLIC_KEY, + reason: None, + }; + + let update = chain + .contract_update( + SIGNER, + ISSUER_ACCOUNT, + ISSUER_ADDRESS, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: init.contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "credential_registry.restoreCredential".to_string(), + ), + message: OwnedParameter::from_serial(¶meter) + .expect("Parameter has valid size."), + }, + ) + .expect("Restore credential call succeeds."); + + // Check that the credential status is active again. + let credential_status = get_credential_status(&mut chain, init.contract_address, PUBLIC_KEY); + assert_eq!(credential_status, CredentialStatus::Active, "Credential is not active"); + + // Check that the restore event was produced. + let events = update + .events() + .flat_map(|(_contract, events)| events.iter().map(|e| e.parse().expect("Parsing event"))) + .collect::>(); + assert_eq!(events, [CredentialEvent::Restore(RestoreCredentialEvent { + holder_id: PUBLIC_KEY, + reason: None, + })]); +} + +// Helpers: + +pub fn issuer_metadata() -> MetadataUrl { + MetadataUrl { + url: ISSUER_METADATA_URL.to_string(), + hash: None, + } +} + +pub fn get_credential_schema() -> (CredentialType, SchemaRef) { + ( + CredentialType { + credential_type: CREDENTIAL_TYPE.to_string(), + }, + SchemaRef { + schema_ref: MetadataUrl { + url: CREDENTIAL_SCHEMA_URL.to_string(), + hash: None, + }, + }, + ) +} + +/// Helper that registers a credential and returns the update type. +fn register_credential( + chain: &mut Chain, + contract_address: ContractAddress, +) -> ContractInvokeSuccess { + let parameter = RegisterCredentialParam { + credential_info: CredentialInfo { + holder_id: PUBLIC_KEY, + holder_revocable: true, + valid_from: Timestamp::from_timestamp_millis(0), + valid_until: None, + metadata_url: MetadataUrl { + url: CREDENTIAL_METADATA_URL.to_string(), + hash: None, + }, + }, + auxiliary_data: Vec::new(), + }; + + chain + .contract_update( + SIGNER, + ISSUER_ACCOUNT, + ISSUER_ADDRESS, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "credential_registry.registerCredential".to_string(), + ), + message: OwnedParameter::from_serial(¶meter) + .expect("Parameter has valid size."), + }, + ) + .expect("Successfully registers credential") +} + +/// Helper that revokes the credential. +fn revoke_credential( + chain: &mut Chain, + contract_address: ContractAddress, + revocation_reason: &Reason, +) -> ContractInvokeSuccess { + // Create signing data. + let signing_data = SigningData { + contract_address, + entry_point: OwnedEntrypointName::new_unchecked("revokeCredentialHolder".into()), + nonce: 0, + timestamp: Timestamp::from_timestamp_millis(10000000000), + }; + // Create input parameters for revocation. + let revoke_param = RevokeCredentialHolderParam { + signature: SIGNATURE, + data: RevocationDataHolder { + credential_id: PUBLIC_KEY, + signing_data, + reason: Some(revocation_reason.clone()), + }, + }; + // Call the revoke credential entrypoint. + let update = chain + .contract_update( + SIGNER, + ISSUER_ACCOUNT, + ISSUER_ADDRESS, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "credential_registry.revokeCredentialHolder".to_string(), + ), + message: OwnedParameter::from_serial(&revoke_param) + .expect("Parameter has valid size."), + }, + ) + .expect("Revoke credential call succeeds."); + update +} + +/// Helper for looking up the status of a credential. +fn get_credential_status( + chain: &mut Chain, + contract_address: ContractAddress, + key: PublicKeyEd25519, +) -> CredentialStatus { + let credential_status = chain + .contract_invoke( + ISSUER_ACCOUNT, + ISSUER_ADDRESS, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "credential_registry.credentialStatus".to_string(), + ), + message: OwnedParameter::from_serial(&key).expect("Parameter has valid size."), + }, + ) + .expect("Credential Status call succeeds."); + + credential_status.parse_return_value().expect("Parse credential status") +} + +/// Setup chain and contract. +fn setup() -> (Chain, ContractInitSuccess) { + let mut chain = Chain::new(); + + chain.create_account(Account::new(ISSUER_ACCOUNT, Amount::from_ccd(10000))); + + let module = module_load_v1("./concordium-out/module.wasm.v1").expect("Module exists"); + let deployment = chain + .module_deploy_v1(SIGNER, ISSUER_ACCOUNT, module) + .expect("Module deploys successfully"); + + let schema = get_credential_schema(); + let init_params = InitParams { + issuer_metadata: issuer_metadata(), + issuer_account: ISSUER_ACCOUNT.into(), + issuer_key: PUBLIC_KEY, + revocation_keys: vec![PUBLIC_KEY], + credential_type: schema.0.clone(), + schema: schema.1.clone(), + }; + + let init = chain + .contract_init(SIGNER, ISSUER_ACCOUNT, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_credential_registry".to_string()), + param: OwnedParameter::from_serial(&init_params) + .expect("Parameter has valid size."), + }) + .expect("Contract initializes successfully"); + (chain, init) +} diff --git a/examples/eSealing/Cargo.toml b/examples/eSealing/Cargo.toml index 07baea45..6aa83dfd 100644 --- a/examples/eSealing/Cargo.toml +++ b/examples/eSealing/Cargo.toml @@ -13,6 +13,9 @@ wee_alloc = ["concordium-std/wee_alloc"] [dependencies] concordium-std = {path = "../../concordium-std", default-features = false} +[dev-dependencies] +concordium-smart-contract-testing = { path = "../../contract-testing" } + [lib] crate-type=["cdylib", "rlib"] diff --git a/examples/eSealing/src/lib.rs b/examples/eSealing/src/lib.rs index bd8e9153..2b30ef37 100644 --- a/examples/eSealing/src/lib.rs +++ b/examples/eSealing/src/lib.rs @@ -21,7 +21,7 @@ use concordium_std::*; /// The different errors the contract can produce. #[derive(Serialize, Debug, PartialEq, Eq, Reject, SchemaType)] -enum ContractError { +pub enum ContractError { /// Failed parsing the parameter. #[from(ParseError)] ParseParams, @@ -46,24 +46,24 @@ impl From for ContractError { } /// The state tracked for each file. -#[derive(Serial, Deserial, Clone, Copy, SchemaType)] -struct FileState { +#[derive(Serialize, Clone, Copy, SchemaType, PartialEq, Eq, Debug)] +pub struct FileState { /// The timestamp when this file hash was registered. - timestamp: Timestamp, + pub timestamp: Timestamp, /// The witness (sender_account) that registered this file hash. - witness: AccountAddress, + pub witness: AccountAddress, } /// The contract state. #[derive(Serial, DeserialWithState)] #[concordium(state_parameter = "S")] -struct State { +struct State { files: StateMap, } -impl State { +impl State { /// Create a new state with no files registered. - fn new(state_builder: &mut StateBuilder) -> Self { + fn new(state_builder: &mut StateBuilder) -> Self { State { files: state_builder.new_map(), } @@ -92,28 +92,25 @@ impl State { } /// Tagged events to be serialized for the event log. -#[derive(Debug, Serial, SchemaType)] -enum Event { +#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] +pub enum Event { Registration(RegistrationEvent), } /// The RegistrationEvent is logged when a new file hash is registered. -#[derive(Debug, Serialize, SchemaType)] +#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] pub struct RegistrationEvent { /// Hash of the file to be registered by the witness (sender_account). - file_hash: HashSha2256, + pub file_hash: HashSha2256, /// Witness (sender_account) that registered the above file hash. - witness: AccountAddress, + pub witness: AccountAddress, /// Timestamp when this file hash was registered in the smart contract. - timestamp: Timestamp, + pub timestamp: Timestamp, } /// Init function that creates this eSealing smart contract. #[init(contract = "eSealing", event = "Event")] -fn contract_init( - _ctx: &impl HasInitContext, - state_builder: &mut StateBuilder, -) -> InitResult> { +fn contract_init(_ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { Ok(State::new(state_builder)) } @@ -131,9 +128,9 @@ fn contract_init( mutable, enable_logger )] -fn register_file( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn register_file( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> Result<(), ContractError> { // Ensure that only accounts can register a file. @@ -174,155 +171,7 @@ fn register_file( error = "ContractError", return_value = "Option" )] -fn get_file( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ReceiveResult> { +fn get_file(ctx: &ReceiveContext, host: &Host) -> ReceiveResult> { let file_hash: HashSha2256 = ctx.parameter_cursor().get()?; Ok(host.state().get_file_state(file_hash)) } - -#[concordium_cfg_test] -mod tests { - use super::*; - use test_infrastructure::*; - - const ACCOUNT_0: AccountAddress = AccountAddress([1u8; 32]); - const ADDRESS_0: Address = Address::Account(ACCOUNT_0); - const FILE_HASH: HashSha2256 = concordium_std::HashSha2256([2u8; 32]); - const TIME: u64 = 1; - - /// Test initializing contract. - #[concordium_test] - fn test_init() { - // Set up the context. - let ctx = TestInitContext::empty(); - let mut builder = TestStateBuilder::new(); - - // Initialize the contract. - let init_result = contract_init(&ctx, &mut builder); - - // Check the state. - let state = init_result.expect_report("Contract Initialization failed"); - claim!(state.files.is_empty(), "No files present after initialization"); - } - - /// Test registering file hash. - #[concordium_test] - fn test_register_file() { - // Set up the context. - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - ctx.set_metadata_slot_time(Timestamp::from_timestamp_millis(TIME)); - - // Set up the parameter. - let param_bytes = to_bytes(&FILE_HASH); - ctx.set_parameter(¶m_bytes); - - // Set up the state and host. - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = State::new(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Register the file hash - let result = register_file(&ctx, &mut host, &mut logger); - claim!(result.is_ok(), "results in rejection"); - - // Check the event. - let event = Event::Registration(RegistrationEvent { - file_hash: FILE_HASH, - witness: ACCOUNT_0, - timestamp: Timestamp::from_timestamp_millis(TIME), - }); - claim!(logger.logs.contains(&to_bytes(&event)), "should contain event"); - claim!(host.state().file_exists(&FILE_HASH), "state should contain file"); - } - - /// Test can not register a file hash twice. - #[concordium_test] - fn test_can_not_register_file_twice() { - // Set up the context. - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - ctx.set_metadata_slot_time(Timestamp::from_timestamp_millis(TIME)); - - // Set up the parameter. - let param_bytes = to_bytes(&FILE_HASH); - ctx.set_parameter(param_bytes.as_slice()); - - // Set up the state and host. - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = State::new(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Register the file hash. - let result = register_file(&ctx, &mut host, &mut logger); - claim!(result.is_ok(), "results in rejection"); - - // Check the event. - let event = Event::Registration(RegistrationEvent { - file_hash: FILE_HASH, - witness: ACCOUNT_0, - timestamp: Timestamp::from_timestamp_millis(TIME), - }); - claim!(logger.logs.contains(&to_bytes(&event)), "should contain event"); - claim!(host.state().file_exists(&FILE_HASH), "state should contain file"); - - // Try to register the file hash a second time. - let result = register_file(&ctx, &mut host, &mut logger); - - // Check that invoke failed. - claim_eq!( - result, - Err(ContractError::AlreadyRegistered), - "invoke should fail because file hash is already registered" - ); - } - - /// Test getting file record from state. - #[concordium_test] - fn test_get_file() { - // Set up the context. - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - ctx.set_metadata_slot_time(Timestamp::from_timestamp_millis(TIME)); - - // Set up the parameter. - let param_bytes = to_bytes(&FILE_HASH); - ctx.set_parameter(param_bytes.as_slice()); - - // Set up the state and host. - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = State::new(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Check that there is no record about the file before it has been registered. - let record_result = get_file(&ctx, &host); - claim!(record_result.is_ok(), "could not get record"); - - // Check that `None` is returned. - let record = record_result.unwrap(); - claim!(record.is_none(), "no file record should exist"); - - // Register the file hash. - let result = register_file(&ctx, &mut host, &mut logger); - claim!(result.is_ok(), "file was not registered"); - - // Get the record about this file. - let record_result = get_file(&ctx, &host); - claim!(record_result.is_ok(), "could not get record"); - - // Check the returned values. - let record = record_result.unwrap(); - claim!(record.is_some(), "a file record should exist"); - claim_eq!( - record.unwrap().timestamp, - Timestamp::from_timestamp_millis(TIME), - "timestamp should match" - ); - claim_eq!(record.unwrap().witness, ACCOUNT_0, "witness account should match"); - } -} diff --git a/examples/eSealing/tests/tests.rs b/examples/eSealing/tests/tests.rs new file mode 100644 index 00000000..df8449c7 --- /dev/null +++ b/examples/eSealing/tests/tests.rs @@ -0,0 +1,148 @@ +//! Tests for the eSealing contract. +use concordium_smart_contract_testing::*; +use concordium_std::HashSha2256; +use e_sealing::*; + +/// Constants: +const ALICE: AccountAddress = AccountAddress([0; 32]); +const ALICE_ADDR: Address = Address::Account(ALICE); +const SIGNER: Signer = Signer::with_one_key(); + +/// Test that registering a file works and produces the expected event. +#[test] +fn test_register_file() { + let (mut chain, contract_address) = init(); + + // The file hash to register. + let parameter = HashSha2256([0; 32]); + + // Increase the block time of the chain to check it later. + chain.tick_block_time(Duration::from_millis(123)).expect("Won't overflow."); + + // Register a file. + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("eSealing.registerFile".to_string()), + message: OwnedParameter::from_serial(¶meter).expect("Valid parameter size"), + }) + .expect("Register file"); + + // Check that the expected event was emitted. + assert_eq!(deserialize_update_events(&update), [Event::Registration(RegistrationEvent { + file_hash: parameter, + witness: ALICE, + timestamp: Timestamp::from_timestamp_millis(123), + })]); +} + +/// Test that you cannot register the same file twice. +#[test] +fn test_can_not_register_file_twice() { + let (mut chain, contract_address) = init(); + + // The file hash to register. + let parameter = HashSha2256([0; 32]); + + // Register a file. + chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("eSealing.registerFile".to_string()), + message: OwnedParameter::from_serial(¶meter).expect("Valid parameter size"), + }) + .expect("Register file"); + + // Register the same file again. Which should fail. + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("eSealing.registerFile".to_string()), + message: OwnedParameter::from_serial(¶meter).expect("Valid parameter size"), + }) + .expect_err("Register file"); + + // Check the error message returned. + let rv: ContractError = update.parse_return_value().expect("Deserialize ContractError."); + assert_eq!(rv, ContractError::AlreadyRegistered); +} + +/// Test that getting a file record works. +#[test] +fn test_get_file() { + let (mut chain, contract_address) = init(); + + // The file hash to register. + let parameter = HashSha2256([0; 32]); + + // Advance the block time to check it later. + chain.tick_block_time(Duration::from_millis(123)).expect("Won't overflow."); + + // Register a file. + chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("eSealing.registerFile".to_string()), + message: OwnedParameter::from_serial(¶meter).expect("Valid parameter size"), + }) + .expect("Register file"); + + // Get the file record. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("eSealing.getFile".to_string()), + message: OwnedParameter::from_serial(¶meter).expect("Valid parameter size"), + }) + .expect("Get file"); + + // Check that get_file returns the filestate. + let file_state: Option = + invoke.parse_return_value().expect("Deserialize FileState."); + assert_eq!( + file_state, + Some(FileState { + timestamp: Timestamp::from_timestamp_millis(123), + witness: ALICE, + }) + ); +} + +// Helpers: + +/// Deserialize the events from an update. +fn deserialize_update_events(update: &ContractInvokeSuccess) -> Vec { + update + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect() +} + +/// Setup chain and contract. +fn init() -> (Chain, ContractAddress) { + let mut chain = Chain::new(); + + // Create some accounts accounts on the chain. + chain.create_account(Account::new(ALICE, Amount::from_ccd(1000))); + + // Load and deploy the module. + let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); + let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Deploy valid module"); + + // Initialize the auction contract. + let init = chain + .contract_init(SIGNER, ALICE, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_eSealing".to_string()), + param: OwnedParameter::empty(), + }) + .expect("Initialize contract"); + + (chain, init.contract_address) +} diff --git a/examples/fib/Cargo.toml b/examples/fib/Cargo.toml index de02adf8..8bf8567b 100644 --- a/examples/fib/Cargo.toml +++ b/examples/fib/Cargo.toml @@ -15,5 +15,8 @@ wee_alloc = ["concordium-std/wee_alloc"] [dependencies] concordium-std = {path = "../../concordium-std", default-features = false} +[dev-dependencies] +concordium-smart-contract-testing = { path = "../../contract-testing"} + [lib] crate-type=["cdylib", "rlib"] diff --git a/examples/fib/src/lib.rs b/examples/fib/src/lib.rs index 15428f2b..3c6b8565 100644 --- a/examples/fib/src/lib.rs +++ b/examples/fib/src/lib.rs @@ -8,10 +8,7 @@ pub struct State { #[init(contract = "fib")] #[inline(always)] -fn contract_init( - _ctx: &impl HasInitContext, - _state_builder: &mut StateBuilder, -) -> InitResult { +fn contract_init(_ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult { let state = State { result: 0, }; @@ -22,10 +19,7 @@ fn contract_init( // This is achieved by recursively calling the contract itself. #[inline(always)] #[receive(contract = "fib", name = "receive", parameter = "u64", return_value = "u64", mutable)] -fn contract_receive( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, -) -> ReceiveResult { +fn contract_receive(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult { // Try to get the parameter (64bit unsigned integer). let n: u64 = ctx.parameter_cursor().get()?; if n <= 1 { @@ -67,63 +61,6 @@ fn contract_receive( /// Retrieve the value of the state. #[inline(always)] #[receive(contract = "fib", name = "view", return_value = "u64")] -fn contract_view( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, -) -> ReceiveResult { +fn contract_view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { Ok(host.state().result) } - -#[concordium_cfg_test] -mod tests { - use super::*; - use concordium_std::claim_eq; - use test_infrastructure::*; - - // Compute the n-th fibonacci number. - fn fib(n: u64) -> u64 { - let mut n1 = 1; - let mut n2 = 1; - for _ in 2..=n { - let t = n1; - n1 = n2; - n2 += t; - } - n2 - } - - #[concordium_test] - fn receive_works() { - let mut ctx = TestReceiveContext::empty(); - let parameter_bytes = to_bytes(&10u64); - let contract_address = ContractAddress { - index: 0, - subindex: 0, - }; - ctx.set_parameter(¶meter_bytes); - ctx.set_self_address(contract_address); - - let mut host = TestHost::new( - State { - result: 0, - }, - TestStateBuilder::new(), - ); - - host.setup_mock_entrypoint( - contract_address, - OwnedEntrypointName::new_unchecked("receive".into()), - MockFn::new_v1(|parameter, _amount, _balance, state: &mut State| { - let n: u64 = match from_bytes(parameter.into()) { - Ok(n) => n, - Err(_) => return Err(CallContractError::Trap), - }; - state.result = fib(n); - Ok((true, state.result)) - }), - ); - let res = contract_receive(&ctx, &mut host).expect_report("Calling receive failed."); - claim_eq!(res, fib(10)); - claim_eq!(host.state().result, fib(10)); - } -} diff --git a/examples/fib/tests/tests.rs b/examples/fib/tests/tests.rs new file mode 100644 index 00000000..26fac90f --- /dev/null +++ b/examples/fib/tests/tests.rs @@ -0,0 +1,80 @@ +use concordium_smart_contract_testing::*; + +const ACC_0: AccountAddress = AccountAddress([0; 32]); +const SIGNER: Signer = Signer::with_one_key(); + +/// Compute the n-th fibonacci number. +fn fib(n: u64) -> u64 { + let mut n1 = 1; + let mut n2 = 1; + for _ in 2..=n { + let t = n1; + n1 = n2; + n2 += t; + } + n2 +} + +/// Test that calling the `receive` entrypoint produces the correct fib value in +/// the state. +#[test] +fn test() { + // Create the test chain. + let mut chain = Chain::new(); + + // Create two accounts on the chain. + chain.create_account(Account::new(ACC_0, Amount::from_ccd(1000))); + + // Load and deploy the module. + let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); + let deployment = chain.module_deploy_v1(SIGNER, ACC_0, module).expect("Deploy valid module"); + + // Initialize the contract. + let initialization = chain + .contract_init(SIGNER, ACC_0, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_fib".to_string()), + param: OwnedParameter::empty(), + }) + .expect("Init should succeed"); + let contract_address = initialization.contract_address; + + // Call the `receive` entrypoint with `7` as input. + let update = chain + .contract_update( + SIGNER, + ACC_0, + Address::Account(ACC_0), + Energy::from(50000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("fib.receive".to_string()), + message: OwnedParameter::from_serial(&7u64) + .expect("Parameter has valid size."), + }, + ) + .expect("Calling receive"); + + let rv: u64 = update.parse_return_value().expect("Return value"); + assert_eq!(rv, fib(7)); + + // Check that the result is persisted by invoking the `view` entrypoint. + let update = chain + .contract_invoke( + ACC_0, + Address::Account(ACC_0), + Energy::from(50000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("fib.view".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect("Calling receive"); + + let rv: u64 = update.parse_return_value().expect("Return value"); + assert_eq!(rv, fib(7)); +} diff --git a/examples/icecream/Cargo.toml b/examples/icecream/Cargo.toml index 0ae42099..5bc665e4 100644 --- a/examples/icecream/Cargo.toml +++ b/examples/icecream/Cargo.toml @@ -10,6 +10,9 @@ license = "MPL-2.0" [dependencies] concordium-std = {path = "../../concordium-std"} +[dev-dependencies] +concordium-smart-contract-testing = { path = "../../contract-testing" } + [features] default = ["std", "wee_alloc"] std = ["concordium-std/std"] diff --git a/examples/icecream/src/lib.rs b/examples/icecream/src/lib.rs index 35253ff0..2376c909 100644 --- a/examples/icecream/src/lib.rs +++ b/examples/icecream/src/lib.rs @@ -43,14 +43,14 @@ struct State { } #[derive(Serialize, SchemaType, Clone, Copy)] -enum Weather { +pub enum Weather { Rainy, Sunny, } /// The custom errors the contract can produce. #[derive(Serialize, Debug, PartialEq, Eq, Reject, SchemaType)] -enum ContractError { +pub enum ContractError { /// Failed parsing the parameter. #[from(ParseError)] ParseParams, @@ -70,10 +70,7 @@ type ContractResult = Result; /// Initialise the contract with the contract address of the weather service. #[init(contract = "icecream", parameter = "ContractAddress")] -fn contract_init( - ctx: &impl HasInitContext, - _state_builder: &mut StateBuilder, -) -> InitResult { +fn contract_init(ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult { let weather_service: ContractAddress = ctx.parameter_cursor().get()?; Ok(State { weather_service, @@ -89,9 +86,9 @@ fn contract_init( mutable, error = "ContractError" )] -fn contract_buy_icecream( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, +fn contract_buy_icecream( + ctx: &ReceiveContext, + host: &mut Host, amount: Amount, ) -> ContractResult<()> { let weather_service = host.state().weather_service; @@ -131,9 +128,9 @@ fn contract_buy_icecream( mutable, error = "ContractError" )] -fn contract_replace_weather_service( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, +fn contract_replace_weather_service( + ctx: &ReceiveContext, + host: &mut Host, ) -> ContractResult<()> { ensure_eq!(Address::Account(ctx.owner()), ctx.sender(), ContractError::Unauthenticated); let new_weather_service: ContractAddress = ctx.parameter_cursor().get()?; @@ -145,20 +142,14 @@ fn contract_replace_weather_service( /// Initialse the weather service with the weather. #[init(contract = "weather", parameter = "Weather")] -fn weather_init( - ctx: &impl HasInitContext, - _state_builder: &mut StateBuilder, -) -> InitResult { +fn weather_init(ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult { let weather = ctx.parameter_cursor().get()?; Ok(weather) } /// Get the current weather. #[receive(contract = "weather", name = "get", return_value = "Weather", error = "ContractError")] -fn weather_get( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, -) -> ContractResult { +fn weather_get(_ctx: &ReceiveContext, host: &Host) -> ContractResult { Ok(*host.state()) } @@ -170,155 +161,8 @@ fn weather_get( mutable, error = "ContractError" )] -fn weather_set( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, -) -> ContractResult<()> { +fn weather_set(ctx: &ReceiveContext, host: &mut ExternHost) -> ContractResult<()> { ensure_eq!(Address::Account(ctx.owner()), ctx.sender(), ContractError::Unauthenticated); // Only the owner can update the weather. *host.state_mut() = ctx.parameter_cursor().get()?; Ok(()) } - -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -#[concordium_cfg_test] -mod tests { - use super::*; - use test_infrastructure::*; - - const INVOKER_ADDR: AccountAddress = AccountAddress([0; 32]); - const WEATHER_SERVICE: ContractAddress = ContractAddress { - index: 1, - subindex: 0, - }; - const ICECREAM_VENDOR: AccountAddress = AccountAddress([1; 32]); - const ICECREAM_PRICE: Amount = Amount { - micro_ccd: 6000000, // 6 CCD - }; - - #[concordium_test] - fn test_sunny_days() { - // Arrange - let mut ctx = TestReceiveContext::empty(); - let state = State { - weather_service: WEATHER_SERVICE, - }; - let mut host = TestHost::new(state, TestStateBuilder::new()); - - // Set up context - let parameter = to_bytes(&ICECREAM_VENDOR); - ctx.set_owner(INVOKER_ADDR); - ctx.set_invoker(INVOKER_ADDR); - ctx.set_parameter(¶meter); - host.set_self_balance(ICECREAM_PRICE); // This should be the balance prior to the call plus the incoming amount. - - // Set up a mock invocation for the weather service. - host.setup_mock_entrypoint( - WEATHER_SERVICE, - OwnedEntrypointName::new_unchecked("get".into()), - MockFn::returning_ok(Weather::Sunny), - ); - - // Act - contract_buy_icecream(&ctx, &mut host, ICECREAM_PRICE) - .expect_report("Calling buy_icecream failed."); - - // Assert - assert!(host.transfer_occurred(&ICECREAM_VENDOR, ICECREAM_PRICE)); - assert!(host.get_transfers_to(INVOKER_ADDR).is_empty()); // Check that - // no - // transfers to - // the invoker - // occured. - } - - #[concordium_test] - fn test_rainy_days() { - // Arrange - let mut ctx = TestReceiveContext::empty(); - let state = State { - weather_service: WEATHER_SERVICE, - }; - let mut host = TestHost::new(state, TestStateBuilder::new()); - - // Set up context - let parameter = to_bytes(&ICECREAM_VENDOR); - ctx.set_owner(INVOKER_ADDR); - ctx.set_invoker(INVOKER_ADDR); - ctx.set_parameter(¶meter); - host.set_self_balance(ICECREAM_PRICE); - - // Set up mock invocation - host.setup_mock_entrypoint( - WEATHER_SERVICE, - OwnedEntrypointName::new_unchecked("get".into()), - MockFn::returning_ok(Weather::Rainy), - ); - - // Act - contract_buy_icecream(&ctx, &mut host, ICECREAM_PRICE) - .expect_report("Calling buy_icecream failed."); - - // Assert - assert!(host.transfer_occurred(&INVOKER_ADDR, ICECREAM_PRICE)); - assert_eq!(host.get_transfers(), &[(INVOKER_ADDR, ICECREAM_PRICE)]); // Check that this is the only transfer. - } - - #[concordium_test] - fn test_missing_icecream_vendor() { - // Arrange - let mut ctx = TestReceiveContext::empty(); - let state = State { - weather_service: WEATHER_SERVICE, - }; - let mut host = TestHost::new(state, TestStateBuilder::new()); - - // Set up context - let parameter = to_bytes(&ICECREAM_VENDOR); - ctx.set_owner(INVOKER_ADDR); - ctx.set_invoker(INVOKER_ADDR); - ctx.set_parameter(¶meter); - host.set_self_balance(ICECREAM_PRICE); - - // By default all transfers to accounts will work, but here we want to test what - // happens when the vendor account doesn't exist. - host.make_account_missing(ICECREAM_VENDOR); - - // Set up mock invocation - host.setup_mock_entrypoint( - WEATHER_SERVICE, - OwnedEntrypointName::new_unchecked("get".into()), - MockFn::returning_ok(Weather::Sunny), - ); - - // Act + Assert - let result = contract_buy_icecream(&ctx, &mut host, ICECREAM_PRICE); - claim_eq!(result, Err(ContractError::TransferError)); - } - - #[concordium_test] - fn test_missing_weather_service() { - // Arrange - let mut ctx = TestReceiveContext::empty(); - let state = State { - weather_service: WEATHER_SERVICE, - }; - let mut host = TestHost::new(state, TestStateBuilder::new()); - - // Set up context - let parameter = to_bytes(&ICECREAM_VENDOR); - ctx.set_owner(INVOKER_ADDR); - ctx.set_parameter(¶meter); - - // Set up mock invocation - host.setup_mock_entrypoint( - WEATHER_SERVICE, - OwnedEntrypointName::new_unchecked("get".into()), - MockFn::returning_err::<()>(CallContractError::MissingContract), - ); - - // Act + Assert (should panic) - let result = contract_buy_icecream(&ctx, &mut host, ICECREAM_PRICE); - claim_eq!(result, Err(ContractError::ContractInvokeError)); - } -} diff --git a/examples/icecream/tests/tests.rs b/examples/icecream/tests/tests.rs new file mode 100644 index 00000000..c3cc94ab --- /dev/null +++ b/examples/icecream/tests/tests.rs @@ -0,0 +1,215 @@ +//! This file contains the tests for the icecream contract. +use concordium_smart_contract_testing::*; +use icecream::*; + +/// The icecream buyer. +const ACC_0: AccountAddress = AccountAddress([0; 32]); +/// The icecream vendor. +const ACC_1: AccountAddress = AccountAddress([1; 32]); +const SIGNER: Signer = Signer::with_one_key(); +const ICECREAM_PRICE: Amount = Amount::from_ccd(1000); +const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10000); + +/// Test that the icecream contract transfers the correct amount of money to the +/// vendor if the weather is sunny. +#[test] +fn test_sunny_day() { + let (mut chain, module_reference) = initialize_chain(); + + // Initialize the weather contract with sunny weather. + let weather = Weather::Sunny; + let weather_address = initialize_weather(&mut chain, module_reference, &weather); + + // Initialize the icecream contract with the weather contract address. + let icecream_address = initialize_icecream(&mut chain, module_reference, weather_address); + + // Buy the icecream. + let update = chain + .contract_update( + SIGNER, + ACC_0, + Address::Account(ACC_0), + Energy::from(10000), + UpdateContractPayload { + amount: ICECREAM_PRICE, + address: icecream_address, + receive_name: OwnedReceiveName::new_unchecked("icecream.buy_icecream".to_string()), + message: OwnedParameter::from_serial(&ACC_1) + .expect("Serialize account address."), + }, + ) + .expect("Call icecream contract"); + + // Check that the icecream vendor received the correct amount of money. + assert_eq!(chain.account_balance_available(ACC_1), Some(ACC_INITIAL_BALANCE + ICECREAM_PRICE)); + // Assert that the transfer to ACC_1 occured via the method on `update`. + assert_eq!(update.account_transfers().collect::>()[..], [( + icecream_address, + ICECREAM_PRICE, + ACC_1 + )]); +} + +/// Test that the icecream contract transfers the correct amount of money back +/// to the sender on rainy days. +#[test] +fn test_rainy_days() { + let (mut chain, module_reference) = initialize_chain(); + + // Initialize the weather contract with rainy weather. + let weather = Weather::Rainy; + let weather_address = initialize_weather(&mut chain, module_reference, &weather); + + // Initialize the icecream contract with the weather contract address. + let icecream_address = initialize_icecream(&mut chain, module_reference, weather_address); + + // Buy the icecream. + let update = chain + .contract_update( + SIGNER, + ACC_0, + Address::Account(ACC_0), + Energy::from(10000), + UpdateContractPayload { + amount: ICECREAM_PRICE, + address: icecream_address, + receive_name: OwnedReceiveName::new_unchecked("icecream.buy_icecream".to_string()), + message: OwnedParameter::from_serial(&ACC_1).expect("Serialize address"), + }, + ) + .expect("Call icecream contract"); + + // Check that the icecream vendor still has the original balance. + assert_eq!(chain.account_balance_available(ACC_1), Some(ACC_INITIAL_BALANCE)); + // Check that the money were returned to the sender, ACC_0. + assert_eq!(update.account_transfers().collect::>()[..], [( + icecream_address, + ICECREAM_PRICE, + ACC_0 + )]); +} + +/// Test that `buy_icecream` returns the error `ContractInvokeError` if the +/// weather contract doesn't return a valid weather, because the contract hasn't +/// been initialized. +#[test] +fn test_missing_weather() { + let (mut chain, module_reference) = initialize_chain(); + + // Initialize the icecream contract with an unused address for the weather + // contract. + let icecream_address = + initialize_icecream(&mut chain, module_reference, ContractAddress::new(0, 0)); + + // Buy the icecream. + let update = chain + .contract_update( + SIGNER, + ACC_0, + Address::Account(ACC_0), + Energy::from(10000), + UpdateContractPayload { + amount: ICECREAM_PRICE, + address: icecream_address, + receive_name: OwnedReceiveName::new_unchecked("icecream.buy_icecream".to_string()), + message: OwnedParameter::from_serial(&ACC_1).expect("Serialize address"), + }, + ) + .expect_err("Call icecream contract"); + + // Deserialize the return value from `update` and check that it is the expected + // error. + let rv: ContractError = update.parse_return_value().expect("Deserialize return value"); + assert_eq!(rv, ContractError::ContractInvokeError); +} + +/// Test that `buy_icecream` returns the error `TrasnferError` if the icecream +/// vendor doesn't exist. +#[test] +fn test_missing_icecream_vendor() { + let (mut chain, module_reference) = initialize_chain(); + + let non_existing_account = AccountAddress([2; 32]); + + // Initialize the weather contract with sunny weather. + let weather = Weather::Sunny; + let weather_address = initialize_weather(&mut chain, module_reference, &weather); + + // Initialize the icecream contract with the weather contract address. + let icecream_address = initialize_icecream(&mut chain, module_reference, weather_address); + + // Buy the icecream. + let update = chain + .contract_update( + SIGNER, + ACC_0, + Address::Account(ACC_0), + Energy::from(10000), + UpdateContractPayload { + amount: ICECREAM_PRICE, + address: icecream_address, + receive_name: OwnedReceiveName::new_unchecked("icecream.buy_icecream".to_string()), + message: OwnedParameter::from_serial(&non_existing_account) + .expect("Serialize address"), + }, + ) + .expect_err("Call icecream contract"); + + // Deserialize the return value from `update` and check that it is the expected + // error. + let rv: ContractError = update.parse_return_value().expect("Deserialize return value"); + assert_eq!(rv, ContractError::TransferError); +} + +// ** HELPERS ** + +/// Initialize the weather contract with the given weather. +fn initialize_weather( + chain: &mut Chain, + module_reference: ModuleReference, + weather: &Weather, +) -> ContractAddress { + chain + .contract_init(SIGNER, ACC_0, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: module_reference, + init_name: OwnedContractName::new_unchecked("init_weather".to_string()), + param: OwnedParameter::from_serial(weather).expect("Serialize weather"), + }) + .expect("Initialize weather") + .contract_address +} + +/// Initialize the icecream contract with the given weather contract address. +fn initialize_icecream( + chain: &mut Chain, + module_reference: ModuleReference, + weather_address: ContractAddress, +) -> ContractAddress { + chain + .contract_init(SIGNER, ACC_0, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: module_reference, + init_name: OwnedContractName::new_unchecked("init_icecream".to_string()), + param: OwnedParameter::from_serial(&weather_address) + .expect("Serialize weather address"), + }) + .expect("Initialize icecream") + .contract_address +} + +/// Initialize the chain, create two accounts (ACC_0, ACC_1) and deploy the +/// module. +fn initialize_chain() -> (Chain, ModuleReference) { + let mut chain = Chain::new(); + + // Create two accounts on the chain. + chain.create_account(Account::new(ACC_0, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(ACC_1, ACC_INITIAL_BALANCE)); + + // Load and deploy the module. + let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); + let deployment = chain.module_deploy_v1(SIGNER, ACC_0, module).expect("Deploy valid module"); + + (chain, deployment.module_reference) +} diff --git a/examples/memo/src/lib.rs b/examples/memo/src/lib.rs index 63d0f7f5..0193814a 100644 --- a/examples/memo/src/lib.rs +++ b/examples/memo/src/lib.rs @@ -16,12 +16,7 @@ struct InitParameter; /// Init function that creates a new contract. #[init(contract = "memo", parameter = "InitParameter")] -fn memo_init( - _ctx: &impl HasInitContext, - _state_builder: &mut StateBuilder, -) -> InitResult<()> { - Ok(()) -} +fn memo_init(_ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult<()> { Ok(()) } const EXPECTED_PARAMETER_SIZE: u32 = 32; @@ -50,11 +45,7 @@ struct State; payable, error = "CustomContractError" )] -fn memo_receive( - ctx: &impl HasReceiveContext, - host: &impl HasHost, - amount: Amount, -) -> ReceiveResult<()> { +fn memo_receive(ctx: &ReceiveContext, host: &Host, amount: Amount) -> ReceiveResult<()> { ensure!(matches!(ctx.sender(), Address::Account(..))); ensure!(ctx.parameter_cursor().size() == EXPECTED_PARAMETER_SIZE); host.invoke_transfer(&ctx.owner(), amount)?; diff --git a/examples/nametoken/Cargo.toml b/examples/nametoken/Cargo.toml index 26be6495..3f9599ec 100644 --- a/examples/nametoken/Cargo.toml +++ b/examples/nametoken/Cargo.toml @@ -6,8 +6,7 @@ edition = "2021" license = "MPL-2.0" [features] -default = ["std", "crypto-primitives", "wee_alloc"] -crypto-primitives = ["concordium-std/crypto-primitives"] +default = ["std", "wee_alloc"] std = ["concordium-std/std", "concordium-cis2/std"] wee_alloc = ["concordium-std/wee_alloc"] @@ -15,6 +14,9 @@ wee_alloc = ["concordium-std/wee_alloc"] concordium-std = {path = "../../concordium-std", default-features = false} concordium-cis2 = {path = "../../concordium-cis2", default-features = false} +[dev-dependencies] +concordium-smart-contract-testing = { path = "../../contract-testing" } + [lib] crate-type=["cdylib", "rlib"] diff --git a/examples/nametoken/src/lib.rs b/examples/nametoken/src/lib.rs index afe18b17..20b6441f 100644 --- a/examples/nametoken/src/lib.rs +++ b/examples/nametoken/src/lib.rs @@ -47,68 +47,68 @@ use concordium_std::*; /// The baseurl for the token metadata, gets appended with the token ID as hex /// encoding before emitted in the TokenMetadata event. -const TOKEN_METADATA_BASE_URL: &str = "https://some.example/nametoken/"; +pub const TOKEN_METADATA_BASE_URL: &str = "https://some.example/nametoken/"; /// List of supported standards by this contract address. -const SUPPORTS_STANDARDS: [StandardIdentifier<'static>; 2] = +pub const SUPPORTS_STANDARDS: [StandardIdentifier<'static>; 2] = [CIS0_STANDARD_IDENTIFIER, CIS2_STANDARD_IDENTIFIER]; // Fees /// Registration fee in CCD -const REGISTRACTION_FEE: Amount = Amount::from_ccd(70); +pub const REGISTRATION_FEE: Amount = Amount::from_ccd(70); /// Data update fee in CCD -const UPDATE_FEE: Amount = Amount::from_ccd(7); +pub const UPDATE_FEE: Amount = Amount::from_ccd(7); /// Renewal fee in CCD -const RENEWAL_FEE: Amount = Amount::from_ccd(7); +pub const RENEWAL_FEE: Amount = Amount::from_ccd(7); /// How long the registered name is owned before it needs to be renewed -const REGISTRATION_PERIOD_DAYS: u64 = 365; +pub const REGISTRATION_PERIOD_DAYS: u64 = 365; // Types /// Contract token ID type. /// We pick `TokenIdFixed`, since we hash names with sha256, that gives a /// fixed-length 32 byte array. -type ContractTokenId = TokenIdFixed<32>; +pub type ContractTokenId = TokenIdFixed<32>; /// Contract token amount. Since the tokens are non-fungible the total supply /// of any token will be at most 1 and it is fine to use a small type for /// representing token amounts. -type ContractTokenAmount = TokenAmountU8; +pub type ContractTokenAmount = TokenAmountU8; /// The parameter for the contract function `register` which registers a name /// for a given address. #[derive(Serial, Deserial, SchemaType)] -struct RegisterNameParams { +pub struct RegisterNameParams { /// Owner of the newly registered name - owner: AccountAddress, + pub owner: AccountAddress, /// Name - name: String, + pub name: String, } /// Data for each name. #[derive(Serial, DeserialWithState, Deletable)] #[concordium(state_parameter = "S")] -struct NameInfo { +pub struct NameInfo { /// Name owner - owner: AccountAddress, + pub owner: AccountAddress, /// Expiration date - name_expires: Timestamp, + pub name_expires: Timestamp, /// Associated data // `StateBox` allows for lazy loading data; this is helpful // in the situations when one wants to do a partial update not touching // this field, which can be large. - data: StateBox, S>, + pub data: StateBox, S>, } -impl NameInfo { +impl NameInfo { fn fresh( owner: AccountAddress, name_expires: Timestamp, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) -> Self { NameInfo { owner, @@ -121,15 +121,15 @@ impl NameInfo { /// The state for each address. #[derive(Serial, DeserialWithState, Deletable)] #[concordium(state_parameter = "S")] -struct AddressState { +pub struct AddressState { /// The tokens owned by this address. - owned_names: StateSet, + pub owned_names: StateSet, /// The address which are currently enabled as operators for this address. - operators: StateSet, + pub operators: StateSet, } -impl AddressState { - fn empty(state_builder: &mut StateBuilder) -> Self { +impl AddressState { + fn empty(state_builder: &mut StateBuilder) -> Self { AddressState { owned_names: state_builder.new_set(), operators: state_builder.new_set(), @@ -141,7 +141,7 @@ impl AddressState { #[derive(Serial, DeserialWithState)] #[concordium(state_parameter = "S")] -struct State { +struct State { /// The address of the administrating account. /// Admin can withdraw the accumulated fees and update the admin account. admin: AccountAddress, @@ -158,22 +158,22 @@ struct State { /// standard identifier and list of contract addresses providing /// implementations of this standard. #[derive(Debug, Serialize, SchemaType)] -struct SetImplementorsParams { +pub struct SetImplementorsParams { /// The identifier for the standard. - id: StandardIdentifierOwned, + pub id: StandardIdentifierOwned, /// The addresses of the implementors of the standard. - implementors: Vec, + pub implementors: Vec, } #[derive(Debug, Serialize, SchemaType)] -struct UpdateDataParams { - name: String, - data: Vec, +pub struct UpdateDataParams { + pub name: String, + pub data: Vec, } /// The custom errors the contract can produce. #[derive(Serialize, Debug, PartialEq, Eq, Reject, SchemaType)] -enum CustomContractError { +pub enum CustomContractError { /// Failed parsing the parameter. #[from(ParseError)] ParseParams, @@ -205,9 +205,9 @@ enum CustomContractError { } /// Wrapping the custom errors in a type with CIS2 errors. -type ContractError = Cis2Error; +pub type ContractError = Cis2Error; -type ContractResult = Result; +pub type ContractResult = Result; /// Mapping the logging errors to CustomContractError. impl From for CustomContractError { @@ -240,9 +240,9 @@ impl From for ContractError { } // Functions for creating, updating and querying the contract state. -impl State { +impl State { /// Creates a new state with no tokens. - fn empty(admin: AccountAddress, state_builder: &mut StateBuilder) -> Self { + fn empty(admin: AccountAddress, state_builder: &mut StateBuilder) -> Self { State { admin, state: state_builder.new_map(), @@ -257,7 +257,7 @@ impl State { name: ContractTokenId, owner: AccountAddress, expires: Timestamp, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) -> ContractResult<()> { let name_info = NameInfo::fresh(owner, expires, state_builder); // make sure that the name is not taken @@ -277,7 +277,7 @@ impl State { now: Timestamp, name: ContractTokenId, owner: AccountAddress, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) -> ContractResult { let mut name_info = self .all_names @@ -395,7 +395,7 @@ impl State { amount: ContractTokenAmount, from: &Address, to: &Address, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) -> ContractResult<()> { ensure!(self.contains_token(token_id), ContractError::InvalidTokenId); @@ -445,7 +445,7 @@ impl State { &mut self, owner: &Address, operator: &Address, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) -> ContractResult<()> { match owner { Address::Account(addr) => { @@ -493,7 +493,7 @@ impl State { /// Build a string from TOKEN_METADATA_BASE_URL appended with the token ID /// encoded as hex. -fn build_token_metadata_url(token_id: &ContractTokenId) -> String { +pub fn build_token_metadata_url(token_id: &ContractTokenId) -> String { let mut token_metadata_url = String::from(TOKEN_METADATA_BASE_URL); token_metadata_url.push_str(&token_id.to_string()); token_metadata_url @@ -504,34 +504,31 @@ fn build_token_metadata_url(token_id: &ContractTokenId) -> String { /// Initialize contract instance with no token types initially. /// Set the account that initialised the contract to be admin #[init(contract = "NameToken")] -fn contract_init( - ctx: &impl HasInitContext, - state_builder: &mut StateBuilder, -) -> InitResult> { +fn contract_init(ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { // Construct the initial contract state. Ok(State::empty(ctx.init_origin(), state_builder)) } -#[derive(Serialize, SchemaType)] -struct ViewNameInfo { - owner: AccountAddress, - name_expires: Timestamp, - data: Vec, +#[derive(Serialize, SchemaType, PartialEq, Eq, Debug)] +pub struct ViewNameInfo { + pub owner: AccountAddress, + pub name_expires: Timestamp, + pub data: Vec, } -#[derive(Serialize, SchemaType)] -struct ViewAddressState { - owned_names: Vec, - operators: Vec
, +#[derive(Serialize, SchemaType, PartialEq, Eq, Debug)] +pub struct ViewAddressState { + pub owned_names: Vec, + pub operators: Vec
, } -#[derive(Serialize, SchemaType)] -struct ViewState { - state: Vec<(AccountAddress, ViewAddressState)>, - all_names: Vec<(ContractTokenId, ViewNameInfo)>, +#[derive(Serialize, SchemaType, PartialEq, Eq, Debug)] +pub struct ViewState { + pub state: Vec<(AccountAddress, ViewAddressState)>, + pub all_names: Vec<(ContractTokenId, ViewNameInfo)>, } -fn into_view_name_info(name_info: &NameInfo) -> ViewNameInfo { +fn into_view_name_info(name_info: &NameInfo) -> ViewNameInfo { ViewNameInfo { owner: name_info.owner, name_expires: name_info.name_expires, @@ -539,8 +536,8 @@ fn into_view_name_info(name_info: &NameInfo) -> ViewNameInfo } } -fn view_nameinfo( - name: (StateRef>, StateRef>), +fn view_nameinfo( + name: (StateRef>, StateRef), ) -> (TokenIdFixed<32>, ViewNameInfo) { let (a_token_id, a_name_info) = name; let name_info = into_view_name_info(&a_name_info); @@ -550,10 +547,7 @@ fn view_nameinfo( /// View function that returns the entire contents of the state. Meant for /// testing. #[receive(contract = "NameToken", name = "view", return_value = "ViewState")] -fn contract_view( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ReceiveResult { +fn contract_view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { let state = host.state(); let mut inner_state = Vec::new(); @@ -587,9 +581,9 @@ fn contract_view( return_value = "ViewNameInfo", error = "ContractError" )] -fn contract_nameinfo_view( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_nameinfo_view( + ctx: &ReceiveContext, + host: &Host, crypto_primitives: &impl HasCryptoPrimitives, ) -> ContractResult { let params: String = ctx.parameter_cursor().get()?; @@ -641,15 +635,15 @@ fn contract_nameinfo_view( enable_logger, mutable )] -fn contract_register( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_register( + ctx: &ReceiveContext, + host: &mut Host, amount: Amount, logger: &mut impl HasLogger, crypto_primitives: &impl HasCryptoPrimitives, ) -> ContractResult<()> { // Validate the amount - ensure_eq!(amount, REGISTRACTION_FEE, CustomContractError::IncorrectFee.into()); + ensure_eq!(amount, REGISTRATION_FEE, CustomContractError::IncorrectFee.into()); // Parse the parameter. let params: RegisterNameParams = ctx.parameter_cursor().get()?; // Hash the name @@ -695,7 +689,7 @@ fn contract_register( } } -type TransferParameter = TransferParams; +pub type TransferParameter = TransferParams; /// Execute a list of token transfers, in the order of the list. /// @@ -719,9 +713,9 @@ type TransferParameter = TransferParams; enable_logger, mutable )] -fn contract_transfer( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_transfer( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Parse the parameter. @@ -791,9 +785,9 @@ fn contract_transfer( payable, mutable )] -fn contract_renew( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_renew( + ctx: &ReceiveContext, + host: &mut Host, amount: Amount, crypto_primitives: &impl HasCryptoPrimitives, ) -> ContractResult<()> { @@ -830,10 +824,7 @@ fn contract_renew( error = "ContractError", mutable )] -fn contract_withdraw( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, -) -> ContractResult<()> { +fn contract_withdraw(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<()> { // Get the contract admin let admin = host.state().admin; // Get the sender of the transaction @@ -854,10 +845,7 @@ fn contract_withdraw( error = "ContractError", mutable )] -fn contract_update_admin( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, -) -> ContractResult<()> { +fn contract_update_admin(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<()> { // Get the contract admin let admin = host.state().admin; @@ -893,9 +881,9 @@ fn contract_update_admin( payable, mutable )] -fn contract_update_data( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_update_data( + ctx: &ReceiveContext, + host: &mut Host, amount: Amount, crypto_primitives: &impl HasCryptoPrimitives, ) -> ContractResult<()> { @@ -938,9 +926,9 @@ fn contract_update_data( enable_logger, mutable )] -fn contract_update_operator( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_update_operator( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Parse the parameter. @@ -980,9 +968,9 @@ fn contract_update_operator( return_value = "OperatorOfQueryResponse", error = "ContractError" )] -fn contract_operator_of( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_operator_of( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: OperatorOfQueryParams = ctx.parameter_cursor().get()?; @@ -1003,10 +991,10 @@ fn contract_operator_of( /// Parameter type for the CIS-2 function `balanceOf` specialized to the subset /// of TokenIDs used by this contract. -type ContractBalanceOfQueryParams = BalanceOfQueryParams; +pub type ContractBalanceOfQueryParams = BalanceOfQueryParams; /// Response type for the CIS-2 function `balanceOf` specialized to the subset /// of TokenAmounts used by this contract. -type ContractBalanceOfQueryResponse = BalanceOfQueryResponse; +pub type ContractBalanceOfQueryResponse = BalanceOfQueryResponse; /// Get the balance of given token IDs and addresses. /// The balance is considered 0 if the name has expired. @@ -1021,9 +1009,9 @@ type ContractBalanceOfQueryResponse = BalanceOfQueryResponse( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_balance_of( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: ContractBalanceOfQueryParams = ctx.parameter_cursor().get()?; @@ -1041,7 +1029,7 @@ fn contract_balance_of( /// Parameter type for the CIS-2 function `tokenMetadata` specialized to the /// subset of TokenIDs used by this contract. -type ContractTokenMetadataQueryParams = TokenMetadataQueryParams; +pub type ContractTokenMetadataQueryParams = TokenMetadataQueryParams; /// Get the token metadata URLs and checksums given a list of token IDs. /// @@ -1055,9 +1043,9 @@ type ContractTokenMetadataQueryParams = TokenMetadataQueryParams( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_token_metadata( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: ContractTokenMetadataQueryParams = ctx.parameter_cursor().get()?; @@ -1089,9 +1077,9 @@ fn contract_token_metadata( return_value = "SupportsQueryResponse", error = "ContractError" )] -fn contract_supports( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_supports( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: SupportsQueryParams = ctx.parameter_cursor().get()?; @@ -1122,10 +1110,7 @@ fn contract_supports( error = "ContractError", mutable )] -fn contract_set_implementor( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, -) -> ContractResult<()> { +fn contract_set_implementor(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<()> { // Authorize the sender. ensure!(ctx.sender().matches_account(&host.state().admin), ContractError::Unauthorized); // Parse the parameter. @@ -1134,754 +1119,3 @@ fn contract_set_implementor( host.state_mut().set_implementors(params.id, params.implementors); Ok(()) } - -// Tests - -#[concordium_cfg_test] -mod tests { - use super::*; - use concordium_std::test_infrastructure::*; - - const CURRENT_TIME: u64 = 1; - const ACCOUNT_0: AccountAddress = AccountAddress([0u8; 32]); - const ADDRESS_0: Address = Address::Account(ACCOUNT_0); - const ACCOUNT_1: AccountAddress = AccountAddress([1u8; 32]); - const ADDRESS_1: Address = Address::Account(ACCOUNT_1); - const ADMIN_ACCOUNT_0: AccountAddress = AccountAddress([2u8; 32]); - const ADMIN_ACCOUNT_1: AccountAddress = AccountAddress([3u8; 32]); - const NAME_0: &str = "MyCoolName"; - const NAME_1: &str = "EvenCoolerName"; - const SAMPLE_DATA: &str = "GitHub: my-github-name"; - - /// Test helper function which creates a contract state with two tokens with - /// id `NAME_0` and id `NAME_1` owned by `ACCOUNT_0` and `ACCOUNT_1` - /// `NAME_1` has some non-empty associated data - fn initial_state( - expires: Timestamp, - state_builder: &mut StateBuilder, - ) -> State { - let mut state = State::empty(ADMIN_ACCOUNT_0, state_builder); - let crypto_primitives = TestCryptoPrimitives::new(); - let token_0 = crypto_primitives.hash_sha2_256(NAME_0.as_bytes()).0; - let token_1 = crypto_primitives.hash_sha2_256(NAME_1.as_bytes()).0; - state - .register_fresh(TokenIdFixed(token_0), ACCOUNT_0, expires, state_builder) - .expect_report("Failed to register NAME_0"); - state - .register_fresh(TokenIdFixed(token_1), ACCOUNT_1, expires, state_builder) - .expect_report("Failed to register NAME_1"); - let data = SAMPLE_DATA.to_string(); - state - .update_data(&TokenIdFixed(token_1), data.as_bytes()) - .expect_report("Failed to update data for NAME_1"); - state - } - - /// Test registering a fresh name, ensuring the the name is owned by the - /// given address and the appropriate events are logged. - #[concordium_test] - #[cfg(feature = "crypto-primitives")] - fn test_register_fresh() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_metadata_slot_time(Timestamp::from_timestamp_millis(CURRENT_TIME)); - ctx.set_sender(ADDRESS_0); - ctx.set_owner(ACCOUNT_0); - - // and parameter. - let parameter = RegisterNameParams { - name: NAME_0.to_string(), - owner: ACCOUNT_0, - }; - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = State::empty(ADMIN_ACCOUNT_0, &mut state_builder); - let mut host = TestHost::new(state, state_builder); - - let crypto_primitives = TestCryptoPrimitives::new(); - - // Call the contract function. - let result: ContractResult<()> = - contract_register(&ctx, &mut host, REGISTRACTION_FEE, &mut logger, &crypto_primitives); - - // Check the result - claim!(result.is_ok(), "Results in rejection"); - let name_hash = crypto_primitives.hash_sha2_256(NAME_0.as_bytes()).0; - let token_0 = TokenIdFixed(name_hash); - - // Check the state - // Note. This is rather expensive as an iterator is created and then traversed - - // should be avoided when writing smart contracts. - claim_eq!(host.state().all_names.iter().count(), 1, "Expected one name in all names."); - claim_eq!(host.state().state.iter().count(), 1, "Expected one name in the state."); - - let name_info = - host.state().all_names.get(&token_0).expect_report("Token is expected to exist"); - claim_eq!(name_info.owner, ACCOUNT_0, "The name must be owned by ACCOUNT_0"); - - let addr_state = - host.state().state.get(&ACCOUNT_0).expect_report("ACCOUNT_0 must own a name"); - claim!(addr_state.owned_names.contains(&token_0), "ACCOUNT_0 must own token 0"); - - let now = ctx.metadata().slot_time(); - let balance0 = host - .state() - .balance(now, &token_0, &ACCOUNT_0.into()) - .expect_report("Token is expected to exist"); - claim_eq!(balance0, 1.into(), "Token balance should be non-zero"); - - // Check the logs - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::Mint(MintEvent { - owner: ADDRESS_0, - token_id: token_0, - amount: ContractTokenAmount::from(1), - }))), - "Expected an event for minting NAME_0" - ); - - let mut base_url = TOKEN_METADATA_BASE_URL.to_string(); - base_url.push_str(&token_0.to_string()); - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::TokenMetadata::<_, ContractTokenAmount>( - TokenMetadataEvent { - token_id: token_0, - metadata_url: MetadataUrl { - url: base_url.to_string(), - hash: None, - }, - } - ))), - "Expected an event for token metadata for TOKEN_1" - ); - } - - /// Test registering an expired name, ensuring the the name is owned by the - /// new given address and that the data is reset to empty - #[concordium_test] - #[cfg(feature = "crypto-primitives")] - fn test_register_expired() { - // The time of expiry in the past - let old_expiry = Timestamp::from_timestamp_millis(1); - // ensure that the current date is beyond the expiry date, - // so we can register the expired name - let now = old_expiry - .checked_add(Duration::from_days(10)) - .expect_report("Failed to calculate the date"); - - let old_owner = ACCOUNT_0; - let new_owner = ACCOUNT_1; - - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_metadata_slot_time(now); - ctx.set_sender(ADDRESS_0); - ctx.set_owner(old_owner); - - // and parameter. - let parameter = RegisterNameParams { - name: NAME_0.to_string(), - owner: new_owner, - }; - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - - // create initial state where NAME_0 was owned by ACCOUNT_1 - let state = initial_state(old_expiry, &mut state_builder); - let mut host = TestHost::new(state, state_builder); - - let crypto_primitives = TestCryptoPrimitives::new(); - - // Call the contract function. - let result: ContractResult<()> = - contract_register(&ctx, &mut host, REGISTRACTION_FEE, &mut logger, &crypto_primitives); - - // Check the result - claim!(result.is_ok(), "Results in rejection"); - - let token_0 = TokenIdFixed(crypto_primitives.hash_sha2_256(NAME_0.as_bytes()).0); - - let name_info = - host.state().all_names.get(&token_0).expect_report("Token is expected to exist"); - claim_eq!(name_info.owner, new_owner, "The name must be owned by the new owner"); - - let addr_state = - host.state().state.get(&new_owner).expect_report("The new owner must own a name"); - claim!(addr_state.owned_names.contains(&token_0), "The new owner must own the name"); - - let balance_new = host - .state() - .balance(now, &token_0, &new_owner.into()) - .expect_report("Token is expected to exist"); - claim_eq!(balance_new, 1.into(), "Token balance should be non-zero"); - - let balance_old = host - .state() - .balance(now, &token_0, &old_owner.into()) - .expect_report("Token is expected to exist"); - claim_eq!(balance_old, 0.into(), "Token balance should be zero"); - - claim_eq!(name_info.data.get().as_slice(), [], "Data should be empty"); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "Only one event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::Transfer(TransferEvent { - from: ADDRESS_0, - to: ADDRESS_1, - token_id: token_0, - amount: ContractTokenAmount::from(1), - })), - "Incorrect event emitted" - ) - } - - // Register fails if the fee in incorrect - #[concordium_test] - #[cfg(feature = "crypto-primitives")] - fn test_register_fails_incorrect_fee() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_metadata_slot_time(Timestamp::from_timestamp_millis(CURRENT_TIME)); - ctx.set_sender(ADDRESS_0); - ctx.set_owner(ACCOUNT_0); - - // and parameter. - let parameter = RegisterNameParams { - name: NAME_0.to_string(), - owner: ACCOUNT_0, - }; - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = State::empty(ADMIN_ACCOUNT_0, &mut state_builder); - let mut host = TestHost::new(state, state_builder); - - let crypto_primitives = TestCryptoPrimitives::new(); - - // Call the contract function wiht zero cdd - let result: ContractResult<()> = contract_register( - &ctx, - &mut host, - Amount::from_ccd(0), - &mut logger, - &crypto_primitives, - ); - - // Check the result - let err = result.expect_err_report("Expected to fail"); - claim_eq!( - err, - ContractError::Custom(CustomContractError::IncorrectFee), - "Error is expected to be IncorrectFee" - ); - } - - /// Test transfer succeeds, when `from` is the sender. - #[concordium_test] - #[cfg(feature = "crypto-primitives")] - fn test_transfer_account() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - let now = Timestamp::from_timestamp_millis(CURRENT_TIME); - ctx.set_metadata_slot_time(now); - let crypto_primitives = TestCryptoPrimitives::new(); - let token_0 = TokenIdFixed(crypto_primitives.hash_sha2_256(NAME_0.as_bytes()).0); - let token_1 = TokenIdFixed(crypto_primitives.hash_sha2_256(NAME_1.as_bytes()).0); - - // and parameter. - let transfer = Transfer { - token_id: token_0, - amount: ContractTokenAmount::from(1), - from: ACCOUNT_0.into(), - to: Receiver::from_account(ACCOUNT_1), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(now, &mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let balance0 = host - .state() - .balance(now, &token_0, &ACCOUNT_0.into()) - .expect_report("Token is expected to exist"); - let balance1 = host - .state() - .balance(now, &token_0, &ACCOUNT_1.into()) - .expect_report("Token is expected to exist"); - let balance2 = host - .state() - .balance(now, &token_1, &ACCOUNT_1.into()) - .expect_report("Token is expected to exist"); - claim_eq!( - balance0, - 0.into(), - "Token owner balance should be decreased by the transferred amount" - ); - claim_eq!( - balance1, - 1.into(), - "Token receiver balance should be increased by the transferred amount" - ); - claim_eq!( - balance2, - 1.into(), - "Token receiver balance for token 1 should be the same as before" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "Only one event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::Transfer(TransferEvent { - from: ACCOUNT_0.into(), - to: ACCOUNT_1.into(), - token_id: token_0, - amount: ContractTokenAmount::from(1), - })), - "Incorrect event emitted" - ) - } - - // Test transfer succeeds, when `from` is the sender. - #[concordium_test] - #[cfg(feature = "crypto-primitives")] - fn test_transfer_failed_expired_name() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - let now = Timestamp::from_timestamp_millis(1000); - let expired = Timestamp::from_timestamp_millis(0); - ctx.set_metadata_slot_time(now); - let crypto_primitives = TestCryptoPrimitives::new(); - let token_0 = TokenIdFixed(crypto_primitives.hash_sha2_256(NAME_0.as_bytes()).0); - - // and parameter. - let transfer = Transfer { - token_id: token_0, - amount: ContractTokenAmount::from(1), - from: ACCOUNT_0.into(), - to: Receiver::from_account(ACCOUNT_1), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(expired, &mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - // Check the result - let err = result.expect_err_report("Expected to fail"); - claim_eq!( - err, - CustomContractError::NameExpired.into(), - "Error is expected to be NameExpired" - ); - } - - // Test transfer succeeds when sender is not the owner, but is an operator - /// of the owner. - #[concordium_test] - #[cfg(feature = "crypto-primitives")] - fn test_operator_transfer() { - let now = Timestamp::from_timestamp_millis(CURRENT_TIME); - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_1); - ctx.set_metadata_slot_time(now); - - let crypto_primitives = TestCryptoPrimitives::new(); - let token_0 = TokenIdFixed(crypto_primitives.hash_sha2_256(NAME_0.as_bytes()).0); - - // and parameter. - let transfer = Transfer { - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - token_id: token_0, - amount: ContractTokenAmount::from(1), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - - let mut state_builder = TestStateBuilder::new(); - let mut state = initial_state(now, &mut state_builder); - state - .add_operator(&ADDRESS_0, &ADDRESS_1, &mut state_builder) - .expect_report("Failed to add operator"); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let balance0 = host - .state() - .balance(now, &token_0, &ADDRESS_0) - .expect_report("Token is expected to exist"); - let balance1 = host - .state_mut() - .balance(now, &token_0, &ADDRESS_1) - .expect_report("Token is expected to exist"); - claim_eq!( - balance0, - 0.into(), - "Token owner balance should be decreased by the transferred amount" - ); - claim_eq!( - balance1, - 1.into(), - "Token receiver balance should be increased by the transferred amount" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "Only one event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::Transfer(TransferEvent { - from: ADDRESS_0, - to: ADDRESS_1, - token_id: token_0, - amount: ContractTokenAmount::from(1), - })), - "Incorrect event emitted" - ) - } - - // Test renewing an existing name - #[concordium_test] - #[cfg(feature = "crypto-primitives")] - fn test_renew_name() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - let now = Timestamp::from_timestamp_millis(CURRENT_TIME); - ctx.set_metadata_slot_time(now); - - // and parameter - let param: String = NAME_0.to_string(); - let parameter_bytes = to_bytes(¶m); - ctx.set_parameter(¶meter_bytes); - - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(now, &mut state_builder); - let mut host = TestHost::new(state, state_builder); - - let crypto_primitives = TestCryptoPrimitives::new(); - - let token_0 = TokenIdFixed(crypto_primitives.hash_sha2_256(NAME_0.as_bytes()).0); - - // Call the contract function. - let result: ContractResult<()> = - contract_renew(&ctx, &mut host, RENEWAL_FEE, &crypto_primitives); - - claim!(result.is_ok(), "Results in rejection"); - - let old_expires = now; - let name_info = - host.state().all_names.get(&token_0).expect_report("Token expected to exist"); - let new_expires = name_info.name_expires; - let expected = old_expires - .checked_add(Duration::from_days(REGISTRATION_PERIOD_DAYS)) - .expect_report("Overflow"); - claim_eq!(expected, new_expires); - } - - // Test renewing fails if the name is expired - #[concordium_test] - #[cfg(feature = "crypto-primitives")] - fn test_renew_name_fails_expired() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - let expired = Timestamp::from_timestamp_millis(0); - let now = Timestamp::from_timestamp_millis(1000); - ctx.set_metadata_slot_time(now); - - // and parameter - let param: String = NAME_0.to_string(); - let parameter_bytes = to_bytes(¶m); - ctx.set_parameter(¶meter_bytes); - - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(expired, &mut state_builder); - let mut host = TestHost::new(state, state_builder); - - let crypto_primitives = TestCryptoPrimitives::new(); - - // Call the contract function. - let result: ContractResult<()> = - contract_renew(&ctx, &mut host, RENEWAL_FEE, &crypto_primitives); - - // Check the result - let err = result.expect_err_report("Expected to fail"); - claim_eq!( - err, - CustomContractError::NameExpired.into(), - "Error is expected to be NameExpired" - ); - } - - // Test updating data for an existing name - #[concordium_test] - #[cfg(feature = "crypto-primitives")] - fn test_update_data() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - let now = Timestamp::from_timestamp_millis(CURRENT_TIME); - ctx.set_metadata_slot_time(now); - - // and parameter - let data = SAMPLE_DATA.to_string(); - let parameter = UpdateDataParams { - name: NAME_0.to_string(), - data: data.as_bytes().to_vec(), - }; - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(now, &mut state_builder); - let mut host = TestHost::new(state, state_builder); - - let crypto_primitives = TestCryptoPrimitives::new(); - - let token_0 = TokenIdFixed(crypto_primitives.hash_sha2_256(NAME_0.as_bytes()).0); - - // Call the contract function. - let result: ContractResult<()> = - contract_update_data(&ctx, &mut host, UPDATE_FEE, &crypto_primitives); - - claim!(result.is_ok(), "Results in rejection"); - - let name_info = - host.state().all_names.get(&token_0).expect_report("Token expected to exist"); - let saved_data = name_info.data.get(); - claim_eq!(*saved_data.as_slice(), *parameter.data.as_slice()); - } - - // Test updating data fails if the name is expired - #[concordium_test] - #[cfg(feature = "crypto-primitives")] - fn test_update_data_fails_expired() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - let expired = Timestamp::from_timestamp_millis(0); - let now = Timestamp::from_timestamp_millis(1000); - ctx.set_metadata_slot_time(now); - - // and parameter - let data = SAMPLE_DATA.to_string(); - let parameter = UpdateDataParams { - name: NAME_0.to_string(), - data: data.as_bytes().to_vec(), - }; - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(expired, &mut state_builder); - let mut host = TestHost::new(state, state_builder); - - let crypto_primitives = TestCryptoPrimitives::new(); - - // Call the contract function. - let result: ContractResult<()> = - contract_update_data(&ctx, &mut host, UPDATE_FEE, &crypto_primitives); - - // Check the result - let err = result.expect_err_report("Expected to fail"); - claim_eq!( - err, - CustomContractError::NameExpired.into(), - "Error is expected to be NameExpired" - ); - } - - #[concordium_test] - #[cfg(feature = "crypto-primitives")] - fn test_nameinfo_view() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_1); - let now = Timestamp::from_timestamp_millis(CURRENT_TIME); - ctx.set_metadata_slot_time(now); - - // and parameter - let parameter: String = NAME_1.to_string(); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(now, &mut state_builder); - let host = TestHost::new(state, state_builder); - - let crypto_primitives = TestCryptoPrimitives::new(); - - // Call the contract function. - let result: ContractResult = - contract_nameinfo_view(&ctx, &host, &crypto_primitives); - - let token_1 = TokenIdFixed(crypto_primitives.hash_sha2_256(NAME_1.as_bytes()).0); - let original_name_info = - host.state().all_names.get(&token_1).expect_report("Token expected to exist"); - if let Ok(name_info) = result { - claim_eq!( - name_info.data.as_slice(), - original_name_info.data.as_slice(), - "Queried data is different" - ); - claim_eq!(name_info.owner, original_name_info.owner, "Queried owner is different"); - } else { - fail!("Resutls in rejection") - } - } - - // Test querying balances for expired and not expired names - #[concordium_test] - #[cfg(feature = "crypto-primitives")] - fn test_balance_of_expired_not_expired() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - let expired = Timestamp::from_timestamp_millis(0); - let now = Timestamp::from_timestamp_millis(1000); - let future = Timestamp::from_timestamp_millis(10000); - ctx.set_metadata_slot_time(now); - - let mut state_builder = TestStateBuilder::new(); - let state = State::empty(ADMIN_ACCOUNT_0, &mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // and parameter - let crypto_primitives = TestCryptoPrimitives::new(); - let token_0 = TokenIdFixed(crypto_primitives.hash_sha2_256(NAME_0.as_bytes()).0); - let token_1 = TokenIdFixed(crypto_primitives.hash_sha2_256(NAME_1.as_bytes()).0); - let mut parameter_vec: Vec> = Vec::new(); - let q1 = BalanceOfQuery { - token_id: token_0, - address: ADDRESS_0, - }; - let q2 = BalanceOfQuery { - token_id: token_1, - address: ADDRESS_1, - }; - parameter_vec.push(q1); - parameter_vec.push(q2); - let parameter = BalanceOfQueryParams { - queries: parameter_vec, - }; - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let (st, sb) = host.state_and_builder(); - - // `token_0` has expired - st.register_fresh(token_0, ACCOUNT_0, expired, sb) - .expect_report("Failed to register NAME_0"); - // `token_1` hasn't expired yet - st.register_fresh(token_1, ACCOUNT_1, future, sb) - .expect_report("Failed to register NAME_1"); - - // Call the contract function. - let result: ContractResult = - contract_balance_of(&ctx, &host); - - if let Ok(BalanceOfQueryResponse(balances)) = result { - claim_eq!(balances.as_slice(), vec![0.into(), 1.into()], "Queried data is different"); - } else { - fail!("Resutls in rejection") - } - } - - // Test updating admin - #[concordium_test] - fn test_update_admin() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADMIN_ACCOUNT_0.into()); - - // and parameter - let parameter = ADMIN_ACCOUNT_1; - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut state_builder = TestStateBuilder::new(); - - // Initial state uses ADMIN_ACCOUNT_0 as the admin address - let state = - initial_state(Timestamp::from_timestamp_millis(CURRENT_TIME), &mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_update_admin(&ctx, &mut host); - - claim!(result.is_ok(), "Results in rejection"); - - let st = host.state(); - - claim_eq!(st.admin, ADMIN_ACCOUNT_1); - } - - // Test updating admin failsfor an existing name - #[concordium_test] - fn test_update_admin_fails_unauthorized() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - // ACCOUNT_0 is not admin - ctx.set_sender(ACCOUNT_0.into()); - - // and parameter - let parameter = ADMIN_ACCOUNT_1; - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut state_builder = TestStateBuilder::new(); - - // Initial state uses ADMIN_ACCOUNT_0 as the admin address - let state = - initial_state(Timestamp::from_timestamp_millis(CURRENT_TIME), &mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_update_admin(&ctx, &mut host); - - // We expect that that ACCOUNT_0 is not authorized to change the admin address - let err = result.expect_err_report("Expected to fail"); - claim_eq!(err, ContractError::Unauthorized, "Error is expected to be Unathorized"); - } -} diff --git a/examples/nametoken/tests/tests.rs b/examples/nametoken/tests/tests.rs new file mode 100644 index 00000000..65536632 --- /dev/null +++ b/examples/nametoken/tests/tests.rs @@ -0,0 +1,702 @@ +//! Tests for the `nametoken` contract. +use concordium_cis2::*; +use concordium_smart_contract_testing::*; +use nametoken::*; + +/// The tests accounts. +const ALICE: AccountAddress = AccountAddress([0; 32]); +const ALICE_ADDR: Address = Address::Account(ALICE); +const BOB: AccountAddress = AccountAddress([1; 32]); +const BOB_ADDR: Address = Address::Account(BOB); +const CHARLIE: AccountAddress = AccountAddress([2; 32]); + +/// Token IDs. +const NAME_0: &str = "MyEvenCoolerName"; +const NAME_0_TOKEN_ID: ContractTokenId = TokenIdFixed([ + 24, 188, 23, 153, 239, 160, 1, 1, 235, 169, 225, 59, 20, 47, 103, 115, 3, 188, 0, 164, 87, 161, + 21, 92, 67, 206, 126, 235, 0, 101, 54, 231, +]); +const NAME_1: &str = "MyCoolName"; +const NAME_1_TOKEN_ID: ContractTokenId = TokenIdFixed([ + 183, 115, 55, 205, 199, 72, 252, 206, 49, 209, 119, 201, 194, 140, 103, 91, 216, 188, 175, 84, + 169, 226, 17, 22, 227, 62, 16, 117, 35, 207, 236, 137, +]); +const SAMPLE_DATA: &str = "GitHub: my-github-name"; + +/// Initial balance of the accounts. +const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10000); + +/// A signer for all the transactions. +const SIGNER: Signer = Signer::with_one_key(); + +/// Timestamps. +const YEAR: u64 = 365 * 24 * 60 * 60 * 1000; +const DAY: u64 = 24 * 60 * 60 * 1000; +const YEAR_ONE: Timestamp = Timestamp::from_timestamp_millis(YEAR); +const YEAR_TWO: Timestamp = Timestamp::from_timestamp_millis(YEAR + YEAR); +const YEAR_TWO_PLUS_DAY: Timestamp = Timestamp::from_timestamp_millis(YEAR + YEAR + DAY); + +/// Test that registering a fresh name works. +#[test] +fn test_register_fresh() { + // Initialize the chain and contract. Also register two names for Alice. + let (chain, contract_address, update) = initialize_contract_with_alice_names(); + + // Invoke the view entrypoint. + let view = invoke_view(&chain, contract_address); + + // Check that Alice owns the two names `NAME_0` and `NAME_1`. + assert_eq!(view, ViewState { + state: vec![(ALICE, ViewAddressState { + owned_names: vec![NAME_0_TOKEN_ID, NAME_1_TOKEN_ID], + operators: Vec::new(), + })], + all_names: vec![ + (NAME_0_TOKEN_ID, ViewNameInfo { + name_expires: YEAR_ONE, + owner: ALICE, + data: Vec::new(), + }), + (NAME_1_TOKEN_ID, ViewNameInfo { + name_expires: YEAR_ONE, + owner: ALICE, + data: Vec::new(), + }) + ], + }); + + // Check that a mint and tokenmetadata event was emitted. + // Since `initialize_contract_with_alice_names` only returns the update for the + // second registration, that is what we check. + let events = deserialize_update_events(&update); + assert_eq!(events, [ + Cis2Event::Mint(MintEvent { + token_id: NAME_1_TOKEN_ID, + amount: 1.into(), + owner: ALICE_ADDR, + }), + Cis2Event::TokenMetadata(TokenMetadataEvent { + token_id: NAME_1_TOKEN_ID, + metadata_url: MetadataUrl { + url: build_token_metadata_url(&NAME_1_TOKEN_ID), + hash: None, + }, + }), + ]); +} + +/// Test registering an expired name. +#[test] +fn test_register_expired() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_names(); + + // Add some data to `TOKEN_0`, so that we can check that it is cleared on + // re-registration. + update_data(&mut chain, contract_address, NAME_0, SAMPLE_DATA).expect("Update data"); + + // Advance time by 366 days, i.e. beyond the expiration date of the name. + chain.tick_block_time(Duration::from_days(366)).expect("Time doesn't overflow."); + + // Register `NAME_0` with Bob as the owner. + let parameter = RegisterNameParams { + name: NAME_0.to_string(), + owner: BOB, + }; + let update = chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: REGISTRATION_FEE, + receive_name: OwnedReceiveName::new_unchecked("NameToken.register".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶meter).expect("Register name params"), + }) + .expect("Register NAME_0"); + + // Check that the name is now owned by Bob and has no data. + // Also check that the `NAME_1` is now expired. + let view = invoke_view(&chain, contract_address); + assert_eq!(view.all_names, vec![ + (NAME_0_TOKEN_ID, ViewNameInfo { + name_expires: YEAR_TWO_PLUS_DAY, // <- New expiration date. + owner: BOB, + data: Vec::new(), // <- No data. + }), + (NAME_1_TOKEN_ID, ViewNameInfo { + name_expires: YEAR_ONE, // <- Expired. + owner: ALICE, + data: Vec::new(), + }) + ]); + + // Check that a trasnfer event was produced, tranferring `NAME_0` from Alice to + // Bob. + let events = deserialize_update_events(&update); + assert_eq!(events, [Cis2Event::Transfer(TransferEvent { + token_id: NAME_0_TOKEN_ID, + from: ALICE_ADDR, + to: BOB_ADDR, + amount: 1.into(), + }),]); +} + +/// Test that registering fails if the fee is incorrect. +#[test] +fn test_register_fails_incorrect_fee() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_names(); + + // Register `NAME_0` with Bob as the owner. + let parameter = RegisterNameParams { + name: NAME_0.to_string(), + owner: BOB, + }; + let update = chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: REGISTRATION_FEE + Amount::from_micro_ccd(1), // Incorrect fee. + receive_name: OwnedReceiveName::new_unchecked("NameToken.register".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶meter).expect("Register name params"), + }) + .expect_err("Register NAME_0 with incorrect fee"); + + // Check that it returns the correct error. + let error: ContractError = update.parse_return_value().expect("Deserialize error"); + assert_eq!(error, ContractError::Custom(CustomContractError::IncorrectFee)); +} + +/// Test transfer succeeds, when `from` is the sender. +#[test] +fn test_transfer_account() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_names(); + + // Transfer `NAME_0` from Alice to Bob. + let parameter: nametoken::TransferParameter = TransferParams(vec![concordium_cis2::Transfer { + token_id: NAME_0_TOKEN_ID, + amount: ContractTokenAmount::from(1), + from: ALICE_ADDR, + to: Receiver::from_account(BOB), + data: AdditionalData::empty(), + }]); + + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("NameToken.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶meter).expect("Transfer name params"), + }) + .expect("Transfer NAME_0"); + + // Check that name 0 is now owned by Bob, and that Alice still owns name 1. + let view = invoke_view(&chain, contract_address); + assert_eq!(view.all_names, vec![ + (NAME_0_TOKEN_ID, ViewNameInfo { + name_expires: YEAR_ONE, + owner: BOB, + data: Vec::new(), + }), + (NAME_1_TOKEN_ID, ViewNameInfo { + name_expires: YEAR_ONE, + owner: ALICE, + data: Vec::new(), + }) + ]); + + // Check that a trasnfer event was produced, tranferring `NAME_0` from Alice to + // Bob. + let events = deserialize_update_events(&update); + assert_eq!(events, [Cis2Event::Transfer(TransferEvent { + token_id: NAME_0_TOKEN_ID, + from: ALICE_ADDR, + to: BOB_ADDR, + amount: 1.into(), + }),]); +} + +/// Test that a transfer fails when the name is expired. +#[test] +fn test_transfer_expired_name_fails() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_names(); + + // Advance time by 366 days, i.e. beyond the expiration date of the name. + chain.tick_block_time(Duration::from_days(366)).expect("Time doesn't overflow."); + + // Transfer `NAME_0` from Alice to Bob. + let parameter: nametoken::TransferParameter = TransferParams(vec![concordium_cis2::Transfer { + token_id: NAME_0_TOKEN_ID, + amount: ContractTokenAmount::from(1), + from: ALICE_ADDR, + to: Receiver::from_account(BOB), + data: AdditionalData::empty(), + }]); + + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("NameToken.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶meter).expect("Transfer name params"), + }) + .expect_err("Transfer NAME_0"); + + // Check that it returns the correct error. + let error: ContractError = update.parse_return_value().expect("Deserialize error"); + assert_eq!(error, ContractError::Custom(CustomContractError::NameExpired)); +} + +/// Test that adding an operator works and produces the correct events. +#[test] +fn test_add_operator() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_names(); + + // Add Bob as an operator for Alice. + let update_operator = add_bob_the_operator(&mut chain, contract_address); + + // Check that the operator was added. + let view = invoke_view(&chain, contract_address); + assert_eq!(view, ViewState { + state: vec![(ALICE, ViewAddressState { + owned_names: vec![NAME_0_TOKEN_ID, NAME_1_TOKEN_ID], + operators: vec![BOB_ADDR], // Bob is now an operator. + })], + all_names: vec![ + (NAME_0_TOKEN_ID, ViewNameInfo { + name_expires: YEAR_ONE, + owner: ALICE, + data: Vec::new(), + }), + (NAME_1_TOKEN_ID, ViewNameInfo { + name_expires: YEAR_ONE, + owner: ALICE, + data: Vec::new(), + }) + ], + }); + + // Check that an operator event was emitted. + let events = deserialize_update_events(&update_operator); + assert_eq!(events, [Cis2Event::UpdateOperator(UpdateOperatorEvent { + owner: ALICE_ADDR, + operator: BOB_ADDR, + update: OperatorUpdate::Add, + }),]); +} + +/// Test that an operator can make a transfer on behalf of the owner. +#[test] +fn test_operator_transfer() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_names(); + + // Add Bob as an operator for Alice. + add_bob_the_operator(&mut chain, contract_address); + + // Transfer `NAME_0` from Alice to Charlie, using Bob as the operator. + let parameter: nametoken::TransferParameter = TransferParams(vec![concordium_cis2::Transfer { + token_id: NAME_0_TOKEN_ID, + amount: ContractTokenAmount::from(1), + from: ALICE_ADDR, + to: Receiver::from_account(CHARLIE), + data: AdditionalData::empty(), + }]); + chain + .contract_update( + SIGNER, + BOB, + BOB_ADDR, // Bob the operator sends it. + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("NameToken.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶meter) + .expect("Transfer name params"), + }, + ) + .expect("Transfer NAME_0"); + + // Check the new balances. + let view = invoke_view(&chain, contract_address); + assert_eq!(view, ViewState { + state: vec![ + (ALICE, ViewAddressState { + owned_names: vec![NAME_1_TOKEN_ID], + operators: vec![BOB_ADDR], + }), + (CHARLIE, ViewAddressState { + owned_names: vec![NAME_0_TOKEN_ID], // Charlie now owns name 0. + operators: Vec::new(), + }) + ], + all_names: vec![ + (NAME_0_TOKEN_ID, ViewNameInfo { + name_expires: YEAR_ONE, + owner: CHARLIE, + data: Vec::new(), + }), + (NAME_1_TOKEN_ID, ViewNameInfo { + name_expires: YEAR_ONE, + owner: ALICE, + data: Vec::new(), + }) + ], + }); +} + +/// Test renewing an existing name. +#[test] +fn test_renew_name() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_names(); + + // Renew `NAME_0`. + let parameter = NAME_0.to_string(); + + chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: RENEWAL_FEE, // Send renewal fee. + receive_name: OwnedReceiveName::new_unchecked("NameToken.renewName".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶meter).expect("Renew name params"), + }) + .expect("Renew NAME_0"); + + // Check that the new expiration date is 1 year from now. + let view = invoke_view(&chain, contract_address); + assert_eq!(view.all_names, vec![ + (NAME_0_TOKEN_ID, ViewNameInfo { + name_expires: YEAR_TWO, // Now expires in two year. + owner: ALICE, + data: Vec::new(), + }), + (NAME_1_TOKEN_ID, ViewNameInfo { + name_expires: YEAR_ONE, + owner: ALICE, + data: Vec::new(), + }) + ]); +} + +/// Test that renewing a name fails if the name has expired. +#[test] +fn test_renew_expired_name_fails() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_names(); + + // Advance time by 366 days, i.e. beyond the expiration date of the name. + chain.tick_block_time(Duration::from_days(366)).expect("Time doesn't overflow."); + + // Renew `NAME_0`. + let parameter = NAME_0.to_string(); + + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: RENEWAL_FEE, // Send renewal fee. + receive_name: OwnedReceiveName::new_unchecked("NameToken.renewName".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶meter).expect("Renew name params"), + }) + .expect_err("Renew NAME_0"); + + // Check that it returns the correct error. + let error: ContractError = update.parse_return_value().expect("Deserialize error"); + assert_eq!(error, ContractError::Custom(CustomContractError::NameExpired)); +} + +/// Test updating data for an existing name. +#[test] +fn test_update_data() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_names(); + + // Update the data. + update_data(&mut chain, contract_address, NAME_0, SAMPLE_DATA).expect("Update data"); + + // Check that the data was updated. + let view = invoke_view(&chain, contract_address); + assert_eq!(view.all_names, vec![ + (NAME_0_TOKEN_ID, ViewNameInfo { + name_expires: YEAR_ONE, + owner: ALICE, + data: SAMPLE_DATA.as_bytes().to_owned(), // The new data. + }), + (NAME_1_TOKEN_ID, ViewNameInfo { + name_expires: YEAR_ONE, + owner: ALICE, + data: Vec::new(), + }) + ]); +} + +/// Test that updating the data on an expired name fails. +#[test] +fn test_update_data_on_expired_fails() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_names(); + + // Advance time by 366 days, i.e. beyond the expiration date of the name. + chain.tick_block_time(Duration::from_days(366)).expect("Time doesn't overflow."); + + // Update the data. + let update = update_data(&mut chain, contract_address, NAME_0, SAMPLE_DATA) + .expect_err("Update data on expired name"); + + // Check that it returns the correct error. + let error: ContractError = update.parse_return_value().expect("Deserialize error"); + assert_eq!(error, ContractError::Custom(CustomContractError::NameExpired)); +} + +/// Test the nameinfo view. +#[test] +fn test_name_info_view() { + let (chain, contract_address, _update) = initialize_contract_with_alice_names(); + + // Invoke the view entrypoint. + let parameter = NAME_0.to_string(); + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("NameToken.viewNameInfo".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶meter).expect("Name info params"), + }) + .expect("Invoke view"); + + // Check that the view returns the correct data. + let view: ViewNameInfo = invoke.parse_return_value().expect("Deserialize view"); + assert_eq!(view, ViewNameInfo { + name_expires: YEAR_ONE, + owner: ALICE, + data: Vec::new(), + }); +} + +/// Test querying balances for expired and unexpired names. +#[test] +fn test_balance_of_expired_not_expired() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_names(); + + // Renew `NAME_0`, so that it expires in year two. + let parameter = NAME_0.to_string(); + chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: RENEWAL_FEE, // Send renewal fee. + receive_name: OwnedReceiveName::new_unchecked("NameToken.renewName".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶meter).expect("Renew name params"), + }) + .expect("Renew NAME_0"); + + // Advance time by 366 days, i.e. beyond the expiration date of the name 1. + chain.tick_block_time(Duration::from_days(366)).expect("Time doesn't overflow."); + + // Construct the balance of parameter. + let parameter: ContractBalanceOfQueryParams = BalanceOfQueryParams { + queries: vec![ + BalanceOfQuery { + address: ALICE_ADDR, + token_id: NAME_0_TOKEN_ID, + }, + BalanceOfQuery { + address: ALICE_ADDR, + token_id: NAME_1_TOKEN_ID, + }, + ], + }; + + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("NameToken.balanceOf".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶meter).expect("Balance of params"), + }) + .expect("Invoke view"); + + // Check that Alice now has one name 0, which is still valid, and zero of name + // 1, which is expired. + let response: ContractBalanceOfQueryResponse = + invoke.parse_return_value().expect("Deserialize view"); + assert_eq!( + response, + BalanceOfQueryResponse(vec![ContractTokenAmount::from(1), ContractTokenAmount::from(0)]) + ); +} + +/// Test that the admin can be updated. +#[test] +fn test_update_admin() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_names(); + + // The new admin is Charlie. + let new_admin = CHARLIE; + + // Update the admin. + chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("NameToken.updateAdmin".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&new_admin).expect("Update admin params"), + }) + .expect("Update admin"); +} + +/// Test that the admin can't be updated by a non-admin. +#[test] +fn test_update_admin_fails_unauthorized() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_names(); + + // The new admin is Charlie. + let new_admin = CHARLIE; + + // Update the admin. + let update = chain + .contract_update( + SIGNER, + BOB, + BOB_ADDR, // Bob is not the admin. + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("NameToken.updateAdmin".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&new_admin).expect("Update admin params"), + }, + ) + .expect_err("Update admin"); + + // Check that it returns the correct error. + let error: ContractError = update.parse_return_value().expect("Deserialize error"); + assert_eq!(error, ContractError::Unauthorized); +} + +// Helpers: + +/// Update the data of a name using the Alice account. +/// Returns the result. +fn update_data( + chain: &mut Chain, + contract_address: ContractAddress, + name: &str, + data: &str, +) -> Result { + let parameter = UpdateDataParams { + name: name.to_string(), + data: data.as_bytes().to_owned(), + }; + + chain.contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: UPDATE_FEE, + receive_name: OwnedReceiveName::new_unchecked("NameToken.updateData".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶meter).expect("Update name data params"), + }) +} + +/// Helper function that sets up the contract with two names owned by Alice, +/// `NAME_0` and `NAME_1`. +/// +/// Returns the `ContractInvokeSuccess` for the `NAME_1` registration. +fn initialize_contract_with_alice_names() -> (Chain, ContractAddress, ContractInvokeSuccess) { + let (mut chain, contract_address) = initialize_chain_and_contract(); + + let parameter_0 = RegisterNameParams { + name: NAME_0.to_string(), + owner: ALICE, + }; + + // Register `NAME_0`. + chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: REGISTRATION_FEE, + receive_name: OwnedReceiveName::new_unchecked("NameToken.register".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶meter_0).expect("Register name params"), + }) + .expect("Register NAME_0"); + + let parameter_1 = RegisterNameParams { + name: NAME_1.to_string(), + owner: ALICE, + }; + + // Register `NAME_1`. + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: REGISTRATION_FEE, + receive_name: OwnedReceiveName::new_unchecked("NameToken.register".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶meter_1).expect("Register name params"), + }) + .expect("Register NAME_1"); + + (chain, contract_address, update) +} + +/// Invoke the view entrypoint and return its results. +fn invoke_view(chain: &Chain, contract_address: ContractAddress) -> ViewState { + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("NameToken.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + invoke.parse_return_value().expect("Deserialize ViewState") +} + +/// Deserialize the events from an update. +fn deserialize_update_events( + update: &ContractInvokeSuccess, +) -> Vec> { + update + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect() +} + +/// Setup chain and contract. +/// +/// Also creates the three accounts, Alice, Bob and Charlie. +/// +/// Alice is the owner and admin of the contract. +fn initialize_chain_and_contract() -> (Chain, ContractAddress) { + let mut chain = Chain::new(); + + // Create some accounts on the chain. + chain.create_account(Account::new(ALICE, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(CHARLIE, ACC_INITIAL_BALANCE)); + + // Load and deploy the module. + let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); + let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Deploy valid module"); + + // Initialize the auction contract. + let init = chain + .contract_init(SIGNER, ALICE, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_NameToken".to_string()), + param: OwnedParameter::empty(), + }) + .expect("Initialize contract"); + + (chain, init.contract_address) +} + +/// Helper function that adds BOB as an operator for ALICE. +/// Returns the `ContractInvokeSuccess` for the update. +fn add_bob_the_operator( + chain: &mut Chain, + contract_address: ContractAddress, +) -> ContractInvokeSuccess { + let parameter = UpdateOperatorParams(vec![UpdateOperator { + update: OperatorUpdate::Add, + operator: BOB_ADDR, + }]); + + chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("NameToken.updateOperator".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶meter).expect("Update operator params"), + }) + .expect("Add Bob as operator") +} diff --git a/examples/offchain-transfers/src/lib.rs b/examples/offchain-transfers/src/lib.rs index 0bfba771..f6a8aa69 100644 --- a/examples/offchain-transfers/src/lib.rs +++ b/examples/offchain-transfers/src/lib.rs @@ -582,6 +582,7 @@ pub fn contract_get_settlement( // Tests // #[concordium_cfg_test] +#[allow(deprecated)] mod tests { use super::*; use concordium_std::test_infrastructure::*; diff --git a/examples/piggy-bank/part1/src/lib.rs b/examples/piggy-bank/part1/src/lib.rs index da976a6d..dbaf77e1 100644 --- a/examples/piggy-bank/part1/src/lib.rs +++ b/examples/piggy-bank/part1/src/lib.rs @@ -27,19 +27,16 @@ enum PiggyBankState { /// Setup a new Intact piggy bank. #[init(contract = "PiggyBank")] -fn piggy_init( - _ctx: &impl HasInitContext, - _state_builder: &mut StateBuilder, -) -> InitResult { +fn piggy_init(_ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult { // Always succeeds Ok(PiggyBankState::Intact) } /// Insert some CCD into a piggy bank, allowed by anyone. #[receive(contract = "PiggyBank", name = "insert", payable)] -fn piggy_insert( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, +fn piggy_insert( + _ctx: &ReceiveContext, + host: &Host, _amount: Amount, ) -> ReceiveResult<()> { // Ensure the piggy bank has not been smashed already. @@ -50,10 +47,7 @@ fn piggy_insert( /// Smash a piggy bank retrieving the CCD, only allowed by the owner. #[receive(contract = "PiggyBank", name = "smash", mutable)] -fn piggy_smash( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, -) -> ReceiveResult<()> { +fn piggy_smash(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { // Get the contract owner, i.e. the account who initialized the contract. let owner = ctx.owner(); // Get the sender, who triggered this function, either a smart contract or @@ -75,9 +69,9 @@ fn piggy_smash( /// View the state and balance of the piggy bank. #[receive(contract = "PiggyBank", name = "view", return_value = "(PiggyBankState, Amount)")] -fn piggy_view( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, +fn piggy_view( + _ctx: &ReceiveContext, + host: &Host, ) -> ReceiveResult<(PiggyBankState, Amount)> { let current_state = *host.state(); let current_balance = host.self_balance(); diff --git a/examples/piggy-bank/part2/Cargo.toml b/examples/piggy-bank/part2/Cargo.toml index 59100ef4..7e246acc 100644 --- a/examples/piggy-bank/part2/Cargo.toml +++ b/examples/piggy-bank/part2/Cargo.toml @@ -23,7 +23,7 @@ path = "../../../concordium-std" default-features = false [dev-dependencies] -concordium-smart-contract-testing = "2.0" +concordium-smart-contract-testing = { path = "../../../contract-testing" } [profile.release] opt-level = 3 diff --git a/examples/piggy-bank/part2/src/lib.rs b/examples/piggy-bank/part2/src/lib.rs index 313ea896..484bb065 100644 --- a/examples/piggy-bank/part2/src/lib.rs +++ b/examples/piggy-bank/part2/src/lib.rs @@ -29,19 +29,16 @@ pub enum PiggyBankState { /// Setup a new Intact piggy bank. #[init(contract = "PiggyBank")] -fn piggy_init( - _ctx: &impl HasInitContext, - _state_builder: &mut StateBuilder, -) -> InitResult { +fn piggy_init(_ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult { // Always succeeds Ok(PiggyBankState::Intact) } /// Insert some CCD into a piggy bank, allowed by anyone. #[receive(contract = "PiggyBank", name = "insert", payable)] -fn piggy_insert( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, +fn piggy_insert( + _ctx: &ReceiveContext, + host: &Host, _amount: Amount, ) -> ReceiveResult<()> { // Ensure the piggy bank has not been smashed already. @@ -59,10 +56,7 @@ pub enum SmashError { /// Smash a piggy bank retrieving the CCD, only allowed by the owner. #[receive(contract = "PiggyBank", name = "smash", mutable)] -fn piggy_smash( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, -) -> Result<(), SmashError> { +fn piggy_smash(ctx: &ReceiveContext, host: &mut Host) -> Result<(), SmashError> { // Get the contract owner, i.e. the account who initialized the contract. let owner = ctx.owner(); // Get the sender, who triggered this function, either a smart contract or @@ -90,9 +84,9 @@ fn piggy_smash( /// View the state and balance of the piggy bank. #[receive(contract = "PiggyBank", name = "view", return_value = "(PiggyBankState, Amount)")] -fn piggy_view( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, +fn piggy_view( + _ctx: &ReceiveContext, + host: &Host, ) -> ReceiveResult<(PiggyBankState, Amount)> { let current_state = *host.state(); let current_balance = host.self_balance(); diff --git a/examples/piggy-bank/part2/tests/tests.rs b/examples/piggy-bank/part2/tests/tests.rs index 512c86d0..fc7b9ebf 100644 --- a/examples/piggy-bank/part2/tests/tests.rs +++ b/examples/piggy-bank/part2/tests/tests.rs @@ -18,7 +18,8 @@ fn setup_chain_and_contract() -> (Chain, ContractInitSuccess) { chain.create_account(Account::new(ACC_ADDR_OWNER, ACC_INITIAL_BALANCE)); chain.create_account(Account::new(ACC_ADDR_OTHER, ACC_INITIAL_BALANCE)); - let module = module_load_v1("piggy_bank_part2.wasm.v1").expect("Module exists and is valid"); + let module = + module_load_v1("./concordium-out/module.wasm.v1").expect("Module exists and is valid"); let deployment = chain .module_deploy_v1(Signer::with_one_key(), ACC_ADDR_OWNER, module) .expect("Deploying valid module should succeed"); @@ -117,7 +118,7 @@ fn test_smash_intact() { // Ensure the values returned by the view function are correct. let (state, balance): (PiggyBankState, Amount) = - from_bytes(&invoke_result.return_value).expect("View should always return a valid result"); + invoke_result.parse_return_value().expect("View should always return a valid result"); assert_eq!(state, PiggyBankState::Smashed); assert_eq!(balance, Amount::zero()); assert_eq!(update.account_transfers().collect::>(), [( @@ -148,9 +149,8 @@ fn test_smash_intact_not_owner() { ) .expect_err("Smashing should only succeed for the owner"); - let return_value = - update_err.return_value().expect("Contract should reject and thus return bytes"); - let error: SmashError = from_bytes(&return_value) + let error: SmashError = update_err + .parse_return_value() .expect("Contract should return a `SmashError` in serialized form"); assert_eq!(error, SmashError::NotOwner, "Contract did not fail due to a NotOwner error"); @@ -158,7 +158,7 @@ fn test_smash_intact_not_owner() { chain.account_balance_available(ACC_ADDR_OTHER), Some(ACC_INITIAL_BALANCE - update_err.transaction_fee), "The invoker account was incorrectly charged" - ) + ); } /// Test that smashing an already smashed piggy bank is not allowed and thus @@ -198,9 +198,8 @@ fn test_smash_smashed() { ) .expect_err("The piggybank cannot be smashed more than once"); - let return_value = - update_second_smash_err.return_value().expect("Contract should reject and return bytes"); - let error: SmashError = from_bytes(&return_value) + let error: SmashError = update_second_smash_err + .parse_return_value() .expect("Contract should return a `SmashError` in serialized form"); assert_eq!(error, SmashError::AlreadySmashed); diff --git a/examples/proxy/Cargo.toml b/examples/proxy/Cargo.toml index bfdca3c0..daee7d9f 100644 --- a/examples/proxy/Cargo.toml +++ b/examples/proxy/Cargo.toml @@ -16,6 +16,9 @@ wee_alloc = ["concordium-std/wee_alloc"] [dependencies] concordium-std = {path = "../../concordium-std"} +[dev-dependencies] +concordium-smart-contract-testing = { path = "../../contract-testing" } + [lib] crate-type = ["cdylib", "rlib"] diff --git a/examples/proxy/src/lib.rs b/examples/proxy/src/lib.rs index cfcde3b5..2a7c81e9 100644 --- a/examples/proxy/src/lib.rs +++ b/examples/proxy/src/lib.rs @@ -14,6 +14,11 @@ //! The proxy also has the entrypoint "________reconfigure", which enables the //! owner of the proxy to change the proxied contract. The entrypoint name is //! prefixed by underscores to avoid naming conflicts with the proxied contract. +//! +//! For testing purposes, the contract "world_appender" is included, which +//! appends ", world" to the parameter and returns it. +//! +//! The tests are located in `/tests/tests.rs`. use concordium_std::*; /// The contract behind this proxy. @@ -22,7 +27,7 @@ type State = ContractAddress; /// Needed for the custom serial instance, which doesn't include the `Option` /// tag and the length of the vector. #[derive(PartialEq, Eq, Debug)] -struct RawReturnValue(Option>); +pub struct RawReturnValue(Option>); impl Serial for RawReturnValue { fn serial(&self, out: &mut W) -> Result<(), W::Err> { @@ -35,10 +40,7 @@ impl Serial for RawReturnValue { /// Initialize the contract by specifying the contract to be proxied. #[init(contract = "proxy", parameter = "ContractAddress")] -fn init( - ctx: &impl HasInitContext, - _state_builder: &mut StateBuilder, -) -> InitResult { +fn init(ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult { let proxied_contract = ctx.parameter_cursor().get()?; Ok(proxied_contract) } @@ -46,9 +48,9 @@ fn init( /// The fallback method, which redirects the invocations to the proxied /// contract. #[receive(contract = "proxy", fallback, mutable, payable)] -fn receive_fallback( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, +fn receive_fallback( + ctx: &ReceiveContext, + host: &mut Host, amount: Amount, ) -> ReceiveResult { let entrypoint = ctx.named_entrypoint(); @@ -80,47 +82,25 @@ fn receive_fallback( /// The underscores in the name are to avoid naming conflicts with entrypoints /// in the proxied contract. #[receive(contract = "proxy", name = "________reconfigure", mutable, parameter = "ContractAddress")] -fn receive_reconfigure( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, -) -> ReceiveResult<()> { +fn receive_reconfigure(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { ensure!(ctx.sender().matches_account(&ctx.owner())); *host.state_mut() = ctx.parameter_cursor().get()?; Ok(()) } -#[concordium_cfg_test] -mod tests { - use super::*; - use concordium_std::test_infrastructure::*; - - #[concordium_test] - fn proxy_forwards_and_returns_data_unaltered() { - // Arrange - let proxied_contract = ContractAddress { - index: 0, - subindex: 0, - }; - let proxied_entrypoint = OwnedEntrypointName::new_unchecked("some_entrypoint".into()); - let mut ctx = TestReceiveContext::empty(); - ctx.set_named_entrypoint(proxied_entrypoint.clone()); - ctx.set_parameter(b"hello"); - - let mut host = TestHost::new(proxied_contract, TestStateBuilder::new()); - host.setup_mock_entrypoint( - proxied_contract, - proxied_entrypoint, - MockFn::new_v1(|parameter, _, &mut _, &mut _| { - let mut rv = Into::<&[u8]>::into(parameter).to_vec(); - rv.extend_from_slice(b", world!"); - Ok((false, RawReturnValue(Some(rv)))) - }), - ); +////////////////////////////////////////// - // Act - let result = receive_fallback(&ctx, &mut host, Amount::zero()); +// Initialize the world_appender contract. +#[init(contract = "world_appender", parameter = "String")] +fn init_world_appender(_ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult<()> { + Ok(()) +} - // Assert - claim_eq!(result, Ok(RawReturnValue(Some(b"hello, world!".to_vec())))) - } +// Append receive method, which appends `", world"` to the parameter and returns +// it. +#[receive(contract = "world_appender", name = "append", parameter = "String")] +fn world_appender_append(ctx: &ReceiveContext, _host: &Host<()>) -> ReceiveResult { + let mut parameter: String = ctx.parameter_cursor().get()?; + parameter.push_str(", world"); + Ok(parameter) } diff --git a/examples/proxy/tests/tests.rs b/examples/proxy/tests/tests.rs new file mode 100644 index 00000000..6a29e46b --- /dev/null +++ b/examples/proxy/tests/tests.rs @@ -0,0 +1,64 @@ +//! Tests for the proxy example. +use concordium_smart_contract_testing::*; + +const ALICE: AccountAddress = AccountAddress([0u8; 32]); +const ALICE_ADDR: Address = Address::Account(ALICE); +const SIGNER: Signer = Signer::with_one_key(); + +/// Tests that the proxy forwards the invocation to the proxied contract and +/// that it returns the return value with any additional bytes prepended (see +/// `RawReturnValue`s `Serial` implemenetation for details). +#[test] +fn test_forwards_and_returns_data_unaltered() { + let mut chain = Chain::new(); + + // Create an account. + chain.create_account(Account::new(ALICE, Amount::from_ccd(1000))); + + // Load and deploy the module. + let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists."); + let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Module deploys."); + + // Initialize the world_appender contract. + let init_world_appender = chain + .contract_init(SIGNER, ALICE, Energy::from(10_000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_world_appender".to_string()), + param: OwnedParameter::empty(), + }) + .expect("Initialize world_appender contract"); + + // Create the proxy contract. + let init_proxy = chain + .contract_init(SIGNER, ALICE, Energy::from(10_000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_proxy".to_string()), + param: OwnedParameter::from_serial(&init_world_appender.contract_address) + .expect("Serialize appender contract address parameter"), + }) + .expect("Initialize proxy contract"); + + // Construct the parameter. + let parameter = "hello"; + + // Call the `append` entrypoint via the proxy contract. Send `"hello"` as the + // input parameter. + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { + amount: Amount::zero(), + address: init_proxy.contract_address, // Note that this is the proxy address. + receive_name: OwnedReceiveName::new_unchecked("proxy.append".to_string()), + message: OwnedParameter::from_serial(¶meter).expect("Serialize parameter"), + }) + .expect("Invoke proxy contract"); + + // Check that the return value can be deserialized and is correct. + // This ensures that the `RawReturnValue`s serial implementation is correct, + // in that it *doesn't* include the option tag and length values in the return + // value. If they were included, the string would have some extra bytes at + // the beginning. + let return_value: String = update.parse_return_value().expect("Deserialize return value"); + assert_eq!(return_value, "hello, world"); +} diff --git a/examples/recorder/Cargo.toml b/examples/recorder/Cargo.toml index 904e70bc..ee6ebdce 100644 --- a/examples/recorder/Cargo.toml +++ b/examples/recorder/Cargo.toml @@ -15,5 +15,8 @@ wee_alloc = ["concordium-std/wee_alloc"] [dependencies] concordium-std = {path = "../../concordium-std", default-features = false} +[dev-dependencies] +concordium-smart-contract-testing = { path = "../../contract-testing" } + [lib] crate-type=["cdylib", "rlib"] diff --git a/examples/recorder/src/lib.rs b/examples/recorder/src/lib.rs index a4956ab4..a27bf6dd 100644 --- a/examples/recorder/src/lib.rs +++ b/examples/recorder/src/lib.rs @@ -10,29 +10,25 @@ //! deleted from the state. //! //! This contract tests a reasonably small example of state interactions. +//! +//! Tests are located in the `./tests` folder. use concordium_std::*; #[derive(Serial, DeserialWithState)] #[concordium(state_parameter = "S")] -struct State { +struct State { addresses: StateSet, } #[init(contract = "recorder")] -fn init( - _ctx: &impl HasInitContext, - state_builder: &mut StateBuilder, -) -> InitResult> { +fn init(_ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { Ok(State { addresses: state_builder.new_set(), }) } #[receive(contract = "recorder", name = "record", parameter = "AccountAddress", mutable)] -fn receive_record( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, -) -> ReceiveResult<()> { +fn receive_record(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { let address: AccountAddress = ctx.parameter_cursor().get()?; host.state_mut().addresses.insert(address); @@ -46,20 +42,14 @@ fn receive_record( return_value = "bool", mutable )] -fn receive_delete( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, -) -> ReceiveResult { +fn receive_delete(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult { let addr_to_remove = ctx.parameter_cursor().get()?; let res = host.state_mut().addresses.remove(&addr_to_remove); Ok(res) } #[receive(contract = "recorder", name = "transfer", return_value = "u64", mutable)] -fn receive_transfer( - _ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, -) -> ReceiveResult { +fn receive_transfer(_ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult { let addresses = &host.state().addresses; let mut count = 0; for addr in addresses.iter() { @@ -72,68 +62,10 @@ fn receive_transfer( } #[receive(contract = "recorder", name = "list", return_value = "Vec")] -fn receive_list( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ReceiveResult> { +fn receive_list(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult> { let mut ret: Vec = Vec::new(); for addr in host.state().addresses.iter() { ret.push(*addr); } Ok(ret) } - -#[concordium_cfg_test] -mod tests { - use super::*; - use test_infrastructure::*; - - #[concordium_test] - fn test_init() { - let ctx = TestInitContext::empty(); - - let mut state_builder = TestStateBuilder::new(); - - let _ = init(&ctx, &mut state_builder).expect_report("Init failed"); - } - - #[concordium_test] - fn test_receive() { - let state_api = TestStateApi::new(); - let mut state_builder = TestStateBuilder::open(state_api); - - // Set up initial state contents via init. - let initial_state = - init(&TestInitContext::empty(), &mut state_builder).expect_report("Init failed"); - - let mut host = TestHost::new(initial_state, state_builder); - - let mut ctx = TestReceiveContext::empty(); - - let addr0 = AccountAddress([0u8; 32]); - ctx.set_parameter(addr0.as_ref()); - receive_record(&ctx, &mut host).expect_report("Recording failed."); - - let addr1 = AccountAddress([1u8; 32]); - ctx.set_parameter(addr1.as_ref()); - receive_record(&ctx, &mut host).expect_report("Recording failed."); - - let list = - receive_list(&TestReceiveContext::empty(), &host).expect_report("Listing failed."); - claim_eq!(&list[..], &[addr0, addr1][..], "Contract has incorrect addresses."); - - let n = receive_transfer(&TestReceiveContext::empty(), &mut host) - .expect_report("Transfer failed."); - claim_eq!(n, 2, "Incorrect number of transfers."); - let transfers_occurred = host.get_transfers(); - claim_eq!( - &transfers_occurred[..], - &[(addr0, Amount::from_micro_ccd(0)), (addr1, Amount::from_micro_ccd(0))][..] - ); - - // now the contract state should be empty - let list = - receive_list(&TestReceiveContext::empty(), &host).expect_report("Listing failed."); - claim_eq!(&list[..], &[][..], "Contract should have no addresses."); - } -} diff --git a/examples/recorder/tests/tests.rs b/examples/recorder/tests/tests.rs new file mode 100644 index 00000000..67767a8a --- /dev/null +++ b/examples/recorder/tests/tests.rs @@ -0,0 +1,124 @@ +//! Tests for the recorder example contract. +use concordium_smart_contract_testing::*; + +const ACC_0: AccountAddress = AccountAddress([0u8; 32]); +const ACC_1: AccountAddress = AccountAddress([1u8; 32]); +const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(1000); +const SIGNER: Signer = Signer::with_one_key(); + +#[test] +fn tests() { + // Create the test chain. + let mut chain = Chain::new(); + + // Create two accounts on the chain. + chain.create_account(Account::new(ACC_0, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(ACC_1, ACC_INITIAL_BALANCE)); + + // Load and deploy the module. + let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); + let deployment = chain.module_deploy_v1(SIGNER, ACC_0, module).expect("Deploy valid module"); + + // Initialize the contract. + let initialization = chain + .contract_init(SIGNER, ACC_0, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_recorder".to_string()), + param: OwnedParameter::empty(), + }) + .expect("Init should succeed"); + let contract_address = initialization.contract_address; + + // Record two addresses. + chain + .contract_update( + SIGNER, + ACC_0, + Address::Account(ACC_0), + Energy::from(5000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("recorder.record".to_string()), + message: OwnedParameter::from_serial(&ACC_0) + .expect("Serialize account address."), + }, + ) + .expect("Recording `ACC_0`"); + chain + .contract_update( + SIGNER, + ACC_0, + Address::Account(ACC_0), + Energy::from(5000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("recorder.record".to_string()), + message: OwnedParameter::from_serial(&ACC_1) + .expect("Serialize account address."), + }, + ) + .expect("Recording `ACC_1`"); + + // Check that both addresses are returned by the 'list' view function. + let view_list_1 = chain + .contract_invoke( + ACC_0, + Address::Account(ACC_0), + Energy::from(5000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("recorder.list".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect("Viewing list with two elements"); + let returned_list_1: Vec = + view_list_1.parse_return_value().expect("Decoding return value"); + assert_eq!(returned_list_1[..], [ACC_0, ACC_1]); + + // Make the transfers to all accounts. + let update_transfer = chain + .contract_update( + SIGNER, + ACC_0, + Address::Account(ACC_0), + Energy::from(5000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("recorder.transfer".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect("Recording`ACC_1`"); + + // Check that the contract returns `2` for the number of transfers made. + let transfers_made: u64 = update_transfer.parse_return_value().expect("Decoding return value."); + assert_eq!(transfers_made, 2); + assert_eq!(update_transfer.account_transfers().collect::>()[..], [ + (contract_address, Amount::zero(), ACC_0), + (contract_address, Amount::zero(), ACC_1) + ]); + + // Check that the 'list' view function now returns an empty list. + let view_list_2 = chain + .contract_invoke( + ACC_0, + Address::Account(ACC_0), + Energy::from(5000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("recorder.list".to_string()), + message: OwnedParameter::empty(), + }, + ) + .expect("Viewing list with two elements"); + let returned_list_2: Vec = + view_list_2.parse_return_value().expect("Decoding return value"); + assert!(returned_list_2.is_empty()); +} diff --git a/examples/signature-verifier/Cargo.toml b/examples/signature-verifier/Cargo.toml index 03da7e98..64067c2d 100644 --- a/examples/signature-verifier/Cargo.toml +++ b/examples/signature-verifier/Cargo.toml @@ -10,10 +10,12 @@ license = "MPL-2.0" [dependencies] concordium-std = {path = "../../concordium-std"} +[dev-dependencies] +concordium-smart-contract-testing = { path = "../../contract-testing" } + [features] default = ["std", "wee_alloc"] std = ["concordium-std/std"] -crypto-primitives = ["concordium-std/crypto-primitives"] wee_alloc = ["concordium-std/wee_alloc"] [lib] diff --git a/examples/signature-verifier/src/lib.rs b/examples/signature-verifier/src/lib.rs index 0e6dc13d..9eb58df2 100644 --- a/examples/signature-verifier/src/lib.rs +++ b/examples/signature-verifier/src/lib.rs @@ -9,17 +9,14 @@ use concordium_std::*; type State = (); #[derive(SchemaType, Serialize)] -struct VerificationParameter { - public_key: PublicKeyEd25519, - signature: SignatureEd25519, - message: Vec, +pub struct VerificationParameter { + pub public_key: PublicKeyEd25519, + pub signature: SignatureEd25519, + pub message: Vec, } #[init(contract = "signature-verifier")] -fn contract_init( - _ctx: &impl HasInitContext, - _state_builder: &mut StateBuilder, -) -> InitResult { +fn contract_init(_ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult { Ok(()) } @@ -32,9 +29,9 @@ fn contract_init( parameter = "VerificationParameter", return_value = "bool" )] -fn contract_receive( - ctx: &impl HasReceiveContext, - _host: &impl HasHost, +fn contract_receive( + ctx: &ReceiveContext, + _host: &Host, crypto_primitives: &impl HasCryptoPrimitives, ) -> ReceiveResult { let param: VerificationParameter = ctx.parameter_cursor().get()?; @@ -45,57 +42,3 @@ fn contract_receive( ); Ok(is_valid) } - -#[concordium_cfg_test] -mod tests { - use super::*; - use concordium_std::test_infrastructure::*; - - #[concordium_test] - #[cfg(not(feature = "crypto-primitives"))] - fn test_receive_with_mocks() { - let mut ctx = TestReceiveContext::empty(); - let host = TestHost::new((), TestStateBuilder::new()); - let crypto_primitives = TestCryptoPrimitives::new(); - - let param = VerificationParameter { - public_key: PublicKeyEd25519([0; 32]), - signature: SignatureEd25519([1; 64]), - message: vec![2; 100], - }; - let param_bytes = to_bytes(¶m); - ctx.set_parameter(¶m_bytes); - - crypto_primitives.setup_verify_ed25519_signature_mock(|_, _, _| true); - - let res = contract_receive(&ctx, &host, &crypto_primitives); - claim_eq!(res, Ok(true)) - } - - #[concordium_test] - #[cfg(feature = "crypto-primitives")] - fn test_receive() { - let mut ctx = TestReceiveContext::empty(); - let host = TestHost::new((), TestStateBuilder::new()); - let crypto_primitives = TestCryptoPrimitives::new(); - - let param = VerificationParameter { - public_key: PublicKeyEd25519([ - 53, 162, 168, 229, 46, 250, 217, 117, 219, 246, 88, 14, 119, 52, 228, 242, 73, 234, - 165, 234, 138, 118, 62, 147, 74, 134, 113, 205, 126, 68, 100, 153, - ]), - signature: SignatureEd25519([ - 170, 242, 191, 224, 247, 247, 70, 49, 133, 3, 112, 66, 33, 24, 243, 14, 135, 135, - 197, 113, 122, 74, 21, 82, 122, 94, 29, 15, 252, 121, 27, 102, 59, 21, 9, 177, 33, - 2, 46, 242, 96, 134, 179, 120, 89, 0, 29, 9, 100, 38, 116, 250, 59, 226, 1, 247, - 217, 220, 39, 8, 245, 230, 236, 2, - ]), - message: b"Concordium".to_vec(), - }; - let param_bytes = to_bytes(¶m); - ctx.set_parameter(¶m_bytes); - - let res = contract_receive(&ctx, &host, &crypto_primitives); - claim_eq!(res, Ok(true)) - } -} diff --git a/examples/signature-verifier/tests/tests.rs b/examples/signature-verifier/tests/tests.rs new file mode 100644 index 00000000..235821d5 --- /dev/null +++ b/examples/signature-verifier/tests/tests.rs @@ -0,0 +1,75 @@ +//! Tests for the signature-verifier contract. +use concordium_smart_contract_testing::*; +use concordium_std::{PublicKeyEd25519, SignatureEd25519}; +use signature_verifier::*; +use std::str::FromStr; + +const ALICE: AccountAddress = AccountAddress([0u8; 32]); +const ALICE_ADDR: Address = Address::Account(ALICE); +const SIGNER: Signer = Signer::with_one_key(); + +/// Tests that the signature verifier contract returns true when the signature +/// is valid. +#[test] +fn test_signature_check() { + let mut chain = Chain::new(); + + // Create an account. + chain.create_account(Account::new(ALICE, Amount::from_ccd(1000))); + + // Load and deploy the module. + let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists."); + let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Module deploys."); + + // Initialize the signature verifier contract. + let init = chain + .contract_init(SIGNER, ALICE, Energy::from(10_000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_signature-verifier".to_string()), + param: OwnedParameter::empty(), + }) + .expect("Initialize signature verifier contract"); + + // Construct a parameter with an invalid signature. + let parameter_invalid = VerificationParameter { + public_key: PublicKeyEd25519([0; 32]), + signature: SignatureEd25519([1; 64]), + message: vec![2; 100], + }; + + // Call the contract with the invalid signature. + let update_invalid = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { + amount: Amount::zero(), + address: init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("signature-verifier.verify".to_string()), + message: OwnedParameter::from_serial(¶meter_invalid) + .expect("Parameter has valid size."), + }) + .expect("Call signature verifier contract with an invalid signature."); + // Check that it returns `false`. + let rv: bool = update_invalid.parse_return_value().expect("Deserializing bool"); + assert_eq!(rv, false); + + // Construct a parameter with a valid signature. + let parameter_valid = VerificationParameter { + public_key: PublicKeyEd25519::from_str("35a2a8e52efad975dbf6580e7734e4f249eaa5ea8a763e934a8671cd7e446499").expect("Valid public key"), + signature: SignatureEd25519::from_str("aaf2bfe0f7f74631850370422118f30e8787c5717a4a15527a5e1d0ffc791b663b1509b121022ef26086b37859001d09642674fa3be201f7d9dc2708f5e6ec02").expect("Valid signature"), + message: b"Concordium".to_vec(), + }; + + // Call the contract with the valid signature. + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { + amount: Amount::zero(), + address: init.contract_address, + receive_name: OwnedReceiveName::new_unchecked("signature-verifier.verify".to_string()), + message: OwnedParameter::from_serial(¶meter_valid) + .expect("Parameter has valid size."), + }) + .expect("Call signature verifier contract with a valid signature."); + // Check that it returns `true`. + let rv: bool = update.parse_return_value().expect("Deserializing bool"); + assert!(rv, "Signature checking failed."); +} diff --git a/examples/smart-contract-upgrade/contract-version1/Cargo.toml b/examples/smart-contract-upgrade/contract-version1/Cargo.toml index e75dc4ff..c963e48f 100644 --- a/examples/smart-contract-upgrade/contract-version1/Cargo.toml +++ b/examples/smart-contract-upgrade/contract-version1/Cargo.toml @@ -17,17 +17,11 @@ wee_alloc = ["concordium-std/wee_alloc"] concordium-std = {path = "../../../concordium-std", default-features = false} [dev-dependencies] -concordium-smart-contract-testing = "2.0" +concordium-smart-contract-testing = { path = "../../../contract-testing" } [lib] crate-type=["cdylib", "rlib"] -#`concordium-smart-contract-testing` uses the below libraries as well. We overwrite them with the ones used -#in this repository so we can test the newest version of the libraries with the Ci pipelines. -[patch.crates-io] -concordium-contracts-common = {path = "../../../concordium-rust-sdk/concordium-base/smart-contracts/contracts-common/concordium-contracts-common"} -concordium-contracts-common-derive = {path = "../../../concordium-rust-sdk/concordium-base/smart-contracts/contracts-common/concordium-contracts-common-derive"} - [profile.release] opt-level = "s" codegen-units = 1 diff --git a/examples/smart-contract-upgrade/contract-version1/src/lib.rs b/examples/smart-contract-upgrade/contract-version1/src/lib.rs index 49ef5a04..b78b303c 100644 --- a/examples/smart-contract-upgrade/contract-version1/src/lib.rs +++ b/examples/smart-contract-upgrade/contract-version1/src/lib.rs @@ -86,10 +86,7 @@ type ContractResult = Result; /// Init function that creates a new smart contract. #[init(contract = "smart_contract_upgrade")] -fn contract_init( - ctx: &impl HasInitContext, - _state_builder: &mut StateBuilder, -) -> InitResult { +fn contract_init(ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult { Ok(State { admin: ctx.init_origin(), not_to_be_migrated_state: "This state should NOT be migrated as part of the smart \ @@ -103,10 +100,7 @@ fn contract_init( /// View function that returns the content of the state. #[receive(contract = "smart_contract_upgrade", name = "view", return_value = "State")] -fn contract_view<'b, S: HasStateApi>( - _ctx: &impl HasReceiveContext, - host: &'b impl HasHost, -) -> ReceiveResult<&'b State> { +fn contract_view<'b>(_ctx: &ReceiveContext, host: &'b Host) -> ReceiveResult<&'b State> { Ok(host.state()) } @@ -132,10 +126,7 @@ fn contract_view<'b, S: HasStateApi>( error = "CustomContractError", low_level )] -fn contract_upgrade( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, -) -> ContractResult<()> { +fn contract_upgrade(ctx: &ReceiveContext, host: &mut LowLevelHost) -> ContractResult<()> { // Read the top-level contract state. let state: State = host.state().read_root()?; diff --git a/examples/smart-contract-upgrade/contract-version1/tests/tests.rs b/examples/smart-contract-upgrade/contract-version1/tests/tests.rs index 4158aaa6..a1916633 100644 --- a/examples/smart-contract-upgrade/contract-version1/tests/tests.rs +++ b/examples/smart-contract-upgrade/contract-version1/tests/tests.rs @@ -1,3 +1,19 @@ +//! Tests for the `smart_contract_upgrade` contract. +//! +//! Run the tests by: +//! +//! 1. Open a terminal and navigate to the: +//! `examples/smart-contract-upgrade-folder` +//! +//! 2. Compile the version 2 contract +//! with: +//! - `cargo concordium build --out +//! contract-version2/concordium-out/module.wasm.v1 -- --manifest-path +//! contract-version2/Cargo.toml` +//! 3. Compile the version 1 contract and run the tests with: +//! - `cargo concordium test --out +//! contract-version1/concordium-out/module.wasm.v1 -- --manifest-path +//! contract-version1/Cargo.toml use concordium_smart_contract_testing::*; use concordium_std::Deserial; use smart_contract_upgrade::UpgradeParams; @@ -22,7 +38,7 @@ fn setup_chain_and_contract() -> (Chain, ContractInitSuccess) { .module_deploy_v1( Signer::with_one_key(), ACC_ADDR_OWNER, - module_load_v1("./smart_contract_upgrade.wasm.v1") + module_load_v1("./concordium-out/module.wasm.v1") .expect("`Contract version1` module should be loaded"), ) .expect("`Contract version1` deployment should always succeed"); @@ -65,7 +81,7 @@ fn test_upgrade_without_migration_function() { .module_deploy_v1( Signer::with_one_key(), ACC_ADDR_OWNER, - module_load_v1("../contract-version2/smart_contract_upgrade.wasm.v1") + module_load_v1("../contract-version2/concordium-out/module.wasm.v1") .expect("`Contract version2` module should be loaded"), ) .expect("`Contract version2` deployment should always succeed"); @@ -112,7 +128,7 @@ fn test_upgrade_without_migration_function() { .expect("Invoking `view` should always succeed"); let state: State = - from_bytes(&invoke.return_value).expect("View should always return a valid result"); + invoke.parse_return_value().expect("View should always return a valid result"); assert_eq!(state, State { admin: ACC_ADDR_OWNER, @@ -132,7 +148,7 @@ fn test_upgrade_with_migration_function() { .module_deploy_v1( Signer::with_one_key(), ACC_ADDR_OWNER, - module_load_v1("../contract-version2/smart_contract_upgrade.wasm.v1") + module_load_v1("../contract-version2/concordium-out/module.wasm.v1") .expect("UpgradeParams should be a valid inut parameter"), ) .expect("`Contract version2` deployment should always succeed"); @@ -183,7 +199,7 @@ fn test_upgrade_with_migration_function() { .expect("Invoking `view` should always succeed"); let state: State = - from_bytes(&invoke.return_value).expect("View should always return a valid result"); + invoke.parse_return_value().expect("View should always return a valid result"); assert_eq!(state, State { admin: ACC_ADDR_OWNER, diff --git a/examples/smart-contract-upgrade/contract-version2/src/lib.rs b/examples/smart-contract-upgrade/contract-version2/src/lib.rs index d1f9522f..472231a1 100644 --- a/examples/smart-contract-upgrade/contract-version2/src/lib.rs +++ b/examples/smart-contract-upgrade/contract-version2/src/lib.rs @@ -94,19 +94,11 @@ type ContractResult = Result; /// Init function that creates a new smart contract. #[init(contract = "smart_contract_upgrade")] -fn contract_init( - _ctx: &impl HasInitContext, - _state_builder: &mut StateBuilder, -) -> InitResult<()> { - Ok(()) -} +fn contract_init(_ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult<()> { Ok(()) } /// View function that returns the content of the state. #[receive(contract = "smart_contract_upgrade", name = "view", return_value = "State")] -fn contract_view<'b, S: HasStateApi>( - _ctx: &impl HasReceiveContext, - host: &'b impl HasHost, -) -> ReceiveResult<&'b State> { +fn contract_view<'b>(_ctx: &ReceiveContext, host: &'b Host) -> ReceiveResult<&'b State> { Ok(host.state()) } @@ -123,10 +115,7 @@ fn contract_view<'b, S: HasStateApi>( error = "CustomContractError", low_level )] -fn contract_migration( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, -) -> ContractResult<()> { +fn contract_migration(ctx: &ReceiveContext, host: &mut LowLevelHost) -> ContractResult<()> { // Check that only this contract instance can call this function. ensure!(ctx.sender().matches_contract(&ctx.self_address()), CustomContractError::Unauthorized); @@ -167,10 +156,7 @@ fn contract_migration( error = "CustomContractError", low_level )] -fn contract_upgrade( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, -) -> ContractResult<()> { +fn contract_upgrade(ctx: &ReceiveContext, host: &mut LowLevelHost) -> ContractResult<()> { // Read the top-level contract state. let state: State = host.state().read_root()?; diff --git a/examples/transfer-policy-check/Cargo.toml b/examples/transfer-policy-check/Cargo.toml index 0b5cff75..bdd37192 100644 --- a/examples/transfer-policy-check/Cargo.toml +++ b/examples/transfer-policy-check/Cargo.toml @@ -10,6 +10,9 @@ license = "MPL-2.0" [dependencies] concordium-std = {path = "../../concordium-std", default-features = false} +[dev-dependencies] +concordium-smart-contract-testing = { path = "../../contract-testing" } + [features] default = ["std", "wee_alloc"] std = ["concordium-std/std"] diff --git a/examples/transfer-policy-check/src/lib.rs b/examples/transfer-policy-check/src/lib.rs index 6ab52989..98401a9b 100644 --- a/examples/transfer-policy-check/src/lib.rs +++ b/examples/transfer-policy-check/src/lib.rs @@ -16,10 +16,10 @@ use concordium_std::*; type State = AccountAddress; -const LOCAL_COUNTRY: [u8; 2] = *b"DK"; +pub const LOCAL_COUNTRY: [u8; 2] = *b"DK"; -#[derive(Serial, Reject, PartialEq, Eq, Debug, SchemaType)] -enum ContractError { +#[derive(Serialize, Reject, PartialEq, Eq, Debug, SchemaType)] +pub enum ContractError { NotLocalSender, TransferErrorAmountTooLarge, TransferErrorAccountMissing, @@ -36,90 +36,21 @@ impl From for ContractError { /// Set the account address that `receive` will forward CCD to. #[init(contract = "transfer-policy-check", parameter = "AccountAddress")] -fn init( - ctx: &impl HasInitContext, - _state_builder: &mut StateBuilder, -) -> InitResult<()> { - Ok(ctx.parameter_cursor().get()?) +fn init(ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult { + let parameter: AccountAddress = ctx.parameter_cursor().get()?; + Ok(parameter) } /// Forward the `amount` to the account defined in the state iff all sender /// policies have the country of residence in `LOCAL_COUNTRY`. #[receive(contract = "transfer-policy-check", name = "receive", payable, error = "ContractError")] -fn receive( - ctx: &impl HasReceiveContext, - host: &impl HasHost, - amount: Amount, -) -> Result<(), ContractError> { +fn receive(ctx: &ReceiveContext, host: &Host, amount: Amount) -> Result<(), ContractError> { for policy in ctx.policies() { - if !policy.attributes().any(|(tag, val)| { - tag == attributes::COUNTRY_OF_RESIDENCE && val.as_ref() == &LOCAL_COUNTRY[..] - }) { - return Err(ContractError::NotLocalSender); + for (tag, value) in policy.attributes() { + if tag == attributes::COUNTRY_OF_RESIDENCE && value.as_ref() != &LOCAL_COUNTRY[..] { + return Err(ContractError::NotLocalSender); + } } } Ok(host.invoke_transfer(host.state(), amount)?) } - -#[concordium_cfg_test] -mod tests { - use super::*; - use concordium_std::test_infrastructure::*; - - const ACCOUNT_0: AccountAddress = AccountAddress([0u8; 32]); - - #[concordium_test] - fn receive_with_correct_policies() { - let mut ctx = TestReceiveContext::empty(); - ctx.push_policy(Policy { - identity_provider: 0, - created_at: Timestamp::from_timestamp_millis(0), - valid_to: Timestamp::from_timestamp_millis(1000), - items: vec![(attributes::COUNTRY_OF_RESIDENCE, LOCAL_COUNTRY.into())], - }); - ctx.push_policy(Policy { - identity_provider: 0, - created_at: Timestamp::from_timestamp_millis(0), - valid_to: Timestamp::from_timestamp_millis(1000), - items: vec![(attributes::COUNTRY_OF_RESIDENCE, LOCAL_COUNTRY.into())], - }); - - let state = ACCOUNT_0; - let state_builder = TestStateBuilder::new(); - let mut host = TestHost::new(state, state_builder); - let transfer_amount = Amount::from_micro_ccd(10); - host.set_self_balance(transfer_amount); - let res = receive(&ctx, &host, transfer_amount); - assert!(res.is_ok()); - assert_eq!(host.get_transfers(), vec![(ACCOUNT_0, transfer_amount)]); - } - - #[concordium_test] - fn receive_with_incorrect_policies() { - let mut ctx = TestReceiveContext::empty(); - ctx.push_policy(Policy { - identity_provider: 0, - created_at: Timestamp::from_timestamp_millis(0), - valid_to: Timestamp::from_timestamp_millis(1000), - items: vec![(attributes::COUNTRY_OF_RESIDENCE, LOCAL_COUNTRY.into())], - }); - ctx.push_policy(Policy { - identity_provider: 0, - created_at: Timestamp::from_timestamp_millis(0), - valid_to: Timestamp::from_timestamp_millis(1000), - items: vec![( - attributes::COUNTRY_OF_RESIDENCE, - b"NOT_LOCAL_COUNTRY".into(), /* Chose an invalid country to avoid conflicts - * with valid settings of `LOCAL_COUNTRY`. */ - )], - }); - - let state = ACCOUNT_0; - let state_builder = TestStateBuilder::new(); - let mut host = TestHost::new(state, state_builder); - let transfer_amount = Amount::from_micro_ccd(10); - host.set_self_balance(transfer_amount); - let res = receive(&ctx, &host, transfer_amount); - assert_eq!(res, Err(ContractError::NotLocalSender)); - } -} diff --git a/examples/transfer-policy-check/tests/tests.rs b/examples/transfer-policy-check/tests/tests.rs new file mode 100644 index 00000000..f98c4014 --- /dev/null +++ b/examples/transfer-policy-check/tests/tests.rs @@ -0,0 +1,93 @@ +//! Tests for the transfer-policy-check contract. +use concordium_smart_contract_testing::*; +use concordium_std::{attributes, OwnedPolicy}; +use transfer_policy_check::*; + +/// Constants. +const ALICE: AccountAddress = AccountAddress([0u8; 32]); +const BOB: AccountAddress = AccountAddress([1u8; 32]); +const BOB_ADDR: Address = Address::Account(BOB); + +const SIGNER: Signer = Signer::with_one_key(); + +// Tests: + +/// Test that sending money via the contract works when the sender has the +/// correct policy. Meaning that the sender has the country of residence in +/// Denmark (`DK`). +#[test] +fn test_amount_forward_on_correct_policy() { + let (mut chain, contract_address) = init(); + + // Construct a policy with the correct country code. + let policy = policy_with_country(LOCAL_COUNTRY); + // Create the account BOB, who is from Denmark. + chain.create_account(Account::new_with_policy( + BOB, + AccountBalance::new(Amount::from_ccd(1000), Amount::zero(), Amount::zero()) + .expect("Staked + locked < total."), + policy, + )); + + let amount_to_send = Amount::from_ccd(10); + + // Send money from Bob to Alice. + let update = chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(50_000), UpdateContractPayload { + amount: amount_to_send, + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "transfer-policy-check.receive".to_string(), + ), + message: OwnedParameter::empty(), + }) + .expect("Contract update succeeds."); + + // Check that the money was forwarded. + assert_eq!(update.account_transfers().collect::>(), [( + contract_address, + amount_to_send, + ALICE + )]); +} + +// Helpers: + +/// Construct a policy with the provided country code. +fn policy_with_country(country_code: [u8; 2]) -> OwnedPolicy { + let policies = OwnedPolicy { + identity_provider: 0, + created_at: Timestamp::from_timestamp_millis(0), + valid_to: Timestamp::from_timestamp_millis(1000), + items: vec![(attributes::COUNTRY_OF_RESIDENCE, country_code.into())], + }; + + policies +} + +/// Initialize the chain and contract. +/// +/// Creates one account (ALICE), deploys the module, and initializes the +/// contract. +fn init() -> (Chain, ContractAddress) { + let mut chain = Chain::new(); + + // Create an account. + chain.create_account(Account::new(ALICE, Amount::from_ccd(1000))); + + // Load and deploy the module. + let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists."); + let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Module deploys."); + + // Initialize the transfer policy check contract. + let init = chain + .contract_init(SIGNER, ALICE, Energy::from(10_000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_transfer-policy-check".to_string()), + param: OwnedParameter::from_serial(&ALICE).expect("Parameter has valid size."), + }) + .expect("Initialize transfer policy check contract"); + + (chain, init.contract_address) +} diff --git a/examples/two-step-transfer/src/lib.rs b/examples/two-step-transfer/src/lib.rs index adaa4689..9e6f91d9 100644 --- a/examples/two-step-transfer/src/lib.rs +++ b/examples/two-step-transfer/src/lib.rs @@ -306,6 +306,7 @@ fn contract_receive_message( // Tests #[concordium_cfg_test] +#[allow(deprecated)] mod tests { use super::*; use concordium_std::test_infrastructure::*; diff --git a/examples/voting/Cargo.toml b/examples/voting/Cargo.toml index 7da19ee2..0e09addc 100644 --- a/examples/voting/Cargo.toml +++ b/examples/voting/Cargo.toml @@ -10,6 +10,9 @@ license = "MPL-2.0" [dependencies] concordium-std = {path = "../../concordium-std", default-features = false} +[dev-dependencies] +concordium-smart-contract-testing = { path = "../../contract-testing" } + [features] default = ["std", "wee_alloc"] std = ["concordium-std/std"] diff --git a/examples/voting/src/lib.rs b/examples/voting/src/lib.rs index 8ed3591a..b30e2495 100644 --- a/examples/voting/src/lib.rs +++ b/examples/voting/src/lib.rs @@ -22,57 +22,57 @@ use concordium_std::*; /// The human-readable description of a voting option. -type VotingOption = String; +pub type VotingOption = String; /// The voting options are stored in a vector. The vector index is used to refer /// to a specific voting option. -type VoteIndex = u32; +pub type VoteIndex = u32; /// Number of votes. -type VoteCount = u32; +pub type VoteCount = u32; /// The parameter type for the contract function `vote`. /// Takes a `vote_index` that the account wants to vote for. -#[derive(Deserial, SchemaType)] -struct VoteParameter { +#[derive(Serialize, SchemaType, Debug, PartialEq, Eq)] +pub struct VoteParameter { /// Voting option index to vote for. - vote_index: VoteIndex, + pub vote_index: VoteIndex, } /// The parameter type for the contract function `init`. /// Takes a description, the voting options, and the `end_time` to start the /// election. -#[derive(Deserial, SchemaType)] -struct InitParameter { +#[derive(Serialize, SchemaType, Debug, PartialEq, Eq)] +pub struct InitParameter { /// The description of the election. - description: String, + pub description: String, /// A vector of all voting options. - options: Vec, + pub options: Vec, /// The last timestamp that an account can vote. /// The election is open from the point in time that this smart contract is /// initialized until the `end_time`. - end_time: Timestamp, + pub end_time: Timestamp, } /// The `return_value` type of the contract function `view`. /// Returns a description, the `end_time`, the voting options as a vector, and /// the number of voting options of the current election. -#[derive(Serial, Deserial, SchemaType)] -struct VotingView { +#[derive(Serialize, SchemaType, Debug, PartialEq, Eq)] +pub struct VotingView { /// The description of the election. - description: String, + pub description: String, /// The last timestamp that an account can vote. /// The election is open from the point in time that this smart contract is /// initialized until the `end_time`. - end_time: Timestamp, + pub end_time: Timestamp, /// A vector of all voting options. - options: Vec, + pub options: Vec, /// The number of voting options. - num_options: u32, + pub num_options: u32, } /// The contract state #[derive(Serial, DeserialWithState)] #[concordium(state_parameter = "S")] -struct State { +struct State { /// The description of the election. /// `StateBox` allows for lazy loading data. This is helpful /// in the situations when one wants to do a partial update not touching @@ -97,8 +97,8 @@ struct State { } /// The different errors that the `vote` function can produce. -#[derive(Reject, Serial, PartialEq, Eq, Debug, SchemaType)] -enum VotingError { +#[derive(Reject, Serialize, PartialEq, Eq, Debug, SchemaType)] +pub enum VotingError { /// Raised when parsing the parameter failed. #[from(ParseError)] ParsingFailed, @@ -127,19 +127,19 @@ impl From for VotingError { /// A custom alias type for the `Result` type with the error type fixed to /// `VotingError`. -type VotingResult = Result; +pub type VotingResult = Result; /// The event is logged when a new (or replacement) vote is cast by an account. -#[derive(Debug, Serialize, SchemaType)] +#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] pub struct VoteEvent { /// The account that casts the vote. - voter: AccountAddress, + pub voter: AccountAddress, /// The index of the voting option that the account is voting for. - vote_index: VoteIndex, + pub vote_index: VoteIndex, } /// The event logged by this smart contract. -#[derive(Debug, Serial, SchemaType)] +#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] pub enum Event { /// The event is logged when a new (or replacement) vote is cast by an /// account. @@ -152,10 +152,7 @@ pub enum Event { /// A description, the vector of all voting options, and an `end_time` /// have to be provided. #[init(contract = "voting", parameter = "InitParameter", event = "Event")] -fn init( - ctx: &impl HasInitContext, - state_builder: &mut StateBuilder, -) -> InitResult> { +fn init(ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { // Parse the parameter. let param: InitParameter = ctx.parameter_cursor().get()?; // Calculate the number of voting options. @@ -176,6 +173,11 @@ fn init( /// change its selected voting option with this function as often as it desires /// until the `end_time` is reached. /// +/// A valid vote produces an `Event::Vote` event. +/// This is also the case if the account recasts its vote for another, or even +/// the same, option. By tracking the events produced, one can reconstruct the +/// current state of the election. +/// /// It rejects if: /// - It fails to parse the parameter. /// - A contract tries to vote. @@ -188,9 +190,9 @@ fn init( parameter = "VoteParameter", error = "VotingError" )] -fn vote( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn vote( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> VotingResult<()> { // Check that the election hasn't finished yet. @@ -245,10 +247,7 @@ fn vote( parameter = "VoteParameter", return_value = "VoteCount" )] -fn get_votes( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ReceiveResult { +fn get_votes(ctx: &ReceiveContext, host: &Host) -> ReceiveResult { // Parse the parameter. let param: VoteIndex = ctx.parameter_cursor().get()?; @@ -263,10 +262,7 @@ fn get_votes( /// Get the election information. #[receive(contract = "voting", name = "view", return_value = "VotingView")] -fn view( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ReceiveResult { +fn view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { // Get information from the state. let description = host.state().description.clone(); let end_time = host.state().end_time; @@ -281,194 +277,3 @@ fn view( num_options, }) } - -// Tests - -#[concordium_cfg_test] -mod tests { - use super::*; - use concordium_std::test_infrastructure::*; - - // Test accounts/addresses. - const ACC_0: AccountAddress = AccountAddress([0; 32]); - const ACC_1: AccountAddress = AccountAddress([1; 32]); - const ADDR_ACC_0: Address = Address::Account(ACC_0); - const ADDR_ACC_1: Address = Address::Account(ACC_1); - - // Set up the test host. - fn make_test_host(options: Vec, end_time: Timestamp) -> TestHost> { - let mut state_builder = TestStateBuilder::new(); - let num_options = options.len() as u32; - - let state = State { - description: state_builder.new_box("Test description".into()), - ballots: state_builder.new_map(), - tally: state_builder.new_map(), - end_time, - options: state_builder.new_box(options), - num_options, - }; - TestHost::new(state, state_builder) - } - - /// Test that an account cannot vote if it is past the `end_time` of the - /// election. - #[concordium_test] - fn test_vote_after_finish_time() { - // Set up the context. - let end_time = Timestamp::from_timestamp_millis(100); - let current_time = Timestamp::from_timestamp_millis(200); - let mut ctx = TestReceiveContext::empty(); - ctx.set_metadata_slot_time(current_time); - - // Set up logger. - let mut logger = TestLogger::init(); - - // Set up the test host. - let mut host = make_test_host(Vec::new(), end_time); - - // Call the contract function. - let res = vote(&ctx, &mut host, &mut logger); - - // Check that error is thrown because the election is finished already. - claim_eq!( - res, - Err(VotingError::VotingFinished), - "Should throw error because voting is finished" - ); - } - - /// Test that voting fails if the voting index is out of range. - #[concordium_test] - fn test_vote_with_invalid_index() { - // Set up the context. - let end_time = Timestamp::from_timestamp_millis(100); - let current_time = Timestamp::from_timestamp_millis(0); - let mut ctx = TestReceiveContext::empty(); - ctx.set_metadata_slot_time(current_time); - ctx.set_sender(ADDR_ACC_0); - - // Set up the parameter. - let vote_parameter = to_bytes(&2); - ctx.set_parameter(&vote_parameter); - - // Set up logger. - let mut logger = TestLogger::init(); - - // Set up the test host. - let mut host = make_test_host(vec!["A".into(), "B".into()], end_time); - - // Call the contract function. - let res = vote(&ctx, &mut host, &mut logger); - - // Check that error is thrown because the voting index is out of range. - claim_eq!( - res, - Err(VotingError::InvalidVoteIndex), - "Should throw error because voting index is invalid" - ); - } - - #[concordium_test] - fn test_vote_with_valid_index() { - // Set up the context. - let end_time = Timestamp::from_timestamp_millis(100); - let current_time = Timestamp::from_timestamp_millis(0); - let mut ctx = TestReceiveContext::empty(); - ctx.set_metadata_slot_time(current_time); - - // Set up logger. - let mut logger = TestLogger::init(); - - // Vote once. - - // Set up the parameter. - let vote_parameter = to_bytes(&0); - ctx.set_parameter(&vote_parameter); - // Set up the sender. - ctx.set_sender(ADDR_ACC_0); - // Set up the test host. - let mut host = make_test_host(vec!["A".into(), "B".into()], end_time); - // Call the contract function. - let result = vote(&ctx, &mut host, &mut logger); - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - // Check the ballots (ACCOUNT_0 voted for voting option index 0). - let ballots = host.state().ballots.iter().map(|(a, b)| (*a, *b)).collect::>(); - let votes_count_0 = host.state().tally.get(&0).unwrap(); - let votes_count_1 = host.state().tally.get(&1); - claim_eq!(*votes_count_0, 1, "Expect: one vote for option 0"); - claim!(votes_count_1.is_none(), "Expect: no votes for option 1"); - claim_eq!(vec![(ACC_0, 0)], ballots, "Expect: ACCOUNT_0 voted for voting option index 0"); - - // Change vote. - - // Set up the parameter. - let vote_parameter = to_bytes(&1); - ctx.set_parameter(&vote_parameter); - // Call the contract function. - let result = vote(&ctx, &mut host, &mut logger); - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - // Check the ballots (ACCOUNT_0 changed its voting option index to 1). - let ballots = host.state().ballots.iter().map(|(a, b)| (*a, *b)).collect::>(); - let votes_count_0 = host.state().tally.get(&0).unwrap(); - let votes_count_1 = host.state().tally.get(&1).unwrap(); - claim_eq!(*votes_count_0, 0, "Expect: no votes for option 0"); - claim_eq!(*votes_count_1, 1, "Expect: one vote for option 1"); - claim_eq!( - vec![(ACC_0, 1)], - ballots, - "Expect: ACCOUNT_0 changed its voting option index to 1" - ); - - // Another vote, by another account. - - // Set up the parameter. - let vote_parameter = to_bytes(&0); - ctx.set_parameter(&vote_parameter); - // Set up the sender. - ctx.set_sender(ADDR_ACC_1); - // Call the contract function. - let result = vote(&ctx, &mut host, &mut logger); - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - // Check the ballots (ACCOUNT_0 voted for voting option index 1 and ACCOUNT_1 - // voted for voting option index 0). - let ballots = host.state().ballots.iter().map(|(a, b)| (*a, *b)).collect::>(); - let votes_count_0 = host.state().tally.get(&0).unwrap(); - let votes_count_1 = host.state().tally.get(&1).unwrap(); - claim_eq!(*votes_count_0, 1, "Expect: one vote for option 0"); - claim_eq!(*votes_count_1, 1, "Expect: one vote for option 1"); - claim_eq!( - vec![(ACC_0, 1), (ACC_1, 0)], - ballots, - "Expect: ACCOUNT_0 voted for voting option index 1 and ACCOUNT_1 voted for voting \ - option index 0" - ); - - // Vote again, using the same index as before. - - // Set up the parameter. - let vote_parameter = to_bytes(&1); - ctx.set_parameter(&vote_parameter); - ctx.set_sender(ADDR_ACC_0); - // Call the contract function. - let result = vote(&ctx, &mut host, &mut logger); - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - // Check the ballots (ACCOUNT_0 still voted for voting option index 1 and - // ACCOUNT_1 still voted for voting option index 0). - let ballots = host.state().ballots.iter().map(|(a, b)| (*a, *b)).collect::>(); - let votes_count_0 = host.state().tally.get(&0).unwrap(); - let votes_count_1 = host.state().tally.get(&1).unwrap(); - claim_eq!(*votes_count_0, 1, "Expect: one vote for option 0"); - claim_eq!(*votes_count_1, 1, "Expect: one vote for option 1"); - claim_eq!( - vec![(ACC_0, 1), (ACC_1, 0)], - ballots, - "ACCOUNT_0 still voted for voting option index 1 and ACCOUNT_1 still voted for voting \ - option index 0" - ); - } -} diff --git a/examples/voting/tests/tests.rs b/examples/voting/tests/tests.rs new file mode 100644 index 00000000..016ea9b1 --- /dev/null +++ b/examples/voting/tests/tests.rs @@ -0,0 +1,266 @@ +use concordium_smart_contract_testing::*; +use voting_contract::*; + +// Constants. +const SIGNER: Signer = Signer::with_one_key(); +const ALICE: AccountAddress = AccountAddress([0; 32]); +const ALICE_ADDR: Address = Address::Account(ALICE); +const BOB: AccountAddress = AccountAddress([1; 32]); +const BOB_ADDR: Address = Address::Account(BOB); +const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(1000); + +/// Test that voting after the `end_time` fails. +#[test] +fn test_vote_after_end_time() { + let (mut chain, contract_address) = init(); + + let params = VoteParameter { + vote_index: 0, + }; + + // Advance time to after the end time. + chain.tick_block_time(Duration::from_millis(1001)).expect("Won't overflow"); + + // Try to vote. + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("voting.vote".to_string()), + message: OwnedParameter::from_serial(¶ms).expect("Valid vote parameter"), + }) + .expect_err("Contract updated"); + + // Check that the error is correct. + let rv: VotingError = update.parse_return_value().expect("Deserialize VotingError"); + assert_eq!(rv, VotingError::VotingFinished); +} + +/// Test that voting with an voting index that is out of range fails. +#[test] +fn test_vote_out_of_range() { + let (mut chain, contract_address) = init(); + + let params = VoteParameter { + vote_index: 3, // Valid indexes are: 0, 1, 2. + }; + + // Try to vote. + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("voting.vote".to_string()), + message: OwnedParameter::from_serial(¶ms).expect("Valid vote parameter"), + }) + .expect_err("Contract updated"); + + // Check that the error is correct. + let rv: VotingError = update.parse_return_value().expect("Deserialize VotingError"); + assert_eq!(rv, VotingError::InvalidVoteIndex); +} + +/// Test that voting and changing your vote works. +/// +/// In particular: +/// - Alice votes for option 0. +/// - Alice changes her vote to option 1. +/// - Bob votes for option 0. +/// - Bob again votes for option 0. +#[test] +fn test_voting_and_changing_vote() { + let (mut chain, contract_address) = init(); + + // Alice votes for option 0. + let update_1 = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("voting.vote".to_string()), + message: OwnedParameter::from_serial(&VoteParameter { + vote_index: 0, + }) + .expect("Valid vote parameter"), + }) + .expect("Contract updated"); + + // Check the events and votes. + check_event(&update_1, ALICE, 0); + assert_eq!(get_votes(&chain, contract_address), [1, 0, 0]); + + // Alice changes her vote to option 1. + let update_2 = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("voting.vote".to_string()), + message: OwnedParameter::from_serial(&VoteParameter { + vote_index: 1, + }) + .expect("Valid vote parameter"), + }) + .expect("Contract updated"); + // Check the events and votes. + check_event(&update_2, ALICE, 1); + assert_eq!(get_votes(&chain, contract_address), [0, 1, 0]); + + // Bob votes for option 0. + let update_3 = chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10_000), UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("voting.vote".to_string()), + message: OwnedParameter::from_serial(&VoteParameter { + vote_index: 0, + }) + .expect("Valid vote parameter"), + }) + .expect("Contract updated"); + // Check the events and votes. + check_event(&update_3, BOB, 0); + assert_eq!(get_votes(&chain, contract_address), [1, 1, 0]); + + // Bob again votes for option 0. + let update_4 = chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10_000), UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("voting.vote".to_string()), + message: OwnedParameter::from_serial(&VoteParameter { + vote_index: 0, + }) + .expect("Valid vote parameter"), + }) + .expect("Contract updated"); + // Check the events and votes. + check_event(&update_4, BOB, 0); + assert_eq!(get_votes(&chain, contract_address), [1, 1, 0]); +} + +/// Test that a contract is not allowed to vote. +#[test] +fn test_contract_voter() { + let (mut chain, contract_address) = init(); + + // Try to vote. + let params = VoteParameter { + vote_index: 0, + }; + let update = chain + .contract_update( + SIGNER, + ALICE, + Address::Contract(contract_address), // The contract itself is the sender. + Energy::from(10_000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("voting.vote".to_string()), + message: OwnedParameter::from_serial(¶ms).expect("Valid vote parameter"), + }, + ) + .expect_err("Contract updated"); + + // Check that the error is correct. + let rv: VotingError = update.parse_return_value().expect("Deserialize VotingError"); + assert_eq!(rv, VotingError::ContractVoter); +} + +// Helpers: + +/// Initialize chain and contract. +/// +/// Also creates the following accounts: +/// - `ALICE`: Account with 1000 CCD. +/// - `BOB`: Account with 1000 CCD. +/// +/// The contract is initialized with the following parameters: +/// - `description`: "Election description" +/// - `options`: ["A", "B", "C"] +/// - `end_time`: 1000 milliseconds after the unix epoch time. +fn init() -> (Chain, ContractAddress) { + let mut chain = Chain::new(); + + // Create accounts. + chain.create_account(Account::new(ALICE, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); + + // Deploy module. + let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); + let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Deploy valid module"); + + // Contract init parameters. + let params = InitParameter { + description: "Election description".to_string(), + options: vec!["A".to_string(), "B".to_string(), "C".to_string()], + end_time: Timestamp::from_timestamp_millis(1000), + }; + + // Initialize contract. + let init = chain + .contract_init(SIGNER, ALICE, Energy::from(10_000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_voting".to_string()), + param: OwnedParameter::from_serial(¶ms).expect("Valid init parameter"), + }) + .expect("Contract initialized"); + + (chain, init.contract_address) +} + +/// Get the number of votes for each voting option. +fn get_votes(chain: &Chain, contract_address: ContractAddress) -> [u32; 3] { + let view_0 = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("voting.getNumberOfVotes".to_string()), + message: OwnedParameter::from_serial(&VoteParameter { + vote_index: 0, + }) + .expect("Valid vote parameter"), + }) + .expect("View invoked"); + let vote_0: VoteCount = view_0.parse_return_value().expect("Deserialize VoteCount"); + + let view_1 = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("voting.getNumberOfVotes".to_string()), + message: OwnedParameter::from_serial(&VoteParameter { + vote_index: 1, + }) + .expect("Valid vote parameter"), + }) + .expect("View invoked"); + let vote_1: VoteCount = view_1.parse_return_value().expect("Deserialize VoteCount"); + + let view_2 = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("voting.getNumberOfVotes".to_string()), + message: OwnedParameter::from_serial(&VoteParameter { + vote_index: 2, + }) + .expect("Valid vote parameter"), + }) + .expect("View invoked"); + let vote_2: VoteCount = view_2.parse_return_value().expect("Deserialize VoteCount"); + + [vote_0, vote_1, vote_2] +} + +/// Check that the voting event is produced and that it is correct. +fn check_event(update: &ContractInvokeSuccess, voter: AccountAddress, vote_index: VoteIndex) { + let events: Vec = update + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect(); + assert_eq!(events, [Event::Vote(VoteEvent { + voter, + vote_index, + })]); +} diff --git a/templates/cis2-nft/Cargo.toml b/templates/cis2-nft/Cargo.toml index 17867069..e5a4fa83 100644 --- a/templates/cis2-nft/Cargo.toml +++ b/templates/cis2-nft/Cargo.toml @@ -12,8 +12,11 @@ std = ["concordium-std/std", "concordium-cis2/std"] wee_alloc = ["concordium-std/wee_alloc"] [dependencies] -concordium-std = {version = "8.0", default-features = false} -concordium-cis2 = {version = "5.0", default-features = false} +concordium-std = {version = "8.1", default-features = false} +concordium-cis2 = {version = "5.1", default-features = false} + +[dev-dependencies] +concordium-smart-contract-testing = "3.0" [lib] crate-type=["cdylib", "rlib"] diff --git a/templates/cis2-nft/src/lib.rs b/templates/cis2-nft/src/lib.rs index 2b722aa4..c055fea4 100644 --- a/templates/cis2-nft/src/lib.rs +++ b/templates/cis2-nft/src/lib.rs @@ -18,6 +18,8 @@ //! address to another address. An address can enable and disable one or more //! addresses as operators. An operator of some address is allowed to transfer //! any tokens owned by this address. +//! +//! Tests are located in `./tests/tests.rs`. #![cfg_attr(not(feature = "std"), no_std)] @@ -26,46 +28,46 @@ use concordium_std::*; /// The baseurl for the token metadata, gets appended with the token ID as hex /// encoding before emitted in the TokenMetadata event. -const TOKEN_METADATA_BASE_URL: &str = "{{tokenMetadataBaseURL}}"; +pub const TOKEN_METADATA_BASE_URL: &str = "{{tokenMetadataBaseURL}}"; /// List of supported standards by this contract address. -const SUPPORTS_STANDARDS: [StandardIdentifier<'static>; 2] = +pub const SUPPORTS_STANDARDS: [StandardIdentifier<'static>; 2] = [CIS0_STANDARD_IDENTIFIER, CIS2_STANDARD_IDENTIFIER]; // Types /// Contract token ID type. /// To save bytes we use a token ID type limited to a `u32`. -type ContractTokenId = TokenIdU32; +pub type ContractTokenId = TokenIdU32; /// Contract token amount. /// Since the tokens are non-fungible the total supply of any token will be at /// most 1 and it is fine to use a small type for representing token amounts. -type ContractTokenAmount = TokenAmountU8; +pub type ContractTokenAmount = TokenAmountU8; /// The parameter for the contract function `mint` which mints a number of /// tokens to a given address. #[derive(Serial, Deserial, SchemaType)] -struct MintParams { +pub struct MintParams { /// Owner of the newly minted tokens. - owner: Address, + pub owner: Address, /// A collection of tokens to mint. #[concordium(size_length = 1)] - tokens: collections::BTreeSet, + pub tokens: collections::BTreeSet, } /// The state for each address. #[derive(Serial, DeserialWithState, Deletable)] #[concordium(state_parameter = "S")] -struct AddressState { +pub struct AddressState { /// The tokens owned by this address. - owned_tokens: StateSet, + pub owned_tokens: StateSet, /// The address which are currently enabled as operators for this address. - operators: StateSet, + pub operators: StateSet, } -impl AddressState { - fn empty(state_builder: &mut StateBuilder) -> Self { +impl AddressState { + fn empty(state_builder: &mut StateBuilder) -> Self { AddressState { owned_tokens: state_builder.new_set(), operators: state_builder.new_set(), @@ -78,30 +80,30 @@ impl AddressState { // and this could be structured in a more space efficient way depending on the use case. #[derive(Serial, DeserialWithState)] #[concordium(state_parameter = "S")] -struct State { +pub struct State { /// The state for each address. - state: StateMap, S>, + pub state: StateMap, S>, /// All of the token IDs - all_tokens: StateSet, + pub all_tokens: StateSet, /// Map with contract addresses providing implementations of additional /// standards. - implementors: StateMap, S>, + pub implementors: StateMap, S>, } /// The parameter type for the contract function `setImplementors`. /// Takes a standard identifier and list of contract addresses providing /// implementations of this standard. #[derive(Debug, Serialize, SchemaType)] -struct SetImplementorsParams { +pub struct SetImplementorsParams { /// The identifier for the standard. - id: StandardIdentifierOwned, + pub id: StandardIdentifierOwned, /// The addresses of the implementors of the standard. - implementors: Vec, + pub implementors: Vec, } /// The custom errors the contract can produce. #[derive(Serialize, Debug, PartialEq, Eq, Reject, SchemaType)] -enum CustomContractError { +pub enum CustomContractError { /// Failed parsing the parameter. #[from(ParseError)] ParseParams, @@ -117,9 +119,9 @@ enum CustomContractError { } /// Wrapping the custom errors in a type with CIS2 errors. -type ContractError = Cis2Error; +pub type ContractError = Cis2Error; -type ContractResult = Result; +pub type ContractResult = Result; /// Mapping the logging errors to CustomContractError. impl From for CustomContractError { @@ -142,9 +144,9 @@ impl From for ContractError { } // Functions for creating, updating and querying the contract state. -impl State { +impl State { /// Creates a new state with no tokens. - fn empty(state_builder: &mut StateBuilder) -> Self { + fn empty(state_builder: &mut StateBuilder) -> Self { State { state: state_builder.new_map(), all_tokens: state_builder.new_set(), @@ -157,7 +159,7 @@ impl State { &mut self, token: ContractTokenId, owner: &Address, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) -> ContractResult<()> { ensure!(self.all_tokens.insert(token), CustomContractError::TokenIdAlreadyExists.into()); @@ -208,7 +210,7 @@ impl State { amount: ContractTokenAmount, from: &Address, to: &Address, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) -> ContractResult<()> { ensure!(self.contains_token(token_id), ContractError::InvalidTokenId); // A zero transfer does not modify the state. @@ -243,7 +245,7 @@ impl State { &mut self, owner: &Address, operator: &Address, - state_builder: &mut StateBuilder, + state_builder: &mut StateBuilder, ) { let mut owner_state = self.state.entry(*owner).or_insert_with(|| AddressState::empty(state_builder)); @@ -289,33 +291,27 @@ fn build_token_metadata_url(token_id: &ContractTokenId) -> String { /// Initialize contract instance with no token types initially. #[init(contract = "{{crate_name}}", event = "Cis2Event")] -fn contract_init( - _ctx: &impl HasInitContext, - state_builder: &mut StateBuilder, -) -> InitResult> { +fn contract_init(_ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { // Construct the initial contract state. Ok(State::empty(state_builder)) } -#[derive(Serialize, SchemaType)] -struct ViewAddressState { - owned_tokens: Vec, - operators: Vec
, +#[derive(Serialize, SchemaType, PartialEq, Eq, Debug)] +pub struct ViewAddressState { + pub owned_tokens: Vec, + pub operators: Vec
, } -#[derive(Serialize, SchemaType)] -struct ViewState { - state: Vec<(Address, ViewAddressState)>, - all_tokens: Vec, +#[derive(Serialize, SchemaType, PartialEq, Eq, Debug)] +pub struct ViewState { + pub state: Vec<(Address, ViewAddressState)>, + pub all_tokens: Vec, } /// View function that returns the entire contents of the state. Meant for /// testing. #[receive(contract = "{{crate_name}}", name = "view", return_value = "ViewState")] -fn contract_view( - _ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, -) -> ReceiveResult { +fn contract_view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { let state = host.state(); let mut inner_state = Vec::new(); @@ -359,9 +355,9 @@ fn contract_view( enable_logger, mutable )] -fn contract_mint( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_mint( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Get the contract owner @@ -423,9 +419,9 @@ type TransferParameter = TransferParams; enable_logger, mutable )] -fn contract_transfer( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_transfer( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Parse the parameter. @@ -489,9 +485,9 @@ fn contract_transfer( enable_logger, mutable )] -fn contract_update_operator( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, +fn contract_update_operator( + ctx: &ReceiveContext, + host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Parse the parameter. @@ -531,9 +527,9 @@ fn contract_update_operator( return_value = "OperatorOfQueryResponse", error = "ContractError" )] -fn contract_operator_of( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_operator_of( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: OperatorOfQueryParams = ctx.parameter_cursor().get()?; @@ -567,9 +563,9 @@ type ContractBalanceOfQueryResponse = BalanceOfQueryResponse( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_balance_of( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: ContractBalanceOfQueryParams = ctx.parameter_cursor().get()?; @@ -600,9 +596,9 @@ type ContractTokenMetadataQueryParams = TokenMetadataQueryParams( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_token_metadata( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: ContractTokenMetadataQueryParams = ctx.parameter_cursor().get()?; @@ -634,9 +630,9 @@ fn contract_token_metadata( return_value = "SupportsQueryResponse", error = "ContractError" )] -fn contract_supports( - ctx: &impl HasReceiveContext, - host: &impl HasHost, StateApiType = S>, +fn contract_supports( + ctx: &ReceiveContext, + host: &Host, ) -> ContractResult { // Parse the parameter. let params: SupportsQueryParams = ctx.parameter_cursor().get()?; @@ -667,10 +663,7 @@ fn contract_supports( error = "ContractError", mutable )] -fn contract_set_implementor( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, -) -> ContractResult<()> { +fn contract_set_implementor(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<()> { // Authorize the sender. ensure!(ctx.sender().matches_account(&ctx.owner()), ContractError::Unauthorized); // Parse the parameter. @@ -679,373 +672,3 @@ fn contract_set_implementor( host.state_mut().set_implementors(params.id, params.implementors); Ok(()) } - -// Tests - -#[concordium_cfg_test] -mod tests { - use super::*; - use test_infrastructure::*; - - const ACCOUNT_0: AccountAddress = AccountAddress([0u8; 32]); - const ADDRESS_0: Address = Address::Account(ACCOUNT_0); - const ACCOUNT_1: AccountAddress = AccountAddress([1u8; 32]); - const ADDRESS_1: Address = Address::Account(ACCOUNT_1); - const TOKEN_0: ContractTokenId = TokenIdU32(0); - const TOKEN_1: ContractTokenId = TokenIdU32(42); - const TOKEN_2: ContractTokenId = TokenIdU32(43); - - /// Test helper function which creates a contract state with two tokens with - /// id `TOKEN_0` and id `TOKEN_1` owned by `ADDRESS_0` - fn initial_state(state_builder: &mut StateBuilder) -> State { - let mut state = State::empty(state_builder); - state.mint(TOKEN_0, &ADDRESS_0, state_builder).expect_report("Failed to mint TOKEN_0"); - state.mint(TOKEN_1, &ADDRESS_0, state_builder).expect_report("Failed to mint TOKEN_1"); - state - } - - /// Test initialization succeeds. - #[concordium_test] - fn test_init() { - // Setup the context - let ctx = TestInitContext::empty(); - let mut builder = TestStateBuilder::new(); - - // Call the contract function. - let result = contract_init(&ctx, &mut builder); - - // Check the result - let state = result.expect_report("Contract initialization failed"); - - // Check the state - // Note. This is rather expensive as an iterator is created and then traversed - - // should be avoided when writing smart contracts. - claim_eq!(state.all_tokens.iter().count(), 0, "No token should be initialized"); - } - - /// Test minting, ensuring the new tokens are owned by the given address and - /// the appropriate events are logged. - #[concordium_test] - fn test_mint() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - ctx.set_owner(ACCOUNT_0); - - // and parameter. - let mut tokens = collections::BTreeSet::new(); - tokens.insert(TOKEN_0); - tokens.insert(TOKEN_1); - tokens.insert(TOKEN_2); - let parameter = MintParams { - tokens, - owner: ADDRESS_0, - }; - - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = State::empty(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_mint(&ctx, &mut host, &mut logger); - - // Check the result - claim!(result.is_ok(), "Results in rejection"); - - // Check the state - // Note. This is rather expensive as an iterator is created and then traversed - - // should be avoided when writing smart contracts. - claim_eq!(host.state().all_tokens.iter().count(), 3, "Expected three tokens in the state."); - - let balance0 = - host.state().balance(&TOKEN_0, &ADDRESS_0).expect_report("Token is expected to exist"); - claim_eq!(balance0, 1.into(), "Tokens should be owned by the given address 0"); - - let balance1 = - host.state().balance(&TOKEN_1, &ADDRESS_0).expect_report("Token is expected to exist"); - claim_eq!(balance1, 1.into(), "Tokens should be owned by the given address 0"); - - let balance2 = - host.state().balance(&TOKEN_2, &ADDRESS_0).expect_report("Token is expected to exist"); - claim_eq!(balance2, 1.into(), "Tokens should be owned by the given address 0"); - - // Check the logs - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::Mint(MintEvent { - owner: ADDRESS_0, - token_id: TOKEN_0, - amount: ContractTokenAmount::from(1), - }))), - "Expected an event for minting TOKEN_0" - ); - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::Mint(MintEvent { - owner: ADDRESS_0, - token_id: TOKEN_1, - amount: ContractTokenAmount::from(1), - }))), - "Expected an event for minting TOKEN_1" - ); - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::TokenMetadata::<_, ContractTokenAmount>( - TokenMetadataEvent { - token_id: TOKEN_0, - metadata_url: MetadataUrl { - url: format!("{}00000000", TOKEN_METADATA_BASE_URL), - hash: None, - }, - } - ))), - "Expected an event for token metadata for TOKEN_0" - ); - claim!( - logger.logs.contains(&to_bytes(&Cis2Event::TokenMetadata::<_, ContractTokenAmount>( - TokenMetadataEvent { - token_id: TOKEN_1, - metadata_url: MetadataUrl { - url: format!("{}2A000000", TOKEN_METADATA_BASE_URL), - hash: None, - }, - } - ))), - "Expected an event for token metadata for TOKEN_1" - ); - } - - /// Test transfer succeeds, when `from` is the sender. - #[concordium_test] - fn test_transfer_account() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - - // and parameter. - let transfer = Transfer { - token_id: TOKEN_0, - amount: ContractTokenAmount::from(1), - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let balance0 = - host.state().balance(&TOKEN_0, &ADDRESS_0).expect_report("Token is expected to exist"); - let balance1 = - host.state().balance(&TOKEN_0, &ADDRESS_1).expect_report("Token is expected to exist"); - let balance2 = - host.state().balance(&TOKEN_1, &ADDRESS_0).expect_report("Token is expected to exist"); - claim_eq!( - balance0, - 0.into(), - "Token owner balance should be decreased by the transferred amount" - ); - claim_eq!( - balance1, - 1.into(), - "Token receiver balance should be increased by the transferred amount" - ); - claim_eq!( - balance2, - 1.into(), - "Token receiver balance for token 1 should be the same as before" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "Only one event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::Transfer(TransferEvent { - from: ADDRESS_0, - to: ADDRESS_1, - token_id: TOKEN_0, - amount: ContractTokenAmount::from(1), - })), - "Incorrect event emitted" - ) - } - - /// Test transfer token fails, when sender is neither the owner or an - /// operator of the owner. - #[concordium_test] - fn test_transfer_not_authorized() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_1); - - // and parameter. - let transfer = Transfer { - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - token_id: TOKEN_0, - amount: ContractTokenAmount::from(1), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - // Check the result. - let err = result.expect_err_report("Expected to fail"); - claim_eq!(err, ContractError::Unauthorized, "Error is expected to be Unauthorized") - } - - /// Test transfer succeeds when sender is not the owner, but is an operator - /// of the owner. - #[concordium_test] - fn test_operator_transfer() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_1); - - // and parameter. - let transfer = Transfer { - from: ADDRESS_0, - to: Receiver::from_account(ACCOUNT_1), - token_id: TOKEN_0, - amount: ContractTokenAmount::from(1), - data: AdditionalData::empty(), - }; - let parameter = TransferParams::from(vec![transfer]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - - let mut state_builder = TestStateBuilder::new(); - let mut state = initial_state(&mut state_builder); - state.add_operator(&ADDRESS_0, &ADDRESS_1, &mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_transfer(&ctx, &mut host, &mut logger); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let balance0 = - host.state().balance(&TOKEN_0, &ADDRESS_0).expect_report("Token is expected to exist"); - let balance1 = host - .state_mut() - .balance(&TOKEN_0, &ADDRESS_1) - .expect_report("Token is expected to exist"); - claim_eq!( - balance0, - 0.into(), - "Token owner balance should be decreased by the transferred amount" - ); - claim_eq!( - balance1, - 1.into(), - "Token receiver balance should be increased by the transferred amount" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "Only one event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::Transfer(TransferEvent { - from: ADDRESS_0, - to: ADDRESS_1, - token_id: TOKEN_0, - amount: ContractTokenAmount::from(1), - })), - "Incorrect event emitted" - ) - } - - /// Test adding an operator succeeds and the appropriate event is logged. - #[concordium_test] - fn test_add_operator() { - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - - // and parameter. - let update = UpdateOperator { - update: OperatorUpdate::Add, - operator: ADDRESS_1, - }; - let parameter = UpdateOperatorParams(vec![update]); - let parameter_bytes = to_bytes(¶meter); - ctx.set_parameter(¶meter_bytes); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let state = initial_state(&mut state_builder); - let mut host = TestHost::new(state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = contract_update_operator(&ctx, &mut host, &mut logger); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - - // Check the state. - let is_operator = host.state().is_operator(&ADDRESS_1, &ADDRESS_0); - claim!(is_operator, "Account should be an operator"); - - // Checking that `ADDRESS_1` is an operator in the query response of the - // `contract_operator_of` function as well. - // Setup parameter. - let operator_of_query = OperatorOfQuery { - address: ADDRESS_1, - owner: ADDRESS_0, - }; - - let operator_of_query_vector = OperatorOfQueryParams { - queries: vec![operator_of_query], - }; - let parameter_bytes = to_bytes(&operator_of_query_vector); - - ctx.set_parameter(¶meter_bytes); - - // Checking the return value of the `contract_operator_of` function - let result: ContractResult = contract_operator_of(&ctx, &host); - - claim_eq!( - result.expect_report("Failed getting result value").0, - [true], - "Account should be an operator in the query response" - ); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "One event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&Cis2Event::::UpdateOperator( - UpdateOperatorEvent { - owner: ADDRESS_0, - operator: ADDRESS_1, - update: OperatorUpdate::Add, - } - )), - "Incorrect event emitted" - ) - } -} diff --git a/templates/cis2-nft/tests/tests.rs b/templates/cis2-nft/tests/tests.rs new file mode 100644 index 00000000..b9f06b2b --- /dev/null +++ b/templates/cis2-nft/tests/tests.rs @@ -0,0 +1,336 @@ +//! Tests for the `{{crate_name}}` contract. +use {{crate_name}}::*; +use concordium_cis2::*; +use concordium_smart_contract_testing::*; +use concordium_std::collections::BTreeSet; + +/// The tests accounts. +const ALICE: AccountAddress = AccountAddress([0; 32]); +const ALICE_ADDR: Address = Address::Account(ALICE); +const BOB: AccountAddress = AccountAddress([1; 32]); +const BOB_ADDR: Address = Address::Account(BOB); + +/// Token IDs. +const TOKEN_0: ContractTokenId = TokenIdU32(2); +const TOKEN_1: ContractTokenId = TokenIdU32(42); + +/// Initial balance of the accounts. +const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10000); + +/// A signer for all the transactions. +const SIGNER: Signer = Signer::with_one_key(); + +/// Test minting succeeds and the tokens are owned by the given address and +/// the appropriate events are logged. +#[test] +fn test_minting() { + let (chain, contract_address, update) = initialize_contract_with_alice_tokens(); + + // Invoke the view entrypoint and check that the tokens are owned by Alice. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("{{crate_name}}.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + + // Check that the tokens are owned by Alice. + let rv: ViewState = invoke.parse_return_value().expect("ViewState return value"); + assert_eq!(rv.all_tokens[..], [TOKEN_0, TOKEN_1]); + assert_eq!(rv.state, vec![(ALICE_ADDR, ViewAddressState { + owned_tokens: vec![TOKEN_0, TOKEN_1], + operators: Vec::new(), + })]); + + // Check that the events are logged. + let events = update.events().flat_map(|(_addr, events)| events); + + let events: Vec> = + events.map(|e| e.parse().expect("Deserialize event")).collect(); + + assert_eq!(events, [ + Cis2Event::Mint(MintEvent { + token_id: TokenIdU32(2), + amount: TokenAmountU8(1), + owner: ALICE_ADDR, + }), + Cis2Event::TokenMetadata(TokenMetadataEvent { + token_id: TokenIdU32(2), + metadata_url: MetadataUrl { + url: format!("{TOKEN_METADATA_BASE_URL}02000000"), + hash: None, + }, + }), + Cis2Event::Mint(MintEvent { + token_id: TokenIdU32(42), + amount: TokenAmountU8(1), + owner: ALICE_ADDR, + }), + Cis2Event::TokenMetadata(TokenMetadataEvent { + token_id: TokenIdU32(42), + metadata_url: MetadataUrl { + url: format!("{TOKEN_METADATA_BASE_URL}2A000000"), + hash: None, + }, + }), + ]); +} + +/// Test regular transfer where sender is the owner. +#[test] +fn test_account_transfer() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Transfer `TOKEN_0` from Alice to Bob. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_0, + amount: TokenAmountU8(1), + data: AdditionalData::empty(), + }]); + + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("{{crate_name}}.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect("Transfer tokens"); + + // Check that Bob now has `TOKEN_0` and that Alice still has `TOKEN_1`. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("{{crate_name}}.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + let rv: ViewState = invoke.parse_return_value().expect("ViewState return value"); + assert_eq!(rv.state, vec![ + (ALICE_ADDR, ViewAddressState { + owned_tokens: vec![TOKEN_1], + operators: Vec::new(), + }), + (BOB_ADDR, ViewAddressState { + owned_tokens: vec![TOKEN_0], + operators: Vec::new(), + }), + ]); + + // Check that the events are logged. + let events = update + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect::>>(); + + assert_eq!(events, [Cis2Event::Transfer(TransferEvent { + token_id: TOKEN_0, + amount: TokenAmountU8(1), + from: ALICE_ADDR, + to: BOB_ADDR, + }),]); +} + +/// Test that you can add an operator. +/// Initialize the contract with two tokens owned by Alice. +/// Then add Bob as an operator for Alice. +#[test] +fn test_add_operator() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Add Bob as an operator for Alice. + let params = UpdateOperatorParams(vec![UpdateOperator { + update: OperatorUpdate::Add, + operator: BOB_ADDR, + }]); + + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("{{crate_name}}.updateOperator".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶ms).expect("UpdateOperator params"), + }) + .expect("Update operator"); + + // Check that an operator event occurred. + let events = update + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect::>>(); + assert_eq!(events, [Cis2Event::UpdateOperator(UpdateOperatorEvent { + operator: BOB_ADDR, + owner: ALICE_ADDR, + update: OperatorUpdate::Add, + }),]); + + // Construct a query parameter to check whether Bob is an operator for Alice. + let query_params = OperatorOfQueryParams { + queries: vec![OperatorOfQuery { + owner: ALICE_ADDR, + address: BOB_ADDR, + }], + }; + + // Invoke the operatorOf view entrypoint and check that Bob is an operator for + // Alice. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("{{crate_name}}.operatorOf".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&query_params).expect("OperatorOf params"), + }) + .expect("Invoke view"); + + let rv: OperatorOfQueryResponse = invoke.parse_return_value().expect("OperatorOf return value"); + assert_eq!(rv, OperatorOfQueryResponse(vec![true])); +} + +/// Test that a transfer fails when the sender is neither an operator or the +/// owner. In particular, Bob will attempt to transfer one of Alice's tokens to +/// himself. +#[test] +fn test_unauthorized_sender() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Construct a transfer of `TOKEN_0` from Alice to Bob, which will be submitted + // by Bob. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_0, + amount: TokenAmountU8(1), + data: AdditionalData::empty(), + }]); + + // Notice that Bob is the sender/invoker. + let update = chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("{{crate_name}}.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect_err("Transfer tokens"); + + // Check that the correct error is returned. + let rv: ContractError = update.parse_return_value().expect("ContractError return value"); + assert_eq!(rv, ContractError::Unauthorized); +} + +/// Test that an operator can make a transfer. +#[test] +fn test_operator_can_transfer() { + let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Add Bob as an operator for Alice. + let params = UpdateOperatorParams(vec![UpdateOperator { + update: OperatorUpdate::Add, + operator: BOB_ADDR, + }]); + chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("{{crate_name}}.updateOperator".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶ms).expect("UpdateOperator params"), + }) + .expect("Update operator"); + + // Let Bob make a transfer to himself on behalf of Alice. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_0, + amount: TokenAmountU8(1), + data: AdditionalData::empty(), + }]); + + chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("{{crate_name}}.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect("Transfer tokens"); + + // Check that Bob now has `TOKEN_0` and that Alice still has `TOKEN_1`. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("{{crate_name}}.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + let rv: ViewState = invoke.parse_return_value().expect("ViewState return value"); + assert_eq!(rv.state, vec![ + (ALICE_ADDR, ViewAddressState { + owned_tokens: vec![TOKEN_1], + operators: vec![BOB_ADDR], + }), + (BOB_ADDR, ViewAddressState { + owned_tokens: vec![TOKEN_0], + operators: Vec::new(), + }), + ]); +} + +/// Helper function that sets up the contract with two tokens minted to +/// Alice, `TOKEN_0` and `TOKEN_1`. +fn initialize_contract_with_alice_tokens() -> (Chain, ContractAddress, ContractInvokeSuccess) { + let (mut chain, contract_address) = initialize_chain_and_contract(); + + let mint_params = MintParams { + owner: ALICE_ADDR, + tokens: BTreeSet::from_iter(vec![TOKEN_0, TOKEN_1]), + }; + + // Mint two tokens to Alice. + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("{{crate_name}}.mint".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&mint_params).expect("Mint params"), + }) + .expect("Mint tokens"); + + (chain, contract_address, update) +} + +/// Setup chain and contract. +/// +/// Also creates the two accounts, Alice and Bob. +/// +/// Alice is the owner of the contract. +fn initialize_chain_and_contract() -> (Chain, ContractAddress) { + let mut chain = Chain::new(); + + // Create some accounts accounts on the chain. + chain.create_account(Account::new(ALICE, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); + + // Load and deploy the module. + let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); + let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Deploy valid module"); + + // Initialize the auction contract. + let init = chain + .contract_init(SIGNER, ALICE, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_{{crate_name}}".to_string()), + param: OwnedParameter::empty(), + }) + .expect("Initialize contract"); + + (chain, init.contract_address) +} diff --git a/templates/credential-registry/Cargo.toml b/templates/credential-registry/Cargo.toml index 461c1c81..f3e790ef 100644 --- a/templates/credential-registry/Cargo.toml +++ b/templates/credential-registry/Cargo.toml @@ -15,10 +15,13 @@ wee_alloc = ["concordium-std/wee_alloc"] crypto-primitives = ["concordium-std/crypto-primitives"] [dependencies] -concordium-std = {version = "8.0", default-features = false, features = ["concordium-quickcheck"]} -concordium-cis2 = {version = "5.0", default-features = false} +concordium-std = {version = "8.1", default-features = false, features = ["concordium-quickcheck"]} +concordium-cis2 = {version = "5.1", default-features = false} quickcheck = {version = "1"} +[dev-dependencies] +concordium-smart-contract-testing = "3.1" + [lib] crate-type=["cdylib", "rlib"] diff --git a/templates/credential-registry/README.md b/templates/credential-registry/README.md index f1aa3094..0360b56c 100644 --- a/templates/credential-registry/README.md +++ b/templates/credential-registry/README.md @@ -5,7 +5,7 @@ part of verifiable credentials (VCs). The contract follows CIS-4: Credential Registry Standard. The contract keeps track of credentials' public data, allows managing the -VC life cycle. and querying VCs data and status. The intended users are +VC life cycle, and querying VCs data and status. The intended users are issuers of VCs, holders of VCs, revocation authorities, and verifiers. When initializing a contract, the issuer provides a type and a schema @@ -44,4 +44,3 @@ private key. - view credential status to verify VC validity; - view credential data to verify proofs (verifiable presentations) requested from holders. - \ No newline at end of file diff --git a/templates/credential-registry/src/lib.rs b/templates/credential-registry/src/lib.rs index 83f4d47e..1b206cdb 100644 --- a/templates/credential-registry/src/lib.rs +++ b/templates/credential-registry/src/lib.rs @@ -7,22 +7,22 @@ pub const CIS4_STANDARD_IDENTIFIER: StandardIdentifier<'static> = StandardIdentifier::new_unchecked("CIS-4"); /// List of supported standards by this contract address. -const SUPPORTS_STANDARDS: [StandardIdentifier<'static>; 2] = +pub const SUPPORTS_STANDARDS: [StandardIdentifier<'static>; 2] = [CIS0_STANDARD_IDENTIFIER, CIS4_STANDARD_IDENTIFIER]; /// Credential type is a string that corresponds to the value of the "name" /// attribute of the JSON credential schema. #[derive(Serialize, SchemaType, PartialEq, Eq, Clone, Debug)] -struct CredentialType { +pub struct CredentialType { #[concordium(size_length = 1)] - credential_type: String, + pub credential_type: String, } /// A schema reference is a schema URL pointing to the JSON /// schema for a verifiable credential. #[derive(Serialize, SchemaType, PartialEq, Eq, Clone, Debug)] -struct SchemaRef { - schema_ref: MetadataUrl, +pub struct SchemaRef { + pub schema_ref: MetadataUrl, } impl From for SchemaRef { @@ -47,22 +47,22 @@ pub enum CredentialStatus { pub struct CredentialEntry { /// If this flag is set to `true` the holder can send a signed message to /// revoke their credential. - holder_revocable: bool, + pub holder_revocable: bool, /// The date from which the credential is considered valid. - valid_from: Timestamp, + pub valid_from: Timestamp, /// After this date, the credential becomes expired. `None` corresponds to a /// credential that cannot expire. - valid_until: Option, + pub valid_until: Option, /// The nonce is used to avoid replay attacks when checking the holder's /// signature on a revocation message. - revocation_nonce: u64, + pub revocation_nonce: u64, /// Revocation flag - revoked: bool, + pub revoked: bool, /// Metadata URL of the credential (not to be confused with the metadata URL /// of the **issuer**). /// This data is only needed when credential info is requested. In other /// operations, `StateBox` defers loading the metadata url. - metadata_url: StateBox, + pub metadata_url: StateBox, } impl CredentialEntry { @@ -126,7 +126,7 @@ pub struct State { /// Contract Errors. #[derive(Debug, PartialEq, Eq, Reject, Serial, SchemaType)] -enum ContractError { +pub enum ContractError { #[from(ParseError)] ParseParamsError, CredentialNotFound, @@ -166,11 +166,11 @@ impl From for ContractError { } } -type ContractResult = Result; +pub type ContractResult = Result; /// Credentials are identified by a holder's public key. /// Each time a credential is issued, a fresh key pair is generated. -type CredentialHolderId = PublicKeyEd25519; +pub type CredentialHolderId = PublicKeyEd25519; /// Functions for creating, updating and querying the contract state. impl State { @@ -322,22 +322,22 @@ impl State { /// Data for events of registering and updating a credential. /// Used by the tagged event `CredentialEvent`. -#[derive(Serialize, SchemaType)] -struct CredentialEventData { +#[derive(Debug, PartialEq, Eq, Serialize, SchemaType)] +pub struct CredentialEventData { /// A public key of the credential's holder. - holder_id: PublicKeyEd25519, + pub holder_id: PublicKeyEd25519, /// A reference to the credential JSON schema. - schema_ref: SchemaRef, + pub schema_ref: SchemaRef, /// Type of the credential. - credential_type: CredentialType, + pub credential_type: CredentialType, /// The original credential's metadata. - metadata_url: MetadataUrl, + pub metadata_url: MetadataUrl, } /// A type for specifying who is revoking a credential, when registering a /// revocation event. -#[derive(Serialize, SchemaType)] -enum Revoker { +#[derive(Debug, PartialEq, Eq, Serialize, SchemaType)] +pub enum Revoker { Issuer, Holder,{% if revocable_by_others %} /// `Other` is used for the cases when the revoker is not the issuer or @@ -349,10 +349,10 @@ enum Revoker { /// A short comment on a reason of revoking or restoring a credential. /// The string is of a limited size of 256 bytes in order to fit into a single /// log entry along with other data. -#[derive(Serialize, SchemaType, Clone)] -struct Reason { +#[derive(PartialEq, Eq, Debug, Serialize, SchemaType, Clone)] +pub struct Reason { #[concordium(size_length = 1)] - reason: String, + pub reason: String, } impl From for Reason { @@ -365,45 +365,45 @@ impl From for Reason { /// An untagged revocation event. /// For a tagged version use `CredentialEvent`. -#[derive(Serialize, SchemaType)] -struct RevokeCredentialEvent { +#[derive(Debug, PartialEq, Eq, Serialize, SchemaType)] +pub struct RevokeCredentialEvent { /// A public key of the credential's holder. - holder_id: CredentialHolderId, + pub holder_id: CredentialHolderId, /// Who revokes the credential. - revoker: Revoker, + pub revoker: Revoker, /// An optional text clarifying the revocation reasons. /// The issuer can use this field to comment on the revocation, so the /// holder can observe it in the wallet. - reason: Option, + pub reason: Option, }{% if restorable %} /// An untagged restoration event. /// For a tagged version use `CredentialEvent`. -#[derive(Serialize, SchemaType)] -struct RestoreCredentialEvent { +#[derive(Debug, PartialEq, Eq, Serialize, SchemaType)] +pub struct RestoreCredentialEvent { /// A public key of the credential's holder. - holder_id: CredentialHolderId, + pub holder_id: CredentialHolderId, /// An optional text clarifying the restoring reasons. - reason: Option, + pub reason: Option, }{% endif %} /// An untagged credential metadata event. Emitted when updating the credential /// metadata. For a tagged version use `CredentialEvent`. -#[derive(Serialize, SchemaType)] -struct CredentialMetadataEvent { - credential_id: CredentialHolderId, - metadata_url: MetadataUrl, +#[derive(Debug, PartialEq, Eq, Serialize, SchemaType)] +pub struct CredentialMetadataEvent { + pub credential_id: CredentialHolderId, + pub metadata_url: MetadataUrl, } /// The schema reference has been updated for the credential type. -#[derive(Serialize, SchemaType)] -struct CredentialSchemaRefEvent { - credential_type: CredentialType, - schema_ref: SchemaRef, +#[derive(Debug, PartialEq, Eq, Serialize, SchemaType)] +pub struct CredentialSchemaRefEvent { + pub credential_type: CredentialType, + pub schema_ref: SchemaRef, } -#[derive(Serialize, SchemaType)] -enum RevocationKeyAction { +#[derive(Debug, PartialEq, Eq, Serialize, SchemaType)] +pub enum RevocationKeyAction { Register, Remove, } @@ -411,17 +411,18 @@ enum RevocationKeyAction { /// An untagged revocation key event. /// Emitted when keys are registered and removed. /// For a tagged version use `CredentialEvent`. -#[derive(Serialize, SchemaType)] -struct RevocationKeyEvent { +#[derive(Debug, PartialEq, Eq, Serialize, SchemaType)] +pub struct RevocationKeyEvent { /// The public key that is registered/removed - key: PublicKeyEd25519, + pub key: PublicKeyEd25519, /// A register/remove action. - action: RevocationKeyAction, + pub action: RevocationKeyAction, } /// Tagged credential registry event. /// This version should be used for logging the events. -enum CredentialEvent { +#[derive(Debug, PartialEq, Eq)] +pub enum CredentialEvent { /// Credential registration event. Logged when an entry in the registry is /// created for the first time. Register(CredentialEventData), @@ -575,19 +576,19 @@ impl Deserial for CredentialEvent { #[derive(Serialize, SchemaType)] pub struct InitParams { /// The issuer's metadata. - issuer_metadata: MetadataUrl, + pub issuer_metadata: MetadataUrl, /// The type of credentials for this registry. - credential_type: CredentialType, + pub credential_type: CredentialType, /// The credential schema for this registry. - schema: SchemaRef, + pub schema: SchemaRef, /// The issuer for the registry. If `None`, the `init_origin` is used as /// `issuer`. - issuer_account: Option, + pub issuer_account: Option, /// The issuer's public key. - issuer_key: PublicKeyEd25519,{% if revocable_by_others %} + pub issuer_key: PublicKeyEd25519,{% if revocable_by_others %} /// Revocation keys available right after initialization. #[concordium(size_length = 1)] - revocation_keys: Vec,{% endif %} + pub revocation_keys: Vec,{% endif %} } /// Init function that creates a fresh registry state given the required @@ -662,42 +663,42 @@ fn sender_is_issuer(ctx: &impl HasReceiveContext, state: &State< #[derive(Serialize, SchemaType, PartialEq, Eq, Clone, Debug)] pub struct CredentialInfo { /// The holder's identifier is a public key. - holder_id: CredentialHolderId, + pub holder_id: CredentialHolderId, /// If this flag is set to `true` the holder can send a signed message to /// revoke their credential. - holder_revocable: bool, + pub holder_revocable: bool, /// The date from which the credential is considered valid. - valid_from: Timestamp, + pub valid_from: Timestamp, /// After this date, the credential becomes expired. `None` corresponds to a /// credential that cannot expire. - valid_until: Option, + pub valid_until: Option, /// Link to the metadata of this credential. - metadata_url: MetadataUrl, + pub metadata_url: MetadataUrl, } /// Parameters for registering a credential #[derive(Serialize, SchemaType, Clone, Debug)] pub struct RegisterCredentialParam { /// Public credential data. - credential_info: CredentialInfo, + pub credential_info: CredentialInfo, /// Any additional data required by the issuer in the registration process. /// This data is not used in this contract. However, it is part of the CIS-4 /// standard that this contract implements; `auxiliary_data` can be /// used, for example, to implement signature-based authentication. #[concordium(size_length = 2)] - auxiliary_data: Vec, + pub auxiliary_data: Vec, } /// Response to a credential data query. #[derive(Serialize, SchemaType, Clone, Debug)] pub struct CredentialQueryResponse { - credential_info: CredentialInfo, + pub credential_info: CredentialInfo, /// A schema URL pointing to the JSON schema for a verifiable /// credential. - schema_ref: SchemaRef, + pub schema_ref: SchemaRef, /// The nonce is used to avoid replay attacks when checking the holder's /// signature on a revocation message. - revocation_nonce: u64, + pub revocation_nonce: u64, } /// A view entrypoint for looking up an entry in the registry by id. @@ -783,26 +784,26 @@ fn contract_register_credential( /// Metadata of the signature. #[derive(Serialize, SchemaType, Clone)] -struct SigningData { +pub struct SigningData { /// The contract_address that the signature is intended for. - contract_address: ContractAddress, + pub contract_address: ContractAddress, /// The entry_point that the signature is intended for. - entry_point: OwnedEntrypointName, + pub entry_point: OwnedEntrypointName, /// A nonce to prevent replay attacks. - nonce: u64, + pub nonce: u64, /// A timestamp to make signatures expire. - timestamp: Timestamp, + pub timestamp: Timestamp, } /// A message prefix for revocation requests by holders and revocation /// authorities. -const SIGNARUTE_DOMAIN: &str = "WEB3ID:REVOKE"; +pub const SIGNARUTE_DOMAIN: &str = "WEB3ID:REVOKE"; /// A parameter type for revoking a credential by the holder. #[derive(Serialize, SchemaType)] pub struct RevokeCredentialHolderParam { - signature: SignatureEd25519, - data: RevocationDataHolder, + pub signature: SignatureEd25519, + pub data: RevocationDataHolder, } /// Prepare the message bytes for the holder @@ -817,11 +818,11 @@ impl RevokeCredentialHolderParam { #[derive(Serialize, SchemaType)] pub struct RevocationDataHolder { /// Id of the credential to revoke. - credential_id: CredentialHolderId, + pub credential_id: CredentialHolderId, /// Info about the signature. - signing_data: SigningData, + pub signing_data: SigningData, /// (Optional) reason for revoking the credential. - reason: Option, + pub reason: Option, } /// Helper function that can be invoked at the front end to serialize @@ -847,22 +848,22 @@ fn contract_serialization_helper_holder_revoke( #[derive(Serialize, SchemaType)] pub struct RevokeCredentialIssuerParam { /// Id of the credential to revoke. - credential_id: CredentialHolderId, + pub credential_id: CredentialHolderId, /// (Optional) reason for revoking the credential. - reason: Option, + pub reason: Option, /// Any additional data required by the issuer in the registration process. /// This data is not used in this contract. However, it is part of the CIS-4 /// standard that this contract implements; `auxiliary_data` can be /// used, for example, to implement signature-based authentication. #[concordium(size_length = 2)] - auxiliary_data: Vec, + pub auxiliary_data: Vec, } /// A parameter type for revoking a credential by a revocation authority. #[derive(Serialize, SchemaType)] pub struct RevokeCredentialOtherParam { - signature: SignatureEd25519, - data: RevocationDataOther, + pub signature: SignatureEd25519, + pub data: RevocationDataOther, } {% if revocable_by_others %} impl RevokeCredentialOtherParam { @@ -875,13 +876,13 @@ impl RevokeCredentialOtherParam { #[derive(Serialize, SchemaType)] pub struct RevocationDataOther { /// Id of the credential to revoke. - credential_id: CredentialHolderId, + pub credential_id: CredentialHolderId, /// Info about the signature. - signing_data: SigningData, + pub signing_data: SigningData, /// The key with which the revocation payload is signed. - revocation_key: PublicKeyEd25519, + pub revocation_key: PublicKeyEd25519, /// (Optional) reason for revoking the credential. - reason: Option, + pub reason: Option, } /// Helper function that can be invoked at the front end to serialize @@ -1376,13 +1377,13 @@ fn contract_revocation_keys( {% endif %} /// A response type for the registry metadata request. #[derive(Serialize, SchemaType)] -struct MetadataResponse { +pub struct MetadataResponse { /// A reference to the issuer's metadata. - issuer_metadata: MetadataUrl, + pub issuer_metadata: MetadataUrl, /// The type of credentials used. - credential_type: CredentialType, + pub credential_type: CredentialType, /// A reference to the JSON schema corresponding to this type. - credential_schema: SchemaRef, + pub credential_schema: SchemaRef, } /// A view entrypoint to get the registry metadata. @@ -1485,11 +1486,11 @@ fn contract_update_credential_schema( } #[derive(Serialize, SchemaType)] -struct CredentialMetadataParam { +pub struct CredentialMetadataParam { /// The id of the credential to update. - credential_id: CredentialHolderId, + pub credential_id: CredentialHolderId, /// The new metadata URL. - metadata_url: MetadataUrl, + pub metadata_url: MetadataUrl, } /// Update existing credential metadata URL. @@ -1536,9 +1537,9 @@ fn contract_update_credential_metadata( #[derive(Serialize, SchemaType)] pub struct RestoreCredentialIssuerParam { /// Id of the credential to restore. - credential_id: CredentialHolderId, + pub credential_id: CredentialHolderId, /// (Optional) reason for restoring the credential. - reason: Option, + pub reason: Option, } /// Restore credential by the issuer. @@ -1625,11 +1626,11 @@ fn contract_supports( /// Takes a standard identifier and list of contract addresses providing /// implementations of this standard. #[derive(Debug, Serialize, SchemaType)] -struct SetImplementorsParams { +pub struct SetImplementorsParams { /// The identifier for the standard. - id: StandardIdentifierOwned, + pub id: StandardIdentifierOwned, /// The addresses of the implementors of the standard. - implementors: Vec, + pub implementors: Vec, } /// Set the addresses for an implementation given a standard identifier and a @@ -1664,11 +1665,11 @@ fn contract_set_implementor( /// fails. This is useful for doing migration in the same transaction triggering /// the upgrade. #[derive(Debug, Serialize, SchemaType)] -struct UpgradeParams { +pub struct UpgradeParams { /// The new module reference. - module: ModuleReference, + pub module: ModuleReference, /// Optional entrypoint to call in the new module after upgrade. - migrate: Option<(OwnedEntrypointName, OwnedParameter)>, + pub migrate: Option<(OwnedEntrypointName, OwnedParameter)>, } /// Mapping errors related to contract invocations to ContractError. @@ -1736,6 +1737,7 @@ fn contract_upgrade( } #[concordium_cfg_test] +#[allow(deprecated)] mod tests { use super::*; @@ -1797,24 +1799,13 @@ mod tests { const ISSUER_ACCOUNT: AccountAddress = AccountAddress([0u8; 32]); const ISSUER_METADATA_URL: &str = "https://example-university.com/university.json"; - const CREDANIAL_METADATA_URL: &str = + const CREDENTIAL_METADATA_URL: &str = "https://example-university.com/diplomas/university-vc-metadata.json"; - const CREDENTIAL_TYPE: &str = "UniversityDegreeCredential"; - const CREDENTIAL_SCHEMA_URL: &str = - "https://credentials-schemas.com/JsonSchema2023-education-certificate.json"; - const ACCOUNT_0: AccountAddress = AccountAddress([0u8; 32]); - const ADDRESS_0: Address = Address::Account(ACCOUNT_0); // Seed: 2FEE333FAD122A45AAB7BEB3228FA7858C48B551EA8EBC49D2D56E2BA22049FF const PUBLIC_KEY: PublicKeyEd25519 = PublicKeyEd25519([ 172, 5, 96, 236, 139, 208, 146, 88, 124, 42, 62, 124, 86, 108, 35, 242, 32, 11, 7, 48, 193, 61, 177, 220, 104, 169, 145, 4, 8, 1, 236, 112, ]); - const SIGNATURE: SignatureEd25519 = SignatureEd25519([ - 254, 138, 58, 131, 209, 45, 191, 52, 98, 228, 26, 234, 155, 245, 244, 226, 0, 153, 104, - 111, 201, 136, 243, 167, 251, 116, 110, 206, 172, 223, 41, 180, 90, 22, 63, 43, 157, 129, - 226, 75, 49, 33, 155, 76, 160, 133, 127, 146, 150, 80, 199, 201, 80, 98, 179, 43, 46, 46, - 211, 222, 185, 216, 12, 4, - ]); /// A helper that returns a credential that is not revoked, cannot expire /// and is immediately activated. It is also possible to revoke it by the @@ -1822,7 +1813,7 @@ mod tests { fn credential_entry(state_builder: &mut StateBuilder) -> CredentialEntry { CredentialEntry { metadata_url: state_builder.new_box(MetadataUrl { - url: CREDANIAL_METADATA_URL.into(), + url: CREDENTIAL_METADATA_URL.into(), hash: None, }), valid_from: Timestamp::from_timestamp_millis(0), @@ -1840,84 +1831,6 @@ mod tests { } } - fn get_credential_schema() -> (CredentialType, SchemaRef) { - ( - CredentialType { - credential_type: CREDENTIAL_TYPE.to_string(), - }, - SchemaRef { - schema_ref: MetadataUrl { - url: CREDENTIAL_SCHEMA_URL.to_string(), - hash: None, - }, - }, - ) - } - - #[concordium_test] - /// Test that initializing the contract succeeds with some state. - fn test_init() { - let mut ctx = TestInitContext::empty(); - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - - ctx.set_init_origin(ISSUER_ACCOUNT); - - let schema = get_credential_schema(); - - let parameter_bytes = to_bytes(&InitParams { - issuer_metadata: issuer_metadata(), - issuer_account: ISSUER_ACCOUNT.into(), - issuer_key: PUBLIC_KEY,{% if revocable_by_others %} - revocation_keys: vec![PUBLIC_KEY],{% endif %} - credential_type: schema.0.clone(), - schema: schema.1.clone(), - }); - ctx.set_parameter(¶meter_bytes); - - let state_result = init(&ctx, &mut state_builder, &mut logger); - let state = state_result.expect_report("Contract initialization results in an error"); - - // Check that the initial parameters are in the state. - claim_eq!(state.credential_schema, schema.1, "Incorrect schema in the state"); - claim_eq!(state.issuer_account, ISSUER_ACCOUNT, "Incorrect issuer in the state"); - claim_eq!( - state.issuer_metadata, - issuer_metadata(), - "Incorrect issuer metadata in the state" - ); - - // Check that the correct events were logged. -{% if revocable_by_others %} - claim_eq!(logger.logs.len(), 3, "Incorrect number of logged events"); -{% else %} - claim_eq!(logger.logs.len(), 2, "Incorrect number of logged events"); -{% endif %} - claim_eq!( - logger.logs[0], - to_bytes(&CredentialEvent::IssuerMetadata(issuer_metadata())), - "Incorrect issuer metadata event logged" - ); -{% if revocable_by_others %} - claim_eq!( - logger.logs[1], - to_bytes(&CredentialEvent::RevocationKey(RevocationKeyEvent { - key: PUBLIC_KEY, - action: RevocationKeyAction::Register, - })), - "Incorrect revocation key event logged" - ); -{% endif %} - claim_eq!( - {% if revocable_by_others %}logger.logs[2],{% else %}logger.logs[1],{% endif %} - to_bytes(&CredentialEvent::Schema(CredentialSchemaRefEvent { - credential_type: schema.0, - schema_ref: schema.1, - })), - "Incorrect schema event logged" - ); - } - /// Not expired and not revoked credential is `Active` #[concordium_test] fn test_get_status_active() { @@ -2110,267 +2023,5 @@ mod tests { } else { false } - } -{% endif %} - /// Test the credential registration entrypoint. - #[concordium_test] - fn test_contract_register_credential() { - let now = Timestamp::from_timestamp_millis(0); - let contract = ContractAddress { - index: 0, - subindex: 0, - }; - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - ctx.set_owner(ACCOUNT_0); - ctx.set_self_address(contract); - ctx.set_metadata_slot_time(now); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let (credential_type, schema_ref) = get_credential_schema(); - let state = State::new( - &mut state_builder, - ISSUER_ACCOUNT, - PUBLIC_KEY, - issuer_metadata(), - credential_type.clone(), - schema_ref.clone(), - ); - let mut host = TestHost::new(state, state_builder); - - let entry = credential_entry(host.state_builder()); - - // Create input parameters. - - let param = RegisterCredentialParam { - credential_info: entry.info(PUBLIC_KEY), - auxiliary_data: Vec::new(), - }; - let parameter_bytes = to_bytes(¶m); - ctx.set_parameter(¶meter_bytes); - - // Create a credential - let res = contract_register_credential(&ctx, &mut host, &mut logger); - - // Check that it was registered successfully - claim!(res.is_ok(), "Credential registration failed: {:?}", res); - let fetched: CredentialQueryResponse = host - .state() - .view_credential_info(PUBLIC_KEY) - .expect_report("Credential is expected to exist"); - claim_eq!( - fetched.credential_info, - entry.info(PUBLIC_KEY), - "Credential info expected to be equal" - ); - claim_eq!(fetched.revocation_nonce, 0, "Revocation nonce expected to be 0"); - - let status = host - .state() - .view_credential_status(now, PUBLIC_KEY) - .expect_report("Status query expected to succeed"); - claim_eq!(status, CredentialStatus::Active); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "One event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&CredentialEvent::Register(CredentialEventData { - holder_id: PUBLIC_KEY, - schema_ref, - credential_type, - metadata_url: MetadataUrl { - url: CREDANIAL_METADATA_URL.into(), - hash: None, - }, - })), - "Incorrect register credential event logged" - ); - } - - /// Test the revoke credential entrypoint, when the holder revokes the - /// credential. - #[concordium_test] - fn test_revoke_by_holder() { - let now = Timestamp::from_timestamp_millis(0); - let contract = ContractAddress { - index: 0, - subindex: 0, - }; - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - ctx.set_owner(ISSUER_ACCOUNT); - ctx.set_invoker(ISSUER_ACCOUNT); - ctx.set_self_address(contract); - ctx.set_named_entrypoint(OwnedEntrypointName::new_unchecked( - "revokeCredentialHolder".into(), - )); - ctx.set_metadata_slot_time(now); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let (credential_type, schema_ref) = get_credential_schema(); - let state = State::new( - &mut state_builder, - ISSUER_ACCOUNT, - PUBLIC_KEY, - issuer_metadata(), - credential_type, - schema_ref, - ); - let mut host = TestHost::new(state, state_builder); - - let (state, state_builder) = host.state_and_builder(); - let entry = credential_entry(state_builder); - let credential_info = entry.info(PUBLIC_KEY); - - claim!( - credential_info.holder_revocable, - "Initial credential expected to be holder-revocable" - ); - - // Create a credential the holder is going to revoke - let res = state.register_credential(&credential_info, state_builder); - - // Check that it was registered successfully - claim!(res.is_ok(), "Credential registration failed"); - - // Create singing data - let signing_data = SigningData { - contract_address: contract, - entry_point: OwnedEntrypointName::new_unchecked("revokeCredentialHolder".into()), - nonce: 0, - timestamp: Timestamp::from_timestamp_millis(10000000000), - }; - - // Create input parameters for revocation. - - let revocation_reason = "Just because"; - - let revoke_param = RevokeCredentialHolderParam { - signature: SIGNATURE, - data: RevocationDataHolder { - credential_id: PUBLIC_KEY, - signing_data, - reason: Some(revocation_reason.to_string().into()), - }, - }; - - let parameter_bytes = to_bytes(&revoke_param); - ctx.set_parameter(¶meter_bytes); - - let crypto_primitives = TestCryptoPrimitives::new(); - // Inovke `permit` function. - let result: ContractResult<()> = - contract_revoke_credential_holder(&ctx, &mut host, &mut logger, &crypto_primitives); - - // Check the result. - claim!(result.is_ok(), "Results in rejection: {:?}", result); - - // Check the status. - let status = host - .state() - .view_credential_status(now, PUBLIC_KEY) - .expect_report("Credential is expected to exist"); - claim_eq!(status, CredentialStatus::Revoked); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "One event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&CredentialEvent::Revoke(RevokeCredentialEvent { - holder_id: PUBLIC_KEY, - revoker: Revoker::Holder, - reason: Some(revocation_reason.to_string().into()), - })), - "Incorrect revoke credential event logged" - ); - }{% if restorable %} - - /// Test the restore credential entrypoint. - #[concordium_test] - fn test_contract_restore_credential() { - let now = Timestamp::from_timestamp_millis(0); - let contract = ContractAddress { - index: 0, - subindex: 0, - }; - // Setup the context - let mut ctx = TestReceiveContext::empty(); - ctx.set_sender(ADDRESS_0); - ctx.set_owner(ACCOUNT_0); - ctx.set_self_address(contract); - ctx.set_metadata_slot_time(now); - - let mut logger = TestLogger::init(); - let mut state_builder = TestStateBuilder::new(); - let (credential_type, schema_ref) = get_credential_schema(); - let state = State::new( - &mut state_builder, - ISSUER_ACCOUNT, - PUBLIC_KEY, - issuer_metadata(), - credential_type, - schema_ref, - ); - let mut host = TestHost::new(state, state_builder); - - let (state, state_builder) = host.state_and_builder(); - let entry = credential_entry(state_builder); - let credential_info = entry.info(PUBLIC_KEY); - - // Create a credential the issuer is going to restore - let res = state.register_credential(&credential_info, state_builder); - - // Check that it was registered successfully - claim!(res.is_ok(), "Credential registration failed"); - - // Make sure the credential has the `Revoked` status - let revoke_res = state.revoke_credential(now, PUBLIC_KEY); - - // Check that the credential was revoked successfully. - claim!(revoke_res.is_ok(), "Credential revocation failed"); - - // Create input parameters. - - let param = RestoreCredentialIssuerParam { - credential_id: PUBLIC_KEY, - reason: None, - }; - let parameter_bytes = to_bytes(¶m); - ctx.set_parameter(¶meter_bytes); - - // Check the status before restoring. - let status = host - .state() - .view_credential_status(now, PUBLIC_KEY) - .expect_report("Credential is expected to exist"); - claim_eq!(status, CredentialStatus::Revoked, "Expected Revoked"); - - // Call the restore credential entrypoint - let res = contract_restore_credential(&ctx, &mut host, &mut logger); - - // Check that it was restored succesfully - claim!(res.is_ok(), "Credential restoring failed"); - // Check the status after restoring. - let status = host - .state() - .view_credential_status(now, PUBLIC_KEY) - .expect_report("Credential is expected to exist"); - claim_eq!(status, CredentialStatus::Active, "Expected Active"); - - // Check the logs. - claim_eq!(logger.logs.len(), 1, "One event should be logged"); - claim_eq!( - logger.logs[0], - to_bytes(&CredentialEvent::Restore(RestoreCredentialEvent { - holder_id: PUBLIC_KEY, - reason: None, - })), - "Incorrect revoke credential event logged" - ); }{% endif %} } diff --git a/templates/credential-registry/tests/tests.rs b/templates/credential-registry/tests/tests.rs new file mode 100644 index 00000000..46cdd471 --- /dev/null +++ b/templates/credential-registry/tests/tests.rs @@ -0,0 +1,343 @@ +//! Tests for the credential registry contract. +use concordium_cis2::*; +use concordium_smart_contract_testing::*; +use concordium_std::{PublicKeyEd25519, SignatureEd25519, Timestamp}; +use {{crate_name}}::*; + +/// Constants for tests +const SIGNER: Signer = Signer::with_one_key(); +pub const ISSUER_ACCOUNT: AccountAddress = AccountAddress([0u8; 32]); +pub const ISSUER_ADDRESS: Address = Address::Account(ISSUER_ACCOUNT); +pub const ISSUER_METADATA_URL: &str = "https://example-university.com/university.json"; +pub const CREDENTIAL_METADATA_URL: &str = + "https://example-university.com/diplomas/university-vc-metadata.json"; +pub const CREDENTIAL_TYPE: &str = "UniversityDegreeCredential"; +pub const CREDENTIAL_SCHEMA_URL: &str = + "https://credentials-schemas.com/JsonSchema2023-education-certificate.json"; +// Seed: 2FEE333FAD122A45AAB7BEB3228FA7858C48B551EA8EBC49D2D56E2BA22049FF +pub const PUBLIC_KEY: PublicKeyEd25519 = PublicKeyEd25519([ + 172, 5, 96, 236, 139, 208, 146, 88, 124, 42, 62, 124, 86, 108, 35, 242, 32, 11, 7, 48, 193, 61, + 177, 220, 104, 169, 145, 4, 8, 1, 236, 112, +]); +pub const SIGNATURE: SignatureEd25519 = SignatureEd25519([ + 254, 138, 58, 131, 209, 45, 191, 52, 98, 228, 26, 234, 155, 245, 244, 226, 0, 153, 104, 111, + 201, 136, 243, 167, 251, 116, 110, 206, 172, 223, 41, 180, 90, 22, 63, 43, 157, 129, 226, 75, + 49, 33, 155, 76, 160, 133, 127, 146, 150, 80, 199, 201, 80, 98, 179, 43, 46, 46, 211, 222, 185, + 216, 12, 4, +]); + +/// Test initialization of the contract. +#[test] +fn test_init() { + let (_chain, init) = setup(); + + let schema = get_credential_schema(); + + let events = init + .events + .iter() + .map(|e| e.parse().expect("Parse event")) + .collect::>(); + + {% if revocable_by_others %}assert_eq!(events.len(), 3);{% else %}assert_eq!(events.len(), 2);{% endif %} + assert_eq!( + events[0], + CredentialEvent::IssuerMetadata(issuer_metadata()), + "Incorrect issuer metadata event logged" + );{% if revocable_by_others %} + assert_eq!( + events[1], + CredentialEvent::RevocationKey(RevocationKeyEvent { + key: PUBLIC_KEY, + action: RevocationKeyAction::Register, + }), + "Incorrect revocation key event logged" + );{% endif %} + assert_eq!( + {% if revocable_by_others %}events[2],{% else %}events[1],{% endif %} + CredentialEvent::Schema(CredentialSchemaRefEvent { + credential_type: schema.0, + schema_ref: schema.1, + }), + "Incorrect schema event logged" + ); +} + +/// Test register credential. +#[test] +fn test_register_credential() { + let (mut chain, init) = setup(); + + let update = register_credential(&mut chain, init.contract_address); + + let credential_status = get_credential_status(&mut chain, init.contract_address, PUBLIC_KEY); + assert_eq!(credential_status, CredentialStatus::Active, "Credential is not active"); + + // Check that the correct register event was produced. + let events = update + .events() + .flat_map(|(_contract, events)| events.iter().map(|e| e.parse().expect("Parsing event"))) + .collect::>(); + + assert_eq!(events, [CredentialEvent::Register(CredentialEventData { + holder_id: PUBLIC_KEY, + schema_ref: SchemaRef { + schema_ref: MetadataUrl { + url: CREDENTIAL_SCHEMA_URL.to_string(), + hash: None, + }, + }, + credential_type: CredentialType { + credential_type: CREDENTIAL_TYPE.to_string(), + }, + metadata_url: MetadataUrl { + url: CREDENTIAL_METADATA_URL.into(), + hash: None, + }, + })]); +} + +/// Test the revoke credential entrypoint, when the holder revokes the +/// credential. +#[test] +fn test_revoke_by_holder() { + let (mut chain, init) = setup(); + // Register a credential that is revocable by the holder. + register_credential(&mut chain, init.contract_address); + + let revocation_reason: Reason = "Just because".to_string().into(); + + let update = revoke_credential(&mut chain, init.contract_address, &revocation_reason); + + // Check the credential status. + let credential_status = get_credential_status(&mut chain, init.contract_address, PUBLIC_KEY); + assert_eq!(credential_status, CredentialStatus::Revoked, "Credential is not revoked"); + + // Check that the correct revoke event was produced. + let events = update + .events() + .flat_map(|(_contract, events)| events.iter().map(|e| e.parse().expect("Parsing event"))) + .collect::>(); + assert_eq!(events, [CredentialEvent::Revoke(RevokeCredentialEvent { + holder_id: PUBLIC_KEY, + revoker: Revoker::Holder, + reason: Some(revocation_reason), + })]); +}{% if restorable %} + +/// Test the restore credential entrypoint. +#[test] +fn test_contract_restore_credential() { + let (mut chain, init) = setup(); + + // Register a credential. + register_credential(&mut chain, init.contract_address); + + // Revoke the credential. + let revocation_reason: Reason = "Just because".to_string().into(); + revoke_credential(&mut chain, init.contract_address, &revocation_reason); + + // Check that the credential status is revoked. + let credential_status = get_credential_status(&mut chain, init.contract_address, PUBLIC_KEY); + assert_eq!(credential_status, CredentialStatus::Revoked, "Credential is not revoked"); + + // Restore the credential. + let parameter = RestoreCredentialIssuerParam { + credential_id: PUBLIC_KEY, + reason: None, + }; + + let update = chain + .contract_update( + SIGNER, + ISSUER_ACCOUNT, + ISSUER_ADDRESS, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: init.contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "{{crate_name}}.restoreCredential".to_string(), + ), + message: OwnedParameter::from_serial(¶meter) + .expect("Parameter has valid size."), + }, + ) + .expect("Restore credential call succeeds."); + + // Check that the credential status is active again. + let credential_status = get_credential_status(&mut chain, init.contract_address, PUBLIC_KEY); + assert_eq!(credential_status, CredentialStatus::Active, "Credential is not active"); + + // Check that the restore event was produced. + let events = update + .events() + .flat_map(|(_contract, events)| events.iter().map(|e| e.parse().expect("Parsing event"))) + .collect::>(); + assert_eq!(events, [CredentialEvent::Restore(RestoreCredentialEvent { + holder_id: PUBLIC_KEY, + reason: None, + })]); +}{% endif %} + +// Helpers: + +pub fn issuer_metadata() -> MetadataUrl { + MetadataUrl { + url: ISSUER_METADATA_URL.to_string(), + hash: None, + } +} + +pub fn get_credential_schema() -> (CredentialType, SchemaRef) { + ( + CredentialType { + credential_type: CREDENTIAL_TYPE.to_string(), + }, + SchemaRef { + schema_ref: MetadataUrl { + url: CREDENTIAL_SCHEMA_URL.to_string(), + hash: None, + }, + }, + ) +} + +/// Helper that registers a credential and returns the update type. +fn register_credential( + chain: &mut Chain, + contract_address: ContractAddress, +) -> ContractInvokeSuccess { + let parameter = RegisterCredentialParam { + credential_info: CredentialInfo { + holder_id: PUBLIC_KEY, + holder_revocable: true, + valid_from: Timestamp::from_timestamp_millis(0), + valid_until: None, + metadata_url: MetadataUrl { + url: CREDENTIAL_METADATA_URL.to_string(), + hash: None, + }, + }, + auxiliary_data: Vec::new(), + }; + + chain + .contract_update( + SIGNER, + ISSUER_ACCOUNT, + ISSUER_ADDRESS, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "{{crate_name}}.registerCredential".to_string(), + ), + message: OwnedParameter::from_serial(¶meter) + .expect("Parameter has valid size."), + }, + ) + .expect("Successfully registers credential") +} + +/// Helper that revokes the credential. +fn revoke_credential( + chain: &mut Chain, + contract_address: ContractAddress, + revocation_reason: &Reason, +) -> ContractInvokeSuccess { + // Create signing data. + let signing_data = SigningData { + contract_address, + entry_point: OwnedEntrypointName::new_unchecked("revokeCredentialHolder".into()), + nonce: 0, + timestamp: Timestamp::from_timestamp_millis(10000000000), + }; + // Create input parameters for revocation. + let revoke_param = RevokeCredentialHolderParam { + signature: SIGNATURE, + data: RevocationDataHolder { + credential_id: PUBLIC_KEY, + signing_data, + reason: Some(revocation_reason.clone()), + }, + }; + // Call the revoke credential entrypoint. + let update = chain + .contract_update( + SIGNER, + ISSUER_ACCOUNT, + ISSUER_ADDRESS, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "{{crate_name}}.revokeCredentialHolder".to_string(), + ), + message: OwnedParameter::from_serial(&revoke_param) + .expect("Parameter has valid size."), + }, + ) + .expect("Revoke credential call succeeds."); + update +} + +/// Helper for looking up the status of a credential. +fn get_credential_status( + chain: &mut Chain, + contract_address: ContractAddress, + key: PublicKeyEd25519, +) -> CredentialStatus { + let credential_status = chain + .contract_invoke( + ISSUER_ACCOUNT, + ISSUER_ADDRESS, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "{{crate_name}}.credentialStatus".to_string(), + ), + message: OwnedParameter::from_serial(&key).expect("Parameter has valid size."), + }, + ) + .expect("Credential Status call succeeds."); + + credential_status.parse_return_value().expect("Parse credential status") +} + +/// Setup chain and contract. +fn setup() -> (Chain, ContractInitSuccess) { + let mut chain = Chain::new(); + + chain.create_account(Account::new(ISSUER_ACCOUNT, Amount::from_ccd(10000))); + + let module = module_load_v1("./concordium-out/module.wasm.v1").expect("Module exists"); + let deployment = chain + .module_deploy_v1(SIGNER, ISSUER_ACCOUNT, module) + .expect("Module deploys successfully"); + + let schema = get_credential_schema(); + let init_params = InitParams { + issuer_metadata: issuer_metadata(), + issuer_account: ISSUER_ACCOUNT.into(), + issuer_key: PUBLIC_KEY,{% if revocable_by_others %} + revocation_keys: vec![PUBLIC_KEY],{% endif %} + credential_type: schema.0.clone(), + schema: schema.1.clone(), + }; + + let init = chain + .contract_init(SIGNER, ISSUER_ACCOUNT, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_{{crate_name}}".to_string()), + param: OwnedParameter::from_serial(&init_params) + .expect("Parameter has valid size."), + }) + .expect("Contract initializes successfully"); + (chain, init) +} diff --git a/templates/default/Cargo.toml b/templates/default/Cargo.toml index f1bb659b..906f148f 100644 --- a/templates/default/Cargo.toml +++ b/templates/default/Cargo.toml @@ -14,7 +14,10 @@ std = ["concordium-std/std"] wee_alloc = ["concordium-std/wee_alloc"] [dependencies] -concordium-std = {version = "8.0", default-features = false} +concordium-std = {version = "8.1", default-features = false} + +[dev-dependencies] +concordium-smart-contract-testing = "3" [lib] crate-type=["cdylib", "rlib"] diff --git a/templates/default/src/lib.rs b/templates/default/src/lib.rs index b9ba8fa5..5e89e282 100644 --- a/templates/default/src/lib.rs +++ b/templates/default/src/lib.rs @@ -11,8 +11,8 @@ pub struct State { } /// Your smart contract errors. -#[derive(Debug, PartialEq, Eq, Reject, Serial, SchemaType)] -enum Error { +#[derive(Debug, PartialEq, Eq, Reject, Serialize, SchemaType)] +pub enum Error { /// Failed parsing the parameter. #[from(ParseError)] ParseParams, @@ -22,10 +22,7 @@ enum Error { /// Init function that creates a new smart contract. #[init(contract = "{{crate_name}}")] -fn init( - _ctx: &impl HasInitContext, - _state_builder: &mut StateBuilder, -) -> InitResult { +fn init(_ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult { // Your code Ok(State {}) @@ -43,10 +40,7 @@ pub type MyInputType = bool; error = "Error", mutable )] -fn receive( - ctx: &impl HasReceiveContext, - _host: &mut impl HasHost, -) -> Result<(), Error> { +fn receive(ctx: &ReceiveContext, _host: &mut Host) -> Result<(), Error> { // Your code let throw_error = ctx.parameter_cursor().get()?; // Returns Error::ParseError on failure @@ -59,80 +53,6 @@ fn receive( /// View function that returns the content of the state. #[receive(contract = "{{crate_name}}", name = "view", return_value = "State")] -fn view<'b, S: HasStateApi>( - _ctx: &impl HasReceiveContext, - host: &'b impl HasHost, -) -> ReceiveResult<&'b State> { +fn view<'b>(_ctx: &ReceiveContext, host: &'b Host) -> ReceiveResult<&'b State> { Ok(host.state()) } - -#[concordium_cfg_test] -mod tests { - use super::*; - use test_infrastructure::*; - - type ContractResult = Result; - - #[concordium_test] - /// Test that initializing the contract succeeds with some state. - fn test_init() { - let ctx = TestInitContext::empty(); - - let mut state_builder = TestStateBuilder::new(); - - let state_result = init(&ctx, &mut state_builder); - state_result.expect_report("Contract initialization results in error"); - } - - #[concordium_test] - /// Test that invoking the `receive` endpoint with the `false` parameter - /// succeeds in updating the contract. - fn test_throw_no_error() { - let ctx = TestInitContext::empty(); - - let mut state_builder = TestStateBuilder::new(); - - // Initializing state - let initial_state = init(&ctx, &mut state_builder).expect("Initialization should pass"); - - let mut ctx = TestReceiveContext::empty(); - - let throw_error = false; - let parameter_bytes = to_bytes(&throw_error); - ctx.set_parameter(¶meter_bytes); - - let mut host = TestHost::new(initial_state, state_builder); - - // Call the contract function. - let result: ContractResult<()> = receive(&ctx, &mut host); - - // Check the result. - claim!(result.is_ok(), "Results in rejection"); - } - - #[concordium_test] - /// Test that invoking the `receive` endpoint with the `true` parameter - /// results in the `YourError` being thrown. - fn test_throw_error() { - let ctx = TestInitContext::empty(); - - let mut state_builder = TestStateBuilder::new(); - - // Initializing state - let initial_state = init(&ctx, &mut state_builder).expect("Initialization should pass"); - - let mut ctx = TestReceiveContext::empty(); - - let throw_error = true; - let parameter_bytes = to_bytes(&throw_error); - ctx.set_parameter(¶meter_bytes); - - let mut host = TestHost::new(initial_state, state_builder); - - // Call the contract function. - let error: ContractResult<()> = receive(&ctx, &mut host); - - // Check the result. - claim_eq!(error, Err(Error::YourError), "Function should throw an error."); - } -} diff --git a/templates/default/tests/tests.rs b/templates/default/tests/tests.rs new file mode 100644 index 00000000..f8764000 --- /dev/null +++ b/templates/default/tests/tests.rs @@ -0,0 +1,83 @@ +use concordium_smart_contract_testing::*; +use {{crate_name}}::*; + +/// A test account. +const ALICE: AccountAddress = AccountAddress([0u8; 32]); +const ALICE_ADDR: Address = Address::Account(ALICE); + +/// The initial balance of the ALICE test account. +const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10_000); + +/// A [`Signer`] with one set of keys, used for signing transactions. +const SIGNER: Signer = Signer::with_one_key(); + +/// Test that invoking the `receive` endpoint with the `false` parameter +/// succeeds in updating the contract. +#[test] +fn test_throw_no_error() { + let (mut chain, init) = initialize(); + + // Update the contract via the `receive` entrypoint with the parameter `false`. + chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { + address: init.contract_address, + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("{{crate_name}}.receive".to_string()), + message: OwnedParameter::from_serial(&false) + .expect("Parameter within size bounds"), + }) + .expect("Update succeeds with `false` as input."); +} + +/// Test that invoking the `receive` endpoint with the `true` parameter +/// results in the `YourError` being thrown. +#[test] +fn test_throw_error() { + let (mut chain, init) = initialize(); + + // Update the contract via the `receive` entrypoint with the parameter `true`. + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { + address: init.contract_address, + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("{{crate_name}}.receive".to_string()), + message: OwnedParameter::from_serial(&true).expect("Parameter within size bounds"), + }) + .expect_err("Update fails with `true` as input."); + + // Check that the contract returned `YourError`. + let error: Error = update.parse_return_value().expect("Deserialize `Error`"); + assert_eq!(error, Error::YourError); +} + +/// Helper method for initializing the contract. +/// +/// Does the following: +/// - Creates the [`Chain`] +/// - Creates one account, `Alice` with `10_000` CCD as the initial balance. +/// - Initializes the contract. +/// - Returns the [`Chain`] and the [`ContractInitSuccess`] +fn initialize() -> (Chain, ContractInitSuccess) { + // Initialize the test chain. + let mut chain = Chain::new(); + + // Create the test account. + chain.create_account(Account::new(ALICE, ACC_INITIAL_BALANCE)); + + // Load the module. + let module = module_load_v1("./concordium-out/module.wasm.v1").expect("Module exists at path"); + // Deploy the module. + let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Deploy valid module"); + + // Initialize the contract. + let init = chain + .contract_init(SIGNER, ALICE, Energy::from(10_000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_{{crate_name}}".to_string()), + param: OwnedParameter::empty(), + }) + .expect("Initializing contract"); + + (chain, init) +}