diff --git a/pallets/living-assets-ownership/src/functions.rs b/pallets/living-assets-ownership/src/functions.rs index c9c8f39e..e73dc456 100644 --- a/pallets/living-assets-ownership/src/functions.rs +++ b/pallets/living-assets-ownership/src/functions.rs @@ -35,7 +35,7 @@ pub fn convert_asset_id_to_owner(value: U256) -> H160 { #[cfg(test)] mod tests { - use super::*; + use crate::{functions::convert_asset_id_to_owner, H160, U256}; #[test] fn check_convert_asset_id_to_owner() { @@ -43,4 +43,34 @@ mod tests { let expected_address = H160::from_low_u64_be(5); assert_eq!(convert_asset_id_to_owner(value), expected_address); } + + #[test] + fn check_two_assets_same_owner() { + // create two different assets + let asset1 = U256::from( + hex::decode("01C0F0f4ab324C46e55D02D0033343B4Be8A55532d").unwrap().as_slice(), + ); + let asset2 = U256::from( + hex::decode("03C0F0f4ab324C46e55D02D0033343B4Be8A55532d").unwrap().as_slice(), + ); + assert_ne!(asset1, asset2); + + // check asset in decimal format + assert_eq!( + U256::from_str_radix("01C0F0f4ab324C46e55D02D0033343B4Be8A55532d", 16).unwrap(), + U256::from_dec_str("2563001357829637001682277476112176020532353127213").unwrap() + ); + assert_eq!( + U256::from_str_radix("03C0F0f4ab324C46e55D02D0033343B4Be8A55532d", 16).unwrap(), + U256::from_dec_str("5486004632491442838089647141544742059844218213165").unwrap() + ); + + let mut owner = [0u8; 20]; + owner.copy_from_slice( + hex::decode("C0F0f4ab324C46e55D02D0033343B4Be8A55532d").unwrap().as_slice(), + ); + let expected_address = H160::from(owner); + assert_eq!(convert_asset_id_to_owner(asset1), expected_address); + assert_eq!(convert_asset_id_to_owner(asset2), expected_address); + } } diff --git a/pallets/living-assets-ownership/src/lib.rs b/pallets/living-assets-ownership/src/lib.rs index c1b64e0f..6acbb7c3 100644 --- a/pallets/living-assets-ownership/src/lib.rs +++ b/pallets/living-assets-ownership/src/lib.rs @@ -13,6 +13,7 @@ pub mod traits; #[frame_support::pallet] pub mod pallet { + use crate::functions::convert_asset_id_to_owner; use super::*; @@ -60,6 +61,15 @@ pub mod pallet { pub(super) type CollectionBaseURI = StorageMap<_, Blake2_128Concat, CollectionId, BaseURI, OptionQuery>; + /// Asset owner + #[pallet::storage] + pub(super) type AssetOwner = + StorageMap<_, Blake2_128Concat, U256, H160, OptionQuery>; + + fn asset_owner(key: U256) -> H160 { + AssetOwner::::get(key).unwrap_or_else(|| convert_asset_id_to_owner(key)) + } + /// Pallet events #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] @@ -67,6 +77,9 @@ pub mod pallet { /// Collection created /// parameters. [collection_id, who] CollectionCreated { collection_id: CollectionId, who: T::AccountId }, + /// Asset transferred to `who` + /// parameters. [asset_id_id, who] + AssetTransferred { asset_id: U256, receiver: H160 }, } // Errors inform users that something went wrong. @@ -75,8 +88,16 @@ pub mod pallet { pub enum Error { /// Collection id overflow CollectionIdOverflow, - /// Unexistent collection - UnexistentCollection, + /// Collection does not exist + CollectionDoesNotExist, + // NoPermission, + NoPermission, + // AssetDoesNotExist, + AssetDoesNotExist, + // CannotTransferSelf, + CannotTransferSelf, + // TransferToNullAddress, + TransferToNullAddress, } impl AsRef<[u8]> for Error { @@ -84,7 +105,11 @@ pub mod pallet { match self { Error::__Ignore(_, _) => b"__Ignore", Error::CollectionIdOverflow => b"CollectionIdOverflow", - Error::UnexistentCollection => b"UnexistentCollection", + Error::CollectionDoesNotExist => b"CollectionDoesNotExist", + Error::NoPermission => b"NoPermission", + Error::AssetDoesNotExist => b"AssetDoesNotExist", + Error::CannotTransferSelf => b"CannotTransferSelf", + Error::TransferToNullAddress => b"TransferToNullAddress", } } } @@ -127,15 +152,32 @@ pub mod pallet { type Error = Error; fn owner_of(collection_id: CollectionId, asset_id: U256) -> Result { - match CollectionBaseURI::::get(collection_id) { - Some(_) => Ok(convert_asset_id_to_owner(asset_id)), - None => Err(Error::UnexistentCollection), - } + Pallet::::collection_base_uri(collection_id).ok_or(Error::CollectionDoesNotExist)?; + Ok(asset_owner::(asset_id)) + } + + fn transfer_from( + origin: H160, + collection_id: CollectionId, + from: H160, + to: H160, + asset_id: U256, + ) -> Result<(), Self::Error> { + Pallet::::collection_base_uri(collection_id).ok_or(Error::CollectionDoesNotExist)?; + ensure!(origin == from, Error::NoPermission); + ensure!(asset_owner::(asset_id) == from, Error::NoPermission); + ensure!(from != to, Error::CannotTransferSelf); + ensure!(to != H160::zero(), Error::TransferToNullAddress); + + AssetOwner::::set(asset_id, Some(to.clone())); + Self::deposit_event(Event::AssetTransferred { asset_id, receiver: to }); + + Ok(()) } fn token_uri(collection_id: CollectionId, asset_id: U256) -> Result, Self::Error> { let base_uri = Pallet::::collection_base_uri(collection_id) - .ok_or(Error::UnexistentCollection)?; + .ok_or(Error::CollectionDoesNotExist)?; // concatenate base_uri with asset_id let mut token_uri = base_uri.to_vec(); diff --git a/pallets/living-assets-ownership/src/mock.rs b/pallets/living-assets-ownership/src/mock.rs index b8ba0c9d..9bb080b6 100644 --- a/pallets/living-assets-ownership/src/mock.rs +++ b/pallets/living-assets-ownership/src/mock.rs @@ -5,6 +5,7 @@ use sp_runtime::{ traits::{BlakeTwo256, IdentityLookup}, BuildStorage, }; +use sp_std::{boxed::Box, prelude::*}; type Block = frame_system::mocking::MockBlock; type Nonce = u32; diff --git a/pallets/living-assets-ownership/src/tests.rs b/pallets/living-assets-ownership/src/tests.rs index b012ccd4..ab812b74 100644 --- a/pallets/living-assets-ownership/src/tests.rs +++ b/pallets/living-assets-ownership/src/tests.rs @@ -1,16 +1,17 @@ use core::str::FromStr; use crate::{ - address_to_collection_id, collection_id_to_address, is_collection_address, mock::*, - CollectionError, Event, + address_to_collection_id, collection_id_to_address, is_collection_address, mock::*, AssetOwner, + CollectionBaseURI, CollectionError, Event, }; use frame_support::assert_ok; use sp_core::H160; -type AccountId = ::AccountId; type BaseURI = crate::BaseURI; +type AccountId = ::AccountId; const ALICE: AccountId = 0x1234; +const BOB: AccountId = 0x2234; #[test] fn base_uri_unexistent_collection_is_none() { @@ -126,7 +127,8 @@ mod traits { traits::{CollectionManager, Erc721}, Error, Event, }; - use frame_support::{assert_err, assert_ok}; + use frame_support::{assert_err, assert_noop, assert_ok}; + use sp_core::U256; #[test] fn base_uri_of_unexistent_collection_is_none() { @@ -223,7 +225,7 @@ mod traits { fn owner_of_asset_of_unexistent_collection_should_error() { new_test_ext().execute_with(|| { let result = ::owner_of(0, 2.into()); - assert_err!(result, Error::UnexistentCollection); + assert_err!(result, Error::CollectionDoesNotExist); }); } @@ -242,11 +244,122 @@ mod traits { }); } + #[test] + fn caller_is_not_current_owner_should_fail() { + let asset_id = U256::from(5); + let sender = H160::from_str("0000000000000000000000000000000000000006").unwrap(); + let receiver = H160::from_low_u64_be(BOB); + new_test_ext().execute_with(|| { + System::set_block_number(1); + assert!(AssetOwner::::get(asset_id).is_none()); + CollectionBaseURI::::insert(1, BaseURI::default()); + assert_noop!( + ::transfer_from( + H160::from_low_u64_be(ALICE), + 1, + sender, + receiver, + asset_id, + ), + Error::::NoPermission + ); + }); + } + + #[test] + fn sender_is_not_current_owner_should_fail() { + let asset_id = U256::from(5); + let sender = H160::from_str("0000000000000000000000000000000000000006").unwrap(); + let receiver = H160::from_low_u64_be(BOB); + new_test_ext().execute_with(|| { + System::set_block_number(1); + assert!(AssetOwner::::get(asset_id).is_none()); + CollectionBaseURI::::insert(1, BaseURI::default()); + assert_noop!( + ::transfer_from( + sender, 1, sender, receiver, asset_id, + ), + Error::::NoPermission + ); + }); + } + + #[test] + fn same_sender_and_receiver_should_fail() { + let asset_id = U256::from(5); + let sender = H160::from_str("0000000000000000000000000000000000000005").unwrap(); + new_test_ext().execute_with(|| { + System::set_block_number(1); + assert!(AssetOwner::::get(asset_id).is_none()); + CollectionBaseURI::::insert(1, BaseURI::default()); + assert_noop!( + ::transfer_from(sender, 1, sender, sender, asset_id,), + Error::::CannotTransferSelf + ); + }); + } + + #[test] + fn receiver_is_the_zero_address_should_fail() { + let asset_id = U256::from(5); + let sender = H160::from_str("0000000000000000000000000000000000000005").unwrap(); + let receiver = H160::from_str("0000000000000000000000000000000000000000").unwrap(); + new_test_ext().execute_with(|| { + System::set_block_number(1); + assert!(AssetOwner::::get(asset_id).is_none()); + CollectionBaseURI::::insert(1, BaseURI::default()); + assert_noop!( + ::transfer_from( + sender, 1, sender, receiver, asset_id, + ), + Error::::TransferToNullAddress + ); + }); + } + + #[test] + fn unexistent_collection_when_transfer_from_should_fail() { + let asset_id = U256::from(5); + let sender = H160::from_str("0000000000000000000000000000000000000005").unwrap(); + let receiver = H160::from_low_u64_be(BOB); + new_test_ext().execute_with(|| { + System::set_block_number(1); + assert!(AssetOwner::::get(asset_id).is_none()); + assert_noop!( + ::transfer_from( + sender, 1, sender, receiver, asset_id, + ), + Error::::CollectionDoesNotExist + ); + }); + } + + #[test] + fn sucessful_transfer_from_trait_should_work() { + let asset_id = U256::from( + hex::decode("03C0F0f4ab324C46e55D02D0033343B4Be8A55532d").unwrap().as_slice(), + ); + let sender = H160::from_str("C0F0f4ab324C46e55D02D0033343B4Be8A55532d").unwrap(); + let receiver = H160::from_low_u64_be(BOB); + new_test_ext().execute_with(|| { + System::set_block_number(1); + CollectionBaseURI::::insert(1, BaseURI::default()); + assert!(AssetOwner::::get(asset_id).is_none()); + assert_eq!(::owner_of(1, asset_id).unwrap(), sender); + assert_ok!(::transfer_from( + sender, 1, sender, receiver, asset_id, + )); + assert_eq!(AssetOwner::::get(asset_id).unwrap(), receiver); + assert_eq!(::owner_of(1, asset_id).unwrap(), receiver); + System::assert_last_event(Event::AssetTransferred { asset_id, receiver }.into()); + }); + } + #[test] fn token_uri_of_unexistent_collection() { new_test_ext().execute_with(|| { let result = ::token_uri(0, 2.into()); - assert_err!(result, Error::UnexistentCollection); + assert_err!(result, Error::CollectionDoesNotExist); }); } diff --git a/pallets/living-assets-ownership/src/traits.rs b/pallets/living-assets-ownership/src/traits.rs index b2110fa0..8af1063d 100644 --- a/pallets/living-assets-ownership/src/traits.rs +++ b/pallets/living-assets-ownership/src/traits.rs @@ -72,11 +72,27 @@ pub trait Erc721 { /// /// # Arguments /// - /// * `collection_id` - The unique identifier for the collection. /// * `asset_id` - The unique identifier for the asset within the collection. /// /// # Returns /// /// A `Vec` representing the URI of the asset or an error if retrieval fails. fn token_uri(collection_id: CollectionId, asset_id: U256) -> Result, Self::Error>; + + /// Transfers the ownership of a asset from one address to another address + /// + /// # Arguments + /// + /// * `origin` - The caller's address. + /// * `collection_id` - The unique identifier for the collection. + /// * `from` - The current owner of the asset. + /// * `to` - The new owner. + /// * `asset_id` - The unique identifier for the asset within the collection. + fn transfer_from( + origin: H160, + collection_id: CollectionId, + from: H160, + to: H160, + asset_id: U256, + ) -> Result<(), Self::Error>; } diff --git a/precompile/erc721/contracts/IERC721.sol b/precompile/erc721/contracts/IERC721.sol index c12dcb51..5c4014cf 100644 --- a/precompile/erc721/contracts/IERC721.sol +++ b/precompile/erc721/contracts/IERC721.sol @@ -9,4 +9,8 @@ interface IERC721 { function tokenURI(uint256 _tokenId) external view returns (string memory); function ownerOf(uint256 _tokenId) external view returns (address); + + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + + function transferFrom(address _from, address _to, uint256 _tokenId) external; } diff --git a/precompile/erc721/src/lib.rs b/precompile/erc721/src/lib.rs index 0df51793..21e9c357 100644 --- a/precompile/erc721/src/lib.rs +++ b/precompile/erc721/src/lib.rs @@ -1,12 +1,17 @@ #![cfg_attr(not(feature = "std"), no_std)] use fp_evm::{Precompile, PrecompileHandle, PrecompileOutput}; +use frame_support::pallet_prelude::*; use pallet_living_assets_ownership::{address_to_collection_id, CollectionId}; use precompile_utils::{ - revert, succeed, Address, Bytes, EvmDataWriter, EvmResult, FunctionModifier, - PrecompileHandleExt, + keccak256, revert, succeed, Address, Bytes, EvmDataWriter, EvmResult, FunctionModifier, LogExt, + LogsBuilder, PrecompileHandleExt, }; -use sp_core::U256; -use sp_std::{fmt::Debug, marker::PhantomData}; + +use sp_core::{H160, H256, U256}; +use sp_std::{fmt::Debug, marker::PhantomData, vec, vec::Vec}; + +/// Solidity selector of the TransferFrom log, which is the Keccak of the Log signature. +pub const SELECTOR_LOG_TRANSFER_FROM: [u8; 32] = keccak256!("Transfer(address,address,uint256)"); #[precompile_utils_macro::generate_function_selector] #[derive(Debug, PartialEq)] @@ -15,6 +20,8 @@ pub enum Action { TokenURI = "tokenURI(uint256)", /// Owner of OwnerOf = "ownerOf(uint256)", + /// Transfer from + TransferFrom = "transferFrom(address,address,uint256)", } /// Wrapper for the precompile function. @@ -34,11 +41,13 @@ where handle.check_function_modifier(match selector { Action::TokenURI => FunctionModifier::View, Action::OwnerOf => FunctionModifier::View, + Action::TransferFrom => FunctionModifier::NonPayable, })?; match selector { Action::TokenURI => Self::token_uri(collection_id, handle), Action::OwnerOf => Self::owner_of(collection_id, handle), + Action::TransferFrom => Self::transfer_from(collection_id, handle), } } } @@ -72,6 +81,33 @@ where let uri = AssetManager::token_uri(collection_id, asset_id).map_err(|err| revert(err))?; Ok(succeed(EvmDataWriter::new().write(Bytes(uri)).build())) } + + fn transfer_from( + collection_id: CollectionId, + handle: &mut impl PrecompileHandle, + ) -> EvmResult { + // get input data + let mut input = handle.read_input()?; + input.expect_arguments(3)?; + let from: H160 = input.read::
()?.into(); + let to: H160 = input.read::
()?.into(); + let asset_id: U256 = input.read()?; + + AssetManager::transfer_from(handle.context().caller, collection_id, from, to, asset_id) + .map_err(|err| revert(err))?; + + LogsBuilder::new(handle.context().address) + .log4( + SELECTOR_LOG_TRANSFER_FROM, + from, + to, + H256::from_slice(asset_id.encode().as_slice()), + Vec::new(), + ) + .record(handle)?; + + Ok(succeed(vec![])) + } } #[cfg(test)] diff --git a/precompile/erc721/src/tests.rs b/precompile/erc721/src/tests.rs index 31897078..df6125ae 100644 --- a/precompile/erc721/src/tests.rs +++ b/precompile/erc721/src/tests.rs @@ -12,6 +12,7 @@ type AccountId = H160; fn check_selectors() { assert_eq!(Action::OwnerOf as u32, 0x6352211E); assert_eq!(Action::TokenURI as u32, 0xC87B56DD); + assert_eq!(Action::TransferFrom as u32, 0x23b872dd); } #[test] @@ -19,7 +20,8 @@ fn owner_of_asset_should_return_an_address() { impl_precompile_mock_simple!( Mock, Ok(H160::from_str("ff00000000000000000000000000000012345678").unwrap()), - Ok(vec![]) + Ok(vec![]), + Ok(()) ); let owner_of_asset_4 = @@ -40,7 +42,7 @@ fn owner_of_asset_should_return_an_address() { #[test] fn if_mock_fails_should_return_the_error() { - impl_precompile_mock_simple!(Mock, Err("this is an error"), Ok(vec![])); + impl_precompile_mock_simple!(Mock, Err("this is an error"), Ok(vec![]), Ok(())); let owner_of_asset_4 = hex::decode("6352211e0000000000000000000000000000000000000000000000000000000000000004") @@ -54,7 +56,7 @@ fn if_mock_fails_should_return_the_error() { #[test] fn invalid_contract_address_should_error() { - impl_precompile_mock_simple!(Mock, Ok(H160::zero()), Ok(vec![])); + impl_precompile_mock_simple!(Mock, Ok(H160::zero()), Ok(vec![]), Ok(())); let mut handle = create_mock_handle_from_input(Vec::new()); handle.code_address = H160::zero(); @@ -65,7 +67,7 @@ fn invalid_contract_address_should_error() { #[test] fn token_owners_should_have_at_least_token_id_as_argument() { - impl_precompile_mock_simple!(Mock, Ok(H160::zero()), Ok(vec![])); + impl_precompile_mock_simple!(Mock, Ok(H160::zero()), Ok(vec![]), Ok(())); let owner_of_with_2_arguments: Vec = hex::decode("6352211e00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004") @@ -83,12 +85,118 @@ fn token_owners_should_have_at_least_token_id_as_argument() { assert_eq!(result.unwrap_err(), revert("input doesn't match expected length")); } +mod transfer_from { + use super::*; + use frame_support::assert_ok; + use precompile_utils::testing::create_mock_handle; + + #[test] + fn send_value_as_money_should_fail() { + impl_precompile_mock_simple!( + Mock, + // owner_of result + Ok(H160::from_str("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap()), + // token_uri result + Ok(vec![]), + // transfer_from result + Ok(()) + ); + + // test data + let from = H160::repeat_byte(0xAA); + let to = H160::repeat_byte(0x0); + let asset_id = 4; + let contract_address = H160::from_str("ffffffffffffffffffffffff0000000000000005"); + + let input_data = EvmDataWriter::new_with_selector(Action::TransferFrom) + .write(Address(from)) + .write(Address(to)) + .write(U256::from(asset_id)) + .build(); + + let mut handle = create_mock_handle(input_data, 0, 1, H160::zero()); + handle.code_address = contract_address.unwrap(); + let result = Mock::execute(&mut handle); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), revert("function is not payable")); + } + + #[test] + fn sucessful_transfer_should_work() { + impl_precompile_mock_simple!( + Mock, + // owner_of result + Ok(H160::from_str("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap()), + // token_uri result + Ok(vec![]), + // transfer_from result + Ok(()) + ); + + // test data + let from = H160::repeat_byte(0xAA); + let to = H160::repeat_byte(0xBB); + let asset_id = 4; + let contract_address = H160::from_str("ffffffffffffffffffffffff0000000000000005"); + + let input_data = EvmDataWriter::new_with_selector(Action::TransferFrom) + .write(Address(from)) + .write(Address(to)) + .write(U256::from(asset_id)) + .build(); + + let mut handle = create_mock_handle( + input_data, + 0, + 0, + H160::from_str("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), + ); + handle.code_address = contract_address.unwrap(); + assert_ok!(Mock::execute(&mut handle)); + } + #[test] + fn unsucessful_transfer_should_fail() { + impl_precompile_mock_simple!( + Mock, + // owner_of result + Ok(H160::from_str("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap()), + // token_uri result + Ok(vec![]), + // transfer_from result + Err("this is an error") + ); + + // test data + let from = H160::repeat_byte(0xAA); + let to = H160::repeat_byte(0xBB); + let asset_id = 4; + let contract_address = H160::from_str("ffffffffffffffffffffffff0000000000000005"); + + let input_data = EvmDataWriter::new_with_selector(Action::TransferFrom) + .write(Address(from)) + .write(Address(to)) + .write(U256::from(asset_id)) + .build(); + + let mut handle = create_mock_handle( + input_data, + 0, + 0, + H160::from_str("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), + ); + handle.code_address = contract_address.unwrap(); + let result = Mock::execute(&mut handle); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), revert("this is an error")); + } +} #[test] fn token_uri_should_return_a_string() { impl_precompile_mock_simple!( Mock, Ok(H160::zero()), - Ok("This is the token URI".to_string().into_bytes()) + Ok("This is the token URI".to_string().into_bytes()), + Ok(()) ); let input = EvmDataWriter::new_with_selector(Action::TokenURI).write(U256::from(4)).build(); @@ -128,17 +236,17 @@ mod helpers { /// ``` #[macro_export] macro_rules! impl_precompile_mock { - ($name:ident, $owner_of_collection:expr, $token_uri:expr) => { + ($name:ident, $owner_of_collection:expr, $token_uri:expr, $transfer_from:expr) => { struct Erc721Mock; impl pallet_living_assets_ownership::traits::Erc721 for Erc721Mock { type Error = &'static str; fn owner_of( - collectio_id: CollectionId, + collection_id: CollectionId, asset_id: U256, ) -> Result { - ($owner_of_collection)(collectio_id, asset_id) + ($owner_of_collection)(collection_id, asset_id) } fn token_uri( @@ -147,6 +255,16 @@ mod helpers { ) -> Result, Self::Error> { ($token_uri)(collectio_id, asset_id) } + + fn transfer_from( + origin: AccountId, + collection_id: CollectionId, + from: AccountId, + to: AccountId, + asset_id: U256, + ) -> Result<(), Self::Error> { + ($transfer_from)(origin, collection_id, from, to, asset_id) + } } type $name = Erc721Precompile; @@ -162,7 +280,7 @@ mod helpers { /// # Arguments /// /// * `$name`: An identifier to name the precompile mock type. - /// * `$owner_of_collection`: An expression that evaluates to a `Result`. + /// * `$owner_of`: An expression that evaluates to a `Result`. /// /// # Example /// @@ -171,11 +289,12 @@ mod helpers { /// ``` #[macro_export] macro_rules! impl_precompile_mock_simple { - ($name:ident, $owner_of_collection:expr, $token_uri:expr) => { + ($name:ident, $owner_of:expr, $token_uri:expr, $transfer_from:expr) => { impl_precompile_mock!( $name, - |_asset_id, _collection_id| { $owner_of_collection }, - |_asset_id, _collection_id| { $token_uri } + |_asset_id, _collection_id| { $owner_of }, + |_asset_id, _collection_id| { $token_uri }, + |_origin, _collection_id, _from, _to, _asset_id| { $transfer_from } ); }; }