diff --git a/Cargo.lock b/Cargo.lock index 2c9ef33a..68ed3967 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7366,6 +7366,7 @@ dependencies = [ "log", "pallet-assets", "pallet-balances", + "pallet-nfts 0.1.0", "parity-scale-codec", "pop-chain-extension", "scale-info", @@ -8239,39 +8240,39 @@ dependencies = [ [[package]] name = "pallet-nfts" -version = "30.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1cd476809de3840e19091a083d5a79178af1f108ad489706e1f9e04c8836a4" +version = "0.1.0" dependencies = [ "enumflags2", "frame-benchmarking", "frame-support", "frame-system", "log", + "pallet-balances", "parity-scale-codec", "scale-info", "sp-core", "sp-io", + "sp-keystore", "sp-runtime", - "sp-std", ] [[package]] name = "pallet-nfts" -version = "31.0.0" +version = "30.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1cd476809de3840e19091a083d5a79178af1f108ad489706e1f9e04c8836a4" dependencies = [ "enumflags2", "frame-benchmarking", "frame-support", "frame-system", "log", - "pallet-balances", "parity-scale-codec", "scale-info", "sp-core", "sp-io", - "sp-keystore", "sp-runtime", + "sp-std", ] [[package]] @@ -10856,7 +10857,7 @@ dependencies = [ "pallet-message-queue", "pallet-multisig", "pallet-nft-fractionalization", - "pallet-nfts 31.0.0", + "pallet-nfts 0.1.0", "pallet-nfts-runtime-api", "pallet-preimage", "pallet-proxy", @@ -11000,7 +11001,7 @@ dependencies = [ "pallet-message-queue", "pallet-multisig", "pallet-nft-fractionalization", - "pallet-nfts 31.0.0", + "pallet-nfts 0.1.0", "pallet-nfts-runtime-api", "pallet-preimage", "pallet-proxy", diff --git a/pallets/api/Cargo.toml b/pallets/api/Cargo.toml index 55a00789..31ccc076 100644 --- a/pallets/api/Cargo.toml +++ b/pallets/api/Cargo.toml @@ -22,6 +22,7 @@ frame-benchmarking.workspace = true frame-support.workspace = true frame-system.workspace = true pallet-assets.workspace = true +pallet-nfts.workspace = true sp-runtime.workspace = true sp-std.workspace = true @@ -37,6 +38,7 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", "pallet-assets/runtime-benchmarks", + "pallet-nfts/runtime-benchmarks", "pop-chain-extension/runtime-benchmarks", "sp-runtime/runtime-benchmarks", ] @@ -47,6 +49,7 @@ std = [ "frame-system/std", "pallet-assets/std", "pallet-balances/std", + "pallet-nfts/std", "pop-chain-extension/std", "scale-info/std", "sp-core/std", @@ -57,5 +60,6 @@ std = [ try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", + "pallet-nfts/try-runtime", "sp-runtime/try-runtime", ] diff --git a/pallets/api/src/fungibles/tests.rs b/pallets/api/src/fungibles/tests.rs index f5c560bb..6e181a6f 100644 --- a/pallets/api/src/fungibles/tests.rs +++ b/pallets/api/src/fungibles/tests.rs @@ -83,17 +83,21 @@ fn transfer_works() { let to = BOB; for origin in vec![root(), none()] { - assert_noop!(Fungibles::transfer(origin, token, to, value), BadOrigin); + assert_noop!(Fungibles::transfer(origin, token, account(to), value), BadOrigin); } // Check error works for `Assets::transfer_keep_alive()`. - assert_noop!(Fungibles::transfer(signed(from), token, to, value), AssetsError::Unknown); + assert_noop!( + Fungibles::transfer(signed(from), token, account(to), value), + AssetsError::Unknown + ); assets::create_and_mint_to(from, token, from, value * 2); - let balance_before_transfer = Assets::balance(token, &to); - assert_ok!(Fungibles::transfer(signed(from), token, to, value)); - let balance_after_transfer = Assets::balance(token, &to); + let balance_before_transfer = Assets::balance(token, &account(to)); + assert_ok!(Fungibles::transfer(signed(from), token, account(to), value)); + let balance_after_transfer = Assets::balance(token, &account(to)); assert_eq!(balance_after_transfer, balance_before_transfer + value); System::assert_last_event( - Event::Transfer { token, from: Some(from), to: Some(to), value }.into(), + Event::Transfer { token, from: Some(account(from)), to: Some(account(to)), value } + .into(), ); }); } @@ -108,26 +112,36 @@ fn transfer_from_works() { let spender = CHARLIE; for origin in vec![root(), none()] { - assert_noop!(Fungibles::transfer_from(origin, token, from, to, value), BadOrigin); + assert_noop!( + Fungibles::transfer_from(origin, token, account(from), account(to), value), + BadOrigin + ); } // Check error works for `Assets::transfer_approved()`. assert_noop!( - Fungibles::transfer_from(signed(spender), token, from, to, value), + Fungibles::transfer_from(signed(spender), token, account(from), account(to), value), AssetsError::Unknown ); // Approve `spender` to transfer up to `value`. assets::create_mint_and_approve(spender, token, from, value * 2, spender, value); // Successfully call transfer from. - let from_balance_before_transfer = Assets::balance(token, &from); - let to_balance_before_transfer = Assets::balance(token, &to); - assert_ok!(Fungibles::transfer_from(signed(spender), token, from, to, value)); - let from_balance_after_transfer = Assets::balance(token, &from); - let to_balance_after_transfer = Assets::balance(token, &to); + let from_balance_before_transfer = Assets::balance(token, &account(from)); + let to_balance_before_transfer = Assets::balance(token, &account(to)); + assert_ok!(Fungibles::transfer_from( + signed(spender), + token, + account(from), + account(to), + value + )); + let from_balance_after_transfer = Assets::balance(token, &account(from)); + let to_balance_after_transfer = Assets::balance(token, &account(to)); // Check that `to` has received the `value` tokens from `from`. assert_eq!(to_balance_after_transfer, to_balance_before_transfer + value); assert_eq!(from_balance_after_transfer, from_balance_before_transfer - value); System::assert_last_event( - Event::Transfer { token, from: Some(from), to: Some(to), value }.into(), + Event::Transfer { token, from: Some(account(from)), to: Some(account(to)), value } + .into(), ); }); } @@ -144,7 +158,7 @@ mod approve { for origin in vec![root(), none()] { assert_noop!( - Fungibles::approve(origin, token, spender, value), + Fungibles::approve(origin, token, account(spender), value), BadOrigin.with_weight(WeightInfo::approve(0, 0)) ); } @@ -161,20 +175,20 @@ mod approve { for origin in vec![root(), none()] { assert_noop!( - Fungibles::approve(origin, token, spender, value), + Fungibles::approve(origin, token, account(spender), value), BadOrigin.with_weight(WeightInfo::approve(0, 0)) ); } // Check error works for `Assets::approve_transfer()` in `Greater` match arm. assert_noop!( - Fungibles::approve(signed(owner), token, spender, value), + Fungibles::approve(signed(owner), token, account(spender), value), AssetsError::Unknown.with_weight(WeightInfo::approve(1, 0)) ); assets::create_mint_and_approve(owner, token, owner, value, spender, value); // Check error works for `Assets::cancel_approval()` in `Less` match arm. assert_ok!(Assets::freeze_asset(signed(owner), token)); assert_noop!( - Fungibles::approve(signed(owner), token, spender, value / 2), + Fungibles::approve(signed(owner), token, account(spender), value / 2), AssetsError::AssetNotLive.with_weight(WeightInfo::approve(0, 1)) ); assert_ok!(Assets::thaw_asset(signed(owner), token)); @@ -193,38 +207,61 @@ mod approve { // Approves a value to spend that is higher than the current allowance. assets::create_and_mint_to(owner, token, owner, value); - assert_eq!(Assets::allowance(token, &owner, &spender), 0); + assert_eq!(Assets::allowance(token, &account(owner), &account(spender)), 0); assert_eq!( - Fungibles::approve(signed(owner), token, spender, value), + Fungibles::approve(signed(owner), token, account(spender), value), Ok(Some(WeightInfo::approve(1, 0)).into()) ); - assert_eq!(Assets::allowance(token, &owner, &spender), value); - System::assert_last_event(Event::Approval { token, owner, spender, value }.into()); + assert_eq!(Assets::allowance(token, &account(owner), &account(spender)), value); + System::assert_last_event( + Event::Approval { token, owner: account(owner), spender: account(spender), value } + .into(), + ); // Approves a value to spend that is lower than the current allowance. assert_eq!( - Fungibles::approve(signed(owner), token, spender, value / 2), + Fungibles::approve(signed(owner), token, account(spender), value / 2), Ok(Some(WeightInfo::approve(1, 1)).into()) ); - assert_eq!(Assets::allowance(token, &owner, &spender), value / 2); + assert_eq!(Assets::allowance(token, &account(owner), &account(spender)), value / 2); System::assert_last_event( - Event::Approval { token, owner, spender, value: value / 2 }.into(), + Event::Approval { + token, + owner: account(owner), + spender: account(spender), + value: value / 2, + } + .into(), ); // Approves a value to spend that is equal to the current allowance. assert_eq!( - Fungibles::approve(signed(owner), token, spender, value / 2), + Fungibles::approve(signed(owner), token, account(spender), value / 2), Ok(Some(WeightInfo::approve(0, 0)).into()) ); - assert_eq!(Assets::allowance(token, &owner, &spender), value / 2); + assert_eq!(Assets::allowance(token, &account(owner), &account(spender)), value / 2); System::assert_last_event( - Event::Approval { token, owner, spender, value: value / 2 }.into(), + Event::Approval { + token, + owner: account(owner), + spender: account(spender), + value: value / 2, + } + .into(), ); // Sets allowance to zero. assert_eq!( - Fungibles::approve(signed(owner), token, spender, 0), + Fungibles::approve(signed(owner), token, account(spender), 0), Ok(Some(WeightInfo::approve(0, 1)).into()) ); - assert_eq!(Assets::allowance(token, &owner, &spender), 0); - System::assert_last_event(Event::Approval { token, owner, spender, value: 0 }.into()); + assert_eq!(Assets::allowance(token, &account(owner), &account(spender)), 0); + System::assert_last_event( + Event::Approval { + token, + owner: account(owner), + spender: account(spender), + value: 0, + } + .into(), + ); }); } } @@ -239,25 +276,34 @@ fn increase_allowance_works() { for origin in vec![root(), none()] { assert_noop!( - Fungibles::increase_allowance(origin, token, spender, value), + Fungibles::increase_allowance(origin, token, account(spender), value), BadOrigin.with_weight(WeightInfo::approve(0, 0)) ); } // Check error works for `Assets::approve_transfer()`. assert_noop!( - Fungibles::increase_allowance(signed(owner), token, spender, value), + Fungibles::increase_allowance(signed(owner), token, account(spender), value), AssetsError::Unknown.with_weight(AssetsWeightInfo::approve_transfer()) ); assets::create_and_mint_to(owner, token, owner, value); - assert_eq!(0, Assets::allowance(token, &owner, &spender)); - assert_ok!(Fungibles::increase_allowance(signed(owner), token, spender, value)); - assert_eq!(Assets::allowance(token, &owner, &spender), value); - System::assert_last_event(Event::Approval { token, owner, spender, value }.into()); + assert_eq!(0, Assets::allowance(token, &account(owner), &account(spender))); + assert_ok!(Fungibles::increase_allowance(signed(owner), token, account(spender), value)); + assert_eq!(Assets::allowance(token, &account(owner), &account(spender)), value); + System::assert_last_event( + Event::Approval { token, owner: account(owner), spender: account(spender), value } + .into(), + ); // Additive. - assert_ok!(Fungibles::increase_allowance(signed(owner), token, spender, value)); - assert_eq!(Assets::allowance(token, &owner, &spender), value * 2); + assert_ok!(Fungibles::increase_allowance(signed(owner), token, account(spender), value)); + assert_eq!(Assets::allowance(token, &account(owner), &account(spender)), value * 2); System::assert_last_event( - Event::Approval { token, owner, spender, value: value * 2 }.into(), + Event::Approval { + token, + owner: account(owner), + spender: account(spender), + value: value * 2, + } + .into(), ); }); } @@ -272,40 +318,46 @@ fn decrease_allowance_works() { for origin in vec![root(), none()] { assert_noop!( - Fungibles::decrease_allowance(origin, token, spender, 0), + Fungibles::decrease_allowance(origin, token, account(spender), 0), BadOrigin.with_weight(WeightInfo::approve(0, 0)) ); } assets::create_mint_and_approve(owner, token, owner, value, spender, value); - assert_eq!(Assets::allowance(token, &owner, &spender), value); + assert_eq!(Assets::allowance(token, &account(owner), &account(spender)), value); // Check error works for `Assets::cancel_approval()`. No error test for `approve_transfer` // because it is not possible. assert_ok!(Assets::freeze_asset(signed(owner), token)); assert_noop!( - Fungibles::decrease_allowance(signed(owner), token, spender, value / 2), + Fungibles::decrease_allowance(signed(owner), token, account(spender), value / 2), AssetsError::AssetNotLive.with_weight(WeightInfo::approve(0, 1)) ); assert_ok!(Assets::thaw_asset(signed(owner), token)); // Owner balance is not changed if decreased by zero. assert_eq!( - Fungibles::decrease_allowance(signed(owner), token, spender, 0), + Fungibles::decrease_allowance(signed(owner), token, account(spender), 0), Ok(Some(WeightInfo::approve(0, 0)).into()) ); - assert_eq!(Assets::allowance(token, &owner, &spender), value); + assert_eq!(Assets::allowance(token, &account(owner), &account(spender)), value); // "Unapproved" error is returned if the current allowance is less than amount to decrease // with. assert_noop!( - Fungibles::decrease_allowance(signed(owner), token, spender, value * 2), + Fungibles::decrease_allowance(signed(owner), token, account(spender), value * 2), AssetsError::Unapproved ); // Decrease allowance successfully. assert_eq!( - Fungibles::decrease_allowance(signed(owner), token, spender, value / 2), + Fungibles::decrease_allowance(signed(owner), token, account(spender), value / 2), Ok(Some(WeightInfo::approve(1, 1)).into()) ); - assert_eq!(Assets::allowance(token, &owner, &spender), value / 2); + assert_eq!(Assets::allowance(token, &account(owner), &account(spender)), value / 2); System::assert_last_event( - Event::Approval { token, owner, spender, value: value / 2 }.into(), + Event::Approval { + token, + owner: account(owner), + spender: account(spender), + value: value / 2, + } + .into(), ); }); } @@ -318,14 +370,19 @@ fn create_works() { let admin = ALICE; for origin in vec![root(), none()] { - assert_noop!(Fungibles::create(origin, id, admin, 100), BadOrigin); + assert_noop!(Fungibles::create(origin, id, account(admin), 100), BadOrigin); } assert!(!Assets::asset_exists(id)); - assert_ok!(Fungibles::create(signed(creator), id, admin, 100)); + assert_ok!(Fungibles::create(signed(creator), id, account(admin), 100)); assert!(Assets::asset_exists(id)); - System::assert_last_event(Event::Created { id, creator, admin }.into()); + System::assert_last_event( + Event::Created { id, creator: account(creator), admin: account(admin) }.into(), + ); // Check error works for `Assets::create()`. - assert_noop!(Fungibles::create(signed(creator), id, admin, 100), AssetsError::InUse); + assert_noop!( + Fungibles::create(signed(creator), id, account(admin), 100), + AssetsError::InUse + ); }); } @@ -336,11 +393,11 @@ fn start_destroy_works() { // Check error works for `Assets::start_destroy()`. assert_noop!(Fungibles::start_destroy(signed(ALICE), token), AssetsError::Unknown); - assert_ok!(Assets::create(signed(ALICE), token, ALICE, 1)); + assert_ok!(Assets::create(signed(ALICE), token, account(ALICE), 1)); assert_ok!(Fungibles::start_destroy(signed(ALICE), token)); // Check that the token is not live after starting the destroy process. assert_noop!( - Assets::mint(signed(ALICE), token, ALICE, 10 * UNIT), + Assets::mint(signed(ALICE), token, account(ALICE), 10 * UNIT), AssetsError::AssetNotLive ); }); @@ -359,7 +416,7 @@ fn set_metadata_works() { Fungibles::set_metadata(signed(ALICE), token, name.clone(), symbol.clone(), decimals), AssetsError::Unknown ); - assert_ok!(Assets::create(signed(ALICE), token, ALICE, 1)); + assert_ok!(Assets::create(signed(ALICE), token, account(ALICE), 1)); assert_ok!(Fungibles::set_metadata( signed(ALICE), token, @@ -398,16 +455,16 @@ fn mint_works() { // Check error works for `Assets::mint()`. assert_noop!( - Fungibles::mint(signed(from), token, to, value), + Fungibles::mint(signed(from), token, account(to), value), sp_runtime::TokenError::UnknownAsset ); - assert_ok!(Assets::create(signed(from), token, from, 1)); - let balance_before_mint = Assets::balance(token, &to); - assert_ok!(Fungibles::mint(signed(from), token, to, value)); - let balance_after_mint = Assets::balance(token, &to); + assert_ok!(Assets::create(signed(from), token, account(from), 1)); + let balance_before_mint = Assets::balance(token, &account(to)); + assert_ok!(Fungibles::mint(signed(from), token, account(to), value)); + let balance_after_mint = Assets::balance(token, &account(to)); assert_eq!(balance_after_mint, balance_before_mint + value); System::assert_last_event( - Event::Transfer { token, from: None, to: Some(to), value }.into(), + Event::Transfer { token, from: None, to: Some(account(to)), value }.into(), ); }); } @@ -423,27 +480,30 @@ fn burn_works() { // "BalanceLow" error is returned if token is not created. assert_noop!( - Fungibles::burn(signed(owner), token, from, value), + Fungibles::burn(signed(owner), token, account(from), value), AssetsError::BalanceLow.with_weight(WeightInfo::balance_of()) ); assets::create_and_mint_to(owner, token, from, total_supply); assert_eq!(Assets::total_supply(TOKEN), total_supply); // Check error works for `Assets::burn()`. assert_ok!(Assets::freeze_asset(signed(owner), token)); - assert_noop!(Fungibles::burn(signed(owner), token, from, value), AssetsError::AssetNotLive); + assert_noop!( + Fungibles::burn(signed(owner), token, account(from), value), + AssetsError::AssetNotLive + ); assert_ok!(Assets::thaw_asset(signed(owner), token)); // "BalanceLow" error is returned if the balance is less than amount to burn. assert_noop!( - Fungibles::burn(signed(owner), token, from, total_supply * 2), + Fungibles::burn(signed(owner), token, account(from), total_supply * 2), AssetsError::BalanceLow.with_weight(WeightInfo::balance_of()) ); - let balance_before_burn = Assets::balance(token, &from); - assert_ok!(Fungibles::burn(signed(owner), token, from, value)); + let balance_before_burn = Assets::balance(token, &account(from)); + assert_ok!(Fungibles::burn(signed(owner), token, account(from), value)); assert_eq!(Assets::total_supply(TOKEN), total_supply - value); - let balance_after_burn = Assets::balance(token, &from); + let balance_after_burn = Assets::balance(token, &account(from)); assert_eq!(balance_after_burn, balance_before_burn - value); System::assert_last_event( - Event::Transfer { token, from: Some(from), to: None, value }.into(), + Event::Transfer { token, from: Some(account(from)), to: None, value }.into(), ); }); } @@ -470,17 +530,17 @@ fn balance_of_works() { new_test_ext().execute_with(|| { let value = 1_000 * UNIT; assert_eq!( - Fungibles::read(BalanceOf { token: TOKEN, owner: ALICE }), + Fungibles::read(BalanceOf { token: TOKEN, owner: account(ALICE) }), ReadResult::BalanceOf(Default::default()) ); assets::create_and_mint_to(ALICE, TOKEN, ALICE, value); assert_eq!( - Fungibles::read(BalanceOf { token: TOKEN, owner: ALICE }), + Fungibles::read(BalanceOf { token: TOKEN, owner: account(ALICE) }), ReadResult::BalanceOf(value) ); assert_eq!( - Fungibles::read(BalanceOf { token: TOKEN, owner: ALICE }).encode(), - Assets::balance(TOKEN, ALICE).encode(), + Fungibles::read(BalanceOf { token: TOKEN, owner: account(ALICE) }).encode(), + Assets::balance(TOKEN, account(ALICE)).encode(), ); }); } @@ -490,17 +550,30 @@ fn allowance_works() { new_test_ext().execute_with(|| { let value = 1_000 * UNIT; assert_eq!( - Fungibles::read(Allowance { token: TOKEN, owner: ALICE, spender: BOB }), + Fungibles::read(Allowance { + token: TOKEN, + owner: account(ALICE), + spender: account(BOB) + }), ReadResult::Allowance(Default::default()) ); assets::create_mint_and_approve(ALICE, TOKEN, ALICE, value * 2, BOB, value); assert_eq!( - Fungibles::read(Allowance { token: TOKEN, owner: ALICE, spender: BOB }), + Fungibles::read(Allowance { + token: TOKEN, + owner: account(ALICE), + spender: account(BOB) + }), ReadResult::Allowance(value) ); assert_eq!( - Fungibles::read(Allowance { token: TOKEN, owner: ALICE, spender: BOB }).encode(), - Assets::allowance(TOKEN, &ALICE, &BOB).encode(), + Fungibles::read(Allowance { + token: TOKEN, + owner: account(ALICE), + spender: account(BOB) + }) + .encode(), + Assets::allowance(TOKEN, &account(ALICE), &account(BOB)).encode(), ); }); } @@ -534,7 +607,7 @@ fn token_metadata_works() { fn token_exists_works() { new_test_ext().execute_with(|| { assert_eq!(Fungibles::read(TokenExists(TOKEN)), ReadResult::TokenExists(false)); - assert_ok!(Assets::create(signed(ALICE), TOKEN, ALICE, 1)); + assert_ok!(Assets::create(signed(ALICE), TOKEN, account(ALICE), 1)); assert_eq!(Fungibles::read(TokenExists(TOKEN)), ReadResult::TokenExists(true)); assert_eq!( Fungibles::read(TokenExists(TOKEN)).encode(), @@ -543,8 +616,8 @@ fn token_exists_works() { }); } -fn signed(account: AccountId) -> RuntimeOrigin { - RuntimeOrigin::signed(account) +fn signed(account_id: u8) -> RuntimeOrigin { + RuntimeOrigin::signed(account(account_id)) } fn root() -> RuntimeOrigin { @@ -559,36 +632,31 @@ fn none() -> RuntimeOrigin { mod assets { use super::*; - pub(super) fn create_and_mint_to( - owner: AccountId, - token: TokenId, - to: AccountId, - value: Balance, - ) { - assert_ok!(Assets::create(signed(owner), token, owner, 1)); - assert_ok!(Assets::mint(signed(owner), token, to, value)); + pub(super) fn create_and_mint_to(owner: u8, token: TokenId, to: u8, value: Balance) { + assert_ok!(Assets::create(signed(owner), token, account(owner), 1)); + assert_ok!(Assets::mint(signed(owner), token, account(to), value)); } pub(super) fn create_mint_and_approve( - owner: AccountId, + owner: u8, token: TokenId, - to: AccountId, + to: u8, mint: Balance, - spender: AccountId, + spender: u8, approve: Balance, ) { create_and_mint_to(owner, token, to, mint); - assert_ok!(Assets::approve_transfer(signed(to), token, spender, approve,)); + assert_ok!(Assets::approve_transfer(signed(to), token, account(spender), approve,)); } pub(super) fn create_and_set_metadata( - owner: AccountId, + owner: u8, token: TokenId, name: Vec, symbol: Vec, decimals: u8, ) { - assert_ok!(Assets::create(signed(owner), token, owner, 1)); + assert_ok!(Assets::create(signed(owner), token, account(owner), 1)); assert_ok!(Assets::set_metadata(signed(owner), token, name, symbol, decimals)); } } @@ -613,11 +681,11 @@ mod read_weights { fn new() -> Self { Self { total_supply: Fungibles::weight(&TotalSupply(TOKEN)), - balance_of: Fungibles::weight(&BalanceOf { token: TOKEN, owner: ALICE }), + balance_of: Fungibles::weight(&BalanceOf { token: TOKEN, owner: account(ALICE) }), allowance: Fungibles::weight(&Allowance { token: TOKEN, - owner: ALICE, - spender: BOB, + owner: account(ALICE), + spender: account(BOB), }), token_name: Fungibles::weight(&TokenName(TOKEN)), token_symbol: Fungibles::weight(&TokenSymbol(TOKEN)), @@ -699,15 +767,15 @@ mod ensure_codec_indexes { [ (TotalSupply::(Default::default()), 0u8, "TotalSupply"), ( - BalanceOf:: { token: Default::default(), owner: Default::default() }, + BalanceOf:: { token: Default::default(), owner: account(Default::default()) }, 1, "BalanceOf", ), ( Allowance:: { token: Default::default(), - owner: Default::default(), - spender: Default::default(), + owner: account(Default::default()), + spender: account(Default::default()), }, 2, "Allowance", @@ -731,7 +799,7 @@ mod ensure_codec_indexes { ( transfer { token: Default::default(), - to: Default::default(), + to: account(Default::default()), value: Default::default(), }, 3u8, @@ -740,8 +808,8 @@ mod ensure_codec_indexes { ( transfer_from { token: Default::default(), - from: Default::default(), - to: Default::default(), + from: account(Default::default()), + to: account(Default::default()), value: Default::default(), }, 4, @@ -750,7 +818,7 @@ mod ensure_codec_indexes { ( approve { token: Default::default(), - spender: Default::default(), + spender: account(Default::default()), value: Default::default(), }, 5, @@ -759,7 +827,7 @@ mod ensure_codec_indexes { ( increase_allowance { token: Default::default(), - spender: Default::default(), + spender: account(Default::default()), value: Default::default(), }, 6, @@ -768,7 +836,7 @@ mod ensure_codec_indexes { ( decrease_allowance { token: Default::default(), - spender: Default::default(), + spender: account(Default::default()), value: Default::default(), }, 7, @@ -777,7 +845,7 @@ mod ensure_codec_indexes { ( create { id: Default::default(), - admin: Default::default(), + admin: account(Default::default()), min_balance: Default::default(), }, 11, @@ -798,7 +866,7 @@ mod ensure_codec_indexes { ( mint { token: Default::default(), - account: Default::default(), + account: account(Default::default()), value: Default::default(), }, 19, @@ -807,7 +875,7 @@ mod ensure_codec_indexes { ( burn { token: Default::default(), - account: Default::default(), + account: account(Default::default()), value: Default::default(), }, 20, diff --git a/pallets/api/src/lib.rs b/pallets/api/src/lib.rs index d94d1978..e3d706e2 100644 --- a/pallets/api/src/lib.rs +++ b/pallets/api/src/lib.rs @@ -7,6 +7,7 @@ pub mod extension; pub mod fungibles; #[cfg(test)] mod mock; +pub mod nonfungibles; /// Trait for performing reads of runtime state. pub trait Read { diff --git a/pallets/api/src/mock.rs b/pallets/api/src/mock.rs index 42c8bf0e..920d590f 100644 --- a/pallets/api/src/mock.rs +++ b/pallets/api/src/mock.rs @@ -1,25 +1,28 @@ use frame_support::{ derive_impl, parameter_types, - traits::{AsEnsureOriginWithArg, ConstU128, ConstU32, Everything}, + traits::{AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU64, Everything}, }; use frame_system::{EnsureRoot, EnsureSigned}; +use pallet_nfts::PalletFeatures; use sp_core::H256; use sp_runtime::{ - traits::{BlakeTwo256, IdentityLookup}, - BuildStorage, + traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Verify}, + BuildStorage, MultiSignature, }; -pub(crate) const ALICE: AccountId = 1; -pub(crate) const BOB: AccountId = 2; -pub(crate) const CHARLIE: AccountId = 3; +pub(crate) const ALICE: u8 = 1; +pub(crate) const BOB: u8 = 2; +pub(crate) const CHARLIE: u8 = 3; pub(crate) const INIT_AMOUNT: Balance = 100_000_000 * UNIT; pub(crate) const UNIT: Balance = 10_000_000_000; type Block = frame_system::mocking::MockBlock; -pub(crate) type AccountId = u64; +pub(crate) type AccountId = ::AccountId; pub(crate) type Balance = u128; // For terminology in tests. pub(crate) type TokenId = u32; +type Signature = MultiSignature; +type AccountPublic = ::Signer; // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( @@ -29,6 +32,8 @@ frame_support::construct_runtime!( Assets: pallet_assets::, Balances: pallet_balances, Fungibles: crate::fungibles, + Nfts: pallet_nfts, + NonFungibles: crate::nonfungibles } ); @@ -91,10 +96,10 @@ impl pallet_assets::Config for Test { #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); type CallbackHandle = (); - type CreateOrigin = AsEnsureOriginWithArg>; + type CreateOrigin = AsEnsureOriginWithArg>; type Currency = Balances; type Extra = (); - type ForceOrigin = EnsureRoot; + type ForceOrigin = EnsureRoot; type Freezer = (); type MetadataDepositBase = ConstU128<1>; type MetadataDepositPerByte = ConstU128<1>; @@ -110,13 +115,62 @@ impl crate::fungibles::Config for Test { type WeightInfo = (); } +parameter_types! { + pub storage Features: PalletFeatures = PalletFeatures::all_enabled(); +} + +impl pallet_nfts::Config for Test { + type ApprovalsLimit = ConstU32<10>; + type AttributeDepositBase = ConstU128<1>; + type CollectionDeposit = ConstU128<2>; + type CollectionId = u32; + type CreateOrigin = AsEnsureOriginWithArg>; + type Currency = Balances; + type DepositPerByte = ConstU128<1>; + type Features = Features; + type ForceOrigin = frame_system::EnsureRoot; + #[cfg(feature = "runtime-benchmarks")] + type Helper = (); + type ItemAttributesApprovalsLimit = ConstU32<2>; + type ItemDeposit = ConstU128<1>; + type ItemId = u32; + type KeyLimit = ConstU32<50>; + type Locker = (); + type MaxAttributesPerCall = ConstU32<2>; + type MaxDeadlineDuration = ConstU64<10000>; + type MaxTips = ConstU32<10>; + type MetadataDepositBase = ConstU128<1>; + /// Using `AccountPublic` here makes it trivial to convert to `AccountId` via `into_account()`. + type OffchainPublic = AccountPublic; + /// Off-chain = signature On-chain - therefore no conversion needed. + /// It needs to be From for benchmarking. + type OffchainSignature = Signature; + type RuntimeEvent = RuntimeEvent; + type StringLimit = ConstU32<50>; + type ValueLimit = ConstU32<50>; + type WeightInfo = (); +} + +impl crate::nonfungibles::Config for Test { + type RuntimeEvent = RuntimeEvent; +} + +/// Initialize a new account ID. +pub(crate) fn account(id: u8) -> AccountId { + [id; 32].into() +} + pub(crate) fn new_test_ext() -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::::default() .build_storage() .expect("Frame system builds valid default genesis config"); pallet_balances::GenesisConfig:: { - balances: vec![(ALICE, INIT_AMOUNT), (BOB, INIT_AMOUNT), (CHARLIE, INIT_AMOUNT)], + balances: vec![ + (account(ALICE), INIT_AMOUNT), + (account(BOB), INIT_AMOUNT), + (account(CHARLIE), INIT_AMOUNT), + ], } .assimilate_storage(&mut t) .expect("Pallet balances storage can be assimilated"); diff --git a/pallets/api/src/nonfungibles/mod.rs b/pallets/api/src/nonfungibles/mod.rs new file mode 100644 index 00000000..7377048f --- /dev/null +++ b/pallets/api/src/nonfungibles/mod.rs @@ -0,0 +1,439 @@ +//! The non-fungibles pallet offers a streamlined interface for interacting with non-fungible +//! assets. The goal is to provide a simplified, consistent API that adheres to standards in the +//! smart contract space. + +pub use pallet::*; +use pallet_nfts::WeightInfo; +use sp_runtime::traits::StaticLookup; + +#[cfg(test)] +mod tests; +mod types; + +#[frame_support::pallet] +pub mod pallet { + use frame_support::{dispatch::DispatchResult, pallet_prelude::*, traits::Incrementable}; + use frame_system::pallet_prelude::*; + use pallet_nfts::{ + CancelAttributesApprovalWitness, CollectionConfig, CollectionSettings, DestroyWitness, + MintSettings, MintWitness, + }; + use sp_std::vec::Vec; + use types::{ + AccountIdOf, AttributeNamespaceOf, BalanceOf, CollectionDetailsFor, CollectionIdOf, + CreateCollectionConfigFor, ItemDetailsFor, ItemIdOf, ItemPriceOf, NextCollectionIdOf, + NftsOf, NftsWeightInfoOf, + }; + + use super::*; + + /// State reads for the non-fungibles API with required input. + #[derive(Encode, Decode, Debug, MaxEncodedLen)] + #[cfg_attr(feature = "std", derive(PartialEq, Clone))] + #[repr(u8)] + #[allow(clippy::unnecessary_cast)] + pub enum Read { + /// Total item supply of a collection. + #[codec(index = 0)] + TotalSupply(CollectionIdOf), + /// Account balance for a specified collection. + #[codec(index = 1)] + BalanceOf { collection: CollectionIdOf, owner: AccountIdOf }, + /// Allowance for an operator approved by an owner, for a specified collection or item. + #[codec(index = 2)] + Allowance { + collection: CollectionIdOf, + owner: AccountIdOf, + operator: AccountIdOf, + item: Option>, + }, + /// Owner of a specified collection item. + #[codec(index = 3)] + OwnerOf { collection: CollectionIdOf, item: ItemIdOf }, + /// Attribute value of a collection item. + #[codec(index = 4)] + GetAttribute { + collection: CollectionIdOf, + item: Option>, + namespace: AttributeNamespaceOf, + key: BoundedVec, + }, + /// Details of a collection. + #[codec(index = 6)] + Collection(CollectionIdOf), + /// Details of a collection item. + #[codec(index = 7)] + Item { collection: CollectionIdOf, item: ItemIdOf }, + /// Next collection ID. + #[codec(index = 8)] + NextCollectionId, + } + + /// Results of state reads for the non-fungibles API. + #[derive(Debug)] + #[cfg_attr(feature = "std", derive(PartialEq, Clone))] + pub enum ReadResult { + /// Total item supply of a collection. + TotalSupply(u32), + /// Account balance for a specified collection. + BalanceOf(u32), + /// Allowance for an operator approved by an owner, for a specified collection or item. + Allowance(bool), + /// Owner of a specified collection owner. + OwnerOf(Option>), + /// Attribute value of a collection item. + GetAttribute(Option>), + /// Details of a collection. + Collection(Option>), + /// Details of a collection item. + Item(Option>), + /// Next collection ID. + NextCollectionId(Option>), + } + + impl ReadResult { + /// Encodes the result. + pub fn encode(&self) -> Vec { + use ReadResult::*; + match self { + OwnerOf(result) => result.encode(), + TotalSupply(result) => result.encode(), + BalanceOf(result) => result.encode(), + Collection(result) => result.encode(), + Item(result) => result.encode(), + Allowance(result) => result.encode(), + GetAttribute(result) => result.encode(), + NextCollectionId(result) => result.encode(), + } + } + } + + /// Configure the pallet by specifying the parameters and types on which it depends. + #[pallet::config] + pub trait Config: frame_system::Config + pallet_nfts::Config { + /// Because this pallet emits events, it depends on the runtime's definition of an event. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + } + + #[pallet::pallet] + pub struct Pallet(_); + + /// The events that can be emitted. + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Event emitted when allowance by `owner` to `operator` changes. + Approval { + /// The collection ID. + collection: CollectionIdOf, + /// The item which is (dis)approved. `None` for all owner's items. + item: Option>, + /// The owner providing the allowance. + owner: AccountIdOf, + /// The beneficiary of the allowance. + operator: AccountIdOf, + /// Whether allowance is set or removed. + approved: bool, + }, + /// Event emitted when a token transfer occurs. + // Differing style: event name abides by the PSP22 standard. + Transfer { + /// The collection ID. + collection: CollectionIdOf, + /// The collection item ID. + item: ItemIdOf, + /// The source of the transfer. `None` when minting. + from: Option>, + /// The recipient of the transfer. `None` when burning. + to: Option>, + /// The price of the collection item. + price: Option>, + }, + /// Event emitted when a collection is created. + Created { + /// The collection identifier. + id: CollectionIdOf, + /// The creator of the collection. + creator: AccountIdOf, + /// The administrator of the collection. + admin: AccountIdOf, + }, + } + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(NftsWeightInfoOf::::mint())] + pub fn mint( + origin: OriginFor, + to: AccountIdOf, + collection: CollectionIdOf, + item: ItemIdOf, + mint_price: Option>, + ) -> DispatchResult { + let account = ensure_signed(origin.clone())?; + let witness_data = MintWitness { mint_price, owned_item: Some(item) }; + NftsOf::::mint( + origin, + collection, + item, + T::Lookup::unlookup(to.clone()), + Some(witness_data), + )?; + Self::deposit_event(Event::Transfer { + collection, + item, + from: None, + to: Some(account), + price: mint_price, + }); + Ok(()) + } + + #[pallet::call_index(1)] + #[pallet::weight(NftsWeightInfoOf::::burn())] + pub fn burn( + origin: OriginFor, + collection: CollectionIdOf, + item: ItemIdOf, + ) -> DispatchResult { + let account = ensure_signed(origin.clone())?; + NftsOf::::burn(origin, collection, item)?; + Self::deposit_event(Event::Transfer { + collection, + item, + from: Some(account), + to: None, + price: None, + }); + Ok(()) + } + + #[pallet::call_index(2)] + #[pallet::weight(NftsWeightInfoOf::::transfer())] + pub fn transfer( + origin: OriginFor, + collection: CollectionIdOf, + item: ItemIdOf, + to: AccountIdOf, + ) -> DispatchResult { + let from = ensure_signed(origin.clone())?; + NftsOf::::transfer(origin, collection, item, T::Lookup::unlookup(to.clone()))?; + Self::deposit_event(Event::Transfer { + collection, + item, + from: Some(from), + to: Some(to), + price: None, + }); + Ok(()) + } + + #[pallet::call_index(3)] + #[pallet::weight(NftsWeightInfoOf::::approve_transfer() + NftsWeightInfoOf::::cancel_approval())] + pub fn approve( + origin: OriginFor, + collection: CollectionIdOf, + item: Option>, + operator: AccountIdOf, + approved: bool, + ) -> DispatchResult { + let owner = ensure_signed(origin.clone())?; + if approved { + NftsOf::::approve_transfer( + origin, + collection, + item, + T::Lookup::unlookup(operator.clone()), + None, + )?; + } else { + NftsOf::::cancel_approval( + origin, + collection, + item, + T::Lookup::unlookup(operator.clone()), + )?; + } + Self::deposit_event(Event::Approval { collection, item, operator, owner, approved }); + Ok(()) + } + + #[pallet::call_index(4)] + #[pallet::weight(NftsWeightInfoOf::::create())] + pub fn create( + origin: OriginFor, + admin: AccountIdOf, + config: CreateCollectionConfigFor, + ) -> DispatchResult { + let id = NextCollectionIdOf::::get() + .or(T::CollectionId::initial_value()) + .ok_or(pallet_nfts::Error::::UnknownCollection)?; + let creator = ensure_signed(origin.clone())?; + let collection_config = CollectionConfig { + settings: CollectionSettings::all_enabled(), + max_supply: config.max_supply, + mint_settings: MintSettings { + mint_type: config.mint_type, + start_block: config.start_block, + end_block: config.end_block, + ..MintSettings::default() + }, + }; + NftsOf::::create(origin, T::Lookup::unlookup(admin.clone()), collection_config)?; + Self::deposit_event(Event::Created { id, admin, creator }); + Ok(()) + } + + #[pallet::call_index(5)] + #[pallet::weight(NftsWeightInfoOf::::destroy( + witness.item_metadatas, + witness.item_configs, + witness.attributes, + ))] + pub fn destroy( + origin: OriginFor, + collection: CollectionIdOf, + witness: DestroyWitness, + ) -> DispatchResultWithPostInfo { + NftsOf::::destroy(origin, collection, witness) + } + + #[pallet::call_index(6)] + #[pallet::weight(NftsWeightInfoOf::::set_attribute())] + pub fn set_attribute( + origin: OriginFor, + collection: CollectionIdOf, + item: Option>, + namespace: AttributeNamespaceOf, + key: BoundedVec, + value: BoundedVec, + ) -> DispatchResult { + NftsOf::::set_attribute(origin, collection, item, namespace, key, value) + } + + #[pallet::call_index(7)] + #[pallet::weight(NftsWeightInfoOf::::clear_attribute())] + pub fn clear_attribute( + origin: OriginFor, + collection: CollectionIdOf, + item: Option>, + namespace: AttributeNamespaceOf, + key: BoundedVec, + ) -> DispatchResult { + NftsOf::::clear_attribute(origin, collection, item, namespace, key) + } + + #[pallet::call_index(8)] + #[pallet::weight(NftsWeightInfoOf::::set_metadata())] + pub fn set_metadata( + origin: OriginFor, + collection: CollectionIdOf, + item: ItemIdOf, + data: BoundedVec, + ) -> DispatchResult { + NftsOf::::set_metadata(origin, collection, item, data) + } + + #[pallet::call_index(9)] + #[pallet::weight(NftsWeightInfoOf::::clear_metadata())] + pub fn clear_metadata( + origin: OriginFor, + collection: CollectionIdOf, + item: ItemIdOf, + ) -> DispatchResult { + NftsOf::::clear_metadata(origin, collection, item) + } + + #[pallet::call_index(10)] + #[pallet::weight(NftsWeightInfoOf::::approve_item_attributes())] + pub fn approve_item_attributes( + origin: OriginFor, + collection: CollectionIdOf, + item: ItemIdOf, + delegate: AccountIdOf, + ) -> DispatchResult { + NftsOf::::approve_item_attributes( + origin, + collection, + item, + T::Lookup::unlookup(delegate.clone()), + ) + } + + #[pallet::call_index(11)] + #[pallet::weight(NftsWeightInfoOf::::cancel_item_attributes_approval(witness.account_attributes))] + pub fn cancel_item_attributes_approval( + origin: OriginFor, + collection: CollectionIdOf, + item: ItemIdOf, + delegate: AccountIdOf, + witness: CancelAttributesApprovalWitness, + ) -> DispatchResult { + NftsOf::::cancel_item_attributes_approval( + origin, + collection, + item, + T::Lookup::unlookup(delegate.clone()), + witness, + ) + } + + #[pallet::call_index(12)] + #[pallet::weight(NftsWeightInfoOf::::set_collection_max_supply())] + pub fn set_max_supply( + origin: OriginFor, + collection: CollectionIdOf, + max_supply: u32, + ) -> DispatchResult { + NftsOf::::set_collection_max_supply(origin, collection, max_supply) + } + } + + impl crate::Read for Pallet { + /// The type of read requested. + type Read = Read; + /// The type or result returned. + type Result = ReadResult; + + /// Determines the weight of the requested read, used to charge the appropriate weight + /// before the read is performed. + /// + /// # Parameters + /// - `request` - The read request. + fn weight(_request: &Self::Read) -> Weight { + Default::default() + } + + /// Performs the requested read and returns the result. + /// + /// # Parameters + /// - `request` - The read request. + fn read(value: Self::Read) -> Self::Result { + use Read::*; + match value { + TotalSupply(collection) => ReadResult::TotalSupply( + NftsOf::::collection_items(collection).unwrap_or_default(), + ), + BalanceOf { collection, owner } => + ReadResult::BalanceOf(pallet_nfts::AccountBalance::::get(collection, owner)), + Allowance { collection, owner, operator, item } => ReadResult::Allowance( + NftsOf::::check_allowance(&collection, &item, &owner, &operator).is_ok(), + ), + OwnerOf { collection, item } => + ReadResult::OwnerOf(NftsOf::::owner(collection, item)), + GetAttribute { collection, item, namespace, key } => ReadResult::GetAttribute( + pallet_nfts::Attribute::::get((collection, item, namespace, key)) + .map(|attribute| attribute.0), + ), + Collection(collection) => + ReadResult::Collection(pallet_nfts::Collection::::get(collection)), + Item { collection, item } => + ReadResult::Item(pallet_nfts::Item::::get(collection, item)), + NextCollectionId => ReadResult::NextCollectionId( + NextCollectionIdOf::::get().or(T::CollectionId::initial_value()), + ), + } + } + } +} diff --git a/pallets/api/src/nonfungibles/tests.rs b/pallets/api/src/nonfungibles/tests.rs new file mode 100644 index 00000000..e89ba0bd --- /dev/null +++ b/pallets/api/src/nonfungibles/tests.rs @@ -0,0 +1,572 @@ +use codec::Encode; +use frame_support::{assert_noop, assert_ok, traits::Incrementable}; +use frame_system::pallet_prelude::BlockNumberFor; +use pallet_nfts::{ + AccountBalance, CollectionConfig, CollectionDetails, CollectionSettings, ItemDeposit, + ItemDetails, MintSettings, +}; +use sp_runtime::{BoundedBTreeMap, BoundedVec, DispatchError::BadOrigin}; + +use super::types::{CollectionIdOf, ItemIdOf}; +use crate::{ + mock::*, + nonfungibles::{Event, Read::*, ReadResult}, + Read, +}; + +const ITEM: u32 = 1; + +type NftsError = pallet_nfts::Error; + +mod encoding_read_result { + use super::*; + + #[test] + fn total_supply() { + let total_supply: u32 = 1_000_000; + assert_eq!(ReadResult::TotalSupply::(total_supply).encode(), total_supply.encode()); + } + + #[test] + fn balance_of() { + let balance: u32 = 100; + assert_eq!(ReadResult::BalanceOf::(balance).encode(), balance.encode()); + } + + #[test] + fn allowance() { + let allowance = false; + assert_eq!(ReadResult::Allowance::(allowance).encode(), allowance.encode()); + } + + #[test] + fn owner_of() { + let mut owner = Some(account(ALICE)); + assert_eq!(ReadResult::OwnerOf::(owner.clone()).encode(), owner.encode()); + owner = None; + assert_eq!(ReadResult::OwnerOf::(owner.clone()).encode(), owner.encode()); + } + + #[test] + fn get_attribute() { + let mut attribute = Some(BoundedVec::truncate_from("some attribute".as_bytes().to_vec())); + assert_eq!( + ReadResult::GetAttribute::(attribute.clone()).encode(), + attribute.encode() + ); + attribute = None; + assert_eq!( + ReadResult::GetAttribute::(attribute.clone()).encode(), + attribute.encode() + ); + } + + #[test] + fn collection() { + let mut collection_details = Some(CollectionDetails { + owner: account(ALICE), + owner_deposit: 0, + items: 0, + item_metadatas: 0, + item_configs: 0, + attributes: 0, + }); + assert_eq!( + ReadResult::Collection::(collection_details.clone()).encode(), + collection_details.encode() + ); + collection_details = None; + assert_eq!( + ReadResult::Collection::(collection_details.clone()).encode(), + collection_details.encode() + ); + } + + #[test] + fn item() { + let mut item_details = Some(ItemDetails { + owner: account(ALICE), + approvals: BoundedBTreeMap::default(), + deposit: ItemDeposit { amount: 0, account: account(BOB) }, + }); + assert_eq!(ReadResult::Item::(item_details.clone()).encode(), item_details.encode()); + item_details = None; + assert_eq!(ReadResult::Item::(item_details.clone()).encode(), item_details.encode()); + } + + #[test] + fn next_collection_id_works() { + let mut next_collection_id = Some(0); + assert_eq!( + ReadResult::NextCollectionId::(next_collection_id).encode(), + next_collection_id.encode() + ); + next_collection_id = None; + assert_eq!( + ReadResult::NextCollectionId::(next_collection_id).encode(), + next_collection_id.encode() + ); + } +} + +#[test] +fn transfer() { + new_test_ext().execute_with(|| { + let owner = ALICE; + let dest = BOB; + + let (collection, item) = nfts::create_collection_mint(owner, ITEM); + for origin in vec![root(), none()] { + assert_noop!( + NonFungibles::transfer(origin, collection, item, account(dest)), + BadOrigin + ); + } + // Successfully burn an existing new collection item. + let balance_before_transfer = AccountBalance::::get(collection, &account(dest)); + assert_ok!(NonFungibles::transfer(signed(owner), collection, ITEM, account(dest))); + let balance_after_transfer = AccountBalance::::get(collection, &account(dest)); + assert_eq!(AccountBalance::::get(collection, &account(owner)), 0); + assert_eq!(balance_after_transfer - balance_before_transfer, 1); + System::assert_last_event( + Event::Transfer { + collection, + item, + from: Some(account(owner)), + to: Some(account(dest)), + price: None, + } + .into(), + ); + }); +} + +#[test] +fn mint_works() { + new_test_ext().execute_with(|| { + let owner = ALICE; + let collection = nfts::create_collection(owner); + + // Successfully mint a new collection item. + let balance_before_mint = AccountBalance::::get(collection, account(owner)); + assert_ok!(NonFungibles::mint(signed(owner), account(owner), collection, ITEM, None)); + let balance_after_mint = AccountBalance::::get(collection, account(owner)); + assert_eq!(balance_after_mint, 1); + assert_eq!(balance_after_mint - balance_before_mint, 1); + System::assert_last_event( + Event::Transfer { + collection, + item: ITEM, + from: None, + to: Some(account(owner)), + price: None, + } + .into(), + ); + }); +} + +#[test] +fn burn_works() { + new_test_ext().execute_with(|| { + let owner = ALICE; + + // Successfully burn an existing new collection item. + let (collection, item) = nfts::create_collection_mint(owner, ITEM); + assert_ok!(NonFungibles::burn(signed(owner), collection, ITEM)); + System::assert_last_event( + Event::Transfer { collection, item, from: Some(account(owner)), to: None, price: None } + .into(), + ); + }); +} + +#[test] +fn approve_works() { + new_test_ext().execute_with(|| { + let owner = ALICE; + let operator = BOB; + let (collection, item) = nfts::create_collection_mint(owner, ITEM); + // Successfully approve `oeprator` to transfer the collection item. + assert_ok!(NonFungibles::approve( + signed(owner), + collection, + Some(item), + account(operator), + true + )); + System::assert_last_event( + Event::Approval { + collection, + item: Some(item), + owner: account(owner), + operator: account(operator), + approved: true, + } + .into(), + ); + // Successfully transfer the item by the delegated account `operator`. + assert_ok!(Nfts::transfer(signed(operator), collection, item, account(operator))); + }); +} + +#[test] +fn cancel_approval_works() { + new_test_ext().execute_with(|| { + let owner = ALICE; + let operator = BOB; + let (collection, item) = nfts::create_collection_mint_and_approve(owner, ITEM, operator); + // Successfully cancel the transfer approval of `operator` by `owner`. + assert_ok!(NonFungibles::approve( + signed(owner), + collection, + Some(item), + account(operator), + false + )); + // Failed to transfer the item by `operator` without permission. + assert_noop!( + Nfts::transfer(signed(operator), collection, item, account(operator)), + NftsError::NoPermission + ); + }); +} + +#[test] +fn set_max_supply_works() { + new_test_ext().execute_with(|| { + let owner = ALICE; + let collection = nfts::create_collection(owner); + assert_ok!(NonFungibles::set_max_supply(signed(owner), collection, 10)); + (0..10).into_iter().for_each(|i| { + assert_ok!(Nfts::mint(signed(owner), collection, i, account(owner), None)); + }); + assert_noop!( + Nfts::mint(signed(owner), collection, 42, account(owner), None), + NftsError::MaxSupplyReached + ); + }); +} + +#[test] +fn owner_of_works() { + new_test_ext().execute_with(|| { + let (collection, item) = nfts::create_collection_mint(ALICE, ITEM); + assert_eq!( + NonFungibles::read(OwnerOf { collection, item }).encode(), + Nfts::owner(collection, item).encode() + ); + }); +} + +#[test] +fn get_attribute_works() { + new_test_ext().execute_with(|| { + let (collection, item) = nfts::create_collection_mint(ALICE, ITEM); + assert_eq!(NonFungibles::read(NextCollectionId).encode(), Some(1).encode()); + let mut attribute = BoundedVec::truncate_from("some attribute".as_bytes().to_vec()); + let value = BoundedVec::truncate_from("some value".as_bytes().to_vec()); + let mut result: Option::ValueLimit>> = None; + // No attribute set. + assert_eq!( + NonFungibles::read(GetAttribute { + collection, + item: Some(item), + namespace: pallet_nfts::AttributeNamespace::CollectionOwner, + key: attribute.clone() + }) + .encode(), + result.encode() + ); + // Successfully get an existing attribute. + result = Some(value.clone()); + assert_ok!(Nfts::set_attribute( + signed(ALICE), + collection, + Some(item), + pallet_nfts::AttributeNamespace::CollectionOwner, + attribute.clone(), + value, + )); + assert_eq!( + NonFungibles::read(GetAttribute { + collection, + item: Some(item), + namespace: pallet_nfts::AttributeNamespace::CollectionOwner, + key: attribute + }) + .encode(), + result.encode() + ); + }); +} + +#[test] +fn clear_attribute_works() { + new_test_ext().execute_with(|| { + let (collection, item) = nfts::create_collection_mint(ALICE, ITEM); + assert_eq!(NonFungibles::read(NextCollectionId).encode(), Some(1).encode()); + let mut attribute = BoundedVec::truncate_from("some attribute".as_bytes().to_vec()); + let mut result: Option::ValueLimit>> = None; + assert_ok!(Nfts::set_attribute( + signed(ALICE), + collection, + Some(item), + pallet_nfts::AttributeNamespace::CollectionOwner, + attribute.clone(), + BoundedVec::truncate_from("some value".as_bytes().to_vec()) + )); + // Successfully clear an attribute. + assert_ok!(Nfts::clear_attribute( + signed(ALICE), + collection, + Some(item), + pallet_nfts::AttributeNamespace::CollectionOwner, + attribute.clone(), + )); + assert_eq!( + NonFungibles::read(GetAttribute { + collection, + item: Some(item), + namespace: pallet_nfts::AttributeNamespace::CollectionOwner, + key: attribute + }) + .encode(), + result.encode() + ); + }); +} + +#[test] +fn approve_item_attribute_works() { + new_test_ext().execute_with(|| { + let (collection, item) = nfts::create_collection_mint(ALICE, ITEM); + assert_eq!(NonFungibles::read(NextCollectionId).encode(), Some(1).encode()); + let mut attribute = BoundedVec::truncate_from("some attribute".as_bytes().to_vec()); + let value = BoundedVec::truncate_from("some value".as_bytes().to_vec()); + let mut result: Option::ValueLimit>> = None; + // Successfully approve delegate to set attributes. + assert_ok!(Nfts::approve_item_attributes(signed(ALICE), collection, item, account(BOB))); + assert_ok!(Nfts::set_attribute( + signed(BOB), + collection, + Some(item), + pallet_nfts::AttributeNamespace::Account(account(BOB)), + attribute.clone(), + value.clone() + )); + result = Some(value); + assert_eq!( + NonFungibles::read(GetAttribute { + collection, + item: Some(item), + namespace: pallet_nfts::AttributeNamespace::Account(account(BOB)), + key: attribute + }) + .encode(), + result.encode() + ); + }); +} + +#[test] +fn cancel_item_attribute_approval_works() { + new_test_ext().execute_with(|| { + let (collection, item) = nfts::create_collection_mint(ALICE, ITEM); + assert_eq!(NonFungibles::read(NextCollectionId).encode(), Some(1).encode()); + let mut attribute = BoundedVec::truncate_from("some attribute".as_bytes().to_vec()); + let value = BoundedVec::truncate_from("some value".as_bytes().to_vec()); + let mut result: Option::ValueLimit>> = None; + // Successfully approve delegate to set attributes. + assert_ok!(Nfts::approve_item_attributes(signed(ALICE), collection, item, account(BOB))); + assert_ok!(Nfts::set_attribute( + signed(BOB), + collection, + Some(item), + pallet_nfts::AttributeNamespace::Account(account(BOB)), + attribute.clone(), + value.clone() + )); + assert_ok!(Nfts::cancel_item_attributes_approval( + signed(ALICE), + collection, + item, + account(BOB), + pallet_nfts::CancelAttributesApprovalWitness { account_attributes: 1 } + )); + assert_noop!( + Nfts::set_attribute( + signed(BOB), + collection, + Some(item), + pallet_nfts::AttributeNamespace::Account(account(BOB)), + attribute.clone(), + value.clone() + ), + NftsError::NoPermission + ); + }); +} + +#[test] +fn next_collection_id_works() { + new_test_ext().execute_with(|| { + let _ = nfts::create_collection_mint(ALICE, ITEM); + assert_eq!(NonFungibles::read(NextCollectionId).encode(), Some(1).encode()); + assert_eq!( + NonFungibles::read(NextCollectionId).encode(), + pallet_nfts::NextCollectionId::::get() + .or(CollectionIdOf::::initial_value()) + .encode(), + ); + }); +} + +#[test] +fn total_supply_works() { + new_test_ext().execute_with(|| { + let owner = ALICE; + let collection = nfts::create_collection(owner); + (0..10).into_iter().for_each(|i| { + assert_ok!(Nfts::mint(signed(owner), collection, i, account(owner), None)); + assert_eq!(NonFungibles::read(TotalSupply(collection)).encode(), (i + 1).encode()); + assert_eq!( + NonFungibles::read(TotalSupply(collection)).encode(), + Nfts::collection_items(collection).unwrap_or_default().encode() + ); + }); + }); +} + +#[test] +fn collection_works() { + new_test_ext().execute_with(|| { + let (collection, _) = nfts::create_collection_mint(ALICE, ITEM); + assert_eq!( + NonFungibles::read(Collection(collection)).encode(), + pallet_nfts::Collection::::get(&collection).encode(), + ); + }); +} + +#[test] +fn item_works() { + new_test_ext().execute_with(|| { + let (collection, item) = nfts::create_collection_mint(ALICE, ITEM); + assert_eq!( + NonFungibles::read(Item { collection, item }).encode(), + pallet_nfts::Item::::get(&collection, &item).encode(), + ); + }); +} + +#[test] +fn balance_of_works() { + new_test_ext().execute_with(|| { + let owner = ALICE; + let collection = nfts::create_collection(owner); + (0..10).into_iter().for_each(|i| { + assert_ok!(Nfts::mint(signed(owner), collection, i, account(owner), None)); + assert_eq!( + NonFungibles::read(BalanceOf { collection, owner: account(owner) }).encode(), + (i + 1).encode() + ); + assert_eq!( + NonFungibles::read(BalanceOf { collection, owner: account(owner) }).encode(), + AccountBalance::::get(collection, account(owner)).encode() + ); + }); + }); +} + +#[test] +fn allowance_works() { + new_test_ext().execute_with(|| { + let owner = ALICE; + let operator = BOB; + let (collection, item) = nfts::create_collection_mint_and_approve(owner, ITEM, operator); + assert_eq!( + NonFungibles::read(Allowance { + collection, + item: Some(item), + owner: account(owner), + operator: account(operator), + }) + .encode(), + true.encode() + ); + assert_eq!( + NonFungibles::read(Allowance { + collection, + item: Some(item), + owner: account(owner), + operator: account(operator), + }) + .encode(), + Nfts::check_allowance(&collection, &Some(item), &account(owner), &account(operator)) + .is_ok() + .encode() + ); + }); +} + +fn signed(account_id: u8) -> RuntimeOrigin { + RuntimeOrigin::signed(account(account_id)) +} + +fn root() -> RuntimeOrigin { + RuntimeOrigin::root() +} + +fn none() -> RuntimeOrigin { + RuntimeOrigin::none() +} + +mod nfts { + use super::*; + + pub(super) fn create_collection_mint_and_approve( + owner: u8, + item: ItemIdOf, + operator: u8, + ) -> (u32, u32) { + let (collection, item) = create_collection_mint(owner, item); + assert_ok!(Nfts::approve_transfer( + signed(owner), + collection, + Some(item), + account(operator), + None + )); + (collection, item) + } + + pub(super) fn create_collection_mint(owner: u8, item: ItemIdOf) -> (u32, u32) { + let collection = create_collection(owner); + assert_ok!(Nfts::mint(signed(owner), collection, item, account(owner), None)); + (collection, item) + } + + pub(super) fn create_collection(owner: u8) -> u32 { + let next_id = next_collection_id(); + assert_ok!(Nfts::create( + signed(owner), + account(owner), + collection_config_with_all_settings_enabled() + )); + next_id + } + + pub(super) fn next_collection_id() -> u32 { + pallet_nfts::NextCollectionId::::get().unwrap_or_default() + } + + pub(super) fn collection_config_with_all_settings_enabled( + ) -> CollectionConfig, CollectionIdOf> { + CollectionConfig { + settings: CollectionSettings::all_enabled(), + max_supply: None, + mint_settings: MintSettings::default(), + } + } +} diff --git a/pallets/api/src/nonfungibles/types.rs b/pallets/api/src/nonfungibles/types.rs new file mode 100644 index 00000000..f57ee697 --- /dev/null +++ b/pallets/api/src/nonfungibles/types.rs @@ -0,0 +1,45 @@ +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::traits::{nonfungibles_v2::Inspect, Currency}; +use frame_system::pallet_prelude::BlockNumberFor; +use pallet_nfts::{AttributeNamespace, CollectionDetails, ItemDeposit, ItemDetails, MintType}; +use scale_info::TypeInfo; +use sp_runtime::{BoundedBTreeMap, RuntimeDebug}; + +// Type aliases for pallet-nfts. +pub(super) type NftsOf = pallet_nfts::Pallet; +pub(super) type NftsErrorOf = pallet_nfts::Error; +pub(super) type NftsWeightInfoOf = ::WeightInfo; +// Type aliases for pallet-nfts storage items. +pub(super) type AccountIdOf = ::AccountId; +pub(super) type BalanceOf = <>::Currency as Currency< + ::AccountId, +>>::Balance; +pub(super) type NextCollectionIdOf = pallet_nfts::NextCollectionId; +pub(super) type CollectionIdOf = + as Inspect<::AccountId>>::CollectionId; +pub(super) type ItemIdOf = + as Inspect<::AccountId>>::ItemId; +pub(super) type ApprovalsOf = BoundedBTreeMap< + AccountIdOf, + Option>, + ::ApprovalsLimit, +>; +pub(super) type ItemPriceOf = BalanceOf; +// TODO: Multi-instances. +pub(super) type ItemDepositOf = ItemDeposit, AccountIdOf>; +pub(super) type CollectionDetailsFor = + CollectionDetails, BalanceOf>; +pub(super) type ItemDetailsFor = + ItemDetails, ItemDepositOf, ApprovalsOf>; +pub(super) type AttributeNamespaceOf = AttributeNamespace>; +pub(super) type CreateCollectionConfigFor = + CreateCollectionConfig, BlockNumberFor, CollectionIdOf>; + +#[derive(Clone, Copy, Decode, Encode, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo)] +pub struct CreateCollectionConfig { + pub max_supply: Option, + pub mint_type: MintType, + pub price: Option, + pub start_block: Option, + pub end_block: Option, +} diff --git a/pallets/nfts/Cargo.toml b/pallets/nfts/Cargo.toml index 19d35803..1fcfe9dd 100644 --- a/pallets/nfts/Cargo.toml +++ b/pallets/nfts/Cargo.toml @@ -1,33 +1,30 @@ [package] authors.workspace = true -description = "FRAME NFTs pallet (polkadot v1.15.0)" +description = "NFTs pallet (polkadot v1.15.0) for Pop Network Runtimes" edition.workspace = true -homepage = "https://substrate.io" -license = "Apache-2.0" +license.workspace = true name = "pallet-nfts" readme = "README.md" -repository.workspace = true -version = "31.0.0" - +version = "0.1.0" [package.metadata.docs.rs] targets = [ "x86_64-unknown-linux-gnu" ] [dependencies] -codec = { workspace = true } -enumflags2 = { workspace = true } +codec.workspace = true +enumflags2.workspace = true frame-benchmarking = { optional = true, workspace = true } frame-support.workspace = true frame-system.workspace = true -log = { workspace = true } +log.workspace = true scale-info = { features = [ "derive" ], workspace = true } sp-core.workspace = true sp-io.workspace = true sp-runtime.workspace = true [dev-dependencies] -pallet-balances = { default-features = true, workspace = true } -sp-keystore = { default-features = true, workspace = true } +pallet-balances.workspace = true +sp-keystore.workspace = true [features] default = [ "std" ] diff --git a/pallets/nfts/src/common_functions.rs b/pallets/nfts/src/common_functions.rs index f51de192..6fe483f1 100644 --- a/pallets/nfts/src/common_functions.rs +++ b/pallets/nfts/src/common_functions.rs @@ -34,6 +34,11 @@ impl, I: 'static> Pallet { Collection::::get(collection).map(|i| i.owner) } + /// Get the total number of items in the collection, if the collection exists. + pub fn collection_items(collection: T::CollectionId) -> Option { + Collection::::get(collection).map(|i| i.items) + } + /// Validates the signature of the given data with the provided signer's account ID. /// /// # Errors @@ -46,7 +51,7 @@ impl, I: 'static> Pallet { signer: &T::AccountId, ) -> DispatchResult { if signature.verify(&**data, &signer) { - return Ok(()) + return Ok(()); } // NOTE: for security reasons modern UIs implicitly wrap the data requested to sign into diff --git a/pallets/nfts/src/features/approvals.rs b/pallets/nfts/src/features/approvals.rs index ad5d93c2..6d71c1a2 100644 --- a/pallets/nfts/src/features/approvals.rs +++ b/pallets/nfts/src/features/approvals.rs @@ -65,7 +65,6 @@ impl, I: 'static> Pallet { if let Some(check_origin) = maybe_check_origin { ensure!(check_origin == details.owner, Error::::NoPermission); } - let now = frame_system::Pallet::::block_number(); let deadline = maybe_deadline.map(|d| d.saturating_add(now)); @@ -74,15 +73,13 @@ impl, I: 'static> Pallet { .try_insert(delegate.clone(), deadline) .map_err(|_| Error::::ReachedApprovalLimit)?; Item::::insert(&collection, &item, &details); - Self::deposit_event(Event::TransferApproved { collection, - item, + item: Some(item), owner: details.owner, delegate, deadline, }); - Ok(()) } @@ -129,7 +126,7 @@ impl, I: 'static> Pallet { Self::deposit_event(Event::ApprovalCancelled { collection, - item, + item: Some(item), owner: details.owner, delegate, }); @@ -173,4 +170,87 @@ impl, I: 'static> Pallet { Ok(()) } + + pub(crate) fn do_approve_collection( + maybe_check_origin: Option, + collection: T::CollectionId, + delegate: T::AccountId, + ) -> DispatchResult { + ensure!( + Self::is_pallet_feature_enabled(PalletFeature::Approvals), + Error::::MethodDisabled + ); + if !Collection::::contains_key(collection) { + return Err(Error::::UnknownCollection.into()); + } + let collection_config = Self::get_collection_config(&collection)?; + ensure!( + collection_config.is_setting_enabled(CollectionSetting::TransferableItems), + Error::::ItemsNonTransferable + ); + + let origin = maybe_check_origin.ok_or(Error::::WrongOrigin)?; + Allowances::::mutate((&collection, &origin, &delegate), |allowance| { + *allowance = true; + }); + + Self::deposit_event(Event::TransferApproved { + collection, + item: None, + owner: origin, + delegate, + deadline: None, + }); + Ok(()) + } + + pub(crate) fn do_cancel_collection( + maybe_check_origin: Option, + collection: T::CollectionId, + delegate: T::AccountId, + ) -> DispatchResult { + if !Collection::::contains_key(collection) { + return Err(Error::::UnknownCollection.into()); + } + + let origin = maybe_check_origin.ok_or(Error::::WrongOrigin)?; + Allowances::::remove((&collection, &origin, &delegate)); + + Self::deposit_event(Event::ApprovalCancelled { + collection, + owner: origin, + item: None, + delegate, + }); + + Ok(()) + } + + pub fn check_allowance( + collection: &T::CollectionId, + item: &Option, + owner: &T::AccountId, + delegate: &T::AccountId, + ) -> Result<(), DispatchError> { + // Check if a `delegate` has a permission to spend the collection. + if Allowances::::get((&collection, &owner, &delegate)) { + if let Some(item) = item { + Item::::get(&collection, &item).ok_or(Error::::UnknownItem)?; + }; + return Ok(()); + } + // Check if a `delegate` has a permission to spend the collection item. + if let Some(item) = item { + let details = + Item::::get(&collection, &item).ok_or(Error::::UnknownItem)?; + + let deadline = details.approvals.get(&delegate).ok_or(Error::::NoPermission)?; + if let Some(d) = deadline { + let block_number = frame_system::Pallet::::block_number(); + ensure!(block_number <= *d, Error::::ApprovalExpired); + } + return Ok(()); + }; + Err(Error::::NoPermission.into()) + } } diff --git a/pallets/nfts/src/features/create_delete_collection.rs b/pallets/nfts/src/features/create_delete_collection.rs index 348ec6b9..b7efd03a 100644 --- a/pallets/nfts/src/features/create_delete_collection.rs +++ b/pallets/nfts/src/features/create_delete_collection.rs @@ -137,6 +137,11 @@ impl, I: 'static> Pallet { } } + // TODO: Do we need another storage item to keep track of number of holders of a + // collection + let _ = + AccountBalance::::clear_prefix(collection, collection_details.items, None); + let _ = Allowances::::clear_prefix((collection,), collection_details.items, None); CollectionAccount::::remove(&collection_details.owner, &collection); T::Currency::unreserve(&collection_details.owner, collection_details.owner_deposit); CollectionConfigOf::::remove(&collection); diff --git a/pallets/nfts/src/features/create_delete_item.rs b/pallets/nfts/src/features/create_delete_item.rs index e9843b2e..cc29f8da 100644 --- a/pallets/nfts/src/features/create_delete_item.rs +++ b/pallets/nfts/src/features/create_delete_item.rs @@ -69,6 +69,9 @@ impl, I: 'static> Pallet { } collection_details.items.saturating_inc(); + AccountBalance::::mutate(collection, &mint_to, |balance| { + balance.saturating_inc(); + }); let collection_config = Self::get_collection_config(&collection)?; let deposit_amount = match collection_config @@ -263,6 +266,9 @@ impl, I: 'static> Pallet { ItemPriceOf::::remove(&collection, &item); PendingSwapOf::::remove(&collection, &item); ItemAttributesApprovalsOf::::remove(&collection, &item); + AccountBalance::::mutate(collection, &owner, |balance| { + balance.saturating_dec(); + }); if remove_config { ItemConfigOf::::remove(&collection, &item); diff --git a/pallets/nfts/src/features/transfer.rs b/pallets/nfts/src/features/transfer.rs index b7223a7c..3b25b014 100644 --- a/pallets/nfts/src/features/transfer.rs +++ b/pallets/nfts/src/features/transfer.rs @@ -87,6 +87,14 @@ impl, I: 'static> Pallet { // Perform the transfer with custom details using the provided closure. with_details(&collection_details, &mut details)?; + // Update account balances. + AccountBalance::::mutate(collection, &details.owner, |balance| { + balance.saturating_dec(); + }); + AccountBalance::::mutate(collection, &dest, |balance| { + balance.saturating_inc(); + }); + // Update account ownership information. Account::::remove((&details.owner, &collection, &item)); Account::::insert((&dest, &collection, &item), ()); @@ -137,7 +145,7 @@ impl, I: 'static> Pallet { // Check if the `origin` is the current owner of the collection. ensure!(origin == details.owner, Error::::NoPermission); if details.owner == new_owner { - return Ok(()) + return Ok(()); } // Move the deposit to the new owner. @@ -212,7 +220,7 @@ impl, I: 'static> Pallet { Collection::::try_mutate(collection, |maybe_details| { let details = maybe_details.as_mut().ok_or(Error::::UnknownCollection)?; if details.owner == owner { - return Ok(()) + return Ok(()); } // Move the deposit to the new owner. diff --git a/pallets/nfts/src/lib.rs b/pallets/nfts/src/lib.rs index 89bfb963..bc8b67b6 100644 --- a/pallets/nfts/src/lib.rs +++ b/pallets/nfts/src/lib.rs @@ -402,6 +402,34 @@ pub mod pallet { pub type CollectionConfigOf, I: 'static = ()> = StorageMap<_, Blake2_128Concat, T::CollectionId, CollectionConfigFor, OptionQuery>; + /// Number of collection items that accounts own. + #[pallet::storage] + pub type AccountBalance, I: 'static = ()> = StorageDoubleMap< + _, + Twox64Concat, + T::CollectionId, + Blake2_128Concat, + T::AccountId, + u32, + ValueQuery, + >; + + /// Permission for the delegate to transfer all owner's items within a collection. + #[pallet::storage] + pub type Allowances, I: 'static = ()> = StorageNMap< + _, + ( + // Collection ID. + NMapKey, + // Collection Owner Id. + NMapKey, + // Delegate Id. + NMapKey, + ), + bool, + ValueQuery, + >; + /// Config of an item. #[pallet::storage] pub type ItemConfigOf, I: 'static = ()> = StorageDoubleMap< @@ -460,7 +488,7 @@ pub mod pallet { /// a `delegate`. TransferApproved { collection: T::CollectionId, - item: T::ItemId, + item: Option, owner: T::AccountId, delegate: T::AccountId, deadline: Option>, @@ -469,7 +497,7 @@ pub mod pallet { /// `collection` was cancelled by its `owner`. ApprovalCancelled { collection: T::CollectionId, - item: T::ItemId, + item: Option, owner: T::AccountId, delegate: T::AccountId, }, @@ -1030,12 +1058,7 @@ pub mod pallet { Self::do_transfer(collection, item, dest, |_, details| { if details.owner != origin { - let deadline = - details.approvals.get(&origin).ok_or(Error::::NoPermission)?; - if let Some(d) = deadline { - let block_number = frame_system::Pallet::::block_number(); - ensure!(block_number <= *d, Error::::ApprovalExpired); - } + Self::check_allowance(&collection, &Some(item), &details.owner, &origin)?; } Ok(()) }) @@ -1090,10 +1113,10 @@ pub mod pallet { if T::Currency::reserve(&details.deposit.account, deposit - old).is_err() { // NOTE: No alterations made to collection_details in this iteration so far, // so this is OK to do. - continue + continue; } } else { - continue + continue; } details.deposit.amount = deposit; Item::::insert(&collection, &item, &details); @@ -1292,7 +1315,7 @@ pub mod pallet { pub fn approve_transfer( origin: OriginFor, collection: T::CollectionId, - item: T::ItemId, + maybe_item: Option, delegate: AccountIdLookupOf, maybe_deadline: Option>, ) -> DispatchResult { @@ -1300,13 +1323,16 @@ pub mod pallet { .map(|_| None) .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; let delegate = T::Lookup::lookup(delegate)?; - Self::do_approve_transfer( - maybe_check_origin, - collection, - item, - delegate, - maybe_deadline, - ) + match maybe_item { + Some(item) => Self::do_approve_transfer( + maybe_check_origin, + collection, + item, + delegate, + maybe_deadline, + ), + None => Self::do_approve_collection(maybe_check_origin, collection, delegate), + } } /// Cancel one of the transfer approvals for a specific item. @@ -1328,14 +1354,18 @@ pub mod pallet { pub fn cancel_approval( origin: OriginFor, collection: T::CollectionId, - item: T::ItemId, + maybe_item: Option, delegate: AccountIdLookupOf, ) -> DispatchResult { let maybe_check_origin = T::ForceOrigin::try_origin(origin) .map(|_| None) .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; let delegate = T::Lookup::lookup(delegate)?; - Self::do_cancel_approval(maybe_check_origin, collection, item, delegate) + match maybe_item { + Some(item) => + Self::do_cancel_approval(maybe_check_origin, collection, item, delegate), + None => Self::do_cancel_collection(maybe_check_origin, collection, delegate), + } } /// Cancel all the approvals of a specific item. diff --git a/pallets/nfts/src/tests.rs b/pallets/nfts/src/tests.rs index 44f2f32a..397a715c 100644 --- a/pallets/nfts/src/tests.rs +++ b/pallets/nfts/src/tests.rs @@ -164,6 +164,7 @@ fn basic_minting_should_work() { )); assert_eq!(collections(), vec![(account(1), 0)]); assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 42, account(1), None)); + assert_eq!(AccountBalance::::get(0, account(1)), 1); assert_eq!(items(), vec![(account(1), 0, 42)]); assert_ok!(Nfts::force_create( @@ -173,6 +174,7 @@ fn basic_minting_should_work() { )); assert_eq!(collections(), vec![(account(1), 0), (account(2), 1)]); assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(2)), 1, 69, account(1), None)); + assert_eq!(AccountBalance::::get(1, account(1)), 1); assert_eq!(items(), vec![(account(1), 0, 42), (account(1), 1, 69)]); }); } @@ -204,6 +206,7 @@ fn lifecycle_should_work() { account(10), default_item_config() )); + assert_eq!(AccountBalance::::get(0, account(10)), 1); assert_eq!(Balances::reserved_balance(&account(1)), 6); assert_ok!(Nfts::force_mint( RuntimeOrigin::signed(account(1)), @@ -212,8 +215,10 @@ fn lifecycle_should_work() { account(20), default_item_config() )); + assert_eq!(AccountBalance::::get(0, account(20)), 1); assert_eq!(Balances::reserved_balance(&account(1)), 7); assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 70, account(1), None)); + assert_eq!(AccountBalance::::get(0, account(1)), 1); assert_eq!(items(), vec![(account(1), 0, 70), (account(10), 0, 42), (account(20), 0, 69)]); assert_eq!(Collection::::get(0).unwrap().items, 3); assert_eq!(Collection::::get(0).unwrap().item_metadatas, 0); @@ -221,6 +226,8 @@ fn lifecycle_should_work() { assert_eq!(Balances::reserved_balance(&account(1)), 8); assert_ok!(Nfts::transfer(RuntimeOrigin::signed(account(1)), 0, 70, account(2))); + assert_eq!(AccountBalance::::get(0, account(1)), 0); + assert_eq!(AccountBalance::::get(0, account(2)), 1); assert_eq!(Balances::reserved_balance(&account(1)), 8); assert_eq!(Balances::reserved_balance(&account(2)), 0); @@ -238,6 +245,7 @@ fn lifecycle_should_work() { Nfts::destroy(RuntimeOrigin::signed(account(1)), 0, w), Error::::CollectionNotEmpty ); + assert_eq!(AccountBalance::::get(0, account(1)), 0); assert_ok!(Nfts::set_attribute( RuntimeOrigin::signed(account(1)), @@ -248,7 +256,9 @@ fn lifecycle_should_work() { bvec![0], )); assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(10)), 0, 42)); + assert_eq!(AccountBalance::::get(0, account(10)), 0); assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(20)), 0, 69)); + assert_eq!(AccountBalance::::get(0, account(10)), 0); assert_ok!(Nfts::burn(RuntimeOrigin::root(), 0, 70)); let w = Nfts::get_destroy_witness(&0).unwrap(); @@ -256,6 +266,7 @@ fn lifecycle_should_work() { assert_eq!(w.item_metadatas, 0); assert_eq!(w.item_configs, 0); assert_ok!(Nfts::destroy(RuntimeOrigin::signed(account(1)), 0, w)); + assert_eq!(AccountBalance::::get(0, account(1)), 0); assert_eq!(Balances::reserved_balance(&account(1)), 0); assert!(!Collection::::contains_key(0)); @@ -305,6 +316,14 @@ fn destroy_should_work() { )); assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 42, account(2), None)); + assert_eq!(AccountBalance::::get(0, account(2)), 1); + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(account(2)), + 0, + None, + account(3), + None + )); assert_noop!( Nfts::destroy( RuntimeOrigin::signed(account(1)), @@ -323,6 +342,8 @@ fn destroy_should_work() { 0, Nfts::get_destroy_witness(&0).unwrap() )); + assert_eq!(AccountBalance::::iter_prefix(0).count(), 0); + assert_eq!(Allowances::::iter_prefix((0,)).count(), 0); assert!(!ItemConfigOf::::contains_key(0, 42)); assert_eq!(ItemConfigOf::::iter_prefix(0).count() as u32, 0); }); @@ -337,6 +358,7 @@ fn mint_should_work() { default_collection_config() )); assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, 42, account(1), None)); + assert_eq!(AccountBalance::::get(0, account(1)), 1); assert_eq!(Nfts::owner(0, 42).unwrap(), account(1)); assert_eq!(collections(), vec![(account(1), 0)]); assert_eq!(items(), vec![(account(1), 0, 42)]); @@ -402,6 +424,7 @@ fn mint_should_work() { account(2), Some(MintWitness { mint_price: Some(1), ..Default::default() }) )); + assert_eq!(AccountBalance::::get(0, account(2)), 1); assert_eq!(Balances::total_balance(&account(2)), 99); // validate types @@ -440,6 +463,7 @@ fn mint_should_work() { account(2), Some(MintWitness { owned_item: Some(43), ..Default::default() }) )); + assert_eq!(AccountBalance::::get(1, account(2)), 1); assert!(events().contains(&Event::::PalletAttributeSet { collection: 0, item: Some(43), @@ -478,6 +502,8 @@ fn transfer_should_work() { )); assert_ok!(Nfts::transfer(RuntimeOrigin::signed(account(2)), 0, 42, account(3))); + assert_eq!(AccountBalance::::get(0, account(2)), 0); + assert_eq!(AccountBalance::::get(0, account(3)), 1); assert_eq!(items(), vec![(account(3), 0, 42)]); assert_noop!( Nfts::transfer(RuntimeOrigin::signed(account(2)), 0, 42, account(4)), @@ -487,12 +513,14 @@ fn transfer_should_work() { assert_ok!(Nfts::approve_transfer( RuntimeOrigin::signed(account(3)), 0, - 42, + Some(42), account(2), None )); assert_ok!(Nfts::transfer(RuntimeOrigin::signed(account(2)), 0, 42, account(4))); - + assert_eq!(AccountBalance::::get(0, account(2)), 0); + assert_eq!(AccountBalance::::get(0, account(3)), 0); + assert_eq!(AccountBalance::::get(0, account(4)), 1); // validate we can't transfer non-transferable items let collection_id = 1; assert_ok!(Nfts::force_create( @@ -1746,15 +1774,17 @@ fn burn_works() { account(5), default_item_config() )); + assert_eq!(AccountBalance::::get(0, account(5)), 2); assert_eq!(Balances::reserved_balance(account(1)), 2); assert_noop!( Nfts::burn(RuntimeOrigin::signed(account(0)), 0, 42), Error::::NoPermission ); - assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(5)), 0, 42)); + assert_eq!(AccountBalance::::get(0, account(5)), 1); assert_ok!(Nfts::burn(RuntimeOrigin::signed(account(5)), 0, 69)); + assert_eq!(AccountBalance::::get(0, account(5)), 0); assert_eq!(Balances::reserved_balance(account(1)), 0); }); } @@ -1777,7 +1807,7 @@ fn approval_lifecycle_works() { assert_ok!(Nfts::approve_transfer( RuntimeOrigin::signed(account(2)), 0, - 42, + Some(42), account(3), None )); @@ -1791,7 +1821,7 @@ fn approval_lifecycle_works() { assert_ok!(Nfts::approve_transfer( RuntimeOrigin::signed(account(4)), 0, - 42, + Some(42), account(2), None )); @@ -1819,7 +1849,7 @@ fn approval_lifecycle_works() { Nfts::approve_transfer( RuntimeOrigin::signed(account(1)), collection_id, - 1, + Some(1), account(2), None ), @@ -1829,7 +1859,7 @@ fn approval_lifecycle_works() { } #[test] -fn cancel_approval_works() { +fn check_allowance_works() { new_test_ext().execute_with(|| { assert_ok!(Nfts::force_create( RuntimeOrigin::root(), @@ -1844,33 +1874,95 @@ fn cancel_approval_works() { default_item_config() )); + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(account(1)), + 0, + None, + account(2), + None + )); + + // collection transfer approved. + assert_noop!( + Nfts::check_allowance(&1, &None, &account(1), &account(2)), + Error::::NoPermission + ); + assert_noop!( + Nfts::check_allowance(&1, &Some(43), &account(1), &account(2)), + Error::::UnknownItem + ); + assert_ok!(Nfts::check_allowance(&0, &None, &account(1), &account(2))); + assert_ok!(Nfts::check_allowance(&0, &Some(42), &account(1), &account(2))); + + // collection item transfer approved. assert_ok!(Nfts::approve_transfer( RuntimeOrigin::signed(account(2)), 0, + Some(42), + account(3), + None + )); + + assert_noop!( + Nfts::check_allowance(&0, &Some(43), &account(2), &account(3)), + Error::::UnknownItem + ); + assert_noop!( + Nfts::check_allowance(&0, &Some(42), &account(2), &account(4)), + Error::::NoPermission + ); + assert_ok!(Nfts::check_allowance(&0, &Some(42), &account(2), &account(3))); + }); +} + +#[test] +fn cancel_approval_works() { + new_test_ext().execute_with(|| { + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + default_collection_config() + )); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, 42, + account(2), + default_item_config() + )); + + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(account(2)), + 0, + Some(42), account(3), None )); assert_noop!( - Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 1, 42, account(3)), + Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 1, Some(42), account(3)), Error::::UnknownItem ); assert_noop!( - Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, 43, account(3)), + Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, Some(43), account(3)), Error::::UnknownItem ); assert_noop!( - Nfts::cancel_approval(RuntimeOrigin::signed(account(3)), 0, 42, account(3)), + Nfts::cancel_approval(RuntimeOrigin::signed(account(3)), 0, Some(42), account(3)), Error::::NoPermission ); assert_noop!( - Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, 42, account(4)), + Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, Some(42), account(4)), Error::::NotDelegate ); - assert_ok!(Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, 42, account(3))); + assert_ok!(Nfts::cancel_approval( + RuntimeOrigin::signed(account(2)), + 0, + Some(42), + account(3) + )); assert_noop!( - Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, 42, account(3)), + Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, Some(42), account(3)), Error::::NotDelegate ); @@ -1887,22 +1979,71 @@ fn cancel_approval_works() { assert_ok!(Nfts::approve_transfer( RuntimeOrigin::signed(account(2)), 0, - 42, + Some(42), account(3), Some(2) )); assert_noop!( - Nfts::cancel_approval(RuntimeOrigin::signed(account(5)), 0, 42, account(3)), + Nfts::cancel_approval(RuntimeOrigin::signed(account(5)), 0, Some(42), account(3)), Error::::NoPermission ); System::set_block_number(current_block + 3); // 5 can cancel the approval since the deadline has passed. - assert_ok!(Nfts::cancel_approval(RuntimeOrigin::signed(account(5)), 0, 42, account(3))); + assert_ok!(Nfts::cancel_approval( + RuntimeOrigin::signed(account(5)), + 0, + Some(42), + account(3) + )); assert_eq!(approvals(0, 69), vec![]); }); } +#[test] +fn cancel_approval_collection_works_with_admin() { + new_test_ext().execute_with(|| { + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + default_collection_config() + )); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, + 42, + account(2), + default_item_config() + )); + + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(account(2)), + 0, + None, + account(3), + None + )); + assert_noop!( + Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 1, None, account(3)), + Error::::UnknownCollection + ); + + assert_ok!(Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, None, account(3))); + assert!(events().contains(&Event::::ApprovalCancelled { + collection: 0, + item: None, + owner: account(2), + delegate: account(3) + })); + assert_eq!(Allowances::::get((0, account(2), account(3))), false); + + assert_noop!( + Nfts::transfer(RuntimeOrigin::signed(account(3)), 0, 42, account(4)), + Error::::NoPermission + ); + }); +} + #[test] fn approving_multiple_accounts_works() { new_test_ext().execute_with(|| { @@ -1924,21 +2065,21 @@ fn approving_multiple_accounts_works() { assert_ok!(Nfts::approve_transfer( RuntimeOrigin::signed(account(2)), 0, - 42, + Some(42), account(3), None )); assert_ok!(Nfts::approve_transfer( RuntimeOrigin::signed(account(2)), 0, - 42, + Some(42), account(4), None )); assert_ok!(Nfts::approve_transfer( RuntimeOrigin::signed(account(2)), 0, - 42, + Some(42), account(5), Some(2) )); @@ -1979,19 +2120,82 @@ fn approvals_limit_works() { assert_ok!(Nfts::approve_transfer( RuntimeOrigin::signed(account(2)), 0, - 42, + Some(42), account(i), None )); } // the limit is 10 assert_noop!( - Nfts::approve_transfer(RuntimeOrigin::signed(account(2)), 0, 42, account(14), None), + Nfts::approve_transfer( + RuntimeOrigin::signed(account(2)), + 0, + Some(42), + account(14), + None + ), Error::::ReachedApprovalLimit ); }); } +#[test] +fn approval_collection_works_with_admin() { + new_test_ext().execute_with(|| { + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + default_collection_config() + )); + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + account(1), + default_collection_config() + )); + assert_ok!(Nfts::force_mint( + RuntimeOrigin::signed(account(1)), + 0, + 42, + account(2), + default_item_config() + )); + + // Error::ItemsNonTransferable. + assert_ok!(Nfts::lock_collection( + RuntimeOrigin::signed(account(1)), + 1, + CollectionSettings::from_disabled(CollectionSetting::TransferableItems.into()) + )); + assert_noop!( + Nfts::approve_transfer(RuntimeOrigin::signed(account(1)), 1, None, account(2), None), + Error::::ItemsNonTransferable + ); + + // Error::UnknownCollection. + assert_noop!( + Nfts::approve_transfer(RuntimeOrigin::signed(account(2)), 2, None, account(3), None), + Error::::UnknownCollection + ); + + assert_ok!(Nfts::approve_transfer( + RuntimeOrigin::signed(account(2)), + 0, + None, + account(3), + None + )); + assert!(events().contains(&Event::::TransferApproved { + collection: 0, + item: None, + owner: account(2), + delegate: account(3), + deadline: None + })); + assert_eq!(Allowances::::get((0, account(2), account(3))), true); + assert_ok!(Nfts::transfer(RuntimeOrigin::signed(account(3)), 0, 42, account(4))); + }); +} + #[test] fn approval_deadline_works() { new_test_ext().execute_with(|| { @@ -2015,7 +2219,7 @@ fn approval_deadline_works() { assert_ok!(Nfts::approve_transfer( RuntimeOrigin::signed(account(2)), 0, - 42, + Some(42), account(3), Some(2) )); @@ -2034,7 +2238,7 @@ fn approval_deadline_works() { assert_ok!(Nfts::approve_transfer( RuntimeOrigin::signed(account(4)), 0, - 42, + Some(42), account(6), Some(4) )); @@ -2063,26 +2267,31 @@ fn cancel_approval_works_with_admin() { assert_ok!(Nfts::approve_transfer( RuntimeOrigin::signed(account(2)), 0, - 42, + Some(42), account(3), None )); assert_noop!( - Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 1, 42, account(1)), + Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 1, Some(42), account(1)), Error::::UnknownItem ); assert_noop!( - Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, 43, account(1)), + Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, Some(43), account(1)), Error::::UnknownItem ); assert_noop!( - Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, 42, account(4)), + Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, Some(42), account(4)), Error::::NotDelegate ); - assert_ok!(Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, 42, account(3))); + assert_ok!(Nfts::cancel_approval( + RuntimeOrigin::signed(account(2)), + 0, + Some(42), + account(3) + )); assert_noop!( - Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, 42, account(1)), + Nfts::cancel_approval(RuntimeOrigin::signed(account(2)), 0, Some(42), account(1)), Error::::NotDelegate ); }); @@ -2107,26 +2316,26 @@ fn cancel_approval_works_with_force() { assert_ok!(Nfts::approve_transfer( RuntimeOrigin::signed(account(2)), 0, - 42, + Some(42), account(3), None )); assert_noop!( - Nfts::cancel_approval(RuntimeOrigin::root(), 1, 42, account(1)), + Nfts::cancel_approval(RuntimeOrigin::root(), 1, Some(42), account(1)), Error::::UnknownItem ); assert_noop!( - Nfts::cancel_approval(RuntimeOrigin::root(), 0, 43, account(1)), + Nfts::cancel_approval(RuntimeOrigin::root(), 0, Some(43), account(1)), Error::::UnknownItem ); assert_noop!( - Nfts::cancel_approval(RuntimeOrigin::root(), 0, 42, account(4)), + Nfts::cancel_approval(RuntimeOrigin::root(), 0, Some(42), account(4)), Error::::NotDelegate ); - assert_ok!(Nfts::cancel_approval(RuntimeOrigin::root(), 0, 42, account(3))); + assert_ok!(Nfts::cancel_approval(RuntimeOrigin::root(), 0, Some(42), account(3))); assert_noop!( - Nfts::cancel_approval(RuntimeOrigin::root(), 0, 42, account(1)), + Nfts::cancel_approval(RuntimeOrigin::root(), 0, Some(42), account(1)), Error::::NotDelegate ); }); @@ -2151,14 +2360,14 @@ fn clear_all_transfer_approvals_works() { assert_ok!(Nfts::approve_transfer( RuntimeOrigin::signed(account(2)), 0, - 42, + Some(42), account(3), None )); assert_ok!(Nfts::approve_transfer( RuntimeOrigin::signed(account(2)), 0, - 42, + Some(42), account(4), None )); @@ -2188,6 +2397,36 @@ fn clear_all_transfer_approvals_works() { }); } +#[test] +fn total_supply_should_works() { + new_test_ext().execute_with(|| { + let collection_id = 0; + let user_id = account(1); + let total_items = 10; + + // no collection. + assert_eq!(Nfts::collection_items(collection_id), None); + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + user_id.clone(), + default_collection_config() + )); + + // mint items and validate the total supply. + (0..total_items).into_iter().for_each(|i| { + assert_ok!(Nfts::force_mint( + RuntimeOrigin::root(), + collection_id, + i, + user_id.clone(), + ItemConfig::default() + )); + }); + assert_eq!(Nfts::collection_items(collection_id), Some(total_items)); + }); +} + #[test] fn max_supply_should_work() { new_test_ext().execute_with(|| { @@ -2524,6 +2763,7 @@ fn buy_item_should_work() { item_1, price_1 + 1, )); + assert_eq!(AccountBalance::::get(collection_id, user_2.clone()), 1); // validate the new owner & balances let item = Item::::get(collection_id, item_1).unwrap(); @@ -2891,6 +3131,8 @@ fn claim_swap_should_work() { default_item_config(), )); + assert_eq!(AccountBalance::::get(collection_id, user_1.clone()), 2); + assert_eq!(AccountBalance::::get(collection_id, user_2.clone()), 3); assert_ok!(Nfts::create_swap( RuntimeOrigin::signed(user_1.clone()), collection_id, @@ -2981,6 +3223,8 @@ fn claim_swap_should_work() { item_1, Some(price_with_direction.clone()), )); + assert_eq!(AccountBalance::::get(collection_id, user_1.clone()), 2); + assert_eq!(AccountBalance::::get(collection_id, user_2.clone()), 3); // validate the new owner let item = Item::::get(collection_id, item_1).unwrap(); @@ -3169,7 +3413,7 @@ fn pallet_level_feature_flags_should_work() { Nfts::approve_transfer( RuntimeOrigin::signed(user_id.clone()), collection_id, - item_id, + Some(item_id), account(2), None ), diff --git a/pallets/nfts/src/types.rs b/pallets/nfts/src/types.rs index f08f1d09..061352c0 100644 --- a/pallets/nfts/src/types.rs +++ b/pallets/nfts/src/types.rs @@ -94,18 +94,18 @@ pub(super) type PreSignedAttributesOf = PreSignedAttributes< #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub struct CollectionDetails { /// Collection's owner. - pub(super) owner: AccountId, + pub owner: AccountId, /// The total balance deposited by the owner for all the storage data associated with this /// collection. Used by `destroy`. - pub(super) owner_deposit: DepositBalance, + pub owner_deposit: DepositBalance, /// The total number of outstanding items of this collection. - pub(super) items: u32, + pub items: u32, /// The total number of outstanding item metadata of this collection. - pub(super) item_metadatas: u32, + pub item_metadatas: u32, /// The total number of outstanding item configs of this collection. - pub(super) item_configs: u32, + pub item_configs: u32, /// The total number of attributes for this collection. - pub(super) attributes: u32, + pub attributes: u32, } /// Witness data for the destroy transactions. @@ -145,21 +145,21 @@ pub struct MintWitness { #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default, TypeInfo, MaxEncodedLen)] pub struct ItemDetails { /// The owner of this item. - pub(super) owner: AccountId, + pub owner: AccountId, /// The approved transferrer of this item, if one is set. - pub(super) approvals: Approvals, + pub approvals: Approvals, /// The amount held in the pallet's default account for this item. Free-hold items will have /// this as zero. - pub(super) deposit: Deposit, + pub deposit: Deposit, } /// Information about the reserved item deposit. #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub struct ItemDeposit { /// A depositor account. - pub(super) account: AccountId, + pub account: AccountId, /// An amount that gets reserved. - pub(super) amount: DepositBalance, + pub amount: DepositBalance, } /// Information about the collection's metadata.