diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index a2aa85a4ee..d9d598ab33 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -14,63 +14,11 @@ permissions: jobs: contracts: name: Contracts - uses: multiversx/mx-sc-actions/.github/workflows/contracts.yml@v3.0.0 + uses: multiversx/mx-sc-actions/.github/workflows/contracts.yml@v3.1.0 with: rust-toolchain: nightly-2023-12-11 path-to-sc-meta: framework/meta mx-scenario-go-version: v2.1.0-alpha + coverage-args: --ignore-filename-regex='meta/src' --ignore-filename-regex='wasm-adapter' --ignore-filename-regex='benchmarks/' --ignore-filename-regex='tests/' --output ./coverage.md secrets: - token: ${{ secrets.GITHUB_TOKEN }} - - test_coverage: - name: Test Coverage - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Install rust - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: nightly-2023-12-11 - - - name: Install prerequisites - run: | - rustup component add llvm-tools-preview - dirname $(find ~/.rustup -name llvm-cov) >> $GITHUB_PATH - - echo $(dirname $(find ~/.rustup -name llvm-cov)) - - - name: Run tests and generate report - env: - RUSTFLAGS: "" - run: | - cargo run --bin sc-meta test-coverage \ - --ignore-filename-regex='meta/src' \ - --ignore-filename-regex='wasm-adapter' \ - --ignore-filename-regex='benchmarks/' \ - --ignore-filename-regex='tests/' \ - --output ./coverage.md - - - name: Upload the report - uses: actions/upload-artifact@v3 - with: - name: coverage - path: coverage.md - - - name: Find the comment containing the report - id: fc - uses: peter-evans/find-comment@v2 - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository - with: - issue-number: ${{ github.event.pull_request.number }} - comment-author: "github-actions[bot]" - body-includes: "Coverage Summary" - - - name: Create or update the report comment - uses: peter-evans/create-or-update-comment@v2 - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository - with: - comment-id: ${{ steps.fc.outputs.comment-id }} - issue-number: ${{ github.event.pull_request.number }} - body-file: ./coverage.md - edit-mode: replace + token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9fd01b92b0..81b6a2feb4 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -908,6 +908,22 @@ dependencies = [ "multiversx-sc-meta", ] +[[package]] +name = "exchange-features" +version = "0.0.0" +dependencies = [ + "multiversx-sc", + "multiversx-sc-scenario", +] + +[[package]] +name = "exchange-features-meta" +version = "0.0.0" +dependencies = [ + "exchange-features", + "multiversx-sc-meta", +] + [[package]] name = "factorial" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index ef92a8a7b9..d97f1292e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -175,4 +175,7 @@ members = [ "contracts/feature-tests/rust-testing-framework-tester/meta", "contracts/feature-tests/use-module", "contracts/feature-tests/use-module/meta", + "contracts/feature-tests/exchange-features", + "contracts/feature-tests/exchange-features/meta", + ] diff --git a/contracts/feature-tests/exchange-features/Cargo.toml b/contracts/feature-tests/exchange-features/Cargo.toml new file mode 100644 index 0000000000..8e7f760f8a --- /dev/null +++ b/contracts/feature-tests/exchange-features/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "exchange-features" +version = "0.0.0" +authors = ["Alin-Marius Cruceat "] +edition = "2021" +publish = false + +[lib] +path = "src/exchange_features.rs" + +[dependencies.multiversx-sc] +version = "0.48.0-alpha.1" +path = "../../../framework/base" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.48.0-alpha.1" +path = "../../../framework/scenario" diff --git a/contracts/feature-tests/exchange-features/meta/Cargo.toml b/contracts/feature-tests/exchange-features/meta/Cargo.toml new file mode 100644 index 0000000000..553871b6cb --- /dev/null +++ b/contracts/feature-tests/exchange-features/meta/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "exchange-features-meta" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies.exchange-features] +path = ".." + +[dependencies.multiversx-sc-meta] +version = "0.48.0-alpha.1" +path = "../../../../framework/meta" +default-features = false diff --git a/contracts/feature-tests/exchange-features/meta/src/main.rs b/contracts/feature-tests/exchange-features/meta/src/main.rs new file mode 100644 index 0000000000..f9a8d31e06 --- /dev/null +++ b/contracts/feature-tests/exchange-features/meta/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + multiversx_sc_meta::cli_main::(); +} diff --git a/contracts/feature-tests/exchange-features/multiversx.json b/contracts/feature-tests/exchange-features/multiversx.json new file mode 100644 index 0000000000..7365539625 --- /dev/null +++ b/contracts/feature-tests/exchange-features/multiversx.json @@ -0,0 +1,3 @@ +{ + "language": "rust" +} \ No newline at end of file diff --git a/contracts/feature-tests/exchange-features/src/exchange_features.rs b/contracts/feature-tests/exchange-features/src/exchange_features.rs new file mode 100644 index 0000000000..fd048e72c8 --- /dev/null +++ b/contracts/feature-tests/exchange-features/src/exchange_features.rs @@ -0,0 +1,72 @@ +#![no_std] + +multiversx_sc::imports!(); +multiversx_sc::derive_imports!(); + +#[derive( + ManagedVecItem, + TopEncode, + TopDecode, + NestedEncode, + NestedDecode, + TypeAbi, + Clone, + PartialEq, + Debug, +)] +pub struct TokenAttributes { + pub amount: BigUint, +} + +impl FixedSupplyToken for TokenAttributes { + fn get_total_supply(&self) -> BigUint { + self.amount.clone() + } + + fn into_part(self, payment_amount: &BigUint) -> Self { + let new_amount = self.rule_of_three_non_zero_result(payment_amount, &self.amount); + TokenAttributes { amount: new_amount } + } +} +impl Mergeable for TokenAttributes { + #[inline] + fn can_merge_with(&self, _other: &Self) -> bool { + true + } + + fn merge_with(&mut self, other: Self) { + self.error_if_not_mergeable(&other); + + self.amount += other.amount + } +} + +#[multiversx_sc::contract] +pub trait ExchangeFeatures { + #[storage_mapper("supply")] + fn supply(&self) -> SingleValueMapper>; + + #[init] + fn init(&self, initial_value: BigUint) { + self.supply().set(TokenAttributes { + amount: initial_value, + }); + } + + #[upgrade] + fn upgrade(&self, value: BigUint) { + let token = self.supply().get(); + self.supply().set(token.into_part(&value)); + } + + #[endpoint] + fn merge(&self, value: BigUint) { + self.supply() + .update(|token| token.merge_with(TokenAttributes { amount: value })); + } + + #[endpoint] + fn get_supply(&self) -> BigUint { + self.supply().get().get_total_supply() + } +} diff --git a/contracts/feature-tests/exchange-features/tests/exchange_features_blackbox_test.rs b/contracts/feature-tests/exchange-features/tests/exchange_features_blackbox_test.rs new file mode 100644 index 0000000000..6ea73fb3a6 --- /dev/null +++ b/contracts/feature-tests/exchange-features/tests/exchange_features_blackbox_test.rs @@ -0,0 +1,83 @@ +use multiversx_sc_scenario::{scenario_model::*, *}; + +const EXCHANGE_FEATURES_PATH_EXPR: &str = "mxsc:output/exchange-features.mxsc.json"; + +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + blockchain.set_current_dir_from_workspace("contracts/feature-tests/exchange-features"); + + blockchain.register_contract( + EXCHANGE_FEATURES_PATH_EXPR, + exchange_features::ContractBuilder, + ); + blockchain +} + +#[test] +fn exchange_features_blackbox_raw() { + let mut world = world(); + let exchange_features_code = world.code_expression(EXCHANGE_FEATURES_PATH_EXPR); + + world + .set_state_step( + SetStateStep::new() + .put_account("address:owner", Account::new().nonce(1)) + .new_address("address:owner", 1, "sc:exchange-features"), + ) + .sc_deploy( + ScDeployStep::new() + .from("address:owner") + .code(&exchange_features_code) + .argument("5") + .expect(TxExpect::ok().no_result()), + ) + .sc_call( + ScCallStep::new() + .from("address:owner") + .to("sc:exchange-features") + .function("get_supply") + .expect(TxExpect::ok().result("5")), + ) + .sc_call( + ScCallStep::new() + .from("address:owner") + .to("sc:exchange-features") + .function("merge") + .argument("3") + .expect(TxExpect::ok().no_result()), + ) + .sc_call( + ScCallStep::new() + .from("address:owner") + .to("sc:exchange-features") + .function("get_supply") + .expect(TxExpect::ok().result("8")), + ) + .sc_call( + ScCallStep::new() + .from("address:owner") + .to("sc:exchange-features") + .function("upgradeContract") + .argument(&exchange_features_code) + .argument("0x0502") // codeMetadata + .argument("0") // contract argument + .expect(TxExpect::user_error("str:Zero amount")), + ) + .sc_call( + ScCallStep::new() + .from("address:owner") + .to("sc:exchange-features") + .function("upgradeContract") + .argument(exchange_features_code) + .argument("0x0502") // codeMetadata + .argument("3") // contract argument + .expect(TxExpect::ok().no_result()), + ) + .sc_call( + ScCallStep::new() + .from("address:owner") + .to("sc:exchange-features") + .function("get_supply") + .expect(TxExpect::ok().result("3")), + ); +} diff --git a/contracts/feature-tests/exchange-features/wasm/Cargo.lock b/contracts/feature-tests/exchange-features/wasm/Cargo.lock new file mode 100644 index 0000000000..e9ffb74f36 --- /dev/null +++ b/contracts/feature-tests/exchange-features/wasm/Cargo.lock @@ -0,0 +1,170 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "autocfg" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "exchange-features" +version = "0.0.0" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "exchange-features-wasm" +version = "0.0.0" +dependencies = [ + "exchange-features", + "multiversx-sc-wasm-adapter", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "multiversx-sc" +version = "0.47.8" +dependencies = [ + "bitflags", + "hex-literal", + "multiversx-sc-codec", + "multiversx-sc-derive", + "num-traits", +] + +[[package]] +name = "multiversx-sc-codec" +version = "0.18.6" +dependencies = [ + "arrayvec", + "multiversx-sc-codec-derive", +] + +[[package]] +name = "multiversx-sc-codec-derive" +version = "0.18.6" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "multiversx-sc-derive" +version = "0.47.8" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "radix_trie", + "syn", +] + +[[package]] +name = "multiversx-sc-wasm-adapter" +version = "0.47.8" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/contracts/feature-tests/exchange-features/wasm/Cargo.toml b/contracts/feature-tests/exchange-features/wasm/Cargo.toml new file mode 100644 index 0000000000..af96286900 --- /dev/null +++ b/contracts/feature-tests/exchange-features/wasm/Cargo.toml @@ -0,0 +1,32 @@ +# Code generated by the multiversx-sc build system. DO NOT EDIT. + +# ########################################## +# ############## AUTO-GENERATED ############# +# ########################################## + +[package] +name = "exchange-features-wasm" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[profile.release] +codegen-units = 1 +opt-level = "z" +lto = true +debug = false +panic = "abort" +overflow-checks = false + +[dependencies.exchange-features] +path = ".." + +[dependencies.multiversx-sc-wasm-adapter] +version = "0.48.0-alpha.1" +path = "../../../../framework/wasm-adapter" + +[workspace] +members = ["."] diff --git a/contracts/feature-tests/exchange-features/wasm/src/lib.rs b/contracts/feature-tests/exchange-features/wasm/src/lib.rs new file mode 100644 index 0000000000..b0842b89d6 --- /dev/null +++ b/contracts/feature-tests/exchange-features/wasm/src/lib.rs @@ -0,0 +1,29 @@ +// Code generated by the multiversx-sc build system. DO NOT EDIT. + +//////////////////////////////////////////////////// +////////////////// AUTO-GENERATED ////////////////// +//////////////////////////////////////////////////// + +// Init: 1 +// Endpoints: 3 +// Async Callback (empty): 1 +// Total number of exported functions: 5 + +#![no_std] +#![allow(internal_features)] +#![feature(lang_items)] + +multiversx_sc_wasm_adapter::allocator!(); +multiversx_sc_wasm_adapter::panic_handler!(); + +multiversx_sc_wasm_adapter::endpoints! { + exchange_features + ( + init => init + upgrade => upgrade + merge => merge + get_supply => get_supply + ) +} + +multiversx_sc_wasm_adapter::async_callback_empty! {} diff --git a/framework/base/src/types/managed/wrapped/mod.rs b/framework/base/src/types/managed/wrapped/mod.rs index 5a5e907403..a6eb5005b8 100644 --- a/framework/base/src/types/managed/wrapped/mod.rs +++ b/framework/base/src/types/managed/wrapped/mod.rs @@ -17,6 +17,7 @@ mod managed_vec_ref_iter; pub(crate) mod preloaded_managed_buffer; mod randomness_source; mod token_identifier; +mod traits; pub use egld_or_esdt_token_identifier::EgldOrEsdtTokenIdentifier; pub use egld_or_esdt_token_payment::EgldOrEsdtTokenPayment; @@ -37,3 +38,8 @@ pub use managed_vec_ref::ManagedVecRef; pub use managed_vec_ref_iter::ManagedVecRefIterator; pub use randomness_source::RandomnessSource; pub use token_identifier::TokenIdentifier; + +pub use traits::{ + fixed_token_supply::FixedSupplyToken, + mergeable::{ExternallyMergeable, Mergeable}, +}; diff --git a/framework/base/src/types/managed/wrapped/traits/fixed_token_supply.rs b/framework/base/src/types/managed/wrapped/traits/fixed_token_supply.rs new file mode 100644 index 0000000000..838486c203 --- /dev/null +++ b/framework/base/src/types/managed/wrapped/traits/fixed_token_supply.rs @@ -0,0 +1,31 @@ +use crate::imports::{BigUint, ErrorApiImpl, ManagedTypeApi}; + +pub trait FixedSupplyToken { + fn get_total_supply(&self) -> BigUint; + + fn into_part(self, payment_amount: &BigUint) -> Self; + + /// full_value * current_supply / total_supply + fn rule_of_three(&self, current_supply: &BigUint, full_value: &BigUint) -> BigUint { + let total_supply = self.get_total_supply(); + if current_supply == &total_supply { + return full_value.clone(); + } + + (full_value * current_supply) / total_supply + } + + /// full_value * current_supply / total_supply + fn rule_of_three_non_zero_result( + &self, + current_supply: &BigUint, + full_value: &BigUint, + ) -> BigUint { + let result = self.rule_of_three(current_supply, full_value); + if result == 0 { + M::error_api_impl().signal_error(b"Zero amount"); + } + + result + } +} diff --git a/framework/base/src/types/managed/wrapped/traits/mergeable.rs b/framework/base/src/types/managed/wrapped/traits/mergeable.rs new file mode 100644 index 0000000000..2ed714973a --- /dev/null +++ b/framework/base/src/types/managed/wrapped/traits/mergeable.rs @@ -0,0 +1,62 @@ +use crate::imports::{ErrorApiImpl, EsdtTokenPayment, ManagedTypeApi, ManagedVec, ManagedVecItem}; + +pub static CANNOT_MERGE_ERR_MSG: &[u8] = b"Cannot merge"; + +/// Used for types that can be merged locally. +pub trait Mergeable { + fn error_if_not_mergeable(&self, other: &Self) { + if !self.can_merge_with(other) { + throw_not_mergeable_error::(); + } + } + + fn can_merge_with(&self, other: &Self) -> bool; + + fn merge_with(&mut self, other: Self); + + fn merge_with_multiple(&mut self, others: ManagedVec) + where + Self: Sized + ManagedVecItem, + { + for item in &others { + self.merge_with(item); + } + } +} + +/// Used when merging is done through an external SC call. +/// Generally, these only need to have the same token ID, with different nonces. +pub trait ExternallyMergeable { + fn error_if_not_externally_mergeable(&self, other: &Self) { + if !self.can_be_merged_externally_with(other) { + throw_not_mergeable_error::(); + } + } + + fn can_be_merged_externally_with(&self, other: &Self) -> bool; +} + +pub fn throw_not_mergeable_error() -> ! { + M::error_api_impl().signal_error(CANNOT_MERGE_ERR_MSG); +} + +impl Mergeable for EsdtTokenPayment { + fn can_merge_with(&self, other: &Self) -> bool { + let same_token_id = self.token_identifier == other.token_identifier; + let same_token_nonce = self.token_nonce == other.token_nonce; + + same_token_id && same_token_nonce + } + + fn merge_with(&mut self, other: Self) { + self.error_if_not_mergeable(&other); + + self.amount += other.amount; + } +} + +impl ExternallyMergeable for EsdtTokenPayment { + fn can_be_merged_externally_with(&self, other: &Self) -> bool { + self.token_identifier == other.token_identifier + } +} diff --git a/framework/base/src/types/managed/wrapped/traits/mod.rs b/framework/base/src/types/managed/wrapped/traits/mod.rs new file mode 100644 index 0000000000..2a9f147fdc --- /dev/null +++ b/framework/base/src/types/managed/wrapped/traits/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod fixed_token_supply; +pub(crate) mod mergeable;