diff --git a/examples/approval-receiver/.cargo/config b/examples/approval-receiver/.cargo/config deleted file mode 100644 index 8a62025..0000000 --- a/examples/approval-receiver/.cargo/config +++ /dev/null @@ -1,3 +0,0 @@ -[build] -rustflags = ["-C", "link-args=-s"] -target = "wasm32-unknown-unknown" \ No newline at end of file diff --git a/examples/approval-receiver/.gitignore b/examples/approval-receiver/.gitignore deleted file mode 100644 index 96ef6c0..0000000 --- a/examples/approval-receiver/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target -Cargo.lock diff --git a/examples/approval-receiver/Cargo.toml b/examples/approval-receiver/Cargo.toml deleted file mode 100644 index ba8bfd0..0000000 --- a/examples/approval-receiver/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "nep-246-ccc" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -nep-246 = { path = "../../" } -near-sdk = "4.0.0-pre.6" - -[profile.release] -codegen-units = 1 -# Tell `rustc` to optimize for small code size. -opt-level = "z" -lto = true -debug = false -panic = "abort" -overflow-checks = true \ No newline at end of file diff --git a/examples/approval-receiver/out/nep_246_ccc.wasm b/examples/approval-receiver/out/nep_246_ccc.wasm deleted file mode 100755 index d92e60e..0000000 Binary files a/examples/approval-receiver/out/nep_246_ccc.wasm and /dev/null differ diff --git a/examples/approval-receiver/scripts/build.sh b/examples/approval-receiver/scripts/build.sh deleted file mode 100644 index 821e85e..0000000 --- a/examples/approval-receiver/scripts/build.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -e - -OUT_DIR=out - -if [ ! -d $OUT_DIR ]; then - echo "Creating '${OUT_DIR}' directory" - mkdir $OUT_DIR; -fi - -# Because we include bytes from FT contract we need to build it before factory - -cargo build --release -cp target/wasm32-unknown-unknown/release/nep_246_ccc.wasm $OUT_DIR \ No newline at end of file diff --git a/examples/multi-token/.cargo/config b/examples/multi-token/.cargo/config index 8a62025..dadf1d3 100644 --- a/examples/multi-token/.cargo/config +++ b/examples/multi-token/.cargo/config @@ -1,3 +1,2 @@ -[build] -rustflags = ["-C", "link-args=-s"] -target = "wasm32-unknown-unknown" \ No newline at end of file +[target.wasm32-unknown-unknown] +rustflags = ["-C", "link-arg=-s"] diff --git a/examples/multi-token/.gitignore b/examples/multi-token/.gitignore deleted file mode 100644 index 96ef6c0..0000000 --- a/examples/multi-token/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target -Cargo.lock diff --git a/examples/multi-token/Cargo.toml b/examples/multi-token/Cargo.toml index 942d358..fb27b97 100644 --- a/examples/multi-token/Cargo.toml +++ b/examples/multi-token/Cargo.toml @@ -1,16 +1,21 @@ [package] -name = "nep-246-test" +name = "multi-token-wrapper" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -crate-type = ["cdylib"] - -[dependencies] -nep-246 = { path = "../../" } +[dev-dependencies] +anyhow = "1.0" +near-primitives = "0.5.0" near-sdk = "4.0.0-pre.6" +near-units = "0.2.0" +serde_json = "1.0" +tokio = { version = "1.14", features = ["full"] } +workspaces = "0.1.1" + +# remember to include a line for each contract +multi-token = { path = "./mt" } +defi = { path = "./test-contract-defi" } +approval-receiver = { path = "./test-approval-receiver" } [profile.release] codegen-units = 1 @@ -19,4 +24,12 @@ opt-level = "z" lto = true debug = false panic = "abort" -overflow-checks = true \ No newline at end of file +overflow-checks = true + +[workspace] +# remember to include a member for each contract +members = [ + "mt", + "test-contract-defi", + "test-approval-receiver", +] \ No newline at end of file diff --git a/examples/multi-token/build.sh b/examples/multi-token/build.sh new file mode 100755 index 0000000..353c66d --- /dev/null +++ b/examples/multi-token/build.sh @@ -0,0 +1,9 @@ +#!/bin/bash +TARGET="${CARGO_TARGET_DIR:-target}" +set -e +cd "`dirname $0`" + +cargo build --all --target wasm32-unknown-unknown --release +cp $TARGET/wasm32-unknown-unknown/release/defi.wasm ./res/ +cp $TARGET/wasm32-unknown-unknown/release/multi_token.wasm ./res/ +cp $TARGET/wasm32-unknown-unknown/release/approval_receiver.wasm ./res/ diff --git a/examples/multi-token/mt/Cargo.toml b/examples/multi-token/mt/Cargo.toml new file mode 100644 index 0000000..50c342b --- /dev/null +++ b/examples/multi-token/mt/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "multi-token" +version = "1.1.0" +authors = ["Near Inc "] +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +near-sdk = "4.0.0-pre.6" +nep-246 = { path = "../../../" } diff --git a/examples/multi-token/mt/src/lib.rs b/examples/multi-token/mt/src/lib.rs new file mode 100644 index 0000000..1541f6c --- /dev/null +++ b/examples/multi-token/mt/src/lib.rs @@ -0,0 +1,199 @@ +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::collections::{LazyOption}; +use near_sdk::json_types::U128; +use near_sdk::Promise; +use near_sdk::{ + env, near_bindgen, require, AccountId, Balance, BorshStorageKey, PanicOnDefault, PromiseOrValue, +}; +use nep_246::multi_token::metadata::MT_METADATA_SPEC; +use nep_246::multi_token::token::{Approval, Token, TokenId}; +use nep_246::multi_token::{ + core::MultiToken, + metadata::{MtContractMetadata, TokenMetadata}, +}; + +#[near_bindgen] +#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] +pub struct ExampleMTContract { + tokens: MultiToken, + metadata: LazyOption, +} + +#[derive(BorshSerialize, BorshStorageKey)] +enum StorageKey { + MultiToken, + Metadata, + TokenMetadata, + Enumeration, + Approval, +} + +#[near_bindgen] +impl ExampleMTContract { + #[init] + pub fn new_default_meta(owner_id: AccountId) -> Self { + let metadata = MtContractMetadata { + spec: MT_METADATA_SPEC.to_string(), + name: "Test".to_string(), + symbol: "OMG".to_string(), + icon: None, + base_uri: None, + reference: None, + reference_hash: None, + }; + + Self::new(owner_id, metadata) + } + + #[init] + pub fn new(owner_id: AccountId, metadata: MtContractMetadata) -> Self { + require!(!env::state_exists(), "Already initialized"); + metadata.assert_valid(); + + Self { + tokens: MultiToken::new( + StorageKey::MultiToken, + owner_id, + Some(StorageKey::TokenMetadata), + Some(StorageKey::Enumeration), + Some(StorageKey::Approval), + ), + metadata: LazyOption::new(StorageKey::Metadata, Some(&metadata)), + } + } + + #[payable] + pub fn mt_mint( + &mut self, + token_owner_id: AccountId, + token_metadata: TokenMetadata, + amount: Balance, + ) -> Token { + // Only the owner of the MFT contract can perform this operation + assert_eq!(env::predecessor_account_id(), self.tokens.owner_id, "Unauthorized: {} != {}", env::predecessor_account_id(), self.tokens.owner_id); + self.tokens.internal_mint(token_owner_id, Some(amount), Some(token_metadata), None) + } + + pub fn register(&mut self, token_id: TokenId, account_id: AccountId) { + self.tokens.internal_register_account(&token_id, &account_id) + } +} + +nep_246::impl_multi_token_core!(ExampleMTContract, tokens); +nep_246::impl_multi_token_approval!(ExampleMTContract, tokens); +nep_246::impl_multi_token_enumeration!(ExampleMTContract, tokens); + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use near_sdk::test_utils::{accounts, VMContextBuilder}; + use near_sdk::{testing_env}; + use super::*; + + fn create_token_md(title: String, description: String) -> TokenMetadata { + TokenMetadata { + title: Some(title), + description: Some(description), + media: None, + media_hash: None, + issued_at: Some(String::from("123456")), + expires_at: None, + starts_at: None, + updated_at: None, + extra: None, + reference: None, + reference_hash: None, + } + } + + #[test] + fn test_transfer() { + let mut context = VMContextBuilder::new(); + testing_env!(context + .signer_account_id(accounts(0)) + .predecessor_account_id(accounts(0)) + .build()); + + let mut contract = ExampleMTContract::new_default_meta(accounts(0)); + let token_md = create_token_md("ABC".into(), "Alphabet token".into()); + + let token = contract.mt_mint(accounts(0), token_md.clone(), 1000); + assert_eq!(contract.mt_balance_of(accounts(0), token.token_id.clone()), 1000.into(), "Wrong balance"); + + contract.register(token.token_id.clone(), accounts(1)); + assert_eq!(contract.mt_balance_of(accounts(1), token.token_id.clone()), 0.into(), "Wrong balance"); + + testing_env!(context.attached_deposit(1).build()); + contract.mt_transfer(accounts(1), token.token_id.clone(), 4.into(), None, None); + + assert_eq!(contract.mt_balance_of(accounts(0), token.token_id.clone()).0, 996, "Wrong balance"); + assert_eq!(contract.mt_balance_of(accounts(1), token.token_id.clone()).0, 4, "Wrong balance"); + } + + #[test] + fn test_batch_transfer() { + let mut context = VMContextBuilder::new(); + testing_env!(context + .signer_account_id(accounts(0)) + .predecessor_account_id(accounts(0)) + .build()); + let mut contract = ExampleMTContract::new_default_meta(accounts(0)); + + let quote_token_md = create_token_md("PYC".into(), "Python token".into()); + let base_token_md = create_token_md("ABC".into(), "Alphabet token".into()); + + let quote_token = contract.mt_mint(accounts(0), quote_token_md.clone(), 1000); + let base_token = contract.mt_mint(accounts(0), base_token_md.clone(), 2000); + contract.register(quote_token.token_id.clone(), accounts(1)); + contract.register(base_token.token_id.clone(), accounts(1)); + + testing_env!(context.attached_deposit(1).build()); + + // Perform the transfers + contract.mt_batch_transfer( + accounts(1), + vec![quote_token.token_id.clone(), base_token.token_id.clone()], + vec![4.into(), 600.into()], + None, + None + ); + + assert_eq!(contract.mt_balance_of(accounts(0), quote_token.token_id.clone()).0, 996, "Wrong balance"); + assert_eq!(contract.mt_balance_of(accounts(1), quote_token.token_id.clone()).0, 4, "Wrong balance"); + + assert_eq!(contract.mt_balance_of(accounts(0), base_token.token_id.clone()).0, 1400, "Wrong balance"); + assert_eq!(contract.mt_balance_of(accounts(1), base_token.token_id.clone()).0, 600, "Wrong balance"); + } + + #[test] + fn test_transfer_call() { + // How to test a multi-contract call? + let mut context = VMContextBuilder::new(); + testing_env!(context + .signer_account_id(accounts(0)) + .predecessor_account_id(accounts(0)) + .attached_deposit(1) + .build()); + let mut contract = ExampleMTContract::new_default_meta(accounts(0)); + let quote_token_md = create_token_md("ABC".into(), "Alphabet token".into()); + + // alice starts with 1000, bob with 0. + let quote_token = contract.mt_mint(accounts(0), quote_token_md.clone(), 1000); + contract.register(quote_token.token_id.clone(), accounts(1)); + + let _result = contract.mt_transfer_call( + accounts(1), // receiver account + quote_token.token_id.clone(), + 100, // amount + None, + String::from("invest"), + ); + + + // println!("result: {}", result); + } + + #[test] + fn test_batch_transfer_call() { + + } +} diff --git a/examples/multi-token/out/nep_246_test.wasm b/examples/multi-token/out/nep_246_test.wasm deleted file mode 100755 index e6fff64..0000000 Binary files a/examples/multi-token/out/nep_246_test.wasm and /dev/null differ diff --git a/examples/multi-token/res/approval_receiver.wasm b/examples/multi-token/res/approval_receiver.wasm new file mode 100755 index 0000000..de38a80 Binary files /dev/null and b/examples/multi-token/res/approval_receiver.wasm differ diff --git a/examples/multi-token/res/defi.wasm b/examples/multi-token/res/defi.wasm new file mode 100755 index 0000000..76eef7b Binary files /dev/null and b/examples/multi-token/res/defi.wasm differ diff --git a/examples/multi-token/res/multi_token.wasm b/examples/multi-token/res/multi_token.wasm new file mode 100755 index 0000000..28d5879 Binary files /dev/null and b/examples/multi-token/res/multi_token.wasm differ diff --git a/examples/multi-token/scripts/build.sh b/examples/multi-token/scripts/build.sh deleted file mode 100644 index 2069854..0000000 --- a/examples/multi-token/scripts/build.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -e - -OUT_DIR=out - -if [ ! -d $OUT_DIR ]; then - echo "Creating '${OUT_DIR}' directory" - mkdir $OUT_DIR; -fi - -# Because we include bytes from FT contract we need to build it before factory - -cargo build --release -cp target/wasm32-unknown-unknown/release/nep_246_test.wasm $OUT_DIR \ No newline at end of file diff --git a/examples/multi-token/src/lib.rs b/examples/multi-token/src/lib.rs deleted file mode 100644 index 615e46f..0000000 --- a/examples/multi-token/src/lib.rs +++ /dev/null @@ -1,83 +0,0 @@ -use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; -use near_sdk::collections::LazyOption; -use near_sdk::json_types::U128; -use near_sdk::Promise; -use near_sdk::{ - env, near_bindgen, require, AccountId, Balance, BorshStorageKey, PanicOnDefault, PromiseOrValue, -}; -use nep_246::multi_token::metadata::MT_METADATA_SPEC; -use nep_246::multi_token::token::{Token, TokenId}; -use nep_246::multi_token::{ - core::MultiToken, - metadata::{MtContractMetadata, TokenMetadata}, -}; - -#[near_bindgen] -#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] -pub struct Contract { - tokens: MultiToken, - metadata: LazyOption, -} - -#[derive(BorshSerialize, BorshStorageKey)] -enum StorageKey { - MultiToken, - Metadata, - TokenMetadata, - Enumeration, - Approval, -} - -#[near_bindgen] -impl Contract { - #[init] - pub fn new_default_meta(owner_id: AccountId) -> Self { - let metadata = MtContractMetadata { - spec: MT_METADATA_SPEC.to_string(), - name: "Test".to_string(), - symbol: "OMG".to_string(), - icon: None, - base_uri: None, - reference: None, - reference_hash: None, - }; - - Self::new(owner_id, metadata) - } - - #[init] - pub fn new(owner_id: AccountId, metadata: MtContractMetadata) -> Self { - require!(!env::state_exists(), "Already initialized"); - metadata.assert_valid(); - - Self { - tokens: MultiToken::new( - StorageKey::MultiToken, - owner_id, - Some(StorageKey::TokenMetadata), - Some(StorageKey::Enumeration), - Some(StorageKey::Approval), - ), - metadata: LazyOption::new(StorageKey::Metadata, Some(&metadata)), - } - } - - #[payable] - pub fn mt_mint( - &mut self, - token_owner_id: AccountId, - token_metadata: TokenMetadata, - amount: Balance, - ) -> Token { - assert_eq!(env::predecessor_account_id(), self.tokens.owner_id, "Unauthorized"); - self.tokens.internal_mint(token_owner_id, Some(amount), Some(token_metadata), None) - } - - pub fn register(&mut self, token_id: TokenId, account_id: AccountId) { - self.tokens.internal_register_account(&token_id, &account_id) - } -} - -nep_246::impl_multi_token_core!(Contract, tokens); -nep_246::impl_multi_token_approval!(Contract, tokens); -nep_246::impl_multi_token_enumeration!(Contract, tokens); \ No newline at end of file diff --git a/examples/multi-token/test-approval-receiver/Cargo.toml b/examples/multi-token/test-approval-receiver/Cargo.toml new file mode 100644 index 0000000..d9daefe --- /dev/null +++ b/examples/multi-token/test-approval-receiver/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "approval-receiver" +version = "0.1.0" +authors = ["Near Inc "] +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +near-sdk = "4.0.0-pre.6" +nep-246 = { path = "../../../" } diff --git a/examples/approval-receiver/src/lib.rs b/examples/multi-token/test-approval-receiver/src/lib.rs similarity index 100% rename from examples/approval-receiver/src/lib.rs rename to examples/multi-token/test-approval-receiver/src/lib.rs diff --git a/examples/multi-token/test-contract-defi/Cargo.toml b/examples/multi-token/test-contract-defi/Cargo.toml new file mode 100644 index 0000000..2f0b1a7 --- /dev/null +++ b/examples/multi-token/test-contract-defi/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "defi" +version = "0.0.1" +authors = ["Near Inc "] +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +near-sdk = "4.0.0-pre.6" +nep-246 = { path = "../../../" } diff --git a/examples/multi-token/test-contract-defi/src/lib.rs b/examples/multi-token/test-contract-defi/src/lib.rs new file mode 100644 index 0000000..10a3a3b --- /dev/null +++ b/examples/multi-token/test-contract-defi/src/lib.rs @@ -0,0 +1,96 @@ +/*! +Some hypothetical DeFi contract that will do smart things with the transferred tokens +*/ +use nep_246::multi_token::core::MultiTokenReceiver; +use nep_246::multi_token::token::TokenId; +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::json_types::U128; +use near_sdk::{ + env, ext_contract, log, near_bindgen, require, AccountId, Balance, Gas, PanicOnDefault, + PromiseOrValue, +}; + +const BASE_GAS: u64 = 5_000_000_000_000; +const PROMISE_CALL: u64 = 5_000_000_000_000; +const GAS_FOR_MT_ON_TRANSFER: Gas = Gas(BASE_GAS + PROMISE_CALL); + +const NO_DEPOSIT: Balance = 0; + +#[near_bindgen] +#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] +pub struct DeFi { + multi_token_account_id: AccountId, +} + +// Defining cross-contract interface. This allows to create a new promise. +#[ext_contract(ext_self)] +pub trait ValueReturnTrait { + fn value_please(&self, num_tokens: usize, amount_to_return: String) -> PromiseOrValue>; +} + +// Have to repeat the same trait for our own implementation. +trait ValueReturnTrait { + fn value_please(&self, num_tokens: usize, amount_to_return: String) -> PromiseOrValue>; +} + +#[near_bindgen] +impl DeFi { + #[init] + pub fn new(multi_token_account_id: AccountId) -> Self { + require!(!env::state_exists(), "Already initialized"); + Self { multi_token_account_id: multi_token_account_id.into() } + } +} + +#[near_bindgen] +impl MultiTokenReceiver for DeFi { + /// If given `msg: "take-my-money", immediately returns U128::From(0) + /// Otherwise, makes a cross-contract call to own `value_please` function, passing `msg` + /// value_please will attempt to parse `msg` as an integer and return a vec of + /// token_ids.len() many copies of the U128 version of it. + fn mt_on_transfer( + &mut self, + sender_id: AccountId, + _previous_owner_id: AccountId, + token_ids: Vec, + amounts: Vec, + msg: String, + ) -> PromiseOrValue> { + // Verifying that we were called by multi-token contract that we expect. + require!( + env::predecessor_account_id() == self.multi_token_account_id, + "Only supports the one multi-token contract" + ); + + log!("received {} types of tokens from @{} mt_on_transfer, msg = {}", token_ids.len(), sender_id.as_ref(), msg); + + for i in 0..token_ids.len() { + log!("-> {} of token {}", token_ids[i], amounts[i].0) + } + + match msg.as_str() { + "take-my-money" => PromiseOrValue::Value(vec![U128::from(0); token_ids.len()]), + _ => { + let prepaid_gas = env::prepaid_gas(); + let account_id = env::current_account_id(); + ext_self::value_please( + token_ids.len(), + msg, + account_id, + NO_DEPOSIT, + prepaid_gas - GAS_FOR_MT_ON_TRANSFER, + ) + .into() + } + } + } +} + +#[near_bindgen] +impl ValueReturnTrait for DeFi { + fn value_please(&self, num_tokens: usize, amount_to_return: String) -> PromiseOrValue> { + log!("in value_please, amount_to_return = {}", amount_to_return); + let amount: Balance = amount_to_return.parse().expect("Not an integer"); + PromiseOrValue::Value(vec![amount.into(); num_tokens]) + } +} diff --git a/examples/multi-token/tests/workspaces.rs b/examples/multi-token/tests/workspaces.rs new file mode 100644 index 0000000..5064b29 --- /dev/null +++ b/examples/multi-token/tests/workspaces.rs @@ -0,0 +1,82 @@ +use near_primitives::views::FinalExecutionStatus; +use near_sdk::json_types::U128; +use near_sdk::ONE_YOCTO; +use near_units::parse_near; +use workspaces::prelude::*; +use workspaces::{Account, AccountId, Contract, DevNetwork, Network, Worker}; + +async fn register_user( + worker: &Worker, + contract: &Contract, + account_id: &AccountId, +) -> anyhow::Result<()> { + let res = contract + .call(&worker, "storage_deposit") + .args_json((account_id, Option::::None))? + .gas(300_000_000_000_000) + .deposit(near_sdk::env::storage_byte_cost() * 125) + .transact() + .await?; + assert!(matches!(res.status, FinalExecutionStatus::SuccessValue(_))); + + Ok(()) +} + +async fn init( + worker: &Worker, + initial_balance: U128, +) -> anyhow::Result<(Contract, Account, Contract, Contract)> { + let mt_contract = + worker.dev_deploy(include_bytes!("../res/multi_token.wasm").to_vec()).await?; + + let res = mt_contract + .call(&worker, "new_default_meta") + .args_json((mt_contract.id(), initial_balance))? + .gas(300_000_000_000_000) + .transact() + .await?; + assert!(matches!(res.status, FinalExecutionStatus::SuccessValue(_))); + + let defi_contract = worker.dev_deploy(include_bytes!("../res/defi.wasm").to_vec()).await?; + let approval_receiver_contract = worker.dev_deploy(include_bytes!("../res/approval_receiver.wasm").to_vec()).await?; + + let res = defi_contract + .call(&worker, "new") + .args_json((mt_contract.id(),))? + .gas(300_000_000_000_000) + .transact() + .await?; + assert!(matches!(res.status, FinalExecutionStatus::SuccessValue(_))); + + let alice = mt_contract + .as_account() + .create_subaccount(&worker, "alice") + .initial_balance(parse_near!("10 N")) + .transact() + .await? + .into_result()?; + register_user(worker, &mt_contract, alice.id()).await?; + + let res = mt_contract + .call(&worker, "storage_deposit") + .args_json((alice.id(), Option::::None))? + .gas(300_000_000_000_000) + .deposit(near_sdk::env::storage_byte_cost() * 125) + .transact() + .await?; + assert!(matches!(res.status, FinalExecutionStatus::SuccessValue(_))); + + return Ok((mt_contract, alice, defi_contract, approval_receiver_contract)); +} + +#[tokio::test] +async fn test_total_supply() -> anyhow::Result<()> { + let initial_balance = U128::from(parse_near!("10000 N")); + let worker = workspaces::sandbox(); + let (contract, _, _, _) = init(&worker, initial_balance).await?; + + let res = contract.call(&worker, "mt_total_supply").view().await?; + assert_eq!(res.json::()?, initial_balance); + + Ok(()) +} diff --git a/src/multi_token/approval/approval_impl.rs b/src/multi_token/approval/approval_impl.rs index 5982bdd..8a05594 100644 --- a/src/multi_token/approval/approval_impl.rs +++ b/src/multi_token/approval/approval_impl.rs @@ -32,7 +32,7 @@ impl MultiTokenApproval for MultiToken { // Check if caller is authorized unauthorized_assert(&owner_id); - // Get the balance to check if user have enough tokens + // Get the balance to check if user has enough tokens let balance = self.balances_per_token.get(&token_id).unwrap().get(&owner_id).unwrap_or(0); require!(balance >= amount, "Not enough balance to approve"); @@ -46,7 +46,7 @@ impl MultiTokenApproval for MultiToken { expect_approval(next_id.get(&token_id), Entity::Token); let new_approval = Approval { amount, approval_id: current_next_id }; - env::log_str(format!("New approva: {:?}", new_approval).as_str()); + env::log_str(format!("New approval: {:?}", new_approval).as_str()); // Get approvals for this token let approvals = &mut approvals_by_id.get(&token_id).unwrap_or_default(); diff --git a/src/multi_token/approval/mod.rs b/src/multi_token/approval/mod.rs index 982201c..181361f 100644 --- a/src/multi_token/approval/mod.rs +++ b/src/multi_token/approval/mod.rs @@ -19,10 +19,10 @@ pub trait MultiTokenApproval { msg: Option ) -> Option; - /// Revoke an approve for specific token + /// Revoke an approval for specific token fn revoke(&mut self, token: TokenId, account: AccountId); - /// Revoke all approves for a token + /// Revoke all approvals for a token fn revoke_all(&mut self, token: TokenId); /// Check if account have access to transfer tokens diff --git a/src/multi_token/core/core_impl.rs b/src/multi_token/core/core_impl.rs index 00e6112..3a304df 100644 --- a/src/multi_token/core/core_impl.rs +++ b/src/multi_token/core/core_impl.rs @@ -17,27 +17,28 @@ pub const GAS_FOR_MT_TRANSFER_CALL: Gas = Gas(25_000_000_000_000 + GAS_FOR_RESOL const NO_DEPOSIT: Balance = 0; -#[ext_contract(ext_self)] -trait MtResolver { - fn resolve_transfer( +#[ext_contract(ext_receiver)] +pub trait MultiTokenReceiver { + fn mt_on_transfer( &mut self, sender_id: AccountId, - receiver: AccountId, - token_id: TokenId, - approvals: Option>, - ) -> Vector; + previous_owner_ids: Vec, + token_ids: Vec, + amounts: Vec, + msg: String, + ) -> PromiseOrValue>; } -#[ext_contract(ext_receiver)] -pub trait MultiTokenReceiver { - fn on_transfer( +#[ext_contract(ext_self)] +trait MultiTokenResolver { + fn mt_resolve_transfer( &mut self, sender_id: AccountId, - previous_owner_id: AccountId, - token_ids: TokenId, - amounts: Balance, - msg: String, - ) -> PromiseOrValue; + receiver_id: AccountId, + token_ids: Vec, + amounts: Vec, + approvals: Option>>, + ) -> Vec; } /// Implementation of the multi-token standard @@ -203,13 +204,28 @@ impl MultiToken { } } + pub fn internal_batch_transfer( + &mut self, + sender_id: &AccountId, + receiver_id: &AccountId, + token_ids: &Vec, + amounts: &Vec, + approval_ids: Option>>, + ) -> (Vec, Vec>>) { + let approval_ids = approval_ids.unwrap_or(vec![None; token_ids.len()]); + (0..token_ids.len()) + .map(|i| self.internal_transfer(&sender_id, &receiver_id, &token_ids[i], amounts[i], approval_ids[i])) + .unzip() + } + + pub fn internal_transfer( &mut self, sender_id: &AccountId, receiver_id: &AccountId, token_id: &TokenId, - approval_id: Option, amount: Balance, + approval_id: Option, ) -> (AccountId, Option>) { // Safety checks require!(sender_id != receiver_id); @@ -220,7 +236,7 @@ impl MultiToken { let approvals = self .approvals_by_id .as_mut() - .and_then(|by_id| by_id.remove(token_id)); + .and_then(|by_id| by_id.remove(token_id)); // Won't this clear all existing approvals for the token? Need to test. let owner_id = if sender_id != &owner_of_token { let approved_accounts = approvals.as_ref().expect("Unauthorized"); @@ -311,12 +327,13 @@ impl MultiToken { } // Increment next id of the token. Panic if it's overflowing u64::MAX - self.next_token_id + self.next_token_id = self.next_token_id .checked_add(1) .expect("u64 overflow, cannot mint any more tokens"); let token_id: TokenId = self.next_token_id.to_string(); + // If contract uses approval management create new LookupMap for approvals self.next_approval_id_by_id .as_mut() @@ -334,7 +351,7 @@ impl MultiToken { .and_then(|by_id| by_id.insert(&token_id, &token_metadata.clone().unwrap())); // Insert new supply - self.total_supply.insert(&token_id, &u128::MAX); + self.total_supply.insert(&token_id, &u128::MAX); // Total supply is always max? // Insert new balance let mut new_set: LookupMap = LookupMap::new(StorageKey::BalancesInner { @@ -369,7 +386,6 @@ impl MultiToken { token_id, owner_id, supply: u128::MAX, - balances: HashMap::new(), metadata: token_metadata, approvals: approved_account_ids, next_approval_id: Some(0), @@ -407,20 +423,38 @@ impl MultiToken { } impl MultiTokenCore for MultiToken { - fn transfer( + + fn mt_transfer( &mut self, receiver_id: AccountId, token_id: TokenId, - amount: Balance, + amount: U128, approval: Option, + memo: Option + ) { + self.mt_batch_transfer(receiver_id, vec![token_id], vec![amount], Some(vec![approval]), memo); + } + + fn mt_batch_transfer( + &mut self, + receiver_id: AccountId, + token_ids: Vec, + amounts: Vec, + approval_ids: Option>>, + memo: Option ) { assert_one_yocto(); let sender_id = env::predecessor_account_id(); env::log_str(format!("Predecessor {}", sender_id).as_str()); - self.internal_transfer(&sender_id, &receiver_id, &token_id, approval, amount); + require!(token_ids.len() == amounts.len()); + require!(token_ids.len() > 0); + + let amounts: Vec = amounts.iter().map(|x| x.0).collect(); + + self.internal_batch_transfer(&sender_id, &receiver_id, &token_ids, &amounts, approval_ids); } - fn transfer_call( + fn mt_transfer_call( &mut self, receiver_id: AccountId, token_id: TokenId, @@ -436,23 +470,24 @@ impl MultiTokenCore for MultiToken { let sender_id = env::predecessor_account_id(); let (old_owner, old_approvals) = - self.internal_transfer(&sender_id, &receiver_id, &token_id, approval_id, amount); + self.internal_transfer(&sender_id, &receiver_id, &token_id, amount, approval_id); - ext_receiver::on_transfer( + ext_receiver::mt_on_transfer( sender_id, - old_owner.clone(), - token_id.clone(), - amount, + vec![old_owner.clone()], + vec![token_id.clone()], + vec![amount.into()], msg, receiver_id.clone(), NO_DEPOSIT, env::prepaid_gas() - GAS_FOR_MT_TRANSFER_CALL, ) - .then(ext_self::resolve_transfer( + .then(ext_self::mt_resolve_transfer( old_owner, receiver_id, - token_id, - old_approvals, + vec![token_id], + vec![amount.into()], + None, // TODO: use old_approvals to restore approvals in case of failure. env::current_account_id(), NO_DEPOSIT, GAS_FOR_RESOLVE_TRANSFER, @@ -460,23 +495,85 @@ impl MultiTokenCore for MultiToken { .into() } - fn approval_for_all(&mut self, owner: AccountId, approved: bool) { - todo!() + fn mt_batch_transfer_call( + &mut self, + receiver_id: AccountId, + token_ids: Vec, + amounts: Vec, + memo: Option, + msg: String, + ) -> PromiseOrValue> { + // WIP: Needs some refactoring to get batch approvals working. + PromiseOrValue::Value(vec![]) + // assert_one_yocto(); + // require!( + // env::prepaid_gas() > GAS_FOR_MT_TRANSFER_CALL + GAS_FOR_RESOLVE_TRANSFER, + // "GAS!GAS!GAS! I gonna to step on the gas" + // ); + // let sender_id = env::predecessor_account_id(); + + // let (old_owner, old_approvals) = + // self.internal_batch_transfer(&sender_id, &receiver_id, &token_ids, &amounts, &None); + + // ext_receiver::mt_on_transfer( + // // function specific args: + // sender_id, + // old_owner.clone(), + // token_ids.clone(), + // amounts, + // msg, + // // generic args for all cross-contract calls: + // receiver_id.clone(), // receiver contract account id + // NO_DEPOSIT, // no attached NEAR + // env::prepaid_gas() - GAS_FOR_MT_TRANSFER_CALL, // some attached gas + // ) + // .then(ext_self::mt_resolve_transfer( + // old_owner, + // receiver_id, + // token_ids, + // amounts, + // old_approvals, + + // env::current_account_id(), + // NO_DEPOSIT, + // GAS_FOR_RESOLVE_TRANSFER, + // )) + // .into() } - fn balance_of(&self, owner: AccountId, id: Vec) -> Vec { - self.balances_per_token + fn mt_token(&self, token_ids: Vec) -> Vec> { + token_ids .iter() - .filter(|(token_id, _)| id.contains(token_id)) - .map(|(_, balances)| { - balances - .get(&owner) - .expect("User does not have account in of the tokens") - }) + .map(|token_id| self.internal_get_token_metadata(&token_id)) .collect() } - fn token(&self, token_id: TokenId) -> Option { + fn mt_balance_of(&self, account_id: AccountId, token_id: TokenId) -> U128 { + self.internal_balance_of(&account_id, &token_id) + } + + fn mt_batch_balance_of(&self, account_id: AccountId, token_ids: Vec) -> Vec { + token_ids + .iter() + .map(|token_id| self.internal_balance_of(&account_id, &token_id)) + .collect() + } + + fn mt_supply(&self, token_id: TokenId) -> Option { + self.internal_supply(&token_id) + } + + fn mt_batch_supply(&self, token_ids: Vec) -> Vec> { + token_ids + .iter() + .map(|token_id| self.internal_supply(&token_id)) + .collect() + } +} + +impl MultiToken { + + fn internal_get_token_metadata(&self, token_id: &TokenId) -> Option { let metadata = if let Some(metadata_by_id) = &self.token_metadata_by_id { metadata_by_id.get(&token_id) } else { @@ -489,48 +586,79 @@ impl MultiTokenCore for MultiToken { .approvals_by_id .as_ref() .and_then(|by_id| by_id.get(&token_id).or_else(|| Some(HashMap::new()))); - let balances = self.balances_per_token.get(&token_id)?; Some(Token { - token_id, + token_id: token_id.clone(), owner_id, supply, - balances: HashMap::new(), metadata, approvals: approved_accounts, next_approval_id, }) } -} -impl MultiToken { - pub fn internal_resolve_transfer( + fn internal_balance_of(&self, account_id: &AccountId, token_id: &TokenId) -> U128 { + let token_balances_by_user = self.balances_per_token.get(token_id).expect("Token not found."); + token_balances_by_user.get(account_id).unwrap_or(0).into() + } + + fn internal_supply(&self, token_id: &TokenId) -> Option { + self.total_supply.get(token_id).map(u128::into) + } + + pub fn internal_resolve_transfers( &mut self, sender_id: &AccountId, receiver: AccountId, - token_id: TokenId, - amount: U128, - ) -> (Balance, Balance) { - let amount: Balance = amount.into(); - - let unused = match env::promise_result(0) { + token_ids: Vec, + amounts: Vec, + approvals: Option>> + ) -> (Vec, Vec) { + + // on_transfer will have returned a promise containing what was unused (refunded) + // by the receiver contract. + let unused: Vec = match env::promise_result(0) { PromiseResult::NotReady => env::abort(), - PromiseResult::Successful(value) => { - if let Ok(unused) = near_sdk::serde_json::from_slice::(&value) { - std::cmp::min(amount, unused.0) + PromiseResult::Successful(values) => { + if let Ok(unused) = near_sdk::serde_json::from_slice::>(&values) { + // we can't be refunded by more than what we sent over + (0..amounts.len()).map(|i| std::cmp::min(amounts[i].into(), unused[i].0).into()).collect() } else { - amount + amounts.clone() } } - PromiseResult::Failed => 0, + // TODO: is this correct behavior? Under what circumstance does promise fail? + PromiseResult::Failed => vec![0.into(); amounts.len()], }; + (0..token_ids.len()).map(|i| self.internal_resolve_single_transfer( + sender_id, + receiver.clone(), + token_ids[i].clone(), + amounts[i].into(), + unused[i].into(), + )).unzip() + } + + pub fn internal_resolve_single_transfer( + &mut self, + sender_id: &AccountId, + receiver: AccountId, + token_id: TokenId, + amount: u128, + unused: u128, + ) -> (Balance, Balance) { + let amount: Balance = amount.into(); + // All this `.get()` will not fail since it would fail before it gets to this call if unused > 0 { + // Whatever was unused gets returned to the original owner. let mut balances = self.balances_per_token.get(&token_id).unwrap(); let receiver_balance = balances.get(&receiver).unwrap_or(0); if receiver_balance > 0 { + // If the receiver doesn't have enough funds to do the + // full refund, just refund all that we can. let refund = std::cmp::min(receiver_balance, unused); balances.insert(&receiver, &(receiver_balance - refund)); @@ -550,15 +678,18 @@ impl MultiToken { } impl MultiTokenResolver for MultiToken { - fn resolve_transfer( + fn mt_resolve_transfer( &mut self, sender_id: AccountId, - receiver: AccountId, - token_id: TokenId, - amount: U128, - ) -> U128 { - self.internal_resolve_transfer(&sender_id, receiver, token_id, amount) + receiver_id: AccountId, + token_ids: Vec, + amounts: Vec, + approvals: Option>>, + ) -> Vec { + self.internal_resolve_transfers(&sender_id, receiver_id, token_ids, amounts, approvals) .0 - .into() + .iter() + .map(|&x| x.into()) + .collect() } } diff --git a/src/multi_token/core/mod.rs b/src/multi_token/core/mod.rs index 4c36db1..f622558 100644 --- a/src/multi_token/core/mod.rs +++ b/src/multi_token/core/mod.rs @@ -13,28 +13,42 @@ pub use self::resolver::*; use crate::multi_token::token::TokenId; use near_sdk::{AccountId, Balance, PromiseOrValue}; +use near_sdk::json_types::U128; use super::token::Token; /// Describes functionality according to this - https://eips.ethereum.org/EIPS/eip-1155 /// And this - pub trait MultiTokenCore { + /// Make a single transfer /// /// # Arguments /// - /// * `receiver_id`: Receiver of tokens - /// * `token_id`: ID of token to send from - /// * `amount`: How much to send + /// * `receiver_id`: the valid NEAR account receiving the token + /// * `token_id`: ID of the token to transfer + /// * `amount`: the number of tokens to transfer /// /// returns: () /// - fn transfer( + fn mt_transfer( &mut self, receiver_id: AccountId, token_id: TokenId, - amount: Balance, + amount: U128, approval: Option, + memo: Option + ); + + // Make a batch transfer + // Behaves similar + fn mt_batch_transfer( + &mut self, + receiver_id: AccountId, + token_ids: Vec, + amounts: Vec, + approval_ids: Option>>, + memo: Option ); /// Transfer MT and call a method on receiver contract. A successful @@ -52,26 +66,33 @@ pub trait MultiTokenCore { /// /// returns: PromiseOrValue /// - fn transfer_call( + fn mt_transfer_call( &mut self, receiver_id: AccountId, token_id: TokenId, amount: Balance, approval_id: Option, - msg: String, + msg: String ) -> PromiseOrValue; - fn approval_for_all(&mut self, owner: AccountId, approved: bool); + fn mt_batch_transfer_call( + &mut self, + receiver_id: AccountId, + token_ids: Vec, + amounts: Vec, + memo: Option, + msg: String + ) -> PromiseOrValue>; - /// Get balance of user in specified tokens - /// - /// # Arguments - /// - /// * `owner`: Account to check - /// # `id`: Vector of token IDs - fn balance_of(&self, owner: AccountId, id: Vec) -> Vec; + // View Methods + fn mt_token(&self, token_ids: Vec) -> Vec>; + + fn mt_balance_of(&self, account_id: AccountId, token_id: TokenId) -> U128; + + fn mt_batch_balance_of(&self, account_id: AccountId, token_ids: Vec) -> Vec; - /// Get all possible info about token - fn token(&self, token_id: TokenId) -> Option; + fn mt_supply(&self, token_id: TokenId) -> Option; + + fn mt_batch_supply(&self, token_ids: Vec) -> Vec>; } diff --git a/src/multi_token/core/receiver.rs b/src/multi_token/core/receiver.rs index ba48dee..0f4c36c 100644 --- a/src/multi_token/core/receiver.rs +++ b/src/multi_token/core/receiver.rs @@ -16,22 +16,22 @@ pub trait MultiTokenReceiver { /// /// ## Arguments: /// * `sender_id`: the sender of `transfer_call` - /// * `previous_owner_id`: the account that owned the NFT prior to it being + /// * `previous_owner_ids`: the accounts that owned the tokens prior to them being /// transferred to this contract, which can differ from `sender_id` if using /// Approval Management extension - /// * `token_ids`: the `token_id` argument given to `transfer_call` - /// * `amounts`: the `token_ids` argument given to `transfer_call` + /// * `token_ids`: the `token_ids` argument given to `transfer_call` + /// * `amounts`: the `amounts` argument given to `transfer_call` /// * `msg`: information necessary for this contract to know how to process the /// request. This may include method names and/or arguments. /// /// Returns the number of unused tokens in integer form. For instance, if `amounts` /// is `[10]` but only 9 are needed, it will return `[1]`. - fn on_transfer( + fn mt_on_transfer( &mut self, sender_id: AccountId, - previous_owner_id: AccountId, - token_id: TokenId, - amounts: U128, + previous_owner_ids: Vec, + token_ids: Vec, + amounts: Vec, msg: String, - ) -> PromiseOrValue; + ) -> PromiseOrValue>; } diff --git a/src/multi_token/core/resolver.rs b/src/multi_token/core/resolver.rs index bd9f69d..3bca448 100644 --- a/src/multi_token/core/resolver.rs +++ b/src/multi_token/core/resolver.rs @@ -1,14 +1,14 @@ -use crate::multi_token::token::TokenId; +use crate::multi_token::token::{Approval, TokenId}; use near_sdk::json_types::U128; use near_sdk::AccountId; /// `resolve_transfer` will be called after `on_transfer` pub trait MultiTokenResolver { - /// Finalizes chain of cross-contract calls that started from `transfer_call` + /// Finalizes chain of cross-contract calls that started from `mt_transfer_call` /// /// Flow: /// - /// 1. Sender calls `transfer_call` on MT contract + /// 1. Sender calls `mt_transfer_call` on MT contract /// 2. MT contract transfers tokens from sender to receiver /// 3. MT contract calls `on_transfer` on receiver contract /// 4+. [receiver may make cross-contract calls] @@ -35,11 +35,12 @@ pub trait MultiTokenResolver { /// but `receiver_id` only uses 80, `on_transfer` will resolve with `["20"]`, and `resolve_transfer` /// will return `[80]`. - fn resolve_transfer( + fn mt_resolve_transfer( &mut self, sender_id: AccountId, - receiver: AccountId, - token_id: TokenId, - amount: U128, - ) -> U128; + receiver_id: AccountId, + token_ids: Vec, + amounts: Vec, + approvals: Option>>, + ) -> Vec; } diff --git a/src/multi_token/enumeration/enumeration_impl.rs b/src/multi_token/enumeration/enumeration_impl.rs index 392253d..0a8976c 100644 --- a/src/multi_token/enumeration/enumeration_impl.rs +++ b/src/multi_token/enumeration/enumeration_impl.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use near_sdk::{AccountId, require}; use crate::multi_token::{core::MultiToken, token::{Token, TokenId}}; @@ -15,7 +13,7 @@ impl MultiToken { let approvals = self.approvals_by_id.as_ref().unwrap().get(&token_id); let next_approval_id = self.next_approval_id_by_id.as_ref().unwrap().get(&token_id); - Token { token_id, owner_id, metadata, approvals, supply, balances: HashMap::new(), next_approval_id } + Token { token_id, owner_id, metadata, approvals, supply, next_approval_id } } } diff --git a/src/multi_token/macros.rs b/src/multi_token/macros.rs index 19a0656..5de5660 100644 --- a/src/multi_token/macros.rs +++ b/src/multi_token/macros.rs @@ -9,56 +9,91 @@ macro_rules! impl_multi_token_core { #[near_bindgen] impl MultiTokenCore for $contract { #[payable] - fn transfer( + fn mt_transfer( &mut self, receiver_id: AccountId, token_id: TokenId, - amount: Balance, + amount: U128, approval: Option, + memo: Option ) { - self.$token.transfer(receiver_id, token_id, amount, approval) + self.$token.mt_transfer(receiver_id, token_id, amount, approval, memo) } #[payable] - fn transfer_call( + fn mt_batch_transfer( + &mut self, + receiver_id: AccountId, + token_ids: Vec, + amounts: Vec, + approval_ids: Option>>, + memo: Option, + ) { + self.$token.mt_batch_transfer(receiver_id, token_ids, amounts, approval_ids, memo) + } + + #[payable] + fn mt_transfer_call( &mut self, receiver_id: AccountId, token_id: TokenId, amount: Balance, approval_id: Option, - msg: String, + msg: String ) -> PromiseOrValue { - self.$token.transfer_call(receiver_id, token_id, amount, approval_id, msg) + self.$token.mt_transfer_call(receiver_id, token_id, amount, approval_id, msg) } - fn token(&self, token_id: TokenId) -> Option { - self.$token.token(token_id) + #[payable] + fn mt_batch_transfer_call( + &mut self, + receiver_id: AccountId, + token_ids: Vec, + amounts: Vec, + memo: Option, + msg: String + ) -> PromiseOrValue> { + self.$token.mt_batch_transfer_call(receiver_id, token_ids, amounts, memo, msg) } - - fn balance_of(&self, owner: AccountId, id: Vec) -> Vec { - self.$token.balance_of(owner, id) - } - - fn approval_for_all(&mut self, owner_id: AccountId, approved: bool) { todo!() } - } + fn mt_token(&self, token_ids: Vec) -> Vec> { + self.$token.mt_token(token_ids) + } + + fn mt_balance_of(&self, account_id: AccountId, token_id: TokenId) -> U128 { + self.$token.mt_balance_of(account_id, token_id) + } + + fn mt_batch_balance_of(&self, account_id: AccountId, token_ids: Vec) -> Vec { + self.$token.mt_batch_balance_of(account_id, token_ids) + } + fn mt_supply(&self, token_id: TokenId) -> Option { + self.$token.mt_supply(token_id) + } + + fn mt_batch_supply(&self, token_ids: Vec) -> Vec> { + self.$token.mt_batch_supply(token_ids) + } + } #[near_bindgen] impl MultiTokenResolver for $contract { #[private] - fn resolve_transfer( + fn mt_resolve_transfer( &mut self, sender_id: AccountId, receiver_id: AccountId, - token_id: TokenId, - amount: U128 - ) -> U128 { - self.$token.resolve_transfer( + token_ids: Vec, + amounts: Vec, + approvals: Option>>, + ) -> Vec { + self.$token.mt_resolve_transfer( sender_id, receiver_id, - token_id, - amount + token_ids, + amounts, + approvals ) } } diff --git a/src/multi_token/token.rs b/src/multi_token/token.rs index 3ab72b3..611541a 100644 --- a/src/multi_token/token.rs +++ b/src/multi_token/token.rs @@ -21,7 +21,6 @@ pub struct Token { pub owner_id: AccountId, /// Total amount generated pub supply: u128, - pub balances: HashMap, pub metadata: Option, pub approvals: Option>, pub next_approval_id: Option,