Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 44 additions & 10 deletions pallets/rmrk-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use frame_system::ensure_signed;
use sp_runtime::traits::{AtLeast32BitUnsigned, CheckedAdd, One, StaticLookup, Zero};
use sp_std::{convert::TryInto, vec::Vec};

use types::{ClassInfo, InstanceInfo};
use types::{AccountIdOrCollectionNftTuple, ClassInfo, InstanceInfo};

#[cfg(test)]
mod mock;
Expand All @@ -23,6 +23,8 @@ pub type ClassInfoOf<T> = ClassInfo<BoundedVec<u8, <T as pallet_uniques::Config>
pub type InstanceInfoOf<T> = InstanceInfo<
<T as frame_system::Config>::AccountId,
BoundedVec<u8, <T as pallet_uniques::Config>::StringLimit>,
<T as pallet::Config>::CollectionId,
<T as pallet::Config>::NftId,
>;

pub mod types;
Expand Down Expand Up @@ -112,7 +114,12 @@ pub mod pallet {
NftMinted(T::AccountId, T::CollectionId, T::NftId),
NFTBurned(T::AccountId, T::NftId),
CollectionBurned(T::AccountId, T::CollectionId),
NFTSent(T::AccountId, T::AccountId, T::CollectionId, T::NftId),
NFTSent(
T::AccountId,
AccountIdOrCollectionNftTuple<T::AccountId, T::CollectionId, T::NftId>,
T::CollectionId,
T::NftId,
),
IssuerChanged(T::AccountId, T::AccountId, T::CollectionId),
PropertySet(
T::CollectionId,
Expand Down Expand Up @@ -141,6 +148,7 @@ pub mod pallet {
NotInRange,
RoyaltyNotSet,
CollectionUnknown,
NoPermission,
}

#[pallet::call]
Expand All @@ -158,6 +166,7 @@ pub mod pallet {
#[transactional]
pub fn mint_nft(
origin: OriginFor<T>,
owner: T::AccountId,
collection_id: T::CollectionId,
author: Option<T::AccountId>,
royalty: Option<u8>,
Expand Down Expand Up @@ -188,10 +197,13 @@ pub mod pallet {
let author = author.ok_or(Error::<T>::AuthorNotSet)?;
let royalty = royalty.ok_or(Error::<T>::RoyaltyNotSet)?;

let rootowner = owner.clone();
let owner = AccountIdOrCollectionNftTuple::AccountId(owner.clone());

NFTs::<T>::insert(
collection_id,
nft_id,
InstanceInfo { author, royalty, metadata: metadata_bounded },
InstanceInfo { owner, rootowner, author, royalty, metadata: metadata_bounded },
);

Self::deposit_event(Event::NftMinted(
Expand All @@ -203,7 +215,7 @@ pub mod pallet {
Ok(())
}

/// Mint a collection
/// Create a collection
#[pallet::weight(10_000 + T::DbWeight::get().reads_writes(1,1))]
#[transactional]
pub fn create_collection(origin: OriginFor<T>, metadata: Vec<u8>) -> DispatchResult {
Expand Down Expand Up @@ -268,24 +280,47 @@ pub mod pallet {
Ok(())
}

/// transfer NFT from account A to account B
/// transfer NFT from account A to (account B or NFT)
#[pallet::weight(10_000 + T::DbWeight::get().reads_writes(1,1))]
#[transactional]
pub fn send(
origin: OriginFor<T>,
collection_id: T::CollectionId,
nft_id: T::NftId,
dest: <T::Lookup as StaticLookup>::Source,
new_owner: AccountIdOrCollectionNftTuple<T::AccountId, T::CollectionId, T::NftId>,
) -> DispatchResult {
let sender = match T::ProtocolOrigin::try_origin(origin) {
Ok(_) => None,
Err(origin) => Some(ensure_signed(origin)?),
};
let dest = T::Lookup::lookup(dest)?;
// TODO

let mut sending_nft =
NFTs::<T>::get(collection_id, nft_id).ok_or(Error::<T>::NoAvailableNftId)?;
ensure!(
sending_nft.rootowner == sender.clone().unwrap_or_default(),
Error::<T>::NoPermission
);

match new_owner.clone() {
AccountIdOrCollectionNftTuple::AccountId(account_id) => {
sending_nft.rootowner = account_id.clone();
}
AccountIdOrCollectionNftTuple::CollectionAndNftTuple(cid, nid) => {
let recipient_nft =
NFTs::<T>::get(cid, nid).ok_or(Error::<T>::NoAvailableNftId)?;
if sending_nft.rootowner != recipient_nft.rootowner {
sending_nft.rootowner = recipient_nft.rootowner
}
}
};
sending_nft.owner = new_owner.clone();

NFTs::<T>::remove(collection_id, nft_id);
NFTs::<T>::insert(collection_id, nft_id, sending_nft);

Self::deposit_event(Event::NFTSent(
sender.unwrap_or_default(),
dest,
new_owner,
collection_id,
nft_id,
));
Expand Down Expand Up @@ -365,7 +400,6 @@ pub mod pallet {
Self::deposit_event(Event::ResourceAdded(nft_id, resource_id));
Ok(())
}

/// accept the addition of a new resource to an existing NFT
#[pallet::weight(10_000 + T::DbWeight::get().reads_writes(1,1))]
#[transactional]
Expand Down
95 changes: 95 additions & 0 deletions pallets/rmrk-core/src/tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use frame_support::{assert_noop, assert_ok, error::BadOrigin};
// use sp_runtime::AccountId32;

// use crate::types::ClassType;

Expand Down Expand Up @@ -44,20 +45,23 @@ fn mint_nft_works() {
assert_ok!(RMRKCore::create_collection(Origin::signed(ALICE), b"metadata".to_vec()));
assert_ok!(RMRKCore::mint_nft(
Origin::signed(ALICE),
ALICE,
0,
Some(ALICE),
Some(0),
Some(b"metadata".to_vec())
));
assert_ok!(RMRKCore::mint_nft(
Origin::signed(ALICE),
ALICE,
COLLECTION_ID_0,
Some(ALICE),
Some(20),
Some(b"metadata".to_vec())
));
assert_ok!(RMRKCore::mint_nft(
Origin::signed(BOB),
BOB,
COLLECTION_ID_0,
Some(CHARLIE),
Some(20),
Expand All @@ -66,6 +70,7 @@ fn mint_nft_works() {
assert_noop!(
RMRKCore::mint_nft(
Origin::signed(ALICE),
ALICE,
NOT_EXISTING_CLASS_ID,
Some(CHARLIE),
Some(20),
Expand All @@ -75,3 +80,93 @@ fn mint_nft_works() {
);
});
}
#[test]
fn send_nft_to_minted_nft_works() {
ExtBuilder::default().build().execute_with(|| {
let collection_metadata = stv("testing");
let nft_metadata = stv("testing");
assert_ok!(RMRKCore::create_collection(Origin::signed(ALICE), collection_metadata));
// Alice mints NFT (0, 0) [will be the parent]
assert_ok!(RMRKCore::mint_nft(
Origin::signed(ALICE),
ALICE,
0,
Some(ALICE),
Some(0),
Some(nft_metadata.clone())
));
// Alice mints NFT (0, 1) [will be the child]
assert_ok!(RMRKCore::mint_nft(
Origin::signed(ALICE),
ALICE,
0,
Some(ALICE),
Some(0),
Some(nft_metadata)
));
// Alice sends NFT (0, 0) [parent] to Bob
assert_ok!(RMRKCore::send(
Origin::signed(ALICE),
0,
0,
AccountIdOrCollectionNftTuple::AccountId(BOB),
));
// Alice sends NFT (0, 1) [child] to NFT (0, 0) [parent]
assert_ok!(RMRKCore::send(
Origin::signed(ALICE),
0,
1,
AccountIdOrCollectionNftTuple::CollectionAndNftTuple(0, 0),
));

// Check that NFT (0,1) [child] is owned by NFT (0,0) [parent]
assert_eq!(
RMRKCore::nfts(0, 1).unwrap().owner,
AccountIdOrCollectionNftTuple::CollectionAndNftTuple(0, 0),
);

// Check that Bob now root-owns NFT (0, 1) [child] since he wasn't originally rootowner
assert_eq!(RMRKCore::nfts(0, 1).unwrap().rootowner, BOB);

// Error if sender doesn't root-own sending NFT
assert_noop!(
RMRKCore::send(
Origin::signed(CHARLIE),
0,
0,
AccountIdOrCollectionNftTuple::AccountId(BOB)
),
Error::<Test>::NoPermission
);

// Error if sending NFT doesn't exist
assert_noop!(
RMRKCore::send(
Origin::signed(ALICE),
666,
666,
AccountIdOrCollectionNftTuple::CollectionAndNftTuple(0, 0)
),
Error::<Test>::NoAvailableNftId
);

// BOB can send back child NFT to ALICE
assert_ok!(RMRKCore::send(
Origin::signed(BOB),
0,
1,
AccountIdOrCollectionNftTuple::AccountId(ALICE)
));

// Error if recipient is NFT and that NFT doesn't exist
assert_noop!(
RMRKCore::send(
Origin::signed(ALICE),
0,
1,
AccountIdOrCollectionNftTuple::CollectionAndNftTuple(666, 666)
),
Error::<Test>::NoAvailableNftId
);
});
}
15 changes: 13 additions & 2 deletions pallets/rmrk-core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ use serde::{Deserialize, Serialize};

use scale_info::TypeInfo;

#[derive(Encode, Decode, Eq, PartialEq, Copy, Clone, RuntimeDebug, TypeInfo)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub enum AccountIdOrCollectionNftTuple<AccountId, CollectionId, NftId> {
AccountId(AccountId),
CollectionAndNftTuple(CollectionId, NftId),
}

#[derive(Encode, Decode, Eq, Copy, PartialEq, Clone, RuntimeDebug, TypeInfo)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub struct ClassInfo<BoundedString> {
Expand All @@ -14,11 +21,15 @@ pub struct ClassInfo<BoundedString> {

#[derive(Encode, Decode, Eq, Copy, PartialEq, Clone, RuntimeDebug, TypeInfo)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub struct InstanceInfo<AccountId, BoundedString> {
pub struct InstanceInfo<AccountId, BoundedString, CollectionId, NftId> {
/// The rootowner of the account, must be an account
pub rootowner: AccountId,
/// The owner of the NFT, can be either an Account or a tuple (CollectionId, NftId)
pub owner: AccountIdOrCollectionNftTuple<AccountId, CollectionId, NftId>,
/// The user account which receives the royalty
pub author: AccountId,
/// Royalty in percent in range 0-99
pub royalty: u8,
/// Arbitrary data about an instance, e.g. IPFS hash
pub metadata: BoundedString,
}
}