From b1023364d49a456f4edb9f3c565f34b7b272f17e Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 26 Sep 2022 09:17:22 +0900 Subject: [PATCH 01/34] feat: nep171 wip --- src/standard/mod.rs | 1 + src/standard/nep171.rs | 102 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 src/standard/nep171.rs diff --git a/src/standard/mod.rs b/src/standard/mod.rs index a8e5fb1..d60d567 100644 --- a/src/standard/mod.rs +++ b/src/standard/mod.rs @@ -2,4 +2,5 @@ pub mod nep141; pub mod nep148; +pub mod nep171; pub mod nep297; diff --git a/src/standard/nep171.rs b/src/standard/nep171.rs new file mode 100644 index 0000000..73b0b79 --- /dev/null +++ b/src/standard/nep171.rs @@ -0,0 +1,102 @@ +use near_sdk::{ + borsh::{self, BorshSerialize}, + ext_contract, AccountId, BorshStorageKey, PromiseOrValue, +}; +use serde::Serialize; + +use crate::{near_contract_tools, slot::Slot, Event}; + +pub type TokenId = String; + +#[derive(Serialize, Event)] +#[event(standard = "nep171", version = "1.0.0", rename_all = "snake_case")] +#[serde(untagged)] +pub enum Nep171Event<'a> { + NftMint { + owner_id: &'a AccountId, + token_ids: &'a [&'a str], + #[serde(skip_serializing_if = "Option::is_none")] + memo: Option<&'a str>, + }, + NftTransfer { + old_owner_id: &'a AccountId, + new_owner_id: &'a AccountId, + token_ids: &'a [&'a str], + #[serde(skip_serializing_if = "Option::is_none")] + authorized_id: Option<&'a AccountId>, + #[serde(skip_serializing_if = "Option::is_none")] + memo: Option<&'a str>, + }, + NftBurn { + owner_id: &'a AccountId, + token_ids: &'a [&'a str], + #[serde(skip_serializing_if = "Option::is_none")] + authorized_id: Option<&'a AccountId>, + #[serde(skip_serializing_if = "Option::is_none")] + memo: Option<&'a str>, + }, +} + +#[derive(BorshSerialize, BorshStorageKey)] +enum StorageKey { + TokenOwner(TokenId), +} + +pub trait Nep171Controller { + fn root() -> Slot<()>; + + fn slot_token_owner(token_id: TokenId) -> Slot { + Self::root().field(StorageKey::TokenOwner(token_id)) + } + + fn transfer_unchecked(token_id: TokenId, receiver_id: &AccountId) -> Option { + let mut slot = Self::slot_token_owner(token_id); + if slot.exists() { + slot.swap(receiver_id) + } else { + None + } + } + + fn transfer(token_id: TokenId, sender_id: &AccountId, receiver_id: &AccountId) {} + + fn mint_unchecked(token_id: TokenId, new_owner_id: &AccountId) -> bool { + let mut slot = Self::slot_token_owner(token_id); + if !slot.exists() { + slot.write(new_owner_id); + true + } else { + false + } + } + + fn burn_unchecked(token_id: TokenId) -> bool { + Self::slot_token_owner(token_id).remove() + } +} + +pub trait Token { + fn get_for(&self, token_id: TokenId, owner_id: AccountId) -> Self; +} + +#[ext_contract(ext_nep171)] +pub trait Nep171External { + fn nft_transfer( + &mut self, + receiver_id: AccountId, + token_id: TokenId, + approval_id: Option, + memo: Option, + ); + + fn nft_transfer_call( + &mut self, + receiver_id: AccountId, + token_id: TokenId, + approval_id: Option, + memo: Option, + msg: String, + ) -> PromiseOrValue; + + fn nft_token(&self, token_id: TokenId) -> Option; +} From eedb85dffb8cb5f2424c9c6bfded0963e8c75c3d Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 3 Oct 2022 22:31:26 +0900 Subject: [PATCH 02/34] feat: nep171 events are owned; some experiments for allowing approval extension --- src/standard/nep171.rs | 148 ++++++++++++++++++++++++++++++----------- 1 file changed, 108 insertions(+), 40 deletions(-) diff --git a/src/standard/nep171.rs b/src/standard/nep171.rs index 73b0b79..07bb835 100644 --- a/src/standard/nep171.rs +++ b/src/standard/nep171.rs @@ -2,65 +2,133 @@ use near_sdk::{ borsh::{self, BorshSerialize}, ext_contract, AccountId, BorshStorageKey, PromiseOrValue, }; -use serde::Serialize; +use thiserror::Error; -use crate::{near_contract_tools, slot::Slot, Event}; +use crate::slot::Slot; -pub type TokenId = String; +use super::nep297::Event; -#[derive(Serialize, Event)] -#[event(standard = "nep171", version = "1.0.0", rename_all = "snake_case")] -#[serde(untagged)] -pub enum Nep171Event<'a> { - NftMint { - owner_id: &'a AccountId, - token_ids: &'a [&'a str], - #[serde(skip_serializing_if = "Option::is_none")] - memo: Option<&'a str>, - }, - NftTransfer { - old_owner_id: &'a AccountId, - new_owner_id: &'a AccountId, - token_ids: &'a [&'a str], +pub mod event { + use near_sdk::AccountId; + use serde::Serialize; + + use crate::event; + + #[event( + standard = "nep171", + version = "1.0.0", + crate = "crate", + macros = "near_contract_tools_macros" + )] + #[derive(Debug, Clone)] + pub struct NftMint(pub Vec); + + #[derive(Serialize, Debug, Clone)] + pub struct NftMintData { + pub owner_id: AccountId, + pub token_ids: Vec, #[serde(skip_serializing_if = "Option::is_none")] - authorized_id: Option<&'a AccountId>, + pub memo: Option, + } + + #[event( + standard = "nep171", + version = "1.0.0", + crate = "crate", + macros = "near_contract_tools_macros" + )] + #[derive(Debug, Clone)] + pub struct NftTransfer(pub Vec); + + #[derive(Serialize, Debug, Clone)] + pub struct NftTransferData { + pub old_owner_id: AccountId, + pub new_owner_id: AccountId, + pub token_ids: Vec, + // #[serde(skip_serializing_if = "Option::is_none")] + // pub authorized_id: Option<&'a AccountId>, #[serde(skip_serializing_if = "Option::is_none")] - memo: Option<&'a str>, - }, - NftBurn { - owner_id: &'a AccountId, - token_ids: &'a [&'a str], + pub memo: Option, + } + + #[event( + standard = "nep171", + version = "1.0.0", + crate = "crate", + macros = "near_contract_tools_macros" + )] + #[derive(Debug, Clone)] + pub struct NftBurn(pub Vec); + + #[derive(Serialize, Debug, Clone)] + pub struct NftBurnData { + pub owner_id: AccountId, + pub token_ids: Vec, #[serde(skip_serializing_if = "Option::is_none")] - authorized_id: Option<&'a AccountId>, + pub authorized_id: Option, #[serde(skip_serializing_if = "Option::is_none")] - memo: Option<&'a str>, - }, + pub memo: Option, + } } #[derive(BorshSerialize, BorshStorageKey)] enum StorageKey { - TokenOwner(TokenId), + TokenOwner(String), +} + +#[derive(Error, Clone, Debug)] +pub enum Nep171TransferError { + #[error("Sender is not the owner")] + SenderIsNotOwner, } pub trait Nep171Controller { fn root() -> Slot<()>; - fn slot_token_owner(token_id: TokenId) -> Slot { + fn slot_token_owner(token_id: String) -> Slot { Self::root().field(StorageKey::TokenOwner(token_id)) } - fn transfer_unchecked(token_id: TokenId, receiver_id: &AccountId) -> Option { - let mut slot = Self::slot_token_owner(token_id); - if slot.exists() { - slot.swap(receiver_id) + fn transfer_unchecked( + &mut self, + token_id: String, + sender_id: AccountId, + receiver_id: AccountId, + memo: Option, + ) -> Result { + let mut slot = Self::slot_token_owner(token_id.clone()); + if slot.exists() + && slot + .read() + .map(|current_owner_id| sender_id == current_owner_id) + .unwrap_or(false) + { + slot.write(&receiver_id); + Ok(event::NftTransfer(vec![event::NftTransferData { + old_owner_id: sender_id, + new_owner_id: receiver_id, + token_ids: vec![token_id], + memo, + }])) } else { - None + Err(Nep171TransferError::SenderIsNotOwner) } } - fn transfer(token_id: TokenId, sender_id: &AccountId, receiver_id: &AccountId) {} + fn transfer( + &mut self, + token_id: String, + sender_id: AccountId, + receiver_id: AccountId, + memo: Option, + ) -> Result<(), Nep171TransferError> { + self.transfer_unchecked(token_id, sender_id, receiver_id, memo) + .map(|e| { + e.emit(); + }) + } - fn mint_unchecked(token_id: TokenId, new_owner_id: &AccountId) -> bool { + fn mint_unchecked(token_id: String, new_owner_id: &AccountId) -> bool { let mut slot = Self::slot_token_owner(token_id); if !slot.exists() { slot.write(new_owner_id); @@ -70,13 +138,13 @@ pub trait Nep171Controller { } } - fn burn_unchecked(token_id: TokenId) -> bool { + fn burn_unchecked(token_id: String) -> bool { Self::slot_token_owner(token_id).remove() } } pub trait Token { - fn get_for(&self, token_id: TokenId, owner_id: AccountId) -> Self; + fn get_for(&self, token_id: String, owner_id: AccountId) -> Self; } #[ext_contract(ext_nep171)] @@ -84,7 +152,7 @@ pub trait Nep171External { fn nft_transfer( &mut self, receiver_id: AccountId, - token_id: TokenId, + token_id: String, approval_id: Option, memo: Option, ); @@ -92,11 +160,11 @@ pub trait Nep171External { fn nft_transfer_call( &mut self, receiver_id: AccountId, - token_id: TokenId, + token_id: String, approval_id: Option, memo: Option, msg: String, ) -> PromiseOrValue; - fn nft_token(&self, token_id: TokenId) -> Option; + fn nft_token(&self, token_id: String) -> Option; } From 05f8df4159ccfb61e959a59ce02d9fe6fb4dc572 Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 10 Oct 2022 16:11:45 +0900 Subject: [PATCH 03/34] feat: some ideas for implementing approval extension (wip) --- macros/src/standard/nep297.rs | 6 ++++-- src/standard/nep171.rs | 8 ++++++++ src/standard/nep297.rs | 37 ++++++++++++++++++++++------------- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/macros/src/standard/nep297.rs b/macros/src/standard/nep297.rs index a29ba70..be1947b 100644 --- a/macros/src/standard/nep297.rs +++ b/macros/src/standard/nep297.rs @@ -60,8 +60,10 @@ pub fn expand(meta: Nep297Meta) -> Result { }; Ok(quote! { - impl #imp #me::standard::nep297::Event<#type_name #ty> for #type_name #ty #wher { - fn event_log<'geld>(&'geld self) -> #me::standard::nep297::EventLog<&'geld Self> { + impl #imp #me::standard::nep297::ToEventLog for #type_name #ty #wher { + type Data = #type_name #ty; + + fn to_event_log<'geld>(&'geld self) -> #me::standard::nep297::EventLog<&'geld Self> { #me::standard::nep297::EventLog { standard: #standard, version: #version, diff --git a/src/standard/nep171.rs b/src/standard/nep171.rs index 07bb835..fb438dc 100644 --- a/src/standard/nep171.rs +++ b/src/standard/nep171.rs @@ -82,6 +82,14 @@ pub enum Nep171TransferError { SenderIsNotOwner, } +pub trait Nep171Extension { + type Event: crate::standard::nep297::Event; + + fn handle_transfer( + result: Result, + ) -> Result; +} + pub trait Nep171Controller { fn root() -> Slot<()>; diff --git a/src/standard/nep297.rs b/src/standard/nep297.rs index 202c575..45905ce 100644 --- a/src/standard/nep297.rs +++ b/src/standard/nep297.rs @@ -26,30 +26,37 @@ use near_sdk::serde::Serialize; /// /// e.emit(); /// ``` -pub trait Event { - /// Retrieves the event log before serialization - fn event_log(&self) -> EventLog<&T>; - +pub trait Event { /// Converts the event into an NEP-297 event-formatted string - fn to_event_string(&self) -> String - where - T: Serialize, - { + fn to_event_string(&self) -> String; + + /// Emits the event string to the blockchain + fn emit(&self); +} + +impl Event for T +where + T::Data: Serialize, +{ + fn to_event_string(&self) -> String { format!( "EVENT_JSON:{}", - serde_json::to_string(&self.event_log()).unwrap_or_else(|_| near_sdk::env::abort()), + serde_json::to_string(&self.to_event_log()).unwrap_or_else(|_| near_sdk::env::abort()), ) } - /// Emits the event string to the blockchain - fn emit(&self) - where - T: Serialize, - { + fn emit(&self) { near_sdk::env::log_str(&self.to_event_string()); } } +pub trait ToEventLog { + type Data: ?Sized; + + /// Retrieves the event log before serialization + fn to_event_log(&self) -> EventLog<&Self::Data>; +} + /// NEP-297 Event Log Data /// #[derive(Serialize, Clone, Debug)] @@ -63,3 +70,5 @@ pub struct EventLog { /// Data type of the event metadata pub data: T, } + +pub trait EventConsumer {} From 6ee231b1881b700f5380ef4b9ef04c292169dcd9 Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Wed, 21 Jun 2023 22:19:56 +0900 Subject: [PATCH 04/34] fix: crate name change --- src/standard/nep171.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/standard/nep171.rs b/src/standard/nep171.rs index fb438dc..78fd158 100644 --- a/src/standard/nep171.rs +++ b/src/standard/nep171.rs @@ -18,7 +18,7 @@ pub mod event { standard = "nep171", version = "1.0.0", crate = "crate", - macros = "near_contract_tools_macros" + macros = "near_sdk_contract_tools_macros" )] #[derive(Debug, Clone)] pub struct NftMint(pub Vec); @@ -35,7 +35,7 @@ pub mod event { standard = "nep171", version = "1.0.0", crate = "crate", - macros = "near_contract_tools_macros" + macros = "near_sdk_contract_tools_macros" )] #[derive(Debug, Clone)] pub struct NftTransfer(pub Vec); @@ -55,7 +55,7 @@ pub mod event { standard = "nep171", version = "1.0.0", crate = "crate", - macros = "near_contract_tools_macros" + macros = "near_sdk_contract_tools_macros" )] #[derive(Debug, Clone)] pub struct NftBurn(pub Vec); From 3a1b4afbf9ec6254b4aef8e47a21a7c729027eb5 Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Thu, 22 Jun 2023 02:28:03 +0900 Subject: [PATCH 05/34] feat: nep171 macro progress --- macros/src/lib.rs | 5 + macros/src/standard/mod.rs | 1 + macros/src/standard/nep171.rs | 167 ++++++++++++++++++++++++++ src/standard/nep171.rs | 206 ++++++++++++++++++++++---------- src/standard/nep297.rs | 2 - tests/macros/standard/mod.rs | 1 + tests/macros/standard/nep171.rs | 108 +++++++++++++++++ 7 files changed, 427 insertions(+), 63 deletions(-) create mode 100644 macros/src/standard/nep171.rs create mode 100644 tests/macros/standard/nep171.rs diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 08d3ca0..5e9141a 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -147,6 +147,11 @@ pub fn derive_fungible_token(input: TokenStream) -> TokenStream { make_derive(input, standard::fungible_token::expand) } +#[proc_macro_derive(Nep171, attributes(nep171))] +pub fn derive_nep171(input: TokenStream) -> TokenStream { + make_derive(input, standard::nep171::expand) +} + /// Migrate a contract's default struct from one schema to another. /// /// Fields may be specified in the `#[migrate(...)]` attribute. diff --git a/macros/src/standard/mod.rs b/macros/src/standard/mod.rs index cecb132..bf993cd 100644 --- a/macros/src/standard/mod.rs +++ b/macros/src/standard/mod.rs @@ -3,4 +3,5 @@ pub mod fungible_token; pub mod nep141; pub mod nep148; +pub mod nep171; pub mod nep297; diff --git a/macros/src/standard/nep171.rs b/macros/src/standard/nep171.rs new file mode 100644 index 0000000..f0a1440 --- /dev/null +++ b/macros/src/standard/nep171.rs @@ -0,0 +1,167 @@ +use std::ops::Not; + +use darling::{util::Flag, FromDeriveInput}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::Expr; + +#[derive(Debug, FromDeriveInput)] +#[darling(attributes(nep171), supports(struct_named))] +pub struct Nep171Meta { + pub storage_key: Option, + pub no_hooks: Flag, + pub generics: syn::Generics, + pub ident: syn::Ident, + + // crates + #[darling(rename = "crate", default = "crate::default_crate_name")] + pub me: syn::Path, + #[darling(default = "crate::default_near_sdk")] + pub near_sdk: syn::Path, +} + +pub fn expand(meta: Nep171Meta) -> Result { + let Nep171Meta { + storage_key, + no_hooks, + generics, + ident, + + me, + near_sdk, + } = meta; + + let (imp, ty, wher) = generics.split_for_impl(); + + let root = storage_key.map(|storage_key| { + quote! { + fn root() -> #me::slot::Slot<()> { + #me::slot::Slot::root(#storage_key) + } + } + }); + + let before_transfer = no_hooks.is_present().not().then(|| { + quote! { + let hook_state = >::before_transfer(self, &transfer); + } + }); + + let after_transfer = no_hooks.is_present().not().then(|| { + quote! { + >::after_transfer(self, &transfer, hook_state); + } + }); + + Ok(quote! { + impl #imp #me::standard::nep171::Nep171ControllerInternal for #ident #ty #wher { + #root + } + + #[#near_sdk::near_bindgen] + impl #imp #me::standard::nep171::Nep171 for #ident #ty #wher { + #[payable] + fn nft_transfer( + &mut self, + receiver_id: #near_sdk::AccountId, + token_id: String, + approval_id: Option, + memo: Option, + ) { + use #me::{ + standard::{ + nep171::{Nep171Controller, event}, + nep297::Event, + }, + }; + + #near_sdk::assert_one_yocto(); + let sender_id = #near_sdk::env::predecessor_account_id(); + let amount: u128 = amount.into(); + + let transfer = #me::standard::nep171::Nep171Transfer { + sender_id: sender_id.clone(), + receiver_id: receiver_id.clone(), + amount, + memo: memo.clone(), + msg: None, + }; + + #before_transfer + + Nep171Controller::transfer( + self, + sender_id.clone(), + receiver_id.clone(), + amount, + memo, + ); + + #after_transfer + } + + #[payable] + fn ft_transfer_call( + &mut self, + receiver_id: #near_sdk::AccountId, + amount: #near_sdk::json_types::U128, + memo: Option, + msg: String, + ) -> #near_sdk::Promise { + #near_sdk::assert_one_yocto(); + let sender_id = #near_sdk::env::predecessor_account_id(); + let amount: u128 = amount.into(); + + let transfer = #me::standard::nep171::Nep171Transfer { + sender_id: sender_id.clone(), + receiver_id: receiver_id.clone(), + amount, + memo: memo.clone(), + msg: None, + }; + + #before_transfer + + let r = #me::standard::nep171::Nep171Controller::transfer_call( + self, + sender_id.clone(), + receiver_id.clone(), + amount, + memo, + msg.clone(), + #near_sdk::env::prepaid_gas(), + ); + + #after_transfer + + r + } + + fn ft_total_supply(&self) -> #near_sdk::json_types::U128 { + ::total_supply().into() + } + + fn ft_balance_of(&self, account_id: #near_sdk::AccountId) -> #near_sdk::json_types::U128 { + ::balance_of(&account_id).into() + } + } + + #[#near_sdk::near_bindgen] + impl #imp #me::standard::nep171::Nep171Resolver for #ident #ty #wher { + #[private] + fn ft_resolve_transfer( + &mut self, + sender_id: #near_sdk::AccountId, + receiver_id: #near_sdk::AccountId, + amount: #near_sdk::json_types::U128, + ) -> #near_sdk::json_types::U128 { + #me::standard::nep171::Nep171Controller::resolve_transfer( + self, + sender_id, + receiver_id, + amount.into(), + ).into() + } + } + }) +} diff --git a/src/standard/nep171.rs b/src/standard/nep171.rs index 78fd158..0aac8e2 100644 --- a/src/standard/nep171.rs +++ b/src/standard/nep171.rs @@ -1,67 +1,62 @@ +use std::collections::HashMap; + use near_sdk::{ - borsh::{self, BorshSerialize}, - ext_contract, AccountId, BorshStorageKey, PromiseOrValue, + borsh::{self, BorshDeserialize, BorshSerialize}, + ext_contract, AccountId, BorshStorageKey, Gas, PromiseOrValue, }; +use near_sdk_contract_tools_macros::event; +use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::slot::Slot; use super::nep297::Event; +pub const GAS_FOR_RESOLVE_TRANSFER: Gas = Gas(5_000_000_000_000); +pub const GAS_FOR_NFT_TRANSFER_CALL: Gas = Gas(25_000_000_000_000 + GAS_FOR_RESOLVE_TRANSFER.0); +pub const INSUFFICIENT_GAS_MESSAGE: &str = "More gas is required"; +pub const APPROVAL_MANAGEMENT_NOT_SUPPORTED_MESSAGE: &str = "NEP-178: Approval Management is not supported"; + +#[event( + crate = "crate", + macros = "crate", + serde = "serde", + standard = "nep171", + version = "1.1.0" +)] +#[derive(Debug, Clone)] +pub enum Nep171Event { + NftMint(Vec), + NftTransfer(Vec), + NftBurn(Vec), + ContractMetadataUpdate(Vec), +} + pub mod event { use near_sdk::AccountId; use serde::Serialize; - use crate::event; - - #[event( - standard = "nep171", - version = "1.0.0", - crate = "crate", - macros = "near_sdk_contract_tools_macros" - )] - #[derive(Debug, Clone)] - pub struct NftMint(pub Vec); - #[derive(Serialize, Debug, Clone)] - pub struct NftMintData { + pub struct NftMintLog { pub owner_id: AccountId, pub token_ids: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub memo: Option, } - #[event( - standard = "nep171", - version = "1.0.0", - crate = "crate", - macros = "near_sdk_contract_tools_macros" - )] - #[derive(Debug, Clone)] - pub struct NftTransfer(pub Vec); - #[derive(Serialize, Debug, Clone)] - pub struct NftTransferData { + pub struct NftTransferLog { + #[serde(skip_serializing_if = "Option::is_none")] + pub authorized_id: Option, pub old_owner_id: AccountId, pub new_owner_id: AccountId, pub token_ids: Vec, - // #[serde(skip_serializing_if = "Option::is_none")] - // pub authorized_id: Option<&'a AccountId>, #[serde(skip_serializing_if = "Option::is_none")] pub memo: Option, } - #[event( - standard = "nep171", - version = "1.0.0", - crate = "crate", - macros = "near_sdk_contract_tools_macros" - )] - #[derive(Debug, Clone)] - pub struct NftBurn(pub Vec); - #[derive(Serialize, Debug, Clone)] - pub struct NftBurnData { + pub struct NftBurnLog { pub owner_id: AccountId, pub token_ids: Vec, #[serde(skip_serializing_if = "Option::is_none")] @@ -69,6 +64,12 @@ pub mod event { #[serde(skip_serializing_if = "Option::is_none")] pub memo: Option, } + + #[derive(Serialize, Debug, Clone)] + pub struct ContractMetadataUpdateLog { + #[serde(skip_serializing_if = "Option::is_none")] + pub memo: Option, + } } #[derive(BorshSerialize, BorshStorageKey)] @@ -80,31 +81,92 @@ enum StorageKey { pub enum Nep171TransferError { #[error("Sender is not the owner")] SenderIsNotOwner, + #[error("Sender and receiver must be different")] + SenderEqualsReceiver, } pub trait Nep171Extension { type Event: crate::standard::nep297::Event; fn handle_transfer( - result: Result, + result: Result, ) -> Result; } -pub trait Nep171Controller { +pub trait Nep171ControllerInternal { fn root() -> Slot<()>; fn slot_token_owner(token_id: String) -> Slot { Self::root().field(StorageKey::TokenOwner(token_id)) } +} + +pub trait Nep171Controller { + fn transfer( + &mut self, + token_id: String, + sender_id: AccountId, + receiver_id: AccountId, + memo: Option, + ) -> Result<(), Nep171TransferError>; + + fn mint(token_id: String, new_owner_id: &AccountId) -> bool; + + fn burn(token_id: String) -> bool; + + fn token_owner(&self, token_id: String) -> Option; +} + +/// Transfer metadata generic over both types of transfer (`nft_transfer` and +/// `nft_transfer_call`). +#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize, PartialEq, Eq, Clone, Debug)] +pub struct Nep171Transfer { + /// Sending account ID. + pub sender_id: AccountId, + /// Receiving account ID. + pub receiver_id: AccountId, + /// Optional approval ID. + pub approval_id: Option, + /// Token ID. + pub token_id: String, + /// Optional memo string. + pub memo: Option, + /// Message passed to contract located at `receiver_id` in the case of `nft_transfer_call`. + pub msg: Option, +} - fn transfer_unchecked( +/// Contracts may implement this trait to inject code into NEP-171 functions. +/// +/// `T` is an optional value for passing state between different lifecycle +/// hooks. This may be useful for charging callers for storage usage, for +/// example. +pub trait Nep171Hook { + /// Executed before a token transfer is conducted. + /// + /// May return an optional state value which will be passed along to the + /// following `after_transfer`. + fn before_transfer(&mut self, _transfer: &Nep171Transfer) -> T; + + /// Executed after a token transfer is conducted. + /// + /// Receives the state value returned by `before_transfer`. + fn after_transfer(&mut self, _transfer: &Nep171Transfer, _state: T) {} +} + +impl Nep171Controller for T { + fn transfer( &mut self, token_id: String, sender_id: AccountId, receiver_id: AccountId, memo: Option, - ) -> Result { + ) -> Result<(), Nep171TransferError> { + if sender_id == receiver_id { + return Err(Nep171TransferError::SenderEqualsReceiver); + } + let mut slot = Self::slot_token_owner(token_id.clone()); + if slot.exists() && slot .read() @@ -112,31 +174,23 @@ pub trait Nep171Controller { .unwrap_or(false) { slot.write(&receiver_id); - Ok(event::NftTransfer(vec![event::NftTransferData { + + Nep171Event::NftTransfer(vec![event::NftTransferLog { + authorized_id: None, old_owner_id: sender_id, new_owner_id: receiver_id, token_ids: vec![token_id], memo, - }])) + }]) + .emit(); + + Ok(()) } else { Err(Nep171TransferError::SenderIsNotOwner) } } - fn transfer( - &mut self, - token_id: String, - sender_id: AccountId, - receiver_id: AccountId, - memo: Option, - ) -> Result<(), Nep171TransferError> { - self.transfer_unchecked(token_id, sender_id, receiver_id, memo) - .map(|e| { - e.emit(); - }) - } - - fn mint_unchecked(token_id: String, new_owner_id: &AccountId) -> bool { + fn mint(token_id: String, new_owner_id: &AccountId) -> bool { let mut slot = Self::slot_token_owner(token_id); if !slot.exists() { slot.write(new_owner_id); @@ -146,17 +200,23 @@ pub trait Nep171Controller { } } - fn burn_unchecked(token_id: String) -> bool { + fn burn(token_id: String) -> bool { Self::slot_token_owner(token_id).remove() } + + fn token_owner(&self, token_id: String) -> Option { + Self::slot_token_owner(token_id).read() + } } -pub trait Token { - fn get_for(&self, token_id: String, owner_id: AccountId) -> Self; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Token { + pub token_id: String, + pub owner_id: AccountId, } #[ext_contract(ext_nep171)] -pub trait Nep171External { +pub trait Nep171 { fn nft_transfer( &mut self, receiver_id: AccountId, @@ -174,5 +234,29 @@ pub trait Nep171External { msg: String, ) -> PromiseOrValue; - fn nft_token(&self, token_id: String) -> Option; + fn nft_token(&self, token_id: String) -> Option; + + fn nft_resolve_transfer( + previous_owner_id: AccountId, + receiver_id: AccountId, + token_id: String, + approved_account_ids: Option>, + ) -> bool; +} + +/// A contract that may be the recipient of an `nft_transfer_call` function +/// call. +#[ext_contract(ext_nep171_receiver)] +pub trait Nep171Receiver { + /// Function that is called in an `nft_transfer_call` promise chain. + /// Returns the number of tokens "used", that is, those that will be kept + /// in the receiving contract's account. (The contract will attempt to + /// refund the difference from `amount` to the original sender.) + fn nft_on_transfer( + &mut self, + sender_id: AccountId, + previous_owner_id: AccountId, + token_id: String, + msg: String, + ) -> PromiseOrValue; } diff --git a/src/standard/nep297.rs b/src/standard/nep297.rs index 863036d..589fe50 100644 --- a/src/standard/nep297.rs +++ b/src/standard/nep297.rs @@ -72,5 +72,3 @@ pub struct EventLog { /// Data type of the event metadata pub data: T, } - -pub trait EventConsumer {} diff --git a/tests/macros/standard/mod.rs b/tests/macros/standard/mod.rs index 5569494..6685e67 100644 --- a/tests/macros/standard/mod.rs +++ b/tests/macros/standard/mod.rs @@ -1,3 +1,4 @@ pub mod fungible_token; pub mod nep141; pub mod nep148; +pub mod nep171; diff --git a/tests/macros/standard/nep171.rs b/tests/macros/standard/nep171.rs new file mode 100644 index 0000000..f102528 --- /dev/null +++ b/tests/macros/standard/nep171.rs @@ -0,0 +1,108 @@ +// use ; + +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk_contract_tools::slot::Slot; + +// #[derive(Nep171)] +#[derive(BorshDeserialize, BorshSerialize)] +#[near_sdk::near_bindgen] +struct NonFungibleToken {} + +impl near_sdk_contract_tools::standard::nep171::Nep171ControllerInternal for NonFungibleToken { + fn root() -> Slot<()> { + Slot::root(b"nft" as &[u8]) + } +} + +impl near_sdk_contract_tools::standard::nep171::Nep171 for NonFungibleToken { + fn nft_transfer( + &mut self, + receiver_id: near_sdk::AccountId, + token_id: String, + approval_id: Option, + memo: Option, + ) { + use near_sdk_contract_tools::standard::nep171::*; + + near_sdk::require!( + approval_id.is_none(), + APPROVAL_MANAGEMENT_NOT_SUPPORTED_MESSAGE, + ); + + near_sdk::assert_one_yocto(); + + let sender_id = near_sdk::env::predecessor_account_id(); + + Nep171Controller::transfer(self, token_id, sender_id, receiver_id, memo).unwrap(); + } + + fn nft_transfer_call( + &mut self, + receiver_id: near_sdk::AccountId, + token_id: String, + approval_id: Option, + memo: Option, + msg: String, + ) -> near_sdk::PromiseOrValue { + use near_sdk_contract_tools::standard::nep171::*; + + near_sdk::require!( + approval_id.is_none(), + APPROVAL_MANAGEMENT_NOT_SUPPORTED_MESSAGE + ); + + near_sdk::assert_one_yocto(); + + near_sdk::require!( + near_sdk::env::prepaid_gas() > GAS_FOR_NFT_TRANSFER_CALL, + INSUFFICIENT_GAS_MESSAGE, + ); + + let sender_id = near_sdk::env::predecessor_account_id(); + + Nep171Controller::transfer( + self, + token_id.clone(), + sender_id.clone(), + receiver_id.clone(), + memo, + ) + .unwrap(); + + ext_nep171_receiver::ext(receiver_id.clone()) + .with_static_gas(near_sdk::env::prepaid_gas() - GAS_FOR_NFT_TRANSFER_CALL) + .nft_on_transfer( + sender_id.clone(), + receiver_id.clone(), + token_id.clone(), + msg, + ) + .then( + ext_nep171::ext(near_sdk::env::current_account_id()) + .with_static_gas(GAS_FOR_RESOLVE_TRANSFER) + .nft_resolve_transfer(sender_id, receiver_id, token_id, None), + ) + .into() + } + + fn nft_token( + &self, + token_id: String, + ) -> Option { + use near_sdk_contract_tools::standard::nep171::*; + + Nep171Controller::token_owner(self, token_id.clone()).map(|owner_id| Token { + token_id, + owner_id, + }) + } + + fn nft_resolve_transfer( + previous_owner_id: near_sdk::AccountId, + receiver_id: near_sdk::AccountId, + token_id: String, + approved_account_ids: Option>, + ) -> bool { + todo!() + } +} From 5d1a6228762c2c7d27b7fe457b9207193c81c8d5 Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Tue, 27 Jun 2023 15:54:05 +0900 Subject: [PATCH 06/34] full nep171 implementation --- src/standard/nep171.rs | 133 +++++++++++++++++++++----------- tests/macros/standard/nep171.rs | 68 +++++++++++----- 2 files changed, 137 insertions(+), 64 deletions(-) diff --git a/src/standard/nep171.rs b/src/standard/nep171.rs index 0aac8e2..d639144 100644 --- a/src/standard/nep171.rs +++ b/src/standard/nep171.rs @@ -15,7 +15,10 @@ use super::nep297::Event; pub const GAS_FOR_RESOLVE_TRANSFER: Gas = Gas(5_000_000_000_000); pub const GAS_FOR_NFT_TRANSFER_CALL: Gas = Gas(25_000_000_000_000 + GAS_FOR_RESOLVE_TRANSFER.0); pub const INSUFFICIENT_GAS_MESSAGE: &str = "More gas is required"; -pub const APPROVAL_MANAGEMENT_NOT_SUPPORTED_MESSAGE: &str = "NEP-178: Approval Management is not supported"; +pub const APPROVAL_MANAGEMENT_NOT_SUPPORTED_MESSAGE: &str = + "NEP-178: Approval Management is not supported"; + +pub type TokenId = String; #[event( crate = "crate", @@ -36,10 +39,12 @@ pub mod event { use near_sdk::AccountId; use serde::Serialize; + use super::TokenId; + #[derive(Serialize, Debug, Clone)] pub struct NftMintLog { pub owner_id: AccountId, - pub token_ids: Vec, + pub token_ids: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub memo: Option, } @@ -50,7 +55,7 @@ pub mod event { pub authorized_id: Option, pub old_owner_id: AccountId, pub new_owner_id: AccountId, - pub token_ids: Vec, + pub token_ids: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub memo: Option, } @@ -58,7 +63,7 @@ pub mod event { #[derive(Serialize, Debug, Clone)] pub struct NftBurnLog { pub owner_id: AccountId, - pub token_ids: Vec, + pub token_ids: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub authorized_id: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -79,10 +84,21 @@ enum StorageKey { #[derive(Error, Clone, Debug)] pub enum Nep171TransferError { - #[error("Sender is not the owner")] - SenderIsNotOwner, - #[error("Sender and receiver must be different")] - SenderEqualsReceiver, + #[error("Sender `{sender_id}` does not have permission to transfer token `{token_id}`")] + NotApproved { + sender_id: AccountId, + token_id: TokenId, + }, + #[error("Receiver must be different from current owner `{current_owner_id}` to transfer token `{token_id}`")] + ReceiverIsCurrentOwner { + current_owner_id: AccountId, + token_id: TokenId, + }, + #[error("Token `{token_id}` is no longer owned by the expected owner `{expected_owner_id}`")] + NotOwnedByExpectedOwner { + expected_owner_id: AccountId, + token_id: TokenId, + }, } pub trait Nep171Extension { @@ -96,7 +112,7 @@ pub trait Nep171Extension { pub trait Nep171ControllerInternal { fn root() -> Slot<()>; - fn slot_token_owner(token_id: String) -> Slot { + fn slot_token_owner(token_id: TokenId) -> Slot { Self::root().field(StorageKey::TokenOwner(token_id)) } } @@ -104,17 +120,18 @@ pub trait Nep171ControllerInternal { pub trait Nep171Controller { fn transfer( &mut self, - token_id: String, + token_id: TokenId, + current_owner_id: AccountId, sender_id: AccountId, receiver_id: AccountId, memo: Option, ) -> Result<(), Nep171TransferError>; - fn mint(token_id: String, new_owner_id: &AccountId) -> bool; + fn mint(token_id: TokenId, new_owner_id: &AccountId) -> bool; - fn burn(token_id: String) -> bool; + fn burn(token_id: TokenId) -> bool; - fn token_owner(&self, token_id: String) -> Option; + fn token_owner(&self, token_id: TokenId) -> Option; } /// Transfer metadata generic over both types of transfer (`nft_transfer` and @@ -128,7 +145,7 @@ pub struct Nep171Transfer { /// Optional approval ID. pub approval_id: Option, /// Token ID. - pub token_id: String, + pub token_id: TokenId, /// Optional memo string. pub memo: Option, /// Message passed to contract located at `receiver_id` in the case of `nft_transfer_call`. @@ -156,41 +173,61 @@ pub trait Nep171Hook { impl Nep171Controller for T { fn transfer( &mut self, - token_id: String, + token_id: TokenId, + current_owner_id: AccountId, sender_id: AccountId, receiver_id: AccountId, memo: Option, ) -> Result<(), Nep171TransferError> { - if sender_id == receiver_id { - return Err(Nep171TransferError::SenderEqualsReceiver); + if current_owner_id == receiver_id { + return Err(Nep171TransferError::ReceiverIsCurrentOwner { + current_owner_id, + token_id, + }); + } + + // This version doesn't implement approval management + if sender_id != current_owner_id { + return Err(Nep171TransferError::NotApproved { + sender_id, + token_id, + }); } let mut slot = Self::slot_token_owner(token_id.clone()); - if slot.exists() - && slot - .read() - .map(|current_owner_id| sender_id == current_owner_id) - .unwrap_or(false) - { - slot.write(&receiver_id); - - Nep171Event::NftTransfer(vec![event::NftTransferLog { - authorized_id: None, - old_owner_id: sender_id, - new_owner_id: receiver_id, - token_ids: vec![token_id], - memo, - }]) - .emit(); - - Ok(()) + let actual_current_owner_id = if let Some(owner_id) = slot.read() { + owner_id } else { - Err(Nep171TransferError::SenderIsNotOwner) + // Using if-let instead of .ok_or_else() to avoid .clone() + return Err(Nep171TransferError::NotOwnedByExpectedOwner { + expected_owner_id: current_owner_id, + token_id, + }); + }; + + if current_owner_id != actual_current_owner_id { + return Err(Nep171TransferError::NotOwnedByExpectedOwner { + expected_owner_id: current_owner_id, + token_id, + }); } + + slot.write(&receiver_id); + + Nep171Event::NftTransfer(vec![event::NftTransferLog { + authorized_id: None, + old_owner_id: actual_current_owner_id, + new_owner_id: receiver_id, + token_ids: vec![token_id], + memo, + }]) + .emit(); + + Ok(()) } - fn mint(token_id: String, new_owner_id: &AccountId) -> bool { + fn mint(token_id: TokenId, new_owner_id: &AccountId) -> bool { let mut slot = Self::slot_token_owner(token_id); if !slot.exists() { slot.write(new_owner_id); @@ -200,18 +237,18 @@ impl Nep171Controller for T { } } - fn burn(token_id: String) -> bool { + fn burn(token_id: TokenId) -> bool { Self::slot_token_owner(token_id).remove() } - fn token_owner(&self, token_id: String) -> Option { + fn token_owner(&self, token_id: TokenId) -> Option { Self::slot_token_owner(token_id).read() } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Token { - pub token_id: String, + pub token_id: TokenId, pub owner_id: AccountId, } @@ -220,7 +257,7 @@ pub trait Nep171 { fn nft_transfer( &mut self, receiver_id: AccountId, - token_id: String, + token_id: TokenId, approval_id: Option, memo: Option, ); @@ -228,19 +265,23 @@ pub trait Nep171 { fn nft_transfer_call( &mut self, receiver_id: AccountId, - token_id: String, + token_id: TokenId, approval_id: Option, memo: Option, msg: String, ) -> PromiseOrValue; - fn nft_token(&self, token_id: String) -> Option; + fn nft_token(&self, token_id: TokenId) -> Option; +} +#[ext_contract(ext_nep171_resolver)] +pub trait Nep171Resolver { fn nft_resolve_transfer( + &mut self, previous_owner_id: AccountId, receiver_id: AccountId, - token_id: String, - approved_account_ids: Option>, + token_id: TokenId, + approved_account_ids: Option>, ) -> bool; } @@ -256,7 +297,7 @@ pub trait Nep171Receiver { &mut self, sender_id: AccountId, previous_owner_id: AccountId, - token_id: String, + token_id: TokenId, msg: String, ) -> PromiseOrValue; } diff --git a/tests/macros/standard/nep171.rs b/tests/macros/standard/nep171.rs index f102528..0231544 100644 --- a/tests/macros/standard/nep171.rs +++ b/tests/macros/standard/nep171.rs @@ -1,5 +1,3 @@ -// use ; - use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk_contract_tools::slot::Slot; @@ -14,6 +12,42 @@ impl near_sdk_contract_tools::standard::nep171::Nep171ControllerInternal for Non } } +#[near_sdk::near_bindgen] +impl near_sdk_contract_tools::standard::nep171::Nep171Resolver for NonFungibleToken { + #[private] + fn nft_resolve_transfer( + &mut self, + previous_owner_id: near_sdk::AccountId, + receiver_id: near_sdk::AccountId, + token_id: near_sdk_contract_tools::standard::nep171::TokenId, + _approved_account_ids: Option>, + ) -> bool { + // Get whether token should be returned + let must_revert = + if let near_sdk::PromiseResult::Successful(value) = near_sdk::env::promise_result(0) { + near_sdk::serde_json::from_slice::(&value).unwrap_or(true) + } else { + true + }; + + // if call succeeded, return early + if !must_revert { + return true; + } + + near_sdk_contract_tools::standard::nep171::Nep171Controller::transfer( + self, + token_id, + receiver_id.clone(), + receiver_id, + previous_owner_id, + None, + ) + .is_err() + } +} + +#[near_sdk::near_bindgen] impl near_sdk_contract_tools::standard::nep171::Nep171 for NonFungibleToken { fn nft_transfer( &mut self, @@ -33,7 +67,15 @@ impl near_sdk_contract_tools::standard::nep171::Nep171 for NonFungibleToken { let sender_id = near_sdk::env::predecessor_account_id(); - Nep171Controller::transfer(self, token_id, sender_id, receiver_id, memo).unwrap(); + Nep171Controller::transfer( + self, + token_id, + sender_id.clone(), + sender_id, + receiver_id, + memo, + ) + .unwrap(); } fn nft_transfer_call( @@ -48,7 +90,7 @@ impl near_sdk_contract_tools::standard::nep171::Nep171 for NonFungibleToken { near_sdk::require!( approval_id.is_none(), - APPROVAL_MANAGEMENT_NOT_SUPPORTED_MESSAGE + APPROVAL_MANAGEMENT_NOT_SUPPORTED_MESSAGE, ); near_sdk::assert_one_yocto(); @@ -64,6 +106,7 @@ impl near_sdk_contract_tools::standard::nep171::Nep171 for NonFungibleToken { self, token_id.clone(), sender_id.clone(), + sender_id.clone(), receiver_id.clone(), memo, ) @@ -78,7 +121,7 @@ impl near_sdk_contract_tools::standard::nep171::Nep171 for NonFungibleToken { msg, ) .then( - ext_nep171::ext(near_sdk::env::current_account_id()) + ext_nep171_resolver::ext(near_sdk::env::current_account_id()) .with_static_gas(GAS_FOR_RESOLVE_TRANSFER) .nft_resolve_transfer(sender_id, receiver_id, token_id, None), ) @@ -91,18 +134,7 @@ impl near_sdk_contract_tools::standard::nep171::Nep171 for NonFungibleToken { ) -> Option { use near_sdk_contract_tools::standard::nep171::*; - Nep171Controller::token_owner(self, token_id.clone()).map(|owner_id| Token { - token_id, - owner_id, - }) - } - - fn nft_resolve_transfer( - previous_owner_id: near_sdk::AccountId, - receiver_id: near_sdk::AccountId, - token_id: String, - approved_account_ids: Option>, - ) -> bool { - todo!() + Nep171Controller::token_owner(self, token_id.clone()) + .map(|owner_id| Token { token_id, owner_id }) } } From fe5cf8ac377d937706dca8dc0ce559ff1110b8ad Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Tue, 27 Jun 2023 17:08:51 +0900 Subject: [PATCH 07/34] chore: simplify nft_resolve_transfer --- tests/macros/standard/nep171.rs | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/tests/macros/standard/nep171.rs b/tests/macros/standard/nep171.rs index 0231544..0b20de7 100644 --- a/tests/macros/standard/nep171.rs +++ b/tests/macros/standard/nep171.rs @@ -22,28 +22,31 @@ impl near_sdk_contract_tools::standard::nep171::Nep171Resolver for NonFungibleTo token_id: near_sdk_contract_tools::standard::nep171::TokenId, _approved_account_ids: Option>, ) -> bool { - // Get whether token should be returned - let must_revert = + near_sdk::require!( + near_sdk::env::promise_results_count() == 1, + "Requires exactly one promise result.", + ); + + let should_revert = if let near_sdk::PromiseResult::Successful(value) = near_sdk::env::promise_result(0) { near_sdk::serde_json::from_slice::(&value).unwrap_or(true) } else { true }; - // if call succeeded, return early - if !must_revert { - return true; + if should_revert { + near_sdk_contract_tools::standard::nep171::Nep171Controller::transfer( + self, + token_id, + receiver_id.clone(), + receiver_id, + previous_owner_id, + None, + ) + .is_err() + } else { + true } - - near_sdk_contract_tools::standard::nep171::Nep171Controller::transfer( - self, - token_id, - receiver_id.clone(), - receiver_id, - previous_owner_id, - None, - ) - .is_err() } } From 4cc562d5b1cca8049306b7c3bf00fa83ad5d24ee Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Thu, 29 Jun 2023 22:13:47 +0900 Subject: [PATCH 08/34] chore: finished nep171 macro --- macros/src/standard/nep171.rs | 168 ++++++++++++++++++++++---------- src/lib.rs | 9 +- src/standard/nep171.rs | 14 ++- tests/macros/standard/nep171.rs | 56 ++++++++++- 4 files changed, 186 insertions(+), 61 deletions(-) diff --git a/macros/src/standard/nep171.rs b/macros/src/standard/nep171.rs index f0a1440..8286814 100644 --- a/macros/src/standard/nep171.rs +++ b/macros/src/standard/nep171.rs @@ -58,31 +58,88 @@ pub fn expand(meta: Nep171Meta) -> Result { #root } + #[#near_sdk::near_bindgen] + impl #imp #me::standard::nep171::Nep171Resolver for #ident #ty #wher { + #[private] + fn nft_resolve_transfer( + &mut self, + previous_owner_id: #near_sdk::AccountId, + receiver_id: #near_sdk::AccountId, + token_id: #me::standard::nep171::TokenId, + approved_account_ids: Option>, + ) -> bool { + let _ = approved_account_ids; // #[near_bindgen] cares about parameter names + + #near_sdk::require!( + #near_sdk::env::promise_results_count() == 1, + "Requires exactly one promise result.", + ); + + let should_revert = + if let #near_sdk::PromiseResult::Successful(value) = #near_sdk::env::promise_result(0) { + #near_sdk::serde_json::from_slice::(&value).unwrap_or(true) + } else { + true + }; + + if should_revert { + let transfer = #me::standard::nep171::Nep171Transfer { + token_id: token_id.clone(), + owner_id: receiver_id.clone(), + sender_id: receiver_id.clone(), + receiver_id: previous_owner_id.clone(), + approval_id: None, + memo: None, + msg: None, + }; + + #before_transfer + + let result = #me::standard::nep171::Nep171Controller::transfer( + self, + token_id, + receiver_id.clone(), + receiver_id, + previous_owner_id, + None, + ) + .is_err(); + + #after_transfer + + result + } else { + true + } + } + } + #[#near_sdk::near_bindgen] impl #imp #me::standard::nep171::Nep171 for #ident #ty #wher { - #[payable] fn nft_transfer( &mut self, receiver_id: #near_sdk::AccountId, - token_id: String, + token_id: #me::standard::nep171::TokenId, approval_id: Option, memo: Option, ) { - use #me::{ - standard::{ - nep171::{Nep171Controller, event}, - nep297::Event, - }, - }; + use #me::standard::nep171::*; + + #near_sdk::require!( + approval_id.is_none(), + APPROVAL_MANAGEMENT_NOT_SUPPORTED_MESSAGE, + ); #near_sdk::assert_one_yocto(); + let sender_id = #near_sdk::env::predecessor_account_id(); - let amount: u128 = amount.into(); let transfer = #me::standard::nep171::Nep171Transfer { + token_id: token_id.clone(), + owner_id: sender_id.clone(), sender_id: sender_id.clone(), receiver_id: receiver_id.clone(), - amount, + approval_id: None, memo: memo.clone(), msg: None, }; @@ -91,76 +148,89 @@ pub fn expand(meta: Nep171Meta) -> Result { Nep171Controller::transfer( self, + token_id, sender_id.clone(), - receiver_id.clone(), - amount, + sender_id, + receiver_id, memo, - ); + ) + .unwrap(); #after_transfer } - #[payable] - fn ft_transfer_call( + fn nft_transfer_call( &mut self, receiver_id: #near_sdk::AccountId, - amount: #near_sdk::json_types::U128, + token_id: #me::standard::nep171::TokenId, + approval_id: Option, memo: Option, msg: String, - ) -> #near_sdk::Promise { + ) -> #near_sdk::PromiseOrValue { + use #me::standard::nep171::*; + + #near_sdk::require!( + approval_id.is_none(), + APPROVAL_MANAGEMENT_NOT_SUPPORTED_MESSAGE, + ); + #near_sdk::assert_one_yocto(); + + #near_sdk::require!( + #near_sdk::env::prepaid_gas() > GAS_FOR_NFT_TRANSFER_CALL, + INSUFFICIENT_GAS_MESSAGE, + ); + let sender_id = #near_sdk::env::predecessor_account_id(); - let amount: u128 = amount.into(); let transfer = #me::standard::nep171::Nep171Transfer { + token_id: token_id.clone(), + owner_id: sender_id.clone(), sender_id: sender_id.clone(), receiver_id: receiver_id.clone(), - amount, + approval_id: None, memo: memo.clone(), - msg: None, + msg: Some(msg.clone()), }; #before_transfer - let r = #me::standard::nep171::Nep171Controller::transfer_call( + Nep171Controller::transfer( self, + token_id.clone(), + sender_id.clone(), sender_id.clone(), receiver_id.clone(), - amount, memo, - msg.clone(), - #near_sdk::env::prepaid_gas(), - ); + ) + .unwrap(); #after_transfer - r - } - - fn ft_total_supply(&self) -> #near_sdk::json_types::U128 { - ::total_supply().into() + ext_nep171_receiver::ext(receiver_id.clone()) + .with_static_gas(#near_sdk::env::prepaid_gas() - GAS_FOR_NFT_TRANSFER_CALL) + .nft_on_transfer( + sender_id.clone(), + receiver_id.clone(), + token_id.clone(), + msg, + ) + .then( + ext_nep171_resolver::ext(#near_sdk::env::current_account_id()) + .with_static_gas(GAS_FOR_RESOLVE_TRANSFER) + .nft_resolve_transfer(sender_id, receiver_id, token_id, None), + ) + .into() } - fn ft_balance_of(&self, account_id: #near_sdk::AccountId) -> #near_sdk::json_types::U128 { - ::balance_of(&account_id).into() - } - } + fn nft_token( + &self, + token_id: #me::standard::nep171::TokenId, + ) -> Option<#me::standard::nep171::Token> { + use #me::standard::nep171::*; - #[#near_sdk::near_bindgen] - impl #imp #me::standard::nep171::Nep171Resolver for #ident #ty #wher { - #[private] - fn ft_resolve_transfer( - &mut self, - sender_id: #near_sdk::AccountId, - receiver_id: #near_sdk::AccountId, - amount: #near_sdk::json_types::U128, - ) -> #near_sdk::json_types::U128 { - #me::standard::nep171::Nep171Controller::resolve_transfer( - self, - sender_id, - receiver_id, - amount.into(), - ).into() + Nep171Controller::token_owner(self, token_id.clone()) + .map(|owner_id| Token { token_id, owner_id }) } } }) diff --git a/src/lib.rs b/src/lib.rs index 4b69af3..aa29c76 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,8 @@ #![doc = include_str!("../README.md")] +use near_sdk::IntoStorageKey; +pub use near_sdk_contract_tools_macros::*; + /// Default storage keys used by various traits' `root()` functions. #[derive(Clone, Debug)] pub enum DefaultStorageKey { @@ -7,6 +10,8 @@ pub enum DefaultStorageKey { ApprovalManager, /// Default storage key for [`standard::nep141::Nep141Controller::root`] Nep141, + /// Default storage key for [`standard::nep171::Nep171Controller::root`] + Nep171, /// Default storage key for [`owner::Owner::root`] Owner, /// Default storage key for [`pause::Pause::root`] @@ -20,6 +25,7 @@ impl IntoStorageKey for DefaultStorageKey { match self { DefaultStorageKey::ApprovalManager => b"~am".to_vec(), DefaultStorageKey::Nep141 => b"~$141".to_vec(), + DefaultStorageKey::Nep171 => b"~$171".to_vec(), DefaultStorageKey::Owner => b"~o".to_vec(), DefaultStorageKey::Pause => b"~p".to_vec(), DefaultStorageKey::Rbac => b"~r".to_vec(), @@ -37,6 +43,3 @@ pub mod rbac; pub mod slot; pub mod upgrade; pub mod utils; - -use near_sdk::IntoStorageKey; -pub use near_sdk_contract_tools_macros::*; diff --git a/src/standard/nep171.rs b/src/standard/nep171.rs index d639144..281ec0b 100644 --- a/src/standard/nep171.rs +++ b/src/standard/nep171.rs @@ -8,7 +8,7 @@ use near_sdk_contract_tools_macros::event; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::slot::Slot; +use crate::{slot::Slot, DefaultStorageKey}; use super::nep297::Event; @@ -110,7 +110,9 @@ pub trait Nep171Extension { } pub trait Nep171ControllerInternal { - fn root() -> Slot<()>; + fn root() -> Slot<()> { + Slot::root(DefaultStorageKey::Nep171) + } fn slot_token_owner(token_id: TokenId) -> Slot { Self::root().field(StorageKey::TokenOwner(token_id)) @@ -136,8 +138,12 @@ pub trait Nep171Controller { /// Transfer metadata generic over both types of transfer (`nft_transfer` and /// `nft_transfer_call`). -#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize, PartialEq, Eq, Clone, Debug)] +#[derive( + Serialize, Deserialize, BorshSerialize, BorshDeserialize, PartialEq, Eq, Clone, Debug, Hash, +)] pub struct Nep171Transfer { + /// Current owner account ID. + pub owner_id: AccountId, /// Sending account ID. pub sender_id: AccountId, /// Receiving account ID. @@ -167,7 +173,7 @@ pub trait Nep171Hook { /// Executed after a token transfer is conducted. /// /// Receives the state value returned by `before_transfer`. - fn after_transfer(&mut self, _transfer: &Nep171Transfer, _state: T) {} + fn after_transfer(&mut self, _transfer: &Nep171Transfer, _state: T); } impl Nep171Controller for T { diff --git a/tests/macros/standard/nep171.rs b/tests/macros/standard/nep171.rs index 0b20de7..d09fda2 100644 --- a/tests/macros/standard/nep171.rs +++ b/tests/macros/standard/nep171.rs @@ -1,17 +1,31 @@ use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; -use near_sdk_contract_tools::slot::Slot; // #[derive(Nep171)] #[derive(BorshDeserialize, BorshSerialize)] #[near_sdk::near_bindgen] struct NonFungibleToken {} -impl near_sdk_contract_tools::standard::nep171::Nep171ControllerInternal for NonFungibleToken { - fn root() -> Slot<()> { - Slot::root(b"nft" as &[u8]) +#[derive(near_sdk_contract_tools_macros::Nep171, BorshDeserialize, BorshSerialize)] +#[near_sdk::near_bindgen] +struct DeriveNonFungibleToken {} + +impl near_sdk_contract_tools::standard::nep171::Nep171Hook for DeriveNonFungibleToken { + fn before_transfer( + &mut self, + _transfer: &near_sdk_contract_tools::standard::nep171::Nep171Transfer, + ) { + } + + fn after_transfer( + &mut self, + _transfer: &near_sdk_contract_tools::standard::nep171::Nep171Transfer, + _state: (), + ) { } } +impl near_sdk_contract_tools::standard::nep171::Nep171ControllerInternal for NonFungibleToken {} + #[near_sdk::near_bindgen] impl near_sdk_contract_tools::standard::nep171::Nep171Resolver for NonFungibleToken { #[private] @@ -20,8 +34,10 @@ impl near_sdk_contract_tools::standard::nep171::Nep171Resolver for NonFungibleTo previous_owner_id: near_sdk::AccountId, receiver_id: near_sdk::AccountId, token_id: near_sdk_contract_tools::standard::nep171::TokenId, - _approved_account_ids: Option>, + approved_account_ids: Option>, ) -> bool { + let _ = approved_account_ids; // #[near_bindgen] cares about parameter names + near_sdk::require!( near_sdk::env::promise_results_count() == 1, "Requires exactly one promise result.", @@ -35,6 +51,16 @@ impl near_sdk_contract_tools::standard::nep171::Nep171Resolver for NonFungibleTo }; if should_revert { + let transfer = near_sdk_contract_tools::standard::nep171::Nep171Transfer { + token_id: token_id.clone(), + owner_id: receiver_id.clone(), + sender_id: receiver_id.clone(), + receiver_id: previous_owner_id.clone(), + approval_id: None, + memo: None, + msg: None, + }; + near_sdk_contract_tools::standard::nep171::Nep171Controller::transfer( self, token_id, @@ -70,6 +96,16 @@ impl near_sdk_contract_tools::standard::nep171::Nep171 for NonFungibleToken { let sender_id = near_sdk::env::predecessor_account_id(); + let transfer = near_sdk_contract_tools::standard::nep171::Nep171Transfer { + token_id: token_id.clone(), + owner_id: sender_id.clone(), + sender_id: sender_id.clone(), + receiver_id: receiver_id.clone(), + approval_id: None, + memo: memo.clone(), + msg: None, + }; + Nep171Controller::transfer( self, token_id, @@ -105,6 +141,16 @@ impl near_sdk_contract_tools::standard::nep171::Nep171 for NonFungibleToken { let sender_id = near_sdk::env::predecessor_account_id(); + let transfer = near_sdk_contract_tools::standard::nep171::Nep171Transfer { + token_id: token_id.clone(), + owner_id: sender_id.clone(), + sender_id: sender_id.clone(), + receiver_id: receiver_id.clone(), + approval_id: None, + memo: memo.clone(), + msg: Some(msg.clone()), + }; + Nep171Controller::transfer( self, token_id.clone(), From 14763399036e36b22a8b026177a7c775c69121c3 Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Mon, 10 Jul 2023 14:26:58 -0500 Subject: [PATCH 09/34] chore: cleanup, tests, renaming --- macros/src/standard/nep171.rs | 20 +-- src/standard/nep171.rs | 250 +++++++++++++++++++--------- tests/macros/standard/nep171.rs | 278 +++++++++++++------------------- 3 files changed, 290 insertions(+), 258 deletions(-) diff --git a/macros/src/standard/nep171.rs b/macros/src/standard/nep171.rs index 8286814..b61321e 100644 --- a/macros/src/standard/nep171.rs +++ b/macros/src/standard/nep171.rs @@ -41,15 +41,15 @@ pub fn expand(meta: Nep171Meta) -> Result { } }); - let before_transfer = no_hooks.is_present().not().then(|| { + let before_nft_transfer = no_hooks.is_present().not().then(|| { quote! { - let hook_state = >::before_transfer(self, &transfer); + let hook_state = >::before_nft_transfer(self, &transfer); } }); - let after_transfer = no_hooks.is_present().not().then(|| { + let after_nft_transfer = no_hooks.is_present().not().then(|| { quote! { - >::after_transfer(self, &transfer, hook_state); + >::after_nft_transfer(self, &transfer, hook_state); } }); @@ -93,7 +93,7 @@ pub fn expand(meta: Nep171Meta) -> Result { msg: None, }; - #before_transfer + #before_nft_transfer let result = #me::standard::nep171::Nep171Controller::transfer( self, @@ -105,7 +105,7 @@ pub fn expand(meta: Nep171Meta) -> Result { ) .is_err(); - #after_transfer + #after_nft_transfer result } else { @@ -144,7 +144,7 @@ pub fn expand(meta: Nep171Meta) -> Result { msg: None, }; - #before_transfer + #before_nft_transfer Nep171Controller::transfer( self, @@ -156,7 +156,7 @@ pub fn expand(meta: Nep171Meta) -> Result { ) .unwrap(); - #after_transfer + #after_nft_transfer } fn nft_transfer_call( @@ -193,7 +193,7 @@ pub fn expand(meta: Nep171Meta) -> Result { msg: Some(msg.clone()), }; - #before_transfer + #before_nft_transfer Nep171Controller::transfer( self, @@ -205,7 +205,7 @@ pub fn expand(meta: Nep171Meta) -> Result { ) .unwrap(); - #after_transfer + #after_nft_transfer ext_nep171_receiver::ext(receiver_id.clone()) .with_static_gas(#near_sdk::env::prepaid_gas() - GAS_FOR_NFT_TRANSFER_CALL) diff --git a/src/standard/nep171.rs b/src/standard/nep171.rs index 281ec0b..1edbabc 100644 --- a/src/standard/nep171.rs +++ b/src/standard/nep171.rs @@ -82,25 +82,74 @@ enum StorageKey { TokenOwner(String), } -#[derive(Error, Clone, Debug)] -pub enum Nep171TransferError { +pub mod error { + use near_sdk::AccountId; + use thiserror::Error; + + use super::TokenId; + + #[derive(Error, Clone, Debug)] + #[error("Token `{token_id}` already exists")] + pub struct AlreadyExists { + pub token_id: TokenId, + } + + #[derive(Error, Clone, Debug)] + #[error("Token `{token_id}` does not exist")] + pub struct DoesNotExist { + pub token_id: TokenId, + } + + #[derive(Error, Clone, Debug)] + #[error( + "Token `{token_id}` is owned by `{actual_owner_id}` instead of expected `{expected_owner_id}`", + )] + pub struct NotOwnedByExpectedOwner { + pub expected_owner_id: AccountId, + pub actual_owner_id: AccountId, + pub token_id: TokenId, + } + + #[derive(Error, Clone, Debug)] #[error("Sender `{sender_id}` does not have permission to transfer token `{token_id}`")] - NotApproved { - sender_id: AccountId, - token_id: TokenId, - }, + pub struct NotApproved { + pub sender_id: AccountId, + pub token_id: TokenId, + } + + #[derive(Error, Clone, Debug)] #[error("Receiver must be different from current owner `{current_owner_id}` to transfer token `{token_id}`")] - ReceiverIsCurrentOwner { - current_owner_id: AccountId, - token_id: TokenId, - }, - #[error("Token `{token_id}` is no longer owned by the expected owner `{expected_owner_id}`")] - NotOwnedByExpectedOwner { - expected_owner_id: AccountId, - token_id: TokenId, - }, + pub struct ReceiverIsCurrentOwner { + pub current_owner_id: AccountId, + pub token_id: TokenId, + } +} + +#[derive(Error, Clone, Debug)] +pub enum Nep171BurnError { + #[error(transparent)] + DoesNotExist(#[from] error::DoesNotExist), + #[error(transparent)] + NotOwnedByExpectedOwner(#[from] error::NotOwnedByExpectedOwner), } +#[derive(Error, Clone, Debug)] +pub enum Nep171MintError { + #[error(transparent)] + AlreadyExists(#[from] error::AlreadyExists), +} + +#[derive(Error, Clone, Debug)] +pub enum Nep171TransferError { + #[error(transparent)] + DoesNotExist(#[from] error::DoesNotExist), + #[error(transparent)] + NotApproved(#[from] error::NotApproved), + #[error(transparent)] + ReceiverIsCurrentOwner(#[from] error::ReceiverIsCurrentOwner), + #[error(transparent)] + NotOwnedByExpectedOwner(#[from] error::NotOwnedByExpectedOwner), +} pub trait Nep171Extension { type Event: crate::standard::nep297::Event; @@ -129,9 +178,15 @@ pub trait Nep171Controller { memo: Option, ) -> Result<(), Nep171TransferError>; - fn mint(token_id: TokenId, new_owner_id: &AccountId) -> bool; + fn mint(&mut self, token_id: TokenId, new_owner_id: &AccountId) -> Result<(), Nep171MintError>; + + fn burn( + &mut self, + token_id: TokenId, + current_owner_id: &AccountId, + ) -> Result<(), Nep171BurnError>; - fn burn(token_id: TokenId) -> bool; + fn burn_unchecked(&mut self, token_id: TokenId) -> bool; fn token_owner(&self, token_id: TokenId) -> Option; } @@ -168,12 +223,12 @@ pub trait Nep171Hook { /// /// May return an optional state value which will be passed along to the /// following `after_transfer`. - fn before_transfer(&mut self, _transfer: &Nep171Transfer) -> T; + fn before_nft_transfer(&mut self, _transfer: &Nep171Transfer) -> T; /// Executed after a token transfer is conducted. /// /// Receives the state value returned by `before_transfer`. - fn after_transfer(&mut self, _transfer: &Nep171Transfer, _state: T); + fn after_nft_transfer(&mut self, _transfer: &Nep171Transfer, _state: T); } impl Nep171Controller for T { @@ -186,18 +241,20 @@ impl Nep171Controller for T { memo: Option, ) -> Result<(), Nep171TransferError> { if current_owner_id == receiver_id { - return Err(Nep171TransferError::ReceiverIsCurrentOwner { + return Err(error::ReceiverIsCurrentOwner { current_owner_id, token_id, - }); + } + .into()); } // This version doesn't implement approval management if sender_id != current_owner_id { - return Err(Nep171TransferError::NotApproved { + return Err(error::NotApproved { sender_id, token_id, - }); + } + .into()); } let mut slot = Self::slot_token_owner(token_id.clone()); @@ -206,17 +263,16 @@ impl Nep171Controller for T { owner_id } else { // Using if-let instead of .ok_or_else() to avoid .clone() - return Err(Nep171TransferError::NotOwnedByExpectedOwner { - expected_owner_id: current_owner_id, - token_id, - }); + return Err(error::DoesNotExist { token_id }.into()); }; if current_owner_id != actual_current_owner_id { - return Err(Nep171TransferError::NotOwnedByExpectedOwner { + return Err(error::NotOwnedByExpectedOwner { expected_owner_id: current_owner_id, + actual_owner_id: actual_current_owner_id, token_id, - }); + } + .into()); } slot.write(&receiver_id); @@ -233,17 +289,42 @@ impl Nep171Controller for T { Ok(()) } - fn mint(token_id: TokenId, new_owner_id: &AccountId) -> bool { - let mut slot = Self::slot_token_owner(token_id); + fn mint(&mut self, token_id: TokenId, new_owner_id: &AccountId) -> Result<(), Nep171MintError> { + let mut slot = Self::slot_token_owner(token_id.clone()); if !slot.exists() { slot.write(new_owner_id); - true + Ok(()) } else { - false + Err(error::AlreadyExists { token_id }.into()) } } - fn burn(token_id: TokenId) -> bool { + fn burn( + &mut self, + token_id: TokenId, + current_owner_id: &AccountId, + ) -> Result<(), Nep171BurnError> { + let mut slot = Self::slot_token_owner(token_id.clone()); + let actual_owner_id = if let Some(account_id) = slot.read() { + account_id + } else { + return Err(error::DoesNotExist { token_id }.into()); + }; + + if current_owner_id != &actual_owner_id { + return Err(error::NotOwnedByExpectedOwner { + expected_owner_id: current_owner_id.clone(), + actual_owner_id, + token_id, + } + .into()); + } + + slot.remove(); + Ok(()) + } + + fn burn_unchecked(&mut self, token_id: TokenId) -> bool { Self::slot_token_owner(token_id).remove() } @@ -258,52 +339,65 @@ pub struct Token { pub owner_id: AccountId, } -#[ext_contract(ext_nep171)] -pub trait Nep171 { - fn nft_transfer( - &mut self, - receiver_id: AccountId, - token_id: TokenId, - approval_id: Option, - memo: Option, - ); +// separate module with re-export because ext_contract doesn't play well with #![warn(missing_docs)] +mod ext { + #![allow(missing_docs)] - fn nft_transfer_call( - &mut self, - receiver_id: AccountId, - token_id: TokenId, - approval_id: Option, - memo: Option, - msg: String, - ) -> PromiseOrValue; + use std::collections::HashMap; - fn nft_token(&self, token_id: TokenId) -> Option; -} + use near_sdk::{ext_contract, AccountId, PromiseOrValue}; -#[ext_contract(ext_nep171_resolver)] -pub trait Nep171Resolver { - fn nft_resolve_transfer( - &mut self, - previous_owner_id: AccountId, - receiver_id: AccountId, - token_id: TokenId, - approved_account_ids: Option>, - ) -> bool; -} + use super::{Token, TokenId}; -/// A contract that may be the recipient of an `nft_transfer_call` function -/// call. -#[ext_contract(ext_nep171_receiver)] -pub trait Nep171Receiver { - /// Function that is called in an `nft_transfer_call` promise chain. - /// Returns the number of tokens "used", that is, those that will be kept - /// in the receiving contract's account. (The contract will attempt to - /// refund the difference from `amount` to the original sender.) - fn nft_on_transfer( - &mut self, - sender_id: AccountId, - previous_owner_id: AccountId, - token_id: TokenId, - msg: String, - ) -> PromiseOrValue; + #[ext_contract(ext_nep171)] + pub trait Nep171 { + fn nft_transfer( + &mut self, + receiver_id: AccountId, + token_id: TokenId, + approval_id: Option, + memo: Option, + ); + + fn nft_transfer_call( + &mut self, + receiver_id: AccountId, + token_id: TokenId, + approval_id: Option, + memo: Option, + msg: String, + ) -> PromiseOrValue; + + fn nft_token(&self, token_id: TokenId) -> Option; + } + + #[ext_contract(ext_nep171_resolver)] + pub trait Nep171Resolver { + fn nft_resolve_transfer( + &mut self, + previous_owner_id: AccountId, + receiver_id: AccountId, + token_id: TokenId, + approved_account_ids: Option>, + ) -> bool; + } + + /// A contract that may be the recipient of an `nft_transfer_call` function + /// call. + #[ext_contract(ext_nep171_receiver)] + pub trait Nep171Receiver { + /// Function that is called in an `nft_transfer_call` promise chain. + /// Returns the number of tokens "used", that is, those that will be kept + /// in the receiving contract's account. (The contract will attempt to + /// refund the difference from `amount` to the original sender.) + fn nft_on_transfer( + &mut self, + sender_id: AccountId, + previous_owner_id: AccountId, + token_id: TokenId, + msg: String, + ) -> PromiseOrValue; + } } + +pub use ext::*; diff --git a/tests/macros/standard/nep171.rs b/tests/macros/standard/nep171.rs index d09fda2..259ec69 100644 --- a/tests/macros/standard/nep171.rs +++ b/tests/macros/standard/nep171.rs @@ -1,189 +1,127 @@ -use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; - -// #[derive(Nep171)] -#[derive(BorshDeserialize, BorshSerialize)] -#[near_sdk::near_bindgen] -struct NonFungibleToken {} - -#[derive(near_sdk_contract_tools_macros::Nep171, BorshDeserialize, BorshSerialize)] -#[near_sdk::near_bindgen] -struct DeriveNonFungibleToken {} - -impl near_sdk_contract_tools::standard::nep171::Nep171Hook for DeriveNonFungibleToken { - fn before_transfer( - &mut self, - _transfer: &near_sdk_contract_tools::standard::nep171::Nep171Transfer, - ) { - } +use near_sdk::{ + borsh::{self, BorshDeserialize, BorshSerialize}, + env, near_bindgen, store, AccountId, +}; +use near_sdk_contract_tools::{standard::nep171::*, Nep171}; + +#[derive(BorshDeserialize, BorshSerialize, Debug, Clone, PartialEq, PartialOrd)] +struct TokenRecord { + owner_id: AccountId, + token_id: TokenId, +} - fn after_transfer( - &mut self, - _transfer: &near_sdk_contract_tools::standard::nep171::Nep171Transfer, - _state: (), - ) { +impl From for TokenRecord { + fn from(token: Token) -> Self { + Self { + owner_id: token.owner_id, + token_id: token.token_id, + } } } -impl near_sdk_contract_tools::standard::nep171::Nep171ControllerInternal for NonFungibleToken {} - -#[near_sdk::near_bindgen] -impl near_sdk_contract_tools::standard::nep171::Nep171Resolver for NonFungibleToken { - #[private] - fn nft_resolve_transfer( - &mut self, - previous_owner_id: near_sdk::AccountId, - receiver_id: near_sdk::AccountId, - token_id: near_sdk_contract_tools::standard::nep171::TokenId, - approved_account_ids: Option>, - ) -> bool { - let _ = approved_account_ids; // #[near_bindgen] cares about parameter names - - near_sdk::require!( - near_sdk::env::promise_results_count() == 1, - "Requires exactly one promise result.", - ); +#[derive(Nep171, BorshDeserialize, BorshSerialize)] +#[near_bindgen] +struct NonFungibleToken { + pub before_nft_transfer_balance_record: store::Vector>, + pub after_nft_transfer_balance_record: store::Vector>, +} - let should_revert = - if let near_sdk::PromiseResult::Successful(value) = near_sdk::env::promise_result(0) { - near_sdk::serde_json::from_slice::(&value).unwrap_or(true) - } else { - true - }; - - if should_revert { - let transfer = near_sdk_contract_tools::standard::nep171::Nep171Transfer { - token_id: token_id.clone(), - owner_id: receiver_id.clone(), - sender_id: receiver_id.clone(), - receiver_id: previous_owner_id.clone(), - approval_id: None, - memo: None, - msg: None, - }; - - near_sdk_contract_tools::standard::nep171::Nep171Controller::transfer( - self, - token_id, - receiver_id.clone(), - receiver_id, - previous_owner_id, - None, - ) - .is_err() - } else { - true - } +impl Nep171Hook for NonFungibleToken { + fn before_nft_transfer(&mut self, transfer: &Nep171Transfer) { + let token = Nep171::nft_token(self, transfer.token_id.clone()); + self.before_nft_transfer_balance_record + .push(token.map(Into::into)); + } + + fn after_nft_transfer(&mut self, transfer: &Nep171Transfer, _state: ()) { + let token = Nep171::nft_token(self, transfer.token_id.clone()); + self.after_nft_transfer_balance_record + .push(token.map(Into::into)); } } -#[near_sdk::near_bindgen] -impl near_sdk_contract_tools::standard::nep171::Nep171 for NonFungibleToken { - fn nft_transfer( - &mut self, - receiver_id: near_sdk::AccountId, - token_id: String, - approval_id: Option, - memo: Option, - ) { - use near_sdk_contract_tools::standard::nep171::*; - - near_sdk::require!( - approval_id.is_none(), - APPROVAL_MANAGEMENT_NOT_SUPPORTED_MESSAGE, - ); +#[near_bindgen] +impl NonFungibleToken { + #[init] + pub fn new() -> Self { + Self { + before_nft_transfer_balance_record: store::Vector::new(b"b"), + after_nft_transfer_balance_record: store::Vector::new(b"a"), + } + } - near_sdk::assert_one_yocto(); - - let sender_id = near_sdk::env::predecessor_account_id(); - - let transfer = near_sdk_contract_tools::standard::nep171::Nep171Transfer { - token_id: token_id.clone(), - owner_id: sender_id.clone(), - sender_id: sender_id.clone(), - receiver_id: receiver_id.clone(), - approval_id: None, - memo: memo.clone(), - msg: None, - }; - - Nep171Controller::transfer( - self, - token_id, - sender_id.clone(), - sender_id, - receiver_id, - memo, - ) - .unwrap(); + pub fn mint(&mut self, token_id: TokenId, owner_id: AccountId) { + Nep171Controller::mint(self, token_id, &owner_id).unwrap_or_else(|e| { + env::panic_str(&format!("Mint failed: {e:?}")); + }); } +} - fn nft_transfer_call( - &mut self, - receiver_id: near_sdk::AccountId, - token_id: String, - approval_id: Option, - memo: Option, - msg: String, - ) -> near_sdk::PromiseOrValue { - use near_sdk_contract_tools::standard::nep171::*; - - near_sdk::require!( - approval_id.is_none(), - APPROVAL_MANAGEMENT_NOT_SUPPORTED_MESSAGE, - ); +mod tests { + use near_sdk::{ + test_utils::{get_logs, VMContextBuilder}, + testing_env, AccountId, + }; + use near_sdk_contract_tools::standard::{nep171::Nep171, nep297::Event}; + + use super::NonFungibleToken; - near_sdk::assert_one_yocto(); + #[test] + fn hook_execution_success() { + let mut contract = NonFungibleToken::new(); + let token_id = "token1"; + let account_alice: AccountId = "alice.near".parse().unwrap(); + let account_bob: AccountId = "bob.near".parse().unwrap(); - near_sdk::require!( - near_sdk::env::prepaid_gas() > GAS_FOR_NFT_TRANSFER_CALL, - INSUFFICIENT_GAS_MESSAGE, + contract.mint(token_id.to_string(), account_alice.clone()); + + assert_eq!( + contract.before_nft_transfer_balance_record.get(0), + None, + "before_nft_transfer_balance_record should be empty", + ); + assert_eq!( + contract.after_nft_transfer_balance_record.get(0), + None, + "after_nft_transfer_balance_record should be empty", ); - let sender_id = near_sdk::env::predecessor_account_id(); - - let transfer = near_sdk_contract_tools::standard::nep171::Nep171Transfer { - token_id: token_id.clone(), - owner_id: sender_id.clone(), - sender_id: sender_id.clone(), - receiver_id: receiver_id.clone(), - approval_id: None, - memo: memo.clone(), - msg: Some(msg.clone()), - }; - - Nep171Controller::transfer( - self, - token_id.clone(), - sender_id.clone(), - sender_id.clone(), - receiver_id.clone(), - memo, - ) - .unwrap(); - - ext_nep171_receiver::ext(receiver_id.clone()) - .with_static_gas(near_sdk::env::prepaid_gas() - GAS_FOR_NFT_TRANSFER_CALL) - .nft_on_transfer( - sender_id.clone(), - receiver_id.clone(), - token_id.clone(), - msg, - ) - .then( - ext_nep171_resolver::ext(near_sdk::env::current_account_id()) - .with_static_gas(GAS_FOR_RESOLVE_TRANSFER) - .nft_resolve_transfer(sender_id, receiver_id, token_id, None), - ) - .into() - } + testing_env!(VMContextBuilder::new() + .predecessor_account_id(account_alice.clone()) + .attached_deposit(1) + .build()); - fn nft_token( - &self, - token_id: String, - ) -> Option { - use near_sdk_contract_tools::standard::nep171::*; + contract.nft_transfer(account_bob.clone(), token_id.to_string(), None, None); - Nep171Controller::token_owner(self, token_id.clone()) - .map(|owner_id| Token { token_id, owner_id }) + assert_eq!( + contract.before_nft_transfer_balance_record.get(0), + Some(&Some(super::TokenRecord { + owner_id: account_alice.clone(), + token_id: token_id.to_string(), + })), + "before_nft_transfer_balance_record should contain the token record for the original owner before transferring", + ); + assert_eq!( + contract.after_nft_transfer_balance_record.get(0), + Some(&Some(super::TokenRecord { + owner_id: account_bob.clone(), + token_id: token_id.to_string(), + })), + "after_nft_transfer_balance_record should contain the token record for the new owner after transferring", + ); + + assert_eq!( + get_logs(), + vec![ + super::Nep171Event::NftTransfer(vec![super::event::NftTransferLog { + memo: None, + authorized_id: None, + old_owner_id: account_alice.clone(), + new_owner_id: account_bob.clone(), + token_ids: vec![token_id.to_string()] + }]) + .to_event_string() + ] + ); } } From 438d83c7fd5d0b0e2695948ef34451ee87dc8de3 Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Thu, 13 Jul 2023 16:37:15 -0700 Subject: [PATCH 10/34] chore: documentation comments --- macros/src/lib.rs | 6 ++ src/standard/nep171.rs | 127 ++++++++++++++++++++++++++++++++--------- 2 files changed, 105 insertions(+), 28 deletions(-) diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 5e9141a..d5c62d1 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -147,6 +147,12 @@ pub fn derive_fungible_token(input: TokenStream) -> TokenStream { make_derive(input, standard::fungible_token::expand) } +/// Adds NEP-171 non-fungible token core functionality to a contract. Exposes +/// `nft_*` functions to the public blockchain, implements internal controller +/// and receiver functionality (see: [`near_sdk_contract_tools::standard::nep171`]). +/// +/// The storage key prefix for the fields can be optionally specified (default: +/// `"~$171"`) using `#[nep171(storage_key = "")]`. #[proc_macro_derive(Nep171, attributes(nep171))] pub fn derive_nep171(input: TokenStream) -> TokenStream { make_derive(input, standard::nep171::expand) diff --git a/src/standard/nep171.rs b/src/standard/nep171.rs index 1edbabc..c88ba4d 100644 --- a/src/standard/nep171.rs +++ b/src/standard/nep171.rs @@ -1,8 +1,10 @@ -use std::collections::HashMap; +//! NEP-171 non-fungible token core implementation. +//! +//! Reference: use near_sdk::{ borsh::{self, BorshDeserialize, BorshSerialize}, - ext_contract, AccountId, BorshStorageKey, Gas, PromiseOrValue, + AccountId, BorshStorageKey, Gas, }; use near_sdk_contract_tools_macros::event; use serde::{Deserialize, Serialize}; @@ -12,14 +14,20 @@ use crate::{slot::Slot, DefaultStorageKey}; use super::nep297::Event; +/// Minimum required gas for [`Nep171Resolver::nft_resolve_transfer`] call in promise chain during [`Nep171::nft_transfer_call`]. pub const GAS_FOR_RESOLVE_TRANSFER: Gas = Gas(5_000_000_000_000); +/// Minimum gas required to execute the main body of [`Nep171::nft_transfer_call`] + gas for [`Nep171Resolver::nft_resolve_transfer`]. pub const GAS_FOR_NFT_TRANSFER_CALL: Gas = Gas(25_000_000_000_000 + GAS_FOR_RESOLVE_TRANSFER.0); +/// Error message when insufficient gas is attached to function calls with a minimum attached gas requirement (i.e. those that produce a promise chain, perform cross-contract calls). pub const INSUFFICIENT_GAS_MESSAGE: &str = "More gas is required"; +/// Error message when the NEP-171 implementation does not also implement NEP-178. pub const APPROVAL_MANAGEMENT_NOT_SUPPORTED_MESSAGE: &str = "NEP-178: Approval Management is not supported"; +/// NFT token IDs. pub type TokenId = String; +/// NEP-171 standard events. #[event( crate = "crate", macros = "crate", @@ -29,49 +37,71 @@ pub type TokenId = String; )] #[derive(Debug, Clone)] pub enum Nep171Event { + /// Emitted when a token is newly minted. NftMint(Vec), + /// Emitted when a token is transferred between two parties. NftTransfer(Vec), + /// Emitted when a token is burned. NftBurn(Vec), + /// Emitted when the metadata associated with an NFT contract is updated. ContractMetadataUpdate(Vec), } +/// Event log metadata & associated structures. pub mod event { use near_sdk::AccountId; use serde::Serialize; use super::TokenId; + /// Tokens minted to a single owner. #[derive(Serialize, Debug, Clone)] pub struct NftMintLog { + /// To whom were the new tokens minted? pub owner_id: AccountId, + /// Which tokens were minted? pub token_ids: Vec, + /// Additional mint information. #[serde(skip_serializing_if = "Option::is_none")] pub memo: Option, } + /// Tokens are transferred from one account to another. #[derive(Serialize, Debug, Clone)] pub struct NftTransferLog { + /// NEP-178 authorized account ID. #[serde(skip_serializing_if = "Option::is_none")] pub authorized_id: Option, + /// Account ID of the previous owner. pub old_owner_id: AccountId, + /// Account ID of the new owner. pub new_owner_id: AccountId, + /// IDs of the transferred tokens. pub token_ids: Vec, + /// Additional transfer information. #[serde(skip_serializing_if = "Option::is_none")] pub memo: Option, } + /// Tokens are burned from a single holder. #[derive(Serialize, Debug, Clone)] pub struct NftBurnLog { + /// What is the ID of the account from which the tokens were burned? pub owner_id: AccountId, + /// IDs of the burned tokens. pub token_ids: Vec, + /// NEP-178 authorized account ID. #[serde(skip_serializing_if = "Option::is_none")] pub authorized_id: Option, + /// Additional burn information. #[serde(skip_serializing_if = "Option::is_none")] pub memo: Option, } + /// Contract metadata update metadata. #[derive(Serialize, Debug, Clone)] pub struct ContractMetadataUpdateLog { + /// Additional update information. #[serde(skip_serializing_if = "Option::is_none")] pub memo: Option, } @@ -82,93 +112,120 @@ enum StorageKey { TokenOwner(String), } +/// Potential errors produced by various token manipulations. pub mod error { use near_sdk::AccountId; use thiserror::Error; use super::TokenId; + /// Occurs when trying to create a token ID that already exists. + /// Overwriting pre-existing token IDs is not allowed. #[derive(Error, Clone, Debug)] #[error("Token `{token_id}` already exists")] - pub struct AlreadyExists { + pub struct TokenAlreadyExistsError { + /// The conflicting token ID. pub token_id: TokenId, } + /// When attempting to interact with a non-existent token ID. #[derive(Error, Clone, Debug)] #[error("Token `{token_id}` does not exist")] - pub struct DoesNotExist { + pub struct TokenDoesNotExistError { + /// The invalid token ID. pub token_id: TokenId, } + /// Occurs when performing a checked operation that expects a token to be + /// owned by a particular account, but the token is _not_ owned by that + /// account. #[derive(Error, Clone, Debug)] #[error( "Token `{token_id}` is owned by `{actual_owner_id}` instead of expected `{expected_owner_id}`", )] - pub struct NotOwnedByExpectedOwner { + pub struct TokenNotOwnedByExpectedOwnerError { + /// The token was supposed to be owned by this account. pub expected_owner_id: AccountId, + /// The token is actually owned by this account. pub actual_owner_id: AccountId, + /// The ID of the token in question. pub token_id: TokenId, } + /// Occurs when a particular account is not allowed to transfer a token (e.g. on behalf of another user). See: NEP-178. #[derive(Error, Clone, Debug)] #[error("Sender `{sender_id}` does not have permission to transfer token `{token_id}`")] - pub struct NotApproved { + pub struct SenderNotApprovedError { + /// The unapproved sender. pub sender_id: AccountId, + /// The ID of the token in question. pub token_id: TokenId, } + /// Occurs when attempting to perform a transfer of a token from one + /// account to the same account. #[derive(Error, Clone, Debug)] #[error("Receiver must be different from current owner `{current_owner_id}` to transfer token `{token_id}`")] - pub struct ReceiverIsCurrentOwner { + pub struct TokenReceiverIsCurrentOwnerError { + /// The account ID of current owner of the token. pub current_owner_id: AccountId, + /// The ID of the token in question. pub token_id: TokenId, } } +/// Potential errors encountered when performing a burn operation. #[derive(Error, Clone, Debug)] pub enum Nep171BurnError { + /// The token could not be burned because it does not exist. #[error(transparent)] - DoesNotExist(#[from] error::DoesNotExist), + TokenDoesNotExist(#[from] error::TokenDoesNotExistError), + /// The token could not be burned because it is not owned by the expected owner. #[error(transparent)] - NotOwnedByExpectedOwner(#[from] error::NotOwnedByExpectedOwner), + TokenNotOwnedByExpectedOwner(#[from] error::TokenNotOwnedByExpectedOwnerError), } +/// Potential errors encountered when attempting to mint a new token. #[derive(Error, Clone, Debug)] pub enum Nep171MintError { + /// The token could not be minted because a token with the same ID already exists. #[error(transparent)] - AlreadyExists(#[from] error::AlreadyExists), + TokenAlreadyExists(#[from] error::TokenAlreadyExistsError), } +/// Potential errors encountered when performing a token transfer. #[derive(Error, Clone, Debug)] pub enum Nep171TransferError { + /// The token could not be transferred because it does not exist. #[error(transparent)] - DoesNotExist(#[from] error::DoesNotExist), + TokenDoesNotExist(#[from] error::TokenDoesNotExistError), + /// The token could not be transferred because the sender is not allowed to perform transfers of this token on behalf of its current owner. See: NEP-178. #[error(transparent)] - NotApproved(#[from] error::NotApproved), + SenderNotApproved(#[from] error::SenderNotApprovedError), + /// The token could not be transferred because the token is being sent to the account that currently owns it. Reflexive transfers are not allowed. #[error(transparent)] - ReceiverIsCurrentOwner(#[from] error::ReceiverIsCurrentOwner), + TokenReceiverIsCurrentOwner(#[from] error::TokenReceiverIsCurrentOwnerError), + /// The token could not be transferred because it is no longer owned by the expected owner. #[error(transparent)] - NotOwnedByExpectedOwner(#[from] error::NotOwnedByExpectedOwner), -} -pub trait Nep171Extension { - type Event: crate::standard::nep297::Event; - - fn handle_transfer( - result: Result, - ) -> Result; + TokenNotOwnedByExpectedOwner(#[from] error::TokenNotOwnedByExpectedOwnerError), } +/// Internal (storage location) methods for implementors of [`Nep171Controller`]. pub trait Nep171ControllerInternal { + /// Root storage slot. fn root() -> Slot<()> { Slot::root(DefaultStorageKey::Nep171) } + /// Storage slot for the owner of a token. fn slot_token_owner(token_id: TokenId) -> Slot { Self::root().field(StorageKey::TokenOwner(token_id)) } } +/// Non-public controller interface for NEP-171 implementations. pub trait Nep171Controller { + /// Transfer a token from `sender_id` to `receiver_id`. fn transfer( &mut self, token_id: TokenId, @@ -178,16 +235,20 @@ pub trait Nep171Controller { memo: Option, ) -> Result<(), Nep171TransferError>; + /// Mints a new token `token_id` to `owner_id`. fn mint(&mut self, token_id: TokenId, new_owner_id: &AccountId) -> Result<(), Nep171MintError>; + /// Burns a token `token_id` owned by `current_owner_id`. fn burn( &mut self, token_id: TokenId, current_owner_id: &AccountId, ) -> Result<(), Nep171BurnError>; + /// Burns a token `token_id` without checking the owner. fn burn_unchecked(&mut self, token_id: TokenId) -> bool; + /// Returns the owner of a token, if it exists. fn token_owner(&self, token_id: TokenId) -> Option; } @@ -241,7 +302,7 @@ impl Nep171Controller for T { memo: Option, ) -> Result<(), Nep171TransferError> { if current_owner_id == receiver_id { - return Err(error::ReceiverIsCurrentOwner { + return Err(error::TokenReceiverIsCurrentOwnerError { current_owner_id, token_id, } @@ -250,7 +311,7 @@ impl Nep171Controller for T { // This version doesn't implement approval management if sender_id != current_owner_id { - return Err(error::NotApproved { + return Err(error::SenderNotApprovedError { sender_id, token_id, } @@ -263,11 +324,11 @@ impl Nep171Controller for T { owner_id } else { // Using if-let instead of .ok_or_else() to avoid .clone() - return Err(error::DoesNotExist { token_id }.into()); + return Err(error::TokenDoesNotExistError { token_id }.into()); }; if current_owner_id != actual_current_owner_id { - return Err(error::NotOwnedByExpectedOwner { + return Err(error::TokenNotOwnedByExpectedOwnerError { expected_owner_id: current_owner_id, actual_owner_id: actual_current_owner_id, token_id, @@ -295,7 +356,7 @@ impl Nep171Controller for T { slot.write(new_owner_id); Ok(()) } else { - Err(error::AlreadyExists { token_id }.into()) + Err(error::TokenAlreadyExistsError { token_id }.into()) } } @@ -308,11 +369,11 @@ impl Nep171Controller for T { let actual_owner_id = if let Some(account_id) = slot.read() { account_id } else { - return Err(error::DoesNotExist { token_id }.into()); + return Err(error::TokenDoesNotExistError { token_id }.into()); }; if current_owner_id != &actual_owner_id { - return Err(error::NotOwnedByExpectedOwner { + return Err(error::TokenNotOwnedByExpectedOwnerError { expected_owner_id: current_owner_id.clone(), actual_owner_id, token_id, @@ -333,9 +394,12 @@ impl Nep171Controller for T { } } +/// Token information structure. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Token { + /// Token ID. pub token_id: TokenId, + /// Current owner of the token. pub owner_id: AccountId, } @@ -349,8 +413,10 @@ mod ext { use super::{Token, TokenId}; + /// Interface of contracts that implement NEP-171. #[ext_contract(ext_nep171)] pub trait Nep171 { + /// Transfer a token. fn nft_transfer( &mut self, receiver_id: AccountId, @@ -359,6 +425,7 @@ mod ext { memo: Option, ); + /// Transfer a token, and call [`Nep171Receiver::nft_on_transfer`] on the receiving account. fn nft_transfer_call( &mut self, receiver_id: AccountId, @@ -368,11 +435,15 @@ mod ext { msg: String, ) -> PromiseOrValue; + /// Get individual token information. fn nft_token(&self, token_id: TokenId) -> Option; } + /// Original token contract follow-up to [`Nep171::nft_transfer_call`]. #[ext_contract(ext_nep171_resolver)] pub trait Nep171Resolver { + /// Final method call on the original token contract during an + /// [`Nep171::nft_transfer_call`] promise chain. fn nft_resolve_transfer( &mut self, previous_owner_id: AccountId, From 0f1445f7e95828aa1a18cd47d18b68a21dd39759 Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Mon, 17 Jul 2023 15:30:38 +0900 Subject: [PATCH 11/34] chore: workspaces tests & multiple token ids --- macros/src/standard/nep171.rs | 8 +- src/standard/nep171.rs | 144 ++++++++++++------ tests/macros/standard/nep171.rs | 2 +- workspaces-tests/Cargo.toml | 3 + .../src/bin/non_fungible_token.rs | 39 +++++ workspaces-tests/tests/non_fungible_token.rs | 87 +++++++++++ 6 files changed, 230 insertions(+), 53 deletions(-) create mode 100644 workspaces-tests/src/bin/non_fungible_token.rs create mode 100644 workspaces-tests/tests/non_fungible_token.rs diff --git a/macros/src/standard/nep171.rs b/macros/src/standard/nep171.rs index b61321e..11291f1 100644 --- a/macros/src/standard/nep171.rs +++ b/macros/src/standard/nep171.rs @@ -97,7 +97,7 @@ pub fn expand(meta: Nep171Meta) -> Result { let result = #me::standard::nep171::Nep171Controller::transfer( self, - token_id, + &token_id, receiver_id.clone(), receiver_id, previous_owner_id, @@ -148,7 +148,7 @@ pub fn expand(meta: Nep171Meta) -> Result { Nep171Controller::transfer( self, - token_id, + &token_id, sender_id.clone(), sender_id, receiver_id, @@ -197,7 +197,7 @@ pub fn expand(meta: Nep171Meta) -> Result { Nep171Controller::transfer( self, - token_id.clone(), + &token_id, sender_id.clone(), sender_id.clone(), receiver_id.clone(), @@ -229,7 +229,7 @@ pub fn expand(meta: Nep171Meta) -> Result { ) -> Option<#me::standard::nep171::Token> { use #me::standard::nep171::*; - Nep171Controller::token_owner(self, token_id.clone()) + Nep171Controller::token_owner(self, &token_id) .map(|owner_id| Token { token_id, owner_id }) } } diff --git a/src/standard/nep171.rs b/src/standard/nep171.rs index c88ba4d..b1d7b11 100644 --- a/src/standard/nep171.rs +++ b/src/standard/nep171.rs @@ -108,8 +108,8 @@ pub mod event { } #[derive(BorshSerialize, BorshStorageKey)] -enum StorageKey { - TokenOwner(String), +enum StorageKey<'a> { + TokenOwner(&'a str), } /// Potential errors produced by various token manipulations. @@ -218,7 +218,7 @@ pub trait Nep171ControllerInternal { } /// Storage slot for the owner of a token. - fn slot_token_owner(token_id: TokenId) -> Slot { + fn slot_token_owner(token_id: &TokenId) -> Slot { Self::root().field(StorageKey::TokenOwner(token_id)) } } @@ -228,7 +228,7 @@ pub trait Nep171Controller { /// Transfer a token from `sender_id` to `receiver_id`. fn transfer( &mut self, - token_id: TokenId, + token_id: &TokenId, // TODO: Change to &[TokenId] current_owner_id: AccountId, sender_id: AccountId, receiver_id: AccountId, @@ -236,20 +236,24 @@ pub trait Nep171Controller { ) -> Result<(), Nep171TransferError>; /// Mints a new token `token_id` to `owner_id`. - fn mint(&mut self, token_id: TokenId, new_owner_id: &AccountId) -> Result<(), Nep171MintError>; + fn mint( + &mut self, + token_ids: &[TokenId], + new_owner_id: &AccountId, + ) -> Result<(), Nep171MintError>; - /// Burns a token `token_id` owned by `current_owner_id`. + /// Burns tokens `token_ids` owned by `current_owner_id`. fn burn( &mut self, - token_id: TokenId, + token_ids: &[TokenId], current_owner_id: &AccountId, ) -> Result<(), Nep171BurnError>; - /// Burns a token `token_id` without checking the owner. - fn burn_unchecked(&mut self, token_id: TokenId) -> bool; + /// Burns tokens `token_ids` without checking the owners. + fn burn_unchecked(&mut self, token_ids: &[TokenId]) -> bool; /// Returns the owner of a token, if it exists. - fn token_owner(&self, token_id: TokenId) -> Option; + fn token_owner(&self, token_id: &TokenId) -> Option; } /// Transfer metadata generic over both types of transfer (`nft_transfer` and @@ -295,7 +299,7 @@ pub trait Nep171Hook { impl Nep171Controller for T { fn transfer( &mut self, - token_id: TokenId, + token_id: &TokenId, current_owner_id: AccountId, sender_id: AccountId, receiver_id: AccountId, @@ -304,7 +308,7 @@ impl Nep171Controller for T { if current_owner_id == receiver_id { return Err(error::TokenReceiverIsCurrentOwnerError { current_owner_id, - token_id, + token_id: token_id.clone(), } .into()); } @@ -313,25 +317,22 @@ impl Nep171Controller for T { if sender_id != current_owner_id { return Err(error::SenderNotApprovedError { sender_id, - token_id, + token_id: token_id.clone(), } .into()); } - let mut slot = Self::slot_token_owner(token_id.clone()); + let mut slot = Self::slot_token_owner(token_id); - let actual_current_owner_id = if let Some(owner_id) = slot.read() { - owner_id - } else { - // Using if-let instead of .ok_or_else() to avoid .clone() - return Err(error::TokenDoesNotExistError { token_id }.into()); - }; + let actual_current_owner_id = slot.read().ok_or_else(|| error::TokenDoesNotExistError { + token_id: token_id.clone(), + })?; if current_owner_id != actual_current_owner_id { return Err(error::TokenNotOwnedByExpectedOwnerError { expected_owner_id: current_owner_id, actual_owner_id: actual_current_owner_id, - token_id, + token_id: token_id.clone(), } .into()); } @@ -342,7 +343,7 @@ impl Nep171Controller for T { authorized_id: None, old_owner_id: actual_current_owner_id, new_owner_id: receiver_id, - token_ids: vec![token_id], + token_ids: vec![token_id.clone()], memo, }]) .emit(); @@ -350,52 +351,99 @@ impl Nep171Controller for T { Ok(()) } - fn mint(&mut self, token_id: TokenId, new_owner_id: &AccountId) -> Result<(), Nep171MintError> { - let mut slot = Self::slot_token_owner(token_id.clone()); - if !slot.exists() { - slot.write(new_owner_id); - Ok(()) - } else { - Err(error::TokenAlreadyExistsError { token_id }.into()) + fn mint( + &mut self, + token_ids: &[TokenId], + new_owner_id: &AccountId, + ) -> Result<(), Nep171MintError> { + for token_id in token_ids { + let slot = Self::slot_token_owner(token_id); + if slot.exists() { + return Err(error::TokenAlreadyExistsError { + token_id: token_id.to_string(), + } + .into()); + } } + + Nep171Event::NftMint(vec![event::NftMintLog { + token_ids: token_ids.iter().map(ToString::to_string).collect(), + owner_id: new_owner_id.clone(), + memo: None, + }]) + .emit(); + + token_ids.iter().for_each(|token_id| { + let mut slot = Self::slot_token_owner(token_id); + slot.write(new_owner_id); + }); + + Ok(()) } fn burn( &mut self, - token_id: TokenId, + token_ids: &[TokenId], current_owner_id: &AccountId, ) -> Result<(), Nep171BurnError> { - let mut slot = Self::slot_token_owner(token_id.clone()); - let actual_owner_id = if let Some(account_id) = slot.read() { - account_id - } else { - return Err(error::TokenDoesNotExistError { token_id }.into()); - }; - - if current_owner_id != &actual_owner_id { - return Err(error::TokenNotOwnedByExpectedOwnerError { - expected_owner_id: current_owner_id.clone(), - actual_owner_id, - token_id, + for token_id in token_ids { + if let Some(actual_owner_id) = self.token_owner(token_id) { + if &actual_owner_id != current_owner_id { + return Err(error::TokenNotOwnedByExpectedOwnerError { + expected_owner_id: current_owner_id.clone(), + actual_owner_id, + token_id: (*token_id).clone(), + } + .into()); + } + } else { + return Err(error::TokenDoesNotExistError { + token_id: (*token_id).clone(), + } + .into()); } - .into()); } - slot.remove(); + self.burn_unchecked(token_ids); + + Nep171Event::NftBurn(vec![event::NftBurnLog { + token_ids: token_ids.iter().map(ToString::to_string).collect(), + owner_id: current_owner_id.clone(), + authorized_id: None, + memo: None, + }]) + .emit(); + Ok(()) } - fn burn_unchecked(&mut self, token_id: TokenId) -> bool { - Self::slot_token_owner(token_id).remove() + fn burn_unchecked(&mut self, token_ids: &[TokenId]) -> bool { + let mut removed_successfully = true; + + for token_id in token_ids { + removed_successfully &= Self::slot_token_owner(token_id).remove(); + } + + removed_successfully } - fn token_owner(&self, token_id: TokenId) -> Option { + fn token_owner(&self, token_id: &TokenId) -> Option { Self::slot_token_owner(token_id).read() } } /// Token information structure. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive( + Debug, + Clone, + Hash, + PartialEq, + PartialOrd, + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, +)] pub struct Token { /// Token ID. pub token_id: TokenId, diff --git a/tests/macros/standard/nep171.rs b/tests/macros/standard/nep171.rs index 259ec69..2b201f7 100644 --- a/tests/macros/standard/nep171.rs +++ b/tests/macros/standard/nep171.rs @@ -51,7 +51,7 @@ impl NonFungibleToken { } pub fn mint(&mut self, token_id: TokenId, owner_id: AccountId) { - Nep171Controller::mint(self, token_id, &owner_id).unwrap_or_else(|e| { + Nep171Controller::mint(self, &[token_id], &owner_id).unwrap_or_else(|e| { env::panic_str(&format!("Mint failed: {e:?}")); }); } diff --git a/workspaces-tests/Cargo.toml b/workspaces-tests/Cargo.toml index 736d207..b9346c3 100644 --- a/workspaces-tests/Cargo.toml +++ b/workspaces-tests/Cargo.toml @@ -20,6 +20,9 @@ name = "fungible_token" [[bin]] name = "native_multisig" +[[bin]] +name = "non_fungible_token" + [[bin]] name = "rbac" diff --git a/workspaces-tests/src/bin/non_fungible_token.rs b/workspaces-tests/src/bin/non_fungible_token.rs new file mode 100644 index 0000000..46efafc --- /dev/null +++ b/workspaces-tests/src/bin/non_fungible_token.rs @@ -0,0 +1,39 @@ +#![allow(missing_docs)] + +// Ignore +pub fn main() {} + +use near_sdk::{ + borsh::{self, BorshDeserialize, BorshSerialize}, + env, + json_types::U128, + log, near_bindgen, PanicOnDefault, +}; +use near_sdk_contract_tools::{standard::nep171::*, Nep171}; + +#[derive(PanicOnDefault, BorshSerialize, BorshDeserialize, Nep171)] +#[near_bindgen] +pub struct Contract {} + +impl Nep171Hook for Contract { + fn before_nft_transfer(&mut self, transfer: &Nep171Transfer) { + log!(format!("before_nft_transfer({})", transfer.token_id)); + } + + fn after_nft_transfer(&mut self, transfer: &Nep171Transfer, _state: ()) { + log!(format!("after_nft_transfer({})", transfer.token_id)); + } +} + +#[near_bindgen] +impl Contract { + #[init] + pub fn new() -> Self { + Self {} + } + + pub fn mint(&mut self, token_ids: Vec) { + Nep171Controller::mint(self, &token_ids, &env::predecessor_account_id()) + .unwrap_or_else(|e| env::panic_str(&format!("Failed to mint: {:#?}", e))); + } +} diff --git a/workspaces-tests/tests/non_fungible_token.rs b/workspaces-tests/tests/non_fungible_token.rs new file mode 100644 index 0000000..dca9c39 --- /dev/null +++ b/workspaces-tests/tests/non_fungible_token.rs @@ -0,0 +1,87 @@ +#![cfg(not(windows))] + +use near_sdk::{json_types::U128, serde_json::json}; +use near_sdk_contract_tools::standard::nep171::Token; +use workspaces::{Account, AccountId, Contract}; + +const WASM: &[u8] = + include_bytes!("../../target/wasm32-unknown-unknown/release/non_fungible_token.wasm"); + +async fn nft_token(contract: &Contract, token_id: &str) -> Option { + contract + .view("nft_token") + .args_json(json!({ "token_id": token_id })) + .await + .unwrap() + .json::>() + .unwrap() +} + +struct Setup { + pub contract: Contract, + pub accounts: Vec, +} + +/// Setup for individual tests +async fn setup(num_accounts: usize) -> Setup { + let worker = workspaces::sandbox().await.unwrap(); + + // Initialize contract + let contract = worker.dev_deploy(WASM).await.unwrap(); + contract.call("new").transact().await.unwrap().unwrap(); + + // Initialize user accounts + let mut accounts = vec![]; + for _ in 0..num_accounts { + accounts.push(worker.dev_create_account().await.unwrap()); + } + + Setup { contract, accounts } +} + +async fn setup_balances(num_accounts: usize, token_ids: impl Fn(usize) -> Vec) -> Setup { + let s = setup(num_accounts).await; + + for (i, account) in s.accounts.iter().enumerate() { + account + .call(s.contract.id(), "mint") + .args_json(json!({ "token_ids": token_ids(i) })) + .transact() + .await + .unwrap() + .unwrap(); + } + + s +} + +#[tokio::test] +async fn mint() { + let Setup { contract, accounts } = setup_balances(3, |i| vec![format!("token_{i}")]).await; + let alice = &accounts[0]; + let bob = &accounts[1]; + let charlie = &accounts[2]; + + // Verify minted tokens + assert_eq!( + nft_token(&contract, "token_0").await, + Some(Token { + token_id: "token_0".to_string(), + owner_id: alice.id().parse().unwrap(), + }), + ); + assert_eq!( + nft_token(&contract, "token_1").await, + Some(Token { + token_id: "token_1".to_string(), + owner_id: bob.id().parse().unwrap(), + }), + ); + assert_eq!( + nft_token(&contract, "token_2").await, + Some(Token { + token_id: "token_2".to_string(), + owner_id: charlie.id().parse().unwrap(), + }), + ); +} From af2d805306e1c1a550ec54b9d1672d442b976e56 Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Mon, 17 Jul 2023 21:38:58 +0900 Subject: [PATCH 12/34] feat: nep171 multiple token id transfer --- macros/src/standard/nep171.rs | 10 ++- src/standard/nep171.rs | 88 ++++++++++++------- tests/macros/standard/nep171.rs | 2 +- .../src/bin/non_fungible_token.rs | 6 +- 4 files changed, 65 insertions(+), 41 deletions(-) diff --git a/macros/src/standard/nep171.rs b/macros/src/standard/nep171.rs index 11291f1..dbac46b 100644 --- a/macros/src/standard/nep171.rs +++ b/macros/src/standard/nep171.rs @@ -97,7 +97,7 @@ pub fn expand(meta: Nep171Meta) -> Result { let result = #me::standard::nep171::Nep171Controller::transfer( self, - &token_id, + &[token_id], receiver_id.clone(), receiver_id, previous_owner_id, @@ -148,7 +148,7 @@ pub fn expand(meta: Nep171Meta) -> Result { Nep171Controller::transfer( self, - &token_id, + &[token_id], sender_id.clone(), sender_id, receiver_id, @@ -195,9 +195,11 @@ pub fn expand(meta: Nep171Meta) -> Result { #before_nft_transfer + let token_ids = [token_id]; + Nep171Controller::transfer( self, - &token_id, + &token_ids, sender_id.clone(), sender_id.clone(), receiver_id.clone(), @@ -205,6 +207,8 @@ pub fn expand(meta: Nep171Meta) -> Result { ) .unwrap(); + let [token_id] = token_ids; + #after_nft_transfer ext_nep171_receiver::ext(receiver_id.clone()) diff --git a/src/standard/nep171.rs b/src/standard/nep171.rs index b1d7b11..1668008 100644 --- a/src/standard/nep171.rs +++ b/src/standard/nep171.rs @@ -228,7 +228,7 @@ pub trait Nep171Controller { /// Transfer a token from `sender_id` to `receiver_id`. fn transfer( &mut self, - token_id: &TokenId, // TODO: Change to &[TokenId] + token_id: &[TokenId], current_owner_id: AccountId, sender_id: AccountId, receiver_id: AccountId, @@ -240,6 +240,7 @@ pub trait Nep171Controller { &mut self, token_ids: &[TokenId], new_owner_id: &AccountId, + memo: Option, ) -> Result<(), Nep171MintError>; /// Burns tokens `token_ids` owned by `current_owner_id`. @@ -247,6 +248,7 @@ pub trait Nep171Controller { &mut self, token_ids: &[TokenId], current_owner_id: &AccountId, + memo: Option, ) -> Result<(), Nep171BurnError>; /// Burns tokens `token_ids` without checking the owners. @@ -299,55 +301,65 @@ pub trait Nep171Hook { impl Nep171Controller for T { fn transfer( &mut self, - token_id: &TokenId, + token_ids: &[TokenId], current_owner_id: AccountId, sender_id: AccountId, receiver_id: AccountId, memo: Option, ) -> Result<(), Nep171TransferError> { - if current_owner_id == receiver_id { - return Err(error::TokenReceiverIsCurrentOwnerError { - current_owner_id, - token_id: token_id.clone(), - } - .into()); + if token_ids.is_empty() { + return Ok(()); } - // This version doesn't implement approval management - if sender_id != current_owner_id { - return Err(error::SenderNotApprovedError { - sender_id, - token_id: token_id.clone(), - } - .into()); - } + for token_id in token_ids { + let slot = Self::slot_token_owner(token_id); - let mut slot = Self::slot_token_owner(token_id); + let actual_current_owner_id = + slot.read().ok_or_else(|| error::TokenDoesNotExistError { + token_id: token_id.clone(), + })?; - let actual_current_owner_id = slot.read().ok_or_else(|| error::TokenDoesNotExistError { - token_id: token_id.clone(), - })?; + if current_owner_id != actual_current_owner_id { + return Err(error::TokenNotOwnedByExpectedOwnerError { + expected_owner_id: current_owner_id, + actual_owner_id: actual_current_owner_id, + token_id: token_id.clone(), + } + .into()); + } - if current_owner_id != actual_current_owner_id { - return Err(error::TokenNotOwnedByExpectedOwnerError { - expected_owner_id: current_owner_id, - actual_owner_id: actual_current_owner_id, - token_id: token_id.clone(), + // This version doesn't implement approval management + if sender_id != current_owner_id { + return Err(error::SenderNotApprovedError { + sender_id, + token_id: token_id.clone(), + } + .into()); } - .into()); - } - slot.write(&receiver_id); + if receiver_id == current_owner_id { + return Err(error::TokenReceiverIsCurrentOwnerError { + current_owner_id, + token_id: token_id.clone(), + } + .into()); + } + } Nep171Event::NftTransfer(vec![event::NftTransferLog { authorized_id: None, - old_owner_id: actual_current_owner_id, - new_owner_id: receiver_id, - token_ids: vec![token_id.clone()], + old_owner_id: current_owner_id, + new_owner_id: receiver_id.clone(), + token_ids: token_ids.iter().map(ToString::to_string).collect(), memo, }]) .emit(); + for token_id in token_ids { + let mut slot = Self::slot_token_owner(token_id); + slot.write(&receiver_id); + } + Ok(()) } @@ -355,7 +367,12 @@ impl Nep171Controller for T { &mut self, token_ids: &[TokenId], new_owner_id: &AccountId, + memo: Option, ) -> Result<(), Nep171MintError> { + if token_ids.is_empty() { + return Ok(()); + } + for token_id in token_ids { let slot = Self::slot_token_owner(token_id); if slot.exists() { @@ -369,7 +386,7 @@ impl Nep171Controller for T { Nep171Event::NftMint(vec![event::NftMintLog { token_ids: token_ids.iter().map(ToString::to_string).collect(), owner_id: new_owner_id.clone(), - memo: None, + memo, }]) .emit(); @@ -385,7 +402,12 @@ impl Nep171Controller for T { &mut self, token_ids: &[TokenId], current_owner_id: &AccountId, + memo: Option, ) -> Result<(), Nep171BurnError> { + if token_ids.is_empty() { + return Ok(()); + } + for token_id in token_ids { if let Some(actual_owner_id) = self.token_owner(token_id) { if &actual_owner_id != current_owner_id { @@ -410,7 +432,7 @@ impl Nep171Controller for T { token_ids: token_ids.iter().map(ToString::to_string).collect(), owner_id: current_owner_id.clone(), authorized_id: None, - memo: None, + memo, }]) .emit(); diff --git a/tests/macros/standard/nep171.rs b/tests/macros/standard/nep171.rs index 2b201f7..c47b7f8 100644 --- a/tests/macros/standard/nep171.rs +++ b/tests/macros/standard/nep171.rs @@ -51,7 +51,7 @@ impl NonFungibleToken { } pub fn mint(&mut self, token_id: TokenId, owner_id: AccountId) { - Nep171Controller::mint(self, &[token_id], &owner_id).unwrap_or_else(|e| { + Nep171Controller::mint(self, &[token_id], &owner_id, None).unwrap_or_else(|e| { env::panic_str(&format!("Mint failed: {e:?}")); }); } diff --git a/workspaces-tests/src/bin/non_fungible_token.rs b/workspaces-tests/src/bin/non_fungible_token.rs index 46efafc..a34880a 100644 --- a/workspaces-tests/src/bin/non_fungible_token.rs +++ b/workspaces-tests/src/bin/non_fungible_token.rs @@ -5,9 +5,7 @@ pub fn main() {} use near_sdk::{ borsh::{self, BorshDeserialize, BorshSerialize}, - env, - json_types::U128, - log, near_bindgen, PanicOnDefault, + env, log, near_bindgen, PanicOnDefault, }; use near_sdk_contract_tools::{standard::nep171::*, Nep171}; @@ -33,7 +31,7 @@ impl Contract { } pub fn mint(&mut self, token_ids: Vec) { - Nep171Controller::mint(self, &token_ids, &env::predecessor_account_id()) + Nep171Controller::mint(self, &token_ids, &env::predecessor_account_id(), None) .unwrap_or_else(|e| env::panic_str(&format!("Failed to mint: {:#?}", e))); } } From fa881d74eb2b2339f4cd724846c68c32f5f1ddeb Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Tue, 18 Jul 2023 10:58:18 +0900 Subject: [PATCH 13/34] chore: more tests + small fixes --- macros/src/standard/nep171.rs | 10 +- src/standard/nep171.rs | 14 +- workspaces-tests/Cargo.toml | 3 + .../src/bin/non_fungible_token.rs | 4 +- .../src/bin/non_fungible_token_receiver.rs | 42 +++ workspaces-tests/tests/non_fungible_token.rs | 322 +++++++++++++++++- 6 files changed, 376 insertions(+), 19 deletions(-) create mode 100644 workspaces-tests/src/bin/non_fungible_token_receiver.rs diff --git a/macros/src/standard/nep171.rs b/macros/src/standard/nep171.rs index dbac46b..1acd8ec 100644 --- a/macros/src/standard/nep171.rs +++ b/macros/src/standard/nep171.rs @@ -116,6 +116,7 @@ pub fn expand(meta: Nep171Meta) -> Result { #[#near_sdk::near_bindgen] impl #imp #me::standard::nep171::Nep171 for #ident #ty #wher { + #[payable] fn nft_transfer( &mut self, receiver_id: #near_sdk::AccountId, @@ -154,11 +155,12 @@ pub fn expand(meta: Nep171Meta) -> Result { receiver_id, memo, ) - .unwrap(); + .unwrap_or_else(|e| #near_sdk::env::panic_str(&e.to_string())); #after_nft_transfer } + #[payable] fn nft_transfer_call( &mut self, receiver_id: #near_sdk::AccountId, @@ -177,7 +179,7 @@ pub fn expand(meta: Nep171Meta) -> Result { #near_sdk::assert_one_yocto(); #near_sdk::require!( - #near_sdk::env::prepaid_gas() > GAS_FOR_NFT_TRANSFER_CALL, + #near_sdk::env::prepaid_gas() >= GAS_FOR_NFT_TRANSFER_CALL, INSUFFICIENT_GAS_MESSAGE, ); @@ -205,7 +207,7 @@ pub fn expand(meta: Nep171Meta) -> Result { receiver_id.clone(), memo, ) - .unwrap(); + .unwrap_or_else(|e| #near_sdk::env::panic_str(&e.to_string())); let [token_id] = token_ids; @@ -215,7 +217,7 @@ pub fn expand(meta: Nep171Meta) -> Result { .with_static_gas(#near_sdk::env::prepaid_gas() - GAS_FOR_NFT_TRANSFER_CALL) .nft_on_transfer( sender_id.clone(), - receiver_id.clone(), + sender_id.clone(), token_id.clone(), msg, ) diff --git a/src/standard/nep171.rs b/src/standard/nep171.rs index 1668008..49aa486 100644 --- a/src/standard/nep171.rs +++ b/src/standard/nep171.rs @@ -52,15 +52,13 @@ pub mod event { use near_sdk::AccountId; use serde::Serialize; - use super::TokenId; - /// Tokens minted to a single owner. #[derive(Serialize, Debug, Clone)] pub struct NftMintLog { /// To whom were the new tokens minted? pub owner_id: AccountId, /// Which tokens were minted? - pub token_ids: Vec, + pub token_ids: Vec, /// Additional mint information. #[serde(skip_serializing_if = "Option::is_none")] pub memo: Option, @@ -77,7 +75,7 @@ pub mod event { /// Account ID of the new owner. pub new_owner_id: AccountId, /// IDs of the transferred tokens. - pub token_ids: Vec, + pub token_ids: Vec, /// Additional transfer information. #[serde(skip_serializing_if = "Option::is_none")] pub memo: Option, @@ -89,7 +87,7 @@ pub mod event { /// What is the ID of the account from which the tokens were burned? pub owner_id: AccountId, /// IDs of the burned tokens. - pub token_ids: Vec, + pub token_ids: Vec, /// NEP-178 authorized account ID. #[serde(skip_serializing_if = "Option::is_none")] pub authorized_id: Option, @@ -528,9 +526,9 @@ mod ext { #[ext_contract(ext_nep171_receiver)] pub trait Nep171Receiver { /// Function that is called in an `nft_transfer_call` promise chain. - /// Returns the number of tokens "used", that is, those that will be kept - /// in the receiving contract's account. (The contract will attempt to - /// refund the difference from `amount` to the original sender.) + /// Performs some action after receiving a non-fungible token. + /// + /// Returns `true` if token should be returned to `sender_id`. fn nft_on_transfer( &mut self, sender_id: AccountId, diff --git a/workspaces-tests/Cargo.toml b/workspaces-tests/Cargo.toml index b9346c3..d04bcee 100644 --- a/workspaces-tests/Cargo.toml +++ b/workspaces-tests/Cargo.toml @@ -23,6 +23,9 @@ name = "native_multisig" [[bin]] name = "non_fungible_token" +[[bin]] +name = "non_fungible_token_receiver" + [[bin]] name = "rbac" diff --git a/workspaces-tests/src/bin/non_fungible_token.rs b/workspaces-tests/src/bin/non_fungible_token.rs index a34880a..e5dd1fe 100644 --- a/workspaces-tests/src/bin/non_fungible_token.rs +++ b/workspaces-tests/src/bin/non_fungible_token.rs @@ -15,11 +15,11 @@ pub struct Contract {} impl Nep171Hook for Contract { fn before_nft_transfer(&mut self, transfer: &Nep171Transfer) { - log!(format!("before_nft_transfer({})", transfer.token_id)); + log!("before_nft_transfer({})", transfer.token_id); } fn after_nft_transfer(&mut self, transfer: &Nep171Transfer, _state: ()) { - log!(format!("after_nft_transfer({})", transfer.token_id)); + log!("after_nft_transfer({})", transfer.token_id); } } diff --git a/workspaces-tests/src/bin/non_fungible_token_receiver.rs b/workspaces-tests/src/bin/non_fungible_token_receiver.rs new file mode 100644 index 0000000..8013de2 --- /dev/null +++ b/workspaces-tests/src/bin/non_fungible_token_receiver.rs @@ -0,0 +1,42 @@ +#![allow(missing_docs)] + +// Ignore +pub fn main() {} + +use near_sdk::{ + borsh::{self, BorshDeserialize, BorshSerialize}, + env, log, near_bindgen, AccountId, PanicOnDefault, PromiseOrValue, +}; +use near_sdk_contract_tools::{standard::nep171::*, Nep171}; + +#[derive(PanicOnDefault, BorshSerialize, BorshDeserialize)] +#[near_bindgen] +pub struct Contract {} + +#[near_bindgen] +impl Nep171Receiver for Contract { + fn nft_on_transfer( + &mut self, + sender_id: AccountId, + previous_owner_id: AccountId, + token_id: TokenId, + msg: String, + ) -> PromiseOrValue { + log!( + "Received {} from {} via {}", + token_id, + previous_owner_id, + sender_id, + ); + + PromiseOrValue::Value(msg == "return") + } +} + +#[near_bindgen] +impl Contract { + #[init] + pub fn new() -> Self { + Self {} + } +} diff --git a/workspaces-tests/tests/non_fungible_token.rs b/workspaces-tests/tests/non_fungible_token.rs index dca9c39..7a16b51 100644 --- a/workspaces-tests/tests/non_fungible_token.rs +++ b/workspaces-tests/tests/non_fungible_token.rs @@ -1,12 +1,22 @@ #![cfg(not(windows))] use near_sdk::{json_types::U128, serde_json::json}; -use near_sdk_contract_tools::standard::nep171::Token; -use workspaces::{Account, AccountId, Contract}; +use near_sdk_contract_tools::standard::{ + nep171::{ + event::NftTransferLog, Nep171Event, Nep171Receiver, Token, GAS_FOR_NFT_TRANSFER_CALL, + }, + nep297::Event, +}; +use workspaces::{ + operations::Function, result::ExecutionFinalResult, Account, AccountId, Contract, +}; const WASM: &[u8] = include_bytes!("../../target/wasm32-unknown-unknown/release/non_fungible_token.wasm"); +const RECEIVER_WASM: &[u8] = + include_bytes!("../../target/wasm32-unknown-unknown/release/non_fungible_token_receiver.wasm"); + async fn nft_token(contract: &Contract, token_id: &str) -> Option { contract .view("nft_token") @@ -62,26 +72,328 @@ async fn mint() { let bob = &accounts[1]; let charlie = &accounts[2]; + let (token_0, token_1, token_2, token_3) = tokio::join!( + nft_token(&contract, "token_0"), + nft_token(&contract, "token_1"), + nft_token(&contract, "token_2"), + nft_token(&contract, "token_3"), + ); + // Verify minted tokens assert_eq!( - nft_token(&contract, "token_0").await, + token_0, Some(Token { token_id: "token_0".to_string(), owner_id: alice.id().parse().unwrap(), }), ); assert_eq!( - nft_token(&contract, "token_1").await, + token_1, Some(Token { token_id: "token_1".to_string(), owner_id: bob.id().parse().unwrap(), }), ); assert_eq!( - nft_token(&contract, "token_2").await, + token_2, Some(Token { token_id: "token_2".to_string(), owner_id: charlie.id().parse().unwrap(), }), ); + assert_eq!(token_3, None); +} + +#[tokio::test] +async fn transfer_success() { + let Setup { contract, accounts } = setup_balances(3, |i| vec![format!("token_{i}")]).await; + let alice = &accounts[0]; + let bob = &accounts[1]; + let charlie = &accounts[2]; + + let result = alice + .call(contract.id(), "nft_transfer") + .args_json(json!({ + "token_id": "token_0", + "receiver_id": bob.id(), + })) + .deposit(1) + .transact() + .await + .unwrap() + .unwrap(); + + assert_eq!( + result.logs(), + vec![ + "before_nft_transfer(token_0)".to_string(), + Nep171Event::NftTransfer(vec![NftTransferLog { + old_owner_id: alice.id().parse().unwrap(), + new_owner_id: bob.id().parse().unwrap(), + authorized_id: None, + memo: None, + token_ids: vec!["token_0".to_string()], + }]) + .to_event_string(), + "after_nft_transfer(token_0)".to_string(), + ], + ); + + let (token_0, token_1, token_2) = tokio::join!( + nft_token(&contract, "token_0"), + nft_token(&contract, "token_1"), + nft_token(&contract, "token_2"), + ); + + assert_eq!( + token_0, + Some(Token { + token_id: "token_0".to_string(), + owner_id: bob.id().parse().unwrap(), + }), + ); + assert_eq!( + token_1, + Some(Token { + token_id: "token_1".to_string(), + owner_id: bob.id().parse().unwrap(), + }), + ); + assert_eq!( + token_2, + Some(Token { + token_id: "token_2".to_string(), + owner_id: charlie.id().parse().unwrap(), + }), + ); +} + +#[tokio::test] +#[should_panic = "Smart contract panicked: Requires attached deposit of exactly 1 yoctoNEAR"] +async fn transfer_fail_no_deposit() { + let Setup { contract, accounts } = setup_balances(2, |i| vec![format!("token_{i}")]).await; + let alice = &accounts[0]; + let bob = &accounts[1]; + + alice + .call(contract.id(), "nft_transfer") + .args_json(json!({ + "token_id": "token_0", + "receiver_id": bob.id(), + })) + .transact() + .await + .unwrap() + .unwrap(); +} + +#[tokio::test] +#[should_panic = "Smart contract panicked: Token `token_5` does not exist"] +async fn transfer_fail_token_dne() { + let Setup { contract, accounts } = setup_balances(2, |i| vec![format!("token_{i}")]).await; + let alice = &accounts[0]; + let bob = &accounts[1]; + + alice + .call(contract.id(), "nft_transfer") + .args_json(json!({ + "token_id": "token_5", + "receiver_id": bob.id(), + })) + .deposit(1) + .transact() + .await + .unwrap() + .unwrap(); +} + +/// For dynamic should_panic messages +fn expect_execution_error(result: &ExecutionFinalResult, expected_error: impl AsRef) { + let failures = result.failures(); + + assert_eq!(failures.len(), 1); + + let actual_error_string = failures[0] + .clone() + .into_result() + .unwrap_err() + .into_inner() + .unwrap() + .to_string(); + + assert_eq!( + format!("Action #0: ExecutionError(\"{}\")", expected_error.as_ref()), + actual_error_string + ); +} + +#[tokio::test] +async fn transfer_fail_not_owner() { + let Setup { contract, accounts } = setup_balances(3, |i| vec![format!("token_{i}")]).await; + let alice = &accounts[0]; + let bob = &accounts[1]; + let charlie = &accounts[2]; + + let result = alice + .call(contract.id(), "nft_transfer") + .args_json(json!({ + "token_id": "token_2", // charlie's token + "receiver_id": bob.id(), + })) + .deposit(1) + .transact() + .await + .unwrap(); + + expect_execution_error( + &result, + format!( + "Smart contract panicked: Token `token_2` is owned by `{}` instead of expected `{}`", + charlie.id(), + alice.id(), + ), + ); +} + +#[tokio::test] +async fn transfer_fail_reflexive_transfer() { + let Setup { contract, accounts } = setup_balances(2, |i| vec![format!("token_{i}")]).await; + let alice = &accounts[0]; + + let result = alice + .call(contract.id(), "nft_transfer") + .args_json(json!({ + "token_id": "token_0", + "receiver_id": alice.id(), + })) + .deposit(1) + .transact() + .await + .unwrap(); + + expect_execution_error(&result, format!("Smart contract panicked: Receiver must be different from current owner `{}` to transfer token `token_0`", alice.id())); +} + +#[tokio::test] +async fn transfer_call_success() { + let Setup { contract, accounts } = setup_balances(2, |i| vec![format!("token_{i}")]).await; + let alice = &accounts[0]; + let bob = &accounts[1]; + + bob.batch(bob.id()) + .deploy(RECEIVER_WASM) + .call(Function::new("new")) + .transact() + .await + .unwrap() + .unwrap(); + + let result = alice + .call(contract.id(), "nft_transfer_call") + .args_json(json!({ + "token_id": "token_0", + "receiver_id": bob.id(), + "msg": "", + })) + .gas(30_000_000_000_000) + .deposit(1) + .transact() + .await + .unwrap() + .unwrap(); + + let logs = result.logs(); + + assert_eq!( + vec![ + "before_nft_transfer(token_0)".to_string(), + Nep171Event::NftTransfer(vec![NftTransferLog { + token_ids: vec!["token_0".to_string()], + authorized_id: None, + old_owner_id: alice.id().parse().unwrap(), + new_owner_id: bob.id().parse().unwrap(), + memo: None, + }]) + .to_event_string(), + "after_nft_transfer(token_0)".to_string(), + format!("Received token_0 from {} via {}", alice.id(), alice.id()), + ], + logs + ); + + // not returned + assert_eq!( + nft_token(&contract, "token_0").await, + Some(Token { + token_id: "token_0".to_string(), + owner_id: bob.id().parse().unwrap(), + }), + ); +} + +#[tokio::test] +async fn transfer_call_return_success() { + let Setup { contract, accounts } = setup_balances(2, |i| vec![format!("token_{i}")]).await; + let alice = &accounts[0]; + let bob = &accounts[1]; + + bob.batch(bob.id()) + .deploy(RECEIVER_WASM) + .call(Function::new("new")) + .transact() + .await + .unwrap() + .unwrap(); + + let result = alice + .call(contract.id(), "nft_transfer_call") + .args_json(json!({ + "token_id": "token_0", + "receiver_id": bob.id(), + "msg": "return", + })) + .gas(30_000_000_000_000) + .deposit(1) + .transact() + .await + .unwrap() + .unwrap(); + + let logs = result.logs(); + + assert_eq!( + vec![ + "before_nft_transfer(token_0)".to_string(), + Nep171Event::NftTransfer(vec![NftTransferLog { + token_ids: vec!["token_0".to_string()], + authorized_id: None, + old_owner_id: alice.id().parse().unwrap(), + new_owner_id: bob.id().parse().unwrap(), + memo: None, + }]) + .to_event_string(), + "after_nft_transfer(token_0)".to_string(), + format!("Received token_0 from {} via {}", alice.id(), alice.id()), + "before_nft_transfer(token_0)".to_string(), + Nep171Event::NftTransfer(vec![NftTransferLog { + token_ids: vec!["token_0".to_string()], + authorized_id: None, + old_owner_id: bob.id().parse().unwrap(), + new_owner_id: alice.id().parse().unwrap(), + memo: None, + }]) + .to_event_string(), + "after_nft_transfer(token_0)".to_string(), + ], + logs + ); + + // returned + assert_eq!( + nft_token(&contract, "token_0").await, + Some(Token { + token_id: "token_0".to_string(), + owner_id: alice.id().parse().unwrap(), + }), + ); } From 66bb5d7c82465d69f391bfccddb6dac1001ff8eb Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Thu, 20 Jul 2023 19:15:53 +0900 Subject: [PATCH 14/34] fix: warnings --- tests/macros/standard/nep171.rs | 8 ++++++++ .../src/bin/non_fungible_token_receiver.rs | 4 ++-- workspaces-tests/tests/non_fungible_token.rs | 10 +++------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/macros/standard/nep171.rs b/tests/macros/standard/nep171.rs index c47b7f8..6a7dfab 100644 --- a/tests/macros/standard/nep171.rs +++ b/tests/macros/standard/nep171.rs @@ -19,6 +19,14 @@ impl From for TokenRecord { } } +#[derive(Nep171, BorshDeserialize, BorshSerialize)] +#[nep171(no_hooks)] +#[near_bindgen] +struct NonFungibleTokenNoHooks { + pub before_nft_transfer_balance_record: store::Vector>, + pub after_nft_transfer_balance_record: store::Vector>, +} + #[derive(Nep171, BorshDeserialize, BorshSerialize)] #[near_bindgen] struct NonFungibleToken { diff --git a/workspaces-tests/src/bin/non_fungible_token_receiver.rs b/workspaces-tests/src/bin/non_fungible_token_receiver.rs index 8013de2..ee8aef8 100644 --- a/workspaces-tests/src/bin/non_fungible_token_receiver.rs +++ b/workspaces-tests/src/bin/non_fungible_token_receiver.rs @@ -5,9 +5,9 @@ pub fn main() {} use near_sdk::{ borsh::{self, BorshDeserialize, BorshSerialize}, - env, log, near_bindgen, AccountId, PanicOnDefault, PromiseOrValue, + log, near_bindgen, AccountId, PanicOnDefault, PromiseOrValue, }; -use near_sdk_contract_tools::{standard::nep171::*, Nep171}; +use near_sdk_contract_tools::standard::nep171::*; #[derive(PanicOnDefault, BorshSerialize, BorshDeserialize)] #[near_bindgen] diff --git a/workspaces-tests/tests/non_fungible_token.rs b/workspaces-tests/tests/non_fungible_token.rs index 7a16b51..93e25ff 100644 --- a/workspaces-tests/tests/non_fungible_token.rs +++ b/workspaces-tests/tests/non_fungible_token.rs @@ -1,15 +1,11 @@ #![cfg(not(windows))] -use near_sdk::{json_types::U128, serde_json::json}; +use near_sdk::serde_json::json; use near_sdk_contract_tools::standard::{ - nep171::{ - event::NftTransferLog, Nep171Event, Nep171Receiver, Token, GAS_FOR_NFT_TRANSFER_CALL, - }, + nep171::{event::NftTransferLog, Nep171Event, Token}, nep297::Event, }; -use workspaces::{ - operations::Function, result::ExecutionFinalResult, Account, AccountId, Contract, -}; +use workspaces::{operations::Function, result::ExecutionFinalResult, Account, Contract}; const WASM: &[u8] = include_bytes!("../../target/wasm32-unknown-unknown/release/non_fungible_token.wasm"); From 89143f26831c93d7774522891f5b6e615a28e80e Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Thu, 20 Jul 2023 21:10:21 +0900 Subject: [PATCH 15/34] feat: predicate api improvements --- macros/src/standard/nep171.rs | 26 ++-- src/standard/nep171.rs | 67 +++++++-- tests/macros/standard/nep171.rs | 15 +- .../src/bin/non_fungible_token.rs | 2 +- .../src/bin/non_fungible_token_receiver.rs | 20 ++- workspaces-tests/tests/non_fungible_token.rs | 139 ++++++++++++++++++ 6 files changed, 236 insertions(+), 33 deletions(-) diff --git a/macros/src/standard/nep171.rs b/macros/src/standard/nep171.rs index 1acd8ec..0fe74d9 100644 --- a/macros/src/standard/nep171.rs +++ b/macros/src/standard/nep171.rs @@ -43,7 +43,14 @@ pub fn expand(meta: Nep171Meta) -> Result { let before_nft_transfer = no_hooks.is_present().not().then(|| { quote! { - let hook_state = >::before_nft_transfer(self, &transfer); + let hook_state = >::before_nft_transfer(&self, &transfer); + } + }); + + let before_nft_transfer_predicate_body = no_hooks.is_present().not().then(|| { + quote! { + let hook_state = >::before_nft_transfer(&self, &transfer); + hook_state } }); @@ -77,7 +84,8 @@ pub fn expand(meta: Nep171Meta) -> Result { let should_revert = if let #near_sdk::PromiseResult::Successful(value) = #near_sdk::env::promise_result(0) { - #near_sdk::serde_json::from_slice::(&value).unwrap_or(true) + let value = #near_sdk::serde_json::from_slice::(&value).unwrap_or(true); + value } else { true }; @@ -93,21 +101,19 @@ pub fn expand(meta: Nep171Meta) -> Result { msg: None, }; - #before_nft_transfer - - let result = #me::standard::nep171::Nep171Controller::transfer( + let result = #me::standard::nep171::Nep171Controller::and_transfer( self, + || { #before_nft_transfer_predicate_body }, &[token_id], receiver_id.clone(), receiver_id, previous_owner_id, None, - ) - .is_err(); - - #after_nft_transfer + ); - result + result.map(|hook_state| { + #after_nft_transfer + }).is_err() } else { true } diff --git a/src/standard/nep171.rs b/src/standard/nep171.rs index 49aa486..5efd018 100644 --- a/src/standard/nep171.rs +++ b/src/standard/nep171.rs @@ -223,10 +223,24 @@ pub trait Nep171ControllerInternal { /// Non-public controller interface for NEP-171 implementations. pub trait Nep171Controller { + /// Transfer a token from `sender_id` to `receiver_id`, calling + /// `predicate` after performing required validity checks, but immediately + /// before performing the actual transfer, and returns the value returned + /// by `predicate`. + fn and_transfer( + &self, + predicate: impl FnOnce() -> T, + token_ids: &[TokenId], + current_owner_id: AccountId, + sender_id: AccountId, + receiver_id: AccountId, + memo: Option, + ) -> Result; + /// Transfer a token from `sender_id` to `receiver_id`. fn transfer( &mut self, - token_id: &[TokenId], + token_ids: &[TokenId], current_owner_id: AccountId, sender_id: AccountId, receiver_id: AccountId, @@ -288,12 +302,16 @@ pub trait Nep171Hook { /// /// May return an optional state value which will be passed along to the /// following `after_transfer`. - fn before_nft_transfer(&mut self, _transfer: &Nep171Transfer) -> T; + /// + /// MUST NOT PANIC. + fn before_nft_transfer(&self, transfer: &Nep171Transfer) -> T; /// Executed after a token transfer is conducted. /// /// Receives the state value returned by `before_transfer`. - fn after_nft_transfer(&mut self, _transfer: &Nep171Transfer, _state: T); + /// + /// MUST NOT PANIC. + fn after_nft_transfer(&mut self, transfer: &Nep171Transfer, state: T); } impl Nep171Controller for T { @@ -305,10 +323,25 @@ impl Nep171Controller for T { receiver_id: AccountId, memo: Option, ) -> Result<(), Nep171TransferError> { - if token_ids.is_empty() { - return Ok(()); - } + self.and_transfer( + || {}, + token_ids, + current_owner_id, + sender_id, + receiver_id, + memo, + ) + } + fn and_transfer

( + &self, + predicate: impl FnOnce() -> P, + token_ids: &[TokenId], + current_owner_id: AccountId, + sender_id: AccountId, + receiver_id: AccountId, + memo: Option, + ) -> Result { for token_id in token_ids { let slot = Self::slot_token_owner(token_id); @@ -344,21 +377,25 @@ impl Nep171Controller for T { } } - Nep171Event::NftTransfer(vec![event::NftTransferLog { - authorized_id: None, - old_owner_id: current_owner_id, - new_owner_id: receiver_id.clone(), - token_ids: token_ids.iter().map(ToString::to_string).collect(), - memo, - }]) - .emit(); + let result = predicate(); + + if !token_ids.is_empty() { + Nep171Event::NftTransfer(vec![event::NftTransferLog { + authorized_id: None, + old_owner_id: current_owner_id, + new_owner_id: receiver_id.clone(), + token_ids: token_ids.iter().map(ToString::to_string).collect(), + memo, + }]) + .emit(); + } for token_id in token_ids { let mut slot = Self::slot_token_owner(token_id); slot.write(&receiver_id); } - Ok(()) + Ok(result) } fn mint( diff --git a/tests/macros/standard/nep171.rs b/tests/macros/standard/nep171.rs index 6a7dfab..0f32107 100644 --- a/tests/macros/standard/nep171.rs +++ b/tests/macros/standard/nep171.rs @@ -34,15 +34,20 @@ struct NonFungibleToken { pub after_nft_transfer_balance_record: store::Vector>, } -impl Nep171Hook for NonFungibleToken { - fn before_nft_transfer(&mut self, transfer: &Nep171Transfer) { +impl Nep171Hook> for NonFungibleToken { + fn before_nft_transfer(&self, transfer: &Nep171Transfer) -> Option { let token = Nep171::nft_token(self, transfer.token_id.clone()); - self.before_nft_transfer_balance_record - .push(token.map(Into::into)); + token.map(Into::into) } - fn after_nft_transfer(&mut self, transfer: &Nep171Transfer, _state: ()) { + fn after_nft_transfer( + &mut self, + transfer: &Nep171Transfer, + before_nft_transfer: Option, + ) { let token = Nep171::nft_token(self, transfer.token_id.clone()); + self.before_nft_transfer_balance_record + .push(before_nft_transfer); self.after_nft_transfer_balance_record .push(token.map(Into::into)); } diff --git a/workspaces-tests/src/bin/non_fungible_token.rs b/workspaces-tests/src/bin/non_fungible_token.rs index e5dd1fe..c15efe0 100644 --- a/workspaces-tests/src/bin/non_fungible_token.rs +++ b/workspaces-tests/src/bin/non_fungible_token.rs @@ -14,7 +14,7 @@ use near_sdk_contract_tools::{standard::nep171::*, Nep171}; pub struct Contract {} impl Nep171Hook for Contract { - fn before_nft_transfer(&mut self, transfer: &Nep171Transfer) { + fn before_nft_transfer(&self, transfer: &Nep171Transfer) { log!("before_nft_transfer({})", transfer.token_id); } diff --git a/workspaces-tests/src/bin/non_fungible_token_receiver.rs b/workspaces-tests/src/bin/non_fungible_token_receiver.rs index ee8aef8..bcd7cd0 100644 --- a/workspaces-tests/src/bin/non_fungible_token_receiver.rs +++ b/workspaces-tests/src/bin/non_fungible_token_receiver.rs @@ -5,9 +5,9 @@ pub fn main() {} use near_sdk::{ borsh::{self, BorshDeserialize, BorshSerialize}, - log, near_bindgen, AccountId, PanicOnDefault, PromiseOrValue, + env, log, near_bindgen, AccountId, PanicOnDefault, PromiseOrValue, }; -use near_sdk_contract_tools::standard::nep171::*; +use near_sdk_contract_tools::standard::nep171::{ext_nep171, *}; #[derive(PanicOnDefault, BorshSerialize, BorshDeserialize)] #[near_bindgen] @@ -29,6 +29,17 @@ impl Nep171Receiver for Contract { sender_id, ); + if msg == "panic" { + near_sdk::env::panic_str("panic requested"); + } else if let Some(account_id) = msg.strip_prefix("transfer:") { + log!("Transferring {} to {}", token_id, account_id); + return ext_nep171::ext(env::predecessor_account_id()) + .with_attached_deposit(1) + .nft_transfer(account_id.parse().unwrap(), token_id, None, None) + .then(Contract::ext(env::current_account_id()).return_true()) // ask to return the token even though we don't own it anymore + .into(); + } + PromiseOrValue::Value(msg == "return") } } @@ -39,4 +50,9 @@ impl Contract { pub fn new() -> Self { Self {} } + + pub fn return_true(&self) -> bool { + log!("returning true"); + true + } } diff --git a/workspaces-tests/tests/non_fungible_token.rs b/workspaces-tests/tests/non_fungible_token.rs index 93e25ff..75e567f 100644 --- a/workspaces-tests/tests/non_fungible_token.rs +++ b/workspaces-tests/tests/non_fungible_token.rs @@ -393,3 +393,142 @@ async fn transfer_call_return_success() { }), ); } + +#[tokio::test] +async fn transfer_call_receiver_panic() { + let Setup { contract, accounts } = setup_balances(2, |i| vec![format!("token_{i}")]).await; + let alice = &accounts[0]; + let bob = &accounts[1]; + + bob.batch(bob.id()) + .deploy(RECEIVER_WASM) + .call(Function::new("new")) + .transact() + .await + .unwrap() + .unwrap(); + + let result = alice + .call(contract.id(), "nft_transfer_call") + .args_json(json!({ + "token_id": "token_0", + "receiver_id": bob.id(), + "msg": "panic", + })) + .gas(30_000_000_000_000) + .deposit(1) + .transact() + .await + .unwrap() + .unwrap(); + + let logs = result.logs(); + + assert_eq!( + vec![ + "before_nft_transfer(token_0)".to_string(), + Nep171Event::NftTransfer(vec![NftTransferLog { + token_ids: vec!["token_0".to_string()], + authorized_id: None, + old_owner_id: alice.id().parse().unwrap(), + new_owner_id: bob.id().parse().unwrap(), + memo: None, + }]) + .to_event_string(), + "after_nft_transfer(token_0)".to_string(), + format!("Received token_0 from {} via {}", alice.id(), alice.id()), + "before_nft_transfer(token_0)".to_string(), + Nep171Event::NftTransfer(vec![NftTransferLog { + token_ids: vec!["token_0".to_string()], + authorized_id: None, + old_owner_id: bob.id().parse().unwrap(), + new_owner_id: alice.id().parse().unwrap(), + memo: None, + }]) + .to_event_string(), + "after_nft_transfer(token_0)".to_string(), + ], + logs + ); + + // returned + assert_eq!( + nft_token(&contract, "token_0").await, + Some(Token { + token_id: "token_0".to_string(), + owner_id: alice.id().parse().unwrap(), + }), + ); +} + +#[tokio::test] +async fn transfer_call_receiver_send_return() { + let Setup { contract, accounts } = setup_balances(3, |i| vec![format!("token_{i}")]).await; + let alice = &accounts[0]; + let bob = &accounts[1]; + let charlie = &accounts[2]; + + bob.batch(bob.id()) + .deploy(RECEIVER_WASM) + .call(Function::new("new")) + .transact() + .await + .unwrap() + .unwrap(); + + let result = alice + .call(contract.id(), "nft_transfer_call") + .args_json(json!({ + "token_id": "token_0", + "receiver_id": bob.id(), + "msg": format!("transfer:{}", charlie.id()), + })) + .gas(300_000_000_000_000) // xtra gas + .deposit(1) + .transact() + .await + .unwrap() + .unwrap(); + + let logs = result.logs(); + + println!("{logs:#?}"); + + assert_eq!( + vec![ + "before_nft_transfer(token_0)".to_string(), + Nep171Event::NftTransfer(vec![NftTransferLog { + token_ids: vec!["token_0".to_string()], + authorized_id: None, + old_owner_id: alice.id().parse().unwrap(), + new_owner_id: bob.id().parse().unwrap(), + memo: None, + }]) + .to_event_string(), + "after_nft_transfer(token_0)".to_string(), + format!("Received token_0 from {} via {}", alice.id(), alice.id()), + format!("Transferring token_0 to {}", charlie.id()), + "before_nft_transfer(token_0)".to_string(), + Nep171Event::NftTransfer(vec![NftTransferLog { + token_ids: vec!["token_0".to_string()], + authorized_id: None, + old_owner_id: bob.id().parse().unwrap(), + new_owner_id: charlie.id().parse().unwrap(), + memo: None, + }]) + .to_event_string(), + "after_nft_transfer(token_0)".to_string(), + "returning true".to_string(), + ], + logs + ); + + // not returned + assert_eq!( + nft_token(&contract, "token_0").await, + Some(Token { + token_id: "token_0".to_string(), + owner_id: charlie.id().parse().unwrap(), + }), + ); +} From c466c91d50dc804bcaadfa9dfd2491e2828a4e94 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 26 Jul 2023 17:43:14 +0900 Subject: [PATCH 16/34] feat: remove predicate, introduce check function --- macros/src/standard/nep171.rs | 107 +++++++++++++++++++--------------- src/standard/nep171.rs | 101 ++++++++++++++++++-------------- 2 files changed, 116 insertions(+), 92 deletions(-) diff --git a/macros/src/standard/nep171.rs b/macros/src/standard/nep171.rs index 0fe74d9..6ef88c0 100644 --- a/macros/src/standard/nep171.rs +++ b/macros/src/standard/nep171.rs @@ -47,13 +47,6 @@ pub fn expand(meta: Nep171Meta) -> Result { } }); - let before_nft_transfer_predicate_body = no_hooks.is_present().not().then(|| { - quote! { - let hook_state = >::before_nft_transfer(&self, &transfer); - hook_state - } - }); - let after_nft_transfer = no_hooks.is_present().not().then(|| { quote! { >::after_nft_transfer(self, &transfer, hook_state); @@ -91,29 +84,45 @@ pub fn expand(meta: Nep171Meta) -> Result { }; if should_revert { - let transfer = #me::standard::nep171::Nep171Transfer { - token_id: token_id.clone(), - owner_id: receiver_id.clone(), - sender_id: receiver_id.clone(), - receiver_id: previous_owner_id.clone(), - approval_id: None, - memo: None, - msg: None, - }; + let token_ids = [token_id]; - let result = #me::standard::nep171::Nep171Controller::and_transfer( + let check_result = #me::standard::nep171::Nep171Controller::check_transfer( self, - || { #before_nft_transfer_predicate_body }, - &[token_id], - receiver_id.clone(), - receiver_id, - previous_owner_id, - None, + &token_ids, + &receiver_id, + &receiver_id, + &previous_owner_id, ); - result.map(|hook_state| { - #after_nft_transfer - }).is_err() + match check_result { + Ok(()) => { + let transfer = #me::standard::nep171::Nep171Transfer { + token_id: &token_ids[0], + owner_id: &receiver_id, + sender_id: &receiver_id, + receiver_id: &previous_owner_id, + approval_id: None, + memo: None, + msg: None, + }; + + #before_nft_transfer + + #me::standard::nep171::Nep171Controller::transfer_unchecked( + self, + &token_ids, + receiver_id.clone(), + receiver_id.clone(), + previous_owner_id.clone(), + None, + ); + + #after_nft_transfer + + false + }, + Err(_) => true, + } } else { true } @@ -141,13 +150,15 @@ pub fn expand(meta: Nep171Meta) -> Result { let sender_id = #near_sdk::env::predecessor_account_id(); + let token_ids = [token_id]; + let transfer = #me::standard::nep171::Nep171Transfer { - token_id: token_id.clone(), - owner_id: sender_id.clone(), - sender_id: sender_id.clone(), - receiver_id: receiver_id.clone(), + token_id: &token_ids[0], + owner_id: &sender_id, + sender_id: &sender_id, + receiver_id: &receiver_id, approval_id: None, - memo: memo.clone(), + memo: memo.as_deref(), msg: None, }; @@ -155,11 +166,11 @@ pub fn expand(meta: Nep171Meta) -> Result { Nep171Controller::transfer( self, - &[token_id], + &token_ids, + sender_id.clone(), sender_id.clone(), - sender_id, - receiver_id, - memo, + receiver_id.clone(), + memo.clone(), ) .unwrap_or_else(|e| #near_sdk::env::panic_str(&e.to_string())); @@ -191,46 +202,46 @@ pub fn expand(meta: Nep171Meta) -> Result { let sender_id = #near_sdk::env::predecessor_account_id(); + let token_ids = [token_id]; + let transfer = #me::standard::nep171::Nep171Transfer { - token_id: token_id.clone(), - owner_id: sender_id.clone(), - sender_id: sender_id.clone(), - receiver_id: receiver_id.clone(), + token_id: &token_ids[0], + owner_id: &sender_id, + sender_id: &sender_id, + receiver_id: &receiver_id, approval_id: None, - memo: memo.clone(), - msg: Some(msg.clone()), + memo: memo.as_deref(), + msg: Some(&msg), }; #before_nft_transfer - let token_ids = [token_id]; - Nep171Controller::transfer( self, &token_ids, sender_id.clone(), sender_id.clone(), receiver_id.clone(), - memo, + memo.clone(), ) .unwrap_or_else(|e| #near_sdk::env::panic_str(&e.to_string())); - let [token_id] = token_ids; - #after_nft_transfer + let [token_id] = token_ids; + ext_nep171_receiver::ext(receiver_id.clone()) .with_static_gas(#near_sdk::env::prepaid_gas() - GAS_FOR_NFT_TRANSFER_CALL) .nft_on_transfer( sender_id.clone(), sender_id.clone(), token_id.clone(), - msg, + msg.clone(), ) .then( ext_nep171_resolver::ext(#near_sdk::env::current_account_id()) .with_static_gas(GAS_FOR_RESOLVE_TRANSFER) - .nft_resolve_transfer(sender_id, receiver_id, token_id, None), + .nft_resolve_transfer(sender_id.clone(), receiver_id.clone(), token_id.clone(), None), ) .into() } diff --git a/src/standard/nep171.rs b/src/standard/nep171.rs index 5efd018..84a11f1 100644 --- a/src/standard/nep171.rs +++ b/src/standard/nep171.rs @@ -223,29 +223,41 @@ pub trait Nep171ControllerInternal { /// Non-public controller interface for NEP-171 implementations. pub trait Nep171Controller { - /// Transfer a token from `sender_id` to `receiver_id`, calling - /// `predicate` after performing required validity checks, but immediately - /// before performing the actual transfer, and returns the value returned - /// by `predicate`. - fn and_transfer( - &self, - predicate: impl FnOnce() -> T, + /// Transfer a token from `sender_id` to `receiver_id`. Checks that the transfer is valid using [`Nep171Controller::check_transfer`] before performing the transfer. + fn transfer( + &mut self, token_ids: &[TokenId], current_owner_id: AccountId, sender_id: AccountId, receiver_id: AccountId, memo: Option, - ) -> Result; + ) -> Result<(), Nep171TransferError>; - /// Transfer a token from `sender_id` to `receiver_id`. - fn transfer( + /// Check if a token transfer is valid without actually performing it. + fn check_transfer( + &self, + token_ids: &[TokenId], + current_owner_id: &AccountId, + sender_id: &AccountId, + receiver_id: &AccountId, + ) -> Result<(), Nep171TransferError>; + + /// Performs a token transfer without running [`Nep171Controller::check_transfer`]. + /// + /// # Warning + /// + /// This function performs _no checks_. It is up to the caller to ensure that the transfer is valid. Possible unintended effects of invalid transfers include: + /// - Transferring a token "from" an account that does not own it. + /// - Creating token IDs that did not previously exist. + /// - Transferring a token to the account that already owns it. + fn transfer_unchecked( &mut self, token_ids: &[TokenId], current_owner_id: AccountId, sender_id: AccountId, receiver_id: AccountId, memo: Option, - ) -> Result<(), Nep171TransferError>; + ); /// Mints a new token `token_id` to `owner_id`. fn mint( @@ -272,24 +284,22 @@ pub trait Nep171Controller { /// Transfer metadata generic over both types of transfer (`nft_transfer` and /// `nft_transfer_call`). -#[derive( - Serialize, Deserialize, BorshSerialize, BorshDeserialize, PartialEq, Eq, Clone, Debug, Hash, -)] -pub struct Nep171Transfer { +#[derive(Serialize, BorshSerialize, PartialEq, Eq, Clone, Debug, Hash)] +pub struct Nep171Transfer<'a> { /// Current owner account ID. - pub owner_id: AccountId, + pub owner_id: &'a AccountId, /// Sending account ID. - pub sender_id: AccountId, + pub sender_id: &'a AccountId, /// Receiving account ID. - pub receiver_id: AccountId, + pub receiver_id: &'a AccountId, /// Optional approval ID. pub approval_id: Option, /// Token ID. - pub token_id: TokenId, + pub token_id: &'a TokenId, /// Optional memo string. - pub memo: Option, + pub memo: Option<&'a str>, /// Message passed to contract located at `receiver_id` in the case of `nft_transfer_call`. - pub msg: Option, + pub msg: Option<&'a str>, } /// Contracts may implement this trait to inject code into NEP-171 functions. @@ -323,25 +333,22 @@ impl Nep171Controller for T { receiver_id: AccountId, memo: Option, ) -> Result<(), Nep171TransferError> { - self.and_transfer( - || {}, - token_ids, - current_owner_id, - sender_id, - receiver_id, - memo, - ) + match self.check_transfer(token_ids, ¤t_owner_id, &sender_id, &receiver_id) { + Ok(()) => { + self.transfer_unchecked(token_ids, current_owner_id, sender_id, receiver_id, memo); + Ok(()) + } + e => e, + } } - fn and_transfer

( + fn check_transfer( &self, - predicate: impl FnOnce() -> P, token_ids: &[TokenId], - current_owner_id: AccountId, - sender_id: AccountId, - receiver_id: AccountId, - memo: Option, - ) -> Result { + current_owner_id: &AccountId, + sender_id: &AccountId, + receiver_id: &AccountId, + ) -> Result<(), Nep171TransferError> { for token_id in token_ids { let slot = Self::slot_token_owner(token_id); @@ -350,9 +357,9 @@ impl Nep171Controller for T { token_id: token_id.clone(), })?; - if current_owner_id != actual_current_owner_id { + if current_owner_id != &actual_current_owner_id { return Err(error::TokenNotOwnedByExpectedOwnerError { - expected_owner_id: current_owner_id, + expected_owner_id: current_owner_id.clone(), actual_owner_id: actual_current_owner_id, token_id: token_id.clone(), } @@ -362,7 +369,7 @@ impl Nep171Controller for T { // This version doesn't implement approval management if sender_id != current_owner_id { return Err(error::SenderNotApprovedError { - sender_id, + sender_id: sender_id.clone(), token_id: token_id.clone(), } .into()); @@ -370,15 +377,23 @@ impl Nep171Controller for T { if receiver_id == current_owner_id { return Err(error::TokenReceiverIsCurrentOwnerError { - current_owner_id, + current_owner_id: current_owner_id.clone(), token_id: token_id.clone(), } .into()); } } + Ok(()) + } - let result = predicate(); - + fn transfer_unchecked( + &mut self, + token_ids: &[TokenId], + current_owner_id: AccountId, + _sender_id: AccountId, + receiver_id: AccountId, + memo: Option, + ) { if !token_ids.is_empty() { Nep171Event::NftTransfer(vec![event::NftTransferLog { authorized_id: None, @@ -394,8 +409,6 @@ impl Nep171Controller for T { let mut slot = Self::slot_token_owner(token_id); slot.write(&receiver_id); } - - Ok(result) } fn mint( From 14bd1006546f0be90ace995ff183f20d59a1ed56 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 26 Jul 2023 21:45:34 +0900 Subject: [PATCH 17/34] feat: nft events version 1.2.0 update --- src/standard/nep171.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/standard/nep171.rs b/src/standard/nep171.rs index 84a11f1..a741e82 100644 --- a/src/standard/nep171.rs +++ b/src/standard/nep171.rs @@ -33,7 +33,7 @@ pub type TokenId = String; macros = "crate", serde = "serde", standard = "nep171", - version = "1.1.0" + version = "1.2.0" )] #[derive(Debug, Clone)] pub enum Nep171Event { @@ -44,7 +44,9 @@ pub enum Nep171Event { /// Emitted when a token is burned. NftBurn(Vec), /// Emitted when the metadata associated with an NFT contract is updated. - ContractMetadataUpdate(Vec), + NftMetadataUpdate(Vec), + /// Emitted when the metadata associated with an NFT contract is updated. + ContractMetadataUpdate(Vec), } /// Event log metadata & associated structures. @@ -96,9 +98,19 @@ pub mod event { pub memo: Option, } - /// Contract metadata update metadata. + /// Token metadata update. + #[derive(Serialize, Debug, Clone)] + pub struct NftMetadataUpdateLog { + /// IDs of the updated tokens. + pub token_ids: Vec, + /// Additional update information. + #[serde(skip_serializing_if = "Option::is_none")] + pub memo: Option, + } + + /// Contract metadata update. #[derive(Serialize, Debug, Clone)] - pub struct ContractMetadataUpdateLog { + pub struct NftContractMetadataUpdateLog { /// Additional update information. #[serde(skip_serializing_if = "Option::is_none")] pub memo: Option, From eded8d8893c4edbcd3eebca7d5c4007ae903166b Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Mon, 31 Jul 2023 19:49:36 +0900 Subject: [PATCH 18/34] Nep177: NFT Metadata (#124) * feat: nep177 * chore: more function implementation * feat: nep177 macro * feat: non_fungible_token macro * feat: documentation comments --- macros/src/lib.rs | 21 ++ macros/src/standard/mod.rs | 2 + macros/src/standard/nep171.rs | 27 +- macros/src/standard/nep177.rs | 54 ++++ macros/src/standard/non_fungible_token.rs | 76 +++++ src/lib.rs | 3 + src/standard/mod.rs | 1 + src/standard/nep171.rs | 23 +- src/standard/nep177.rs | 296 ++++++++++++++++++ workspaces-tests/Cargo.toml | 7 +- .../src/bin/non_fungible_token_meta.rs | 59 ++++ workspaces-tests/tests/non_fungible_token.rs | 137 ++++++-- 12 files changed, 668 insertions(+), 38 deletions(-) create mode 100644 macros/src/standard/nep177.rs create mode 100644 macros/src/standard/non_fungible_token.rs create mode 100644 src/standard/nep177.rs create mode 100644 workspaces-tests/src/bin/non_fungible_token_meta.rs diff --git a/macros/src/lib.rs b/macros/src/lib.rs index d5c62d1..3e78ad3 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -153,11 +153,32 @@ pub fn derive_fungible_token(input: TokenStream) -> TokenStream { /// /// The storage key prefix for the fields can be optionally specified (default: /// `"~$171"`) using `#[nep171(storage_key = "")]`. +/// +/// Fields: +/// - `no_hooks`: Flag. Removes the requirement for the contract to implement [`near_sdk_contract_tools::standard::nep171::Nep171Hooks`]. +/// - `token_type`: specify the type of the token returned by `nft_token`. +/// - `token_loader`: A function that takes a reference to the contract, and +/// the desired token ID, and returns a value of type `token_type`. #[proc_macro_derive(Nep171, attributes(nep171))] pub fn derive_nep171(input: TokenStream) -> TokenStream { make_derive(input, standard::nep171::expand) } +/// Adds NEP-177 non-fungible token metadata functionality to a contract. +/// +/// The storage key prefix for the fields can be optionally specified (default: +/// `"~$177"`) using `#[nep177(storage_key = "")]`. +#[proc_macro_derive(Nep177, attributes(nep177))] +pub fn derive_nep177(input: TokenStream) -> TokenStream { + make_derive(input, standard::nep177::expand) +} + +/// Implements all NFT functionality at once, like `#[derive(Nep171, Nep177)]`. +#[proc_macro_derive(NonFungibleToken, attributes(non_fungible_token))] +pub fn derive_non_fungible_token(input: TokenStream) -> TokenStream { + make_derive(input, standard::non_fungible_token::expand) +} + /// Migrate a contract's default struct from one schema to another. /// /// Fields may be specified in the `#[migrate(...)]` attribute. diff --git a/macros/src/standard/mod.rs b/macros/src/standard/mod.rs index bf993cd..65d6dab 100644 --- a/macros/src/standard/mod.rs +++ b/macros/src/standard/mod.rs @@ -1,7 +1,9 @@ pub mod event; pub mod fungible_token; +pub mod non_fungible_token; pub mod nep141; pub mod nep148; pub mod nep171; +pub mod nep177; pub mod nep297; diff --git a/macros/src/standard/nep171.rs b/macros/src/standard/nep171.rs index 6ef88c0..b090069 100644 --- a/macros/src/standard/nep171.rs +++ b/macros/src/standard/nep171.rs @@ -10,6 +10,9 @@ use syn::Expr; pub struct Nep171Meta { pub storage_key: Option, pub no_hooks: Flag, + pub token_loader: Option, + pub token_type: Option, + pub generics: syn::Generics, pub ident: syn::Ident, @@ -24,6 +27,9 @@ pub fn expand(meta: Nep171Meta) -> Result { let Nep171Meta { storage_key, no_hooks, + token_loader, + token_type, + generics, ident, @@ -33,6 +39,18 @@ pub fn expand(meta: Nep171Meta) -> Result { let (imp, ty, wher) = generics.split_for_impl(); + let token_type = token_type + .map(|token_type| quote! { #token_type }) + .unwrap_or_else(|| { + quote! { #me::standard::nep171::Token } + }); + + let token_loader = token_loader + .map(|token_loader| quote! { #token_loader }) + .unwrap_or_else(|| { + quote! { #token_type::load } + }); + let root = storage_key.map(|storage_key| { quote! { fn root() -> #me::slot::Slot<()> { @@ -130,7 +148,7 @@ pub fn expand(meta: Nep171Meta) -> Result { } #[#near_sdk::near_bindgen] - impl #imp #me::standard::nep171::Nep171 for #ident #ty #wher { + impl #imp #me::standard::nep171::Nep171<#token_type> for #ident #ty #wher { #[payable] fn nft_transfer( &mut self, @@ -249,11 +267,8 @@ pub fn expand(meta: Nep171Meta) -> Result { fn nft_token( &self, token_id: #me::standard::nep171::TokenId, - ) -> Option<#me::standard::nep171::Token> { - use #me::standard::nep171::*; - - Nep171Controller::token_owner(self, &token_id) - .map(|owner_id| Token { token_id, owner_id }) + ) -> Option<#token_type> { + #token_loader(self, token_id) } } }) diff --git a/macros/src/standard/nep177.rs b/macros/src/standard/nep177.rs new file mode 100644 index 0000000..dccc7f3 --- /dev/null +++ b/macros/src/standard/nep177.rs @@ -0,0 +1,54 @@ +use darling::FromDeriveInput; +use proc_macro2::TokenStream; +use quote::quote; +use syn::Expr; + +#[derive(Debug, FromDeriveInput)] +#[darling(attributes(nep177), supports(struct_named))] +pub struct Nep177Meta { + pub storage_key: Option, + + pub generics: syn::Generics, + pub ident: syn::Ident, + + // crates + #[darling(rename = "crate", default = "crate::default_crate_name")] + pub me: syn::Path, + #[darling(default = "crate::default_near_sdk")] + pub near_sdk: syn::Path, +} + +pub fn expand(meta: Nep177Meta) -> Result { + let Nep177Meta { + storage_key, + + generics, + ident, + + me, + near_sdk, + } = meta; + + let (imp, ty, wher) = generics.split_for_impl(); + + let root = storage_key.map(|storage_key| { + quote! { + fn root() -> #me::slot::Slot<()> { + #me::slot::Slot::root(#storage_key) + } + } + }); + + Ok(quote! { + impl #imp #me::standard::nep177::Nep177ControllerInternal for #ident #ty #wher { + #root + } + + #[#near_sdk::near_bindgen] + impl #imp #me::standard::nep177::Nep177 for #ident #ty #wher { + fn nft_metadata(&self) -> #me::standard::nep177::ContractMetadata { + #me::standard::nep177::Nep177Controller::contract_metadata(self) + } + } + }) +} diff --git a/macros/src/standard/non_fungible_token.rs b/macros/src/standard/non_fungible_token.rs new file mode 100644 index 0000000..8e58a10 --- /dev/null +++ b/macros/src/standard/non_fungible_token.rs @@ -0,0 +1,76 @@ +use darling::{util::Flag, FromDeriveInput}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::Expr; + +use super::{nep171, nep177}; + +#[derive(Debug, FromDeriveInput)] +#[darling(attributes(non_fungible_token), supports(struct_named))] +pub struct NonFungibleTokenMeta { + // NEP-171 fields + pub storage_key: Option, + pub no_hooks: Flag, + + // NEP-177 fields + pub metadata_storage_key: Option, + + // darling + pub generics: syn::Generics, + pub ident: syn::Ident, + + // crates + #[darling(rename = "crate", default = "crate::default_crate_name")] + pub me: syn::Path, + #[darling(default = "crate::default_near_sdk")] + pub near_sdk: syn::Path, +} + +pub fn expand(meta: NonFungibleTokenMeta) -> Result { + let NonFungibleTokenMeta { + storage_key, + no_hooks, + + metadata_storage_key, + + generics, + ident, + + me, + near_sdk, + } = meta; + + let expand_nep171 = nep171::expand(nep171::Nep171Meta { + storage_key, + no_hooks, + + token_type: Some(syn::parse_quote! { #me::standard::nep177::Token }), + token_loader: None, + + generics: generics.clone(), + ident: ident.clone(), + + me: me.clone(), + near_sdk: near_sdk.clone(), + }); + + let expand_nep177 = nep177::expand(nep177::Nep177Meta { + storage_key: metadata_storage_key, + + generics, + ident, + + me, + near_sdk, + }); + + let mut e = darling::Error::accumulator(); + + let nep171 = e.handle(expand_nep171); + let nep177 = e.handle(expand_nep177); + + e.finish_with(quote! { + #nep171 + #nep177 + }) +} diff --git a/src/lib.rs b/src/lib.rs index aa29c76..becbee6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,8 @@ pub enum DefaultStorageKey { Nep141, /// Default storage key for [`standard::nep171::Nep171Controller::root`] Nep171, + /// Default storage key for [`standard::nep177::Nep177Controller::root`] + Nep177, /// Default storage key for [`owner::Owner::root`] Owner, /// Default storage key for [`pause::Pause::root`] @@ -26,6 +28,7 @@ impl IntoStorageKey for DefaultStorageKey { DefaultStorageKey::ApprovalManager => b"~am".to_vec(), DefaultStorageKey::Nep141 => b"~$141".to_vec(), DefaultStorageKey::Nep171 => b"~$171".to_vec(), + DefaultStorageKey::Nep177 => b"~$177".to_vec(), DefaultStorageKey::Owner => b"~o".to_vec(), DefaultStorageKey::Pause => b"~p".to_vec(), DefaultStorageKey::Rbac => b"~r".to_vec(), diff --git a/src/standard/mod.rs b/src/standard/mod.rs index d60d567..9df090d 100644 --- a/src/standard/mod.rs +++ b/src/standard/mod.rs @@ -3,4 +3,5 @@ pub mod nep141; pub mod nep148; pub mod nep171; +pub mod nep177; pub mod nep297; diff --git a/src/standard/nep171.rs b/src/standard/nep171.rs index a741e82..745a3d1 100644 --- a/src/standard/nep171.rs +++ b/src/standard/nep171.rs @@ -516,15 +516,7 @@ impl Nep171Controller for T { /// Token information structure. #[derive( - Debug, - Clone, - Hash, - PartialEq, - PartialOrd, - Serialize, - Deserialize, - BorshSerialize, - BorshDeserialize, + Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize, BorshSerialize, BorshDeserialize, )] pub struct Token { /// Token ID. @@ -533,6 +525,15 @@ pub struct Token { pub owner_id: AccountId, } +impl Token { + /// Load token information from the contract. + pub fn load(contract: &impl Nep171Controller, token_id: TokenId) -> Option { + contract + .token_owner(&token_id) + .map(|owner_id| Self { token_id, owner_id }) + } +} + // separate module with re-export because ext_contract doesn't play well with #![warn(missing_docs)] mod ext { #![allow(missing_docs)] @@ -545,7 +546,7 @@ mod ext { /// Interface of contracts that implement NEP-171. #[ext_contract(ext_nep171)] - pub trait Nep171 { + pub trait Nep171 { /// Transfer a token. fn nft_transfer( &mut self, @@ -566,7 +567,7 @@ mod ext { ) -> PromiseOrValue; /// Get individual token information. - fn nft_token(&self, token_id: TokenId) -> Option; + fn nft_token(&self, token_id: TokenId) -> Option; } /// Original token contract follow-up to [`Nep171::nft_transfer_call`]. diff --git a/src/standard/nep177.rs b/src/standard/nep177.rs new file mode 100644 index 0000000..455f70e --- /dev/null +++ b/src/standard/nep177.rs @@ -0,0 +1,296 @@ +//! NEP-177 non-fungible token contract metadata implementation. +//! +//! Reference: +use near_sdk::{ + borsh::{self, BorshDeserialize, BorshSerialize}, + env, + json_types::U64, + serde::*, + AccountId, BorshStorageKey, +}; +use thiserror::Error; + +use crate::{ + slot::Slot, + standard::{ + nep171::{ + self, + error::TokenDoesNotExistError, + event::{NftContractMetadataUpdateLog, NftMetadataUpdateLog}, + *, + }, + nep297::Event, + }, + DefaultStorageKey, +}; + +pub use ext::*; + +const CONTRACT_METADATA_NOT_INITIALIZED_ERROR: &str = "Contract metadata not initialized"; + +/// Non-fungible token with metadata. +#[derive( + Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize, BorshSerialize, BorshDeserialize, +)] +#[serde(crate = "near_sdk::serde")] +pub struct Token { + /// The token ID. + pub token_id: TokenId, + /// The token owner. + pub owner_id: AccountId, + /// The token metadata. + pub metadata: TokenMetadata, +} + +impl Token { + /// Loads token metadata. + pub fn load( + contract: &(impl Nep171Controller + Nep177Controller), + token_id: TokenId, + ) -> Option { + let owner_id = contract.token_owner(&token_id)?; + let metadata = contract.token_metadata(&token_id)?; + Some(Self { + token_id, + owner_id, + metadata, + }) + } +} + +/// Non-fungible token contract metadata. +#[derive( + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, +)] +#[serde(crate = "near_sdk::serde")] +pub struct ContractMetadata { + /// The metadata specification version. Essentially a version like "nft-2.0.0", replacing "2.0.0" with the implemented version of NEP-177. + pub spec: String, + /// The name of the NFT contract, e.g. "Mochi Rising — Digital Edition" or "Metaverse 3". + pub name: String, + /// The symbol of the NFT contract, e.g. "MOCHI" or "M3". + pub symbol: String, + /// Data URI for the contract icon. + pub icon: Option, + /// Gateway known to have reliable access to decentralized storage assets referenced by `reference` or `media` URLs. + pub base_uri: Option, + /// URL to a JSON file with more info about the NFT contract. + pub reference: Option, + /// Base-64-encoded SHA-256 hash of the referenced JSON file. Required if `reference` is present. + pub reference_hash: Option, +} + +impl ContractMetadata { + /// The metadata specification version. + pub const SPEC: &'static str = "nft-2.1.0"; + + /// Creates a new contract metadata, specifying the name, symbol, and + /// optional base URI. Other fields are set to `None`. + pub fn new(name: String, symbol: String, base_uri: Option) -> Self { + Self { + spec: Self::SPEC.to_string(), + name, + symbol, + icon: None, + base_uri, + reference: None, + reference_hash: None, + } + } +} + +/// Non-fungible token metadata. +#[derive( + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, +)] +#[serde(crate = "near_sdk::serde")] +pub struct TokenMetadata { + /// This token's title, e.g. "Arch Nemesis: Mail Carrier" or "Parcel #5055". + pub title: Option, + /// Free-text description of this specific token. + pub description: Option, + /// The token's image or other associated media. + pub media: Option, + /// Base-64-encoded SHA-256 hash of the media. Required if `media` is present. + pub media_hash: Option, + /// Number of copies of this set of metadata in existence when token was minted. + pub copies: Option, + /// When the token was issued, in milliseconds since the UNIX epoch. + pub issued_at: Option, + /// When the token expires, in milliseconds since the UNIX epoch. + pub expires_at: Option, + /// When the token starts being valid, in milliseconds since the UNIX epoch. + pub starts_at: Option, + /// When the token was last updated, in milliseconds since the UNIX epoch. + pub updated_at: Option, + /// Anything extra the NFT wants to store on-chain. Can be stringified JSON. + pub extra: Option, + /// URL to an off-chain JSON file with more info about the token. + pub reference: Option, + /// Base-64-encoded SHA-256 hash of the referenced JSON file. Required if `reference` is present. + pub reference_hash: Option, +} + +#[derive(BorshSerialize, BorshStorageKey)] +enum StorageKey<'a> { + ContractMetadata, + TokenMetadata(&'a TokenId), +} + +/// Internal functions for [`Nep177Controller`]. +pub trait Nep177ControllerInternal { + /// Storage root. + fn root() -> Slot<()> { + Slot::root(DefaultStorageKey::Nep177) + } + + /// Storage slot for contract metadata. + fn slot_contract_metadata() -> Slot { + Self::root().field(StorageKey::ContractMetadata) + } + + /// Storage slot for token metadata. + fn slot_token_metadata(token_id: &TokenId) -> Slot { + Self::root().field(StorageKey::TokenMetadata(token_id)) + } +} + +/// Functions for managing non-fungible tokens with attached metadata, NEP-177. +pub trait Nep177Controller { + /// Mint a new token with metadata. + fn mint_with_metadata( + &mut self, + token_id: TokenId, + owner_id: AccountId, + metadata: TokenMetadata, + ) -> Result<(), Nep171MintError>; + + /// Burn a token with metadata. + fn burn_with_metadata( + &mut self, + token_id: TokenId, + current_owner_id: &AccountId, + ) -> Result<(), Nep171BurnError>; + + /// Sets the metadata for a token ID without checking whether the token exists, etc. and emits an [`Nep171Event::NftMetadataUpdate`] event. + fn set_token_metadata_unchecked(&mut self, token_id: TokenId, metadata: Option); + + /// Sets the metadata for a token ID and emits an [`Nep171Event::NftMetadataUpdate`] event. + fn set_token_metadata( + &mut self, + token_id: TokenId, + metadata: TokenMetadata, + ) -> Result<(), UpdateTokenMetadataError>; + + /// Sets the contract metadata and emits an [`Nep171Event::ContractMetadataUpdate`] event. + fn set_contract_metadata(&mut self, metadata: ContractMetadata); + + /// Returns the contract metadata. + fn contract_metadata(&self) -> ContractMetadata; + + /// Returns the metadata for a token ID. + fn token_metadata(&self, token_id: &TokenId) -> Option; +} + +/// Error returned when a token update fails. +#[derive(Error, Debug)] +pub enum UpdateTokenMetadataError { + /// The token does not exist. + #[error(transparent)] + TokenNotFound(#[from] TokenDoesNotExistError), +} + +impl Nep177Controller for T { + fn set_token_metadata( + &mut self, + token_id: TokenId, + metadata: TokenMetadata, + ) -> Result<(), UpdateTokenMetadataError> { + if self.token_owner(&token_id).is_some() { + self.set_token_metadata_unchecked(token_id, Some(metadata)); + Ok(()) + } else { + Err(TokenDoesNotExistError { token_id }.into()) + } + } + + fn set_contract_metadata(&mut self, metadata: ContractMetadata) { + Self::slot_contract_metadata().set(Some(&metadata)); + Nep171Event::ContractMetadataUpdate(vec![NftContractMetadataUpdateLog { memo: None }]) + .emit(); + } + + fn mint_with_metadata( + &mut self, + token_id: TokenId, + owner_id: AccountId, + metadata: TokenMetadata, + ) -> Result<(), Nep171MintError> { + let token_ids = [token_id]; + self.mint(&token_ids, &owner_id, None)?; + let [token_id] = token_ids; + self.set_token_metadata_unchecked(token_id, Some(metadata)); + Ok(()) + } + + fn burn_with_metadata( + &mut self, + token_id: TokenId, + current_owner_id: &AccountId, + ) -> Result<(), Nep171BurnError> { + let token_ids = [token_id]; + self.burn(&token_ids, current_owner_id, None)?; + let [token_id] = token_ids; + self.set_token_metadata_unchecked(token_id, None); + Ok(()) + } + + fn set_token_metadata_unchecked(&mut self, token_id: TokenId, metadata: Option) { + ::slot_token_metadata(&token_id).set(metadata.as_ref()); + nep171::Nep171Event::NftMetadataUpdate(vec![NftMetadataUpdateLog { + token_ids: vec![token_id], + memo: None, + }]) + .emit(); + } + + fn token_metadata(&self, token_id: &TokenId) -> Option { + ::slot_token_metadata(token_id).read() + } + + fn contract_metadata(&self) -> ContractMetadata { + Self::slot_contract_metadata() + .read() + .unwrap_or_else(|| env::panic_str(CONTRACT_METADATA_NOT_INITIALIZED_ERROR)) + } +} + +// separate module with re-export because ext_contract doesn't play well with #![warn(missing_docs)] +mod ext { + #![allow(missing_docs)] + + use super::*; + + #[near_sdk::ext_contract(ext_nep171)] + pub trait Nep177 { + fn nft_metadata(&self) -> ContractMetadata; + } +} diff --git a/workspaces-tests/Cargo.toml b/workspaces-tests/Cargo.toml index d04bcee..a6aa512 100644 --- a/workspaces-tests/Cargo.toml +++ b/workspaces-tests/Cargo.toml @@ -23,6 +23,9 @@ name = "native_multisig" [[bin]] name = "non_fungible_token" +[[bin]] +name = "non_fungible_token_meta" + [[bin]] name = "non_fungible_token_receiver" @@ -54,8 +57,8 @@ name = "upgrade_old_multisig" name = "upgrade_old_raw" [dependencies] -near-sdk = {version = "4.1.1", default-features = false} -near-sdk-contract-tools = {path = "../", features = ["unstable"]} +near-sdk = { version = "4.1.1", default-features = false } +near-sdk-contract-tools = { path = "../", features = ["unstable"] } strum = "0.24.1" strum_macros = "0.24.3" thiserror = "1.0.34" diff --git a/workspaces-tests/src/bin/non_fungible_token_meta.rs b/workspaces-tests/src/bin/non_fungible_token_meta.rs new file mode 100644 index 0000000..30826f6 --- /dev/null +++ b/workspaces-tests/src/bin/non_fungible_token_meta.rs @@ -0,0 +1,59 @@ +#![allow(missing_docs)] + +// Ignore +pub fn main() {} + +use near_sdk::{ + borsh::{self, BorshDeserialize, BorshSerialize}, + env, near_bindgen, PanicOnDefault, +}; +use near_sdk_contract_tools::{ + standard::{nep171::*, nep177::*}, + NonFungibleToken, +}; + +#[derive(PanicOnDefault, BorshSerialize, BorshDeserialize, NonFungibleToken)] +#[non_fungible_token(no_hooks)] +#[near_bindgen] +pub struct Contract {} + +#[near_bindgen] +impl Contract { + #[init] + pub fn new() -> Self { + let mut contract = Self {}; + + contract.set_contract_metadata(ContractMetadata::new( + "My NFT Smart Contract".to_string(), + "MNSC".to_string(), + None, + )); + + contract + } + + pub fn mint(&mut self, token_ids: Vec) { + let receiver = env::predecessor_account_id(); + for token_id in token_ids { + self.mint_with_metadata( + token_id.clone(), + receiver.clone(), + TokenMetadata { + title: Some(token_id), + description: Some("description".to_string()), + media: None, + media_hash: None, + copies: None, + issued_at: None, + expires_at: None, + starts_at: None, + updated_at: None, + extra: None, + reference: None, + reference_hash: None, + }, + ) + .unwrap_or_else(|e| env::panic_str(&format!("Failed to mint: {:#?}", e))); + } + } +} diff --git a/workspaces-tests/tests/non_fungible_token.rs b/workspaces-tests/tests/non_fungible_token.rs index 75e567f..cdb6726 100644 --- a/workspaces-tests/tests/non_fungible_token.rs +++ b/workspaces-tests/tests/non_fungible_token.rs @@ -1,8 +1,9 @@ #![cfg(not(windows))] -use near_sdk::serde_json::json; +use near_sdk::{serde::de::DeserializeOwned, serde_json::json}; use near_sdk_contract_tools::standard::{ nep171::{event::NftTransferLog, Nep171Event, Token}, + nep177::{self, TokenMetadata}, nep297::Event, }; use workspaces::{operations::Function, result::ExecutionFinalResult, Account, Contract}; @@ -10,16 +11,19 @@ use workspaces::{operations::Function, result::ExecutionFinalResult, Account, Co const WASM: &[u8] = include_bytes!("../../target/wasm32-unknown-unknown/release/non_fungible_token.wasm"); +const WASM_177: &[u8] = + include_bytes!("../../target/wasm32-unknown-unknown/release/non_fungible_token_meta.wasm"); + const RECEIVER_WASM: &[u8] = include_bytes!("../../target/wasm32-unknown-unknown/release/non_fungible_token_receiver.wasm"); -async fn nft_token(contract: &Contract, token_id: &str) -> Option { +async fn nft_token(contract: &Contract, token_id: &str) -> Option { contract .view("nft_token") .args_json(json!({ "token_id": token_id })) .await .unwrap() - .json::>() + .json::>() .unwrap() } @@ -29,11 +33,11 @@ struct Setup { } /// Setup for individual tests -async fn setup(num_accounts: usize) -> Setup { +async fn setup(wasm: &[u8], num_accounts: usize) -> Setup { let worker = workspaces::sandbox().await.unwrap(); // Initialize contract - let contract = worker.dev_deploy(WASM).await.unwrap(); + let contract = worker.dev_deploy(wasm).await.unwrap(); contract.call("new").transact().await.unwrap().unwrap(); // Initialize user accounts @@ -45,8 +49,12 @@ async fn setup(num_accounts: usize) -> Setup { Setup { contract, accounts } } -async fn setup_balances(num_accounts: usize, token_ids: impl Fn(usize) -> Vec) -> Setup { - let s = setup(num_accounts).await; +async fn setup_balances( + wasm: &[u8], + num_accounts: usize, + token_ids: impl Fn(usize) -> Vec, +) -> Setup { + let s = setup(wasm, num_accounts).await; for (i, account) in s.accounts.iter().enumerate() { account @@ -62,8 +70,9 @@ async fn setup_balances(num_accounts: usize, token_ids: impl Fn(usize) -> Vec); +} + +#[tokio::test] +async fn create_and_mint_with_metadata() { + let Setup { contract, accounts } = + setup_balances(WASM_177, 3, |i| vec![format!("token_{i}")]).await; + let alice = &accounts[0]; + let bob = &accounts[1]; + let charlie = &accounts[2]; + + let metadata = contract + .view("nft_metadata") + .await + .unwrap() + .json::>() + .unwrap() + .unwrap(); + + assert_eq!( + metadata, + nep177::ContractMetadata { + spec: nep177::ContractMetadata::SPEC.to_string(), + name: "My NFT Smart Contract".to_string(), + symbol: "MNSC".to_string(), + icon: None, + base_uri: None, + reference: None, + reference_hash: None, + }, + ); + + let (token_0, token_1, token_2, token_3) = tokio::join!( + nft_token(&contract, "token_0"), + nft_token(&contract, "token_1"), + nft_token(&contract, "token_2"), + nft_token(&contract, "token_3"), + ); + + fn token_meta(id: String) -> TokenMetadata { + TokenMetadata { + title: Some(id), + description: Some("description".to_string()), + media: None, + media_hash: None, + copies: None, + issued_at: None, + expires_at: None, + starts_at: None, + updated_at: None, + extra: None, + reference: None, + reference_hash: None, + } + } + + // Verify minted tokens + assert_eq!( + token_0, + Some(nep177::Token { + token_id: "token_0".to_string(), + owner_id: alice.id().parse().unwrap(), + metadata: token_meta("token_0".to_string()), + }), + ); + assert_eq!( + token_1, + Some(nep177::Token { + token_id: "token_1".to_string(), + owner_id: bob.id().parse().unwrap(), + metadata: token_meta("token_1".to_string()), + }), + ); + assert_eq!( + token_2, + Some(nep177::Token { + token_id: "token_2".to_string(), + owner_id: charlie.id().parse().unwrap(), + metadata: token_meta("token_2".to_string()), + }), + ); + assert_eq!(token_3, None::); } #[tokio::test] async fn transfer_success() { - let Setup { contract, accounts } = setup_balances(3, |i| vec![format!("token_{i}")]).await; + let Setup { contract, accounts } = + setup_balances(WASM, 3, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; let bob = &accounts[1]; let charlie = &accounts[2]; @@ -167,7 +258,8 @@ async fn transfer_success() { #[tokio::test] #[should_panic = "Smart contract panicked: Requires attached deposit of exactly 1 yoctoNEAR"] async fn transfer_fail_no_deposit() { - let Setup { contract, accounts } = setup_balances(2, |i| vec![format!("token_{i}")]).await; + let Setup { contract, accounts } = + setup_balances(WASM, 2, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; let bob = &accounts[1]; @@ -186,7 +278,8 @@ async fn transfer_fail_no_deposit() { #[tokio::test] #[should_panic = "Smart contract panicked: Token `token_5` does not exist"] async fn transfer_fail_token_dne() { - let Setup { contract, accounts } = setup_balances(2, |i| vec![format!("token_{i}")]).await; + let Setup { contract, accounts } = + setup_balances(WASM, 2, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; let bob = &accounts[1]; @@ -225,7 +318,8 @@ fn expect_execution_error(result: &ExecutionFinalResult, expected_error: impl As #[tokio::test] async fn transfer_fail_not_owner() { - let Setup { contract, accounts } = setup_balances(3, |i| vec![format!("token_{i}")]).await; + let Setup { contract, accounts } = + setup_balances(WASM, 3, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; let bob = &accounts[1]; let charlie = &accounts[2]; @@ -253,7 +347,8 @@ async fn transfer_fail_not_owner() { #[tokio::test] async fn transfer_fail_reflexive_transfer() { - let Setup { contract, accounts } = setup_balances(2, |i| vec![format!("token_{i}")]).await; + let Setup { contract, accounts } = + setup_balances(WASM, 2, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; let result = alice @@ -272,7 +367,8 @@ async fn transfer_fail_reflexive_transfer() { #[tokio::test] async fn transfer_call_success() { - let Setup { contract, accounts } = setup_balances(2, |i| vec![format!("token_{i}")]).await; + let Setup { contract, accounts } = + setup_balances(WASM, 2, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; let bob = &accounts[1]; @@ -329,7 +425,8 @@ async fn transfer_call_success() { #[tokio::test] async fn transfer_call_return_success() { - let Setup { contract, accounts } = setup_balances(2, |i| vec![format!("token_{i}")]).await; + let Setup { contract, accounts } = + setup_balances(WASM, 2, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; let bob = &accounts[1]; @@ -396,7 +493,8 @@ async fn transfer_call_return_success() { #[tokio::test] async fn transfer_call_receiver_panic() { - let Setup { contract, accounts } = setup_balances(2, |i| vec![format!("token_{i}")]).await; + let Setup { contract, accounts } = + setup_balances(WASM, 2, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; let bob = &accounts[1]; @@ -463,7 +561,8 @@ async fn transfer_call_receiver_panic() { #[tokio::test] async fn transfer_call_receiver_send_return() { - let Setup { contract, accounts } = setup_balances(3, |i| vec![format!("token_{i}")]).await; + let Setup { contract, accounts } = + setup_balances(WASM, 3, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; let bob = &accounts[1]; let charlie = &accounts[2]; From 76e1b341091c693d9b64cee0fea3863ec7afa16c Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Mon, 31 Jul 2023 21:06:08 +0900 Subject: [PATCH 19/34] feat: switch to using #[serde(flatten)] for token metadata --- macros/src/standard/nep171.rs | 18 +- macros/src/standard/non_fungible_token.rs | 3 +- src/standard/nep171/error.rs | 60 +++++ src/standard/nep171/event.rs | 66 +++++ src/standard/nep171/ext.rs | 64 +++++ src/standard/{nep171.rs => nep171/mod.rs} | 268 +++++-------------- src/standard/nep177.rs | 55 ++-- workspaces-tests/tests/non_fungible_token.rs | 32 ++- 8 files changed, 308 insertions(+), 258 deletions(-) create mode 100644 src/standard/nep171/error.rs create mode 100644 src/standard/nep171/event.rs create mode 100644 src/standard/nep171/ext.rs rename src/standard/{nep171.rs => nep171/mod.rs} (65%) diff --git a/macros/src/standard/nep171.rs b/macros/src/standard/nep171.rs index b090069..0baa05a 100644 --- a/macros/src/standard/nep171.rs +++ b/macros/src/standard/nep171.rs @@ -10,8 +10,7 @@ use syn::Expr; pub struct Nep171Meta { pub storage_key: Option, pub no_hooks: Flag, - pub token_loader: Option, - pub token_type: Option, + pub token_type: Option, pub generics: syn::Generics, pub ident: syn::Ident, @@ -27,7 +26,6 @@ pub fn expand(meta: Nep171Meta) -> Result { let Nep171Meta { storage_key, no_hooks, - token_loader, token_type, generics, @@ -42,13 +40,7 @@ pub fn expand(meta: Nep171Meta) -> Result { let token_type = token_type .map(|token_type| quote! { #token_type }) .unwrap_or_else(|| { - quote! { #me::standard::nep171::Token } - }); - - let token_loader = token_loader - .map(|token_loader| quote! { #token_loader }) - .unwrap_or_else(|| { - quote! { #token_type::load } + quote! { () } }); let root = storage_key.map(|storage_key| { @@ -148,7 +140,7 @@ pub fn expand(meta: Nep171Meta) -> Result { } #[#near_sdk::near_bindgen] - impl #imp #me::standard::nep171::Nep171<#token_type> for #ident #ty #wher { + impl #imp #me::standard::nep171::Nep171 for #ident #ty #wher { #[payable] fn nft_transfer( &mut self, @@ -267,8 +259,8 @@ pub fn expand(meta: Nep171Meta) -> Result { fn nft_token( &self, token_id: #me::standard::nep171::TokenId, - ) -> Option<#token_type> { - #token_loader(self, token_id) + ) -> Option<#me::standard::nep171::Token> { + #me::standard::nep171::Nep171Controller::load_token::<#token_type>(self, &token_id) } } }) diff --git a/macros/src/standard/non_fungible_token.rs b/macros/src/standard/non_fungible_token.rs index 8e58a10..8cb177b 100644 --- a/macros/src/standard/non_fungible_token.rs +++ b/macros/src/standard/non_fungible_token.rs @@ -44,8 +44,7 @@ pub fn expand(meta: NonFungibleTokenMeta) -> Result storage_key, no_hooks, - token_type: Some(syn::parse_quote! { #me::standard::nep177::Token }), - token_loader: None, + token_type: Some(syn::parse_quote! { ( #me::standard::nep177::TokenMetadata ) }), generics: generics.clone(), ident: ident.clone(), diff --git a/src/standard/nep171/error.rs b/src/standard/nep171/error.rs new file mode 100644 index 0000000..ba6d562 --- /dev/null +++ b/src/standard/nep171/error.rs @@ -0,0 +1,60 @@ +//! Potential errors produced by various token manipulations. + +use near_sdk::AccountId; +use thiserror::Error; + +use super::TokenId; + +/// Occurs when trying to create a token ID that already exists. +/// Overwriting pre-existing token IDs is not allowed. +#[derive(Error, Clone, Debug)] +#[error("Token `{token_id}` already exists")] +pub struct TokenAlreadyExistsError { + /// The conflicting token ID. + pub token_id: TokenId, +} + +/// When attempting to interact with a non-existent token ID. +#[derive(Error, Clone, Debug)] +#[error("Token `{token_id}` does not exist")] +pub struct TokenDoesNotExistError { + /// The invalid token ID. + pub token_id: TokenId, +} + +/// Occurs when performing a checked operation that expects a token to be +/// owned by a particular account, but the token is _not_ owned by that +/// account. +#[derive(Error, Clone, Debug)] +#[error( + "Token `{token_id}` is owned by `{actual_owner_id}` instead of expected `{expected_owner_id}`" +)] +pub struct TokenNotOwnedByExpectedOwnerError { + /// The token was supposed to be owned by this account. + pub expected_owner_id: AccountId, + /// The token is actually owned by this account. + pub actual_owner_id: AccountId, + /// The ID of the token in question. + pub token_id: TokenId, +} + +/// Occurs when a particular account is not allowed to transfer a token (e.g. on behalf of another user). See: NEP-178. +#[derive(Error, Clone, Debug)] +#[error("Sender `{sender_id}` does not have permission to transfer token `{token_id}`")] +pub struct SenderNotApprovedError { + /// The unapproved sender. + pub sender_id: AccountId, + /// The ID of the token in question. + pub token_id: TokenId, +} + +/// Occurs when attempting to perform a transfer of a token from one +/// account to the same account. +#[derive(Error, Clone, Debug)] +#[error("Receiver must be different from current owner `{current_owner_id}` to transfer token `{token_id}`")] +pub struct TokenReceiverIsCurrentOwnerError { + /// The account ID of current owner of the token. + pub current_owner_id: AccountId, + /// The ID of the token in question. + pub token_id: TokenId, +} diff --git a/src/standard/nep171/event.rs b/src/standard/nep171/event.rs new file mode 100644 index 0000000..74f012a --- /dev/null +++ b/src/standard/nep171/event.rs @@ -0,0 +1,66 @@ +//! Event log metadata & associated structures. + +use near_sdk::AccountId; +use serde::Serialize; + +/// Tokens minted to a single owner. +#[derive(Serialize, Debug, Clone)] +pub struct NftMintLog { + /// To whom were the new tokens minted? + pub owner_id: AccountId, + /// Which tokens were minted? + pub token_ids: Vec, + /// Additional mint information. + #[serde(skip_serializing_if = "Option::is_none")] + pub memo: Option, +} + +/// Tokens are transferred from one account to another. +#[derive(Serialize, Debug, Clone)] +pub struct NftTransferLog { + /// NEP-178 authorized account ID. + #[serde(skip_serializing_if = "Option::is_none")] + pub authorized_id: Option, + /// Account ID of the previous owner. + pub old_owner_id: AccountId, + /// Account ID of the new owner. + pub new_owner_id: AccountId, + /// IDs of the transferred tokens. + pub token_ids: Vec, + /// Additional transfer information. + #[serde(skip_serializing_if = "Option::is_none")] + pub memo: Option, +} + +/// Tokens are burned from a single holder. +#[derive(Serialize, Debug, Clone)] +pub struct NftBurnLog { + /// What is the ID of the account from which the tokens were burned? + pub owner_id: AccountId, + /// IDs of the burned tokens. + pub token_ids: Vec, + /// NEP-178 authorized account ID. + #[serde(skip_serializing_if = "Option::is_none")] + pub authorized_id: Option, + /// Additional burn information. + #[serde(skip_serializing_if = "Option::is_none")] + pub memo: Option, +} + +/// Token metadata update. +#[derive(Serialize, Debug, Clone)] +pub struct NftMetadataUpdateLog { + /// IDs of the updated tokens. + pub token_ids: Vec, + /// Additional update information. + #[serde(skip_serializing_if = "Option::is_none")] + pub memo: Option, +} + +/// Contract metadata update. +#[derive(Serialize, Debug, Clone)] +pub struct NftContractMetadataUpdateLog { + /// Additional update information. + #[serde(skip_serializing_if = "Option::is_none")] + pub memo: Option, +} diff --git a/src/standard/nep171/ext.rs b/src/standard/nep171/ext.rs new file mode 100644 index 0000000..9659a52 --- /dev/null +++ b/src/standard/nep171/ext.rs @@ -0,0 +1,64 @@ +#![allow(missing_docs)] + +use std::collections::HashMap; + +use near_sdk::{ext_contract, AccountId, PromiseOrValue}; + +use super::TokenId; + +/// Interface of contracts that implement NEP-171. +#[ext_contract(ext_nep171)] +pub trait Nep171 { + /// Transfer a token. + fn nft_transfer( + &mut self, + receiver_id: AccountId, + token_id: TokenId, + approval_id: Option, + memo: Option, + ); + + /// Transfer a token, and call [`Nep171Receiver::nft_on_transfer`] on the receiving account. + fn nft_transfer_call( + &mut self, + receiver_id: AccountId, + token_id: TokenId, + approval_id: Option, + memo: Option, + msg: String, + ) -> PromiseOrValue; + + /// Get individual token information. + fn nft_token(&self, token_id: TokenId) -> Option; +} + +/// Original token contract follow-up to [`Nep171::nft_transfer_call`]. +#[ext_contract(ext_nep171_resolver)] +pub trait Nep171Resolver { + /// Final method call on the original token contract during an + /// [`Nep171::nft_transfer_call`] promise chain. + fn nft_resolve_transfer( + &mut self, + previous_owner_id: AccountId, + receiver_id: AccountId, + token_id: TokenId, + approved_account_ids: Option>, + ) -> bool; +} + +/// A contract that may be the recipient of an `nft_transfer_call` function +/// call. +#[ext_contract(ext_nep171_receiver)] +pub trait Nep171Receiver { + /// Function that is called in an `nft_transfer_call` promise chain. + /// Performs some action after receiving a non-fungible token. + /// + /// Returns `true` if token should be returned to `sender_id`. + fn nft_on_transfer( + &mut self, + sender_id: AccountId, + previous_owner_id: AccountId, + token_id: TokenId, + msg: String, + ) -> PromiseOrValue; +} diff --git a/src/standard/nep171.rs b/src/standard/nep171/mod.rs similarity index 65% rename from src/standard/nep171.rs rename to src/standard/nep171/mod.rs index 745a3d1..37abc45 100644 --- a/src/standard/nep171.rs +++ b/src/standard/nep171/mod.rs @@ -2,18 +2,26 @@ //! //! Reference: +use std::error::Error; + use near_sdk::{ - borsh::{self, BorshDeserialize, BorshSerialize}, + borsh::{self, BorshSerialize}, + serde::{Deserialize, Serialize}, AccountId, BorshStorageKey, Gas, }; use near_sdk_contract_tools_macros::event; -use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::{slot::Slot, DefaultStorageKey}; use super::nep297::Event; +pub mod error; +pub mod event; +// separate module with re-export because ext_contract doesn't play well with #![warn(missing_docs)] +mod ext; +pub use ext::*; + /// Minimum required gas for [`Nep171Resolver::nft_resolve_transfer`] call in promise chain during [`Nep171::nft_transfer_call`]. pub const GAS_FOR_RESOLVE_TRANSFER: Gas = Gas(5_000_000_000_000); /// Minimum gas required to execute the main body of [`Nep171::nft_transfer_call`] + gas for [`Nep171Resolver::nft_resolve_transfer`]. @@ -49,141 +57,11 @@ pub enum Nep171Event { ContractMetadataUpdate(Vec), } -/// Event log metadata & associated structures. -pub mod event { - use near_sdk::AccountId; - use serde::Serialize; - - /// Tokens minted to a single owner. - #[derive(Serialize, Debug, Clone)] - pub struct NftMintLog { - /// To whom were the new tokens minted? - pub owner_id: AccountId, - /// Which tokens were minted? - pub token_ids: Vec, - /// Additional mint information. - #[serde(skip_serializing_if = "Option::is_none")] - pub memo: Option, - } - - /// Tokens are transferred from one account to another. - #[derive(Serialize, Debug, Clone)] - pub struct NftTransferLog { - /// NEP-178 authorized account ID. - #[serde(skip_serializing_if = "Option::is_none")] - pub authorized_id: Option, - /// Account ID of the previous owner. - pub old_owner_id: AccountId, - /// Account ID of the new owner. - pub new_owner_id: AccountId, - /// IDs of the transferred tokens. - pub token_ids: Vec, - /// Additional transfer information. - #[serde(skip_serializing_if = "Option::is_none")] - pub memo: Option, - } - - /// Tokens are burned from a single holder. - #[derive(Serialize, Debug, Clone)] - pub struct NftBurnLog { - /// What is the ID of the account from which the tokens were burned? - pub owner_id: AccountId, - /// IDs of the burned tokens. - pub token_ids: Vec, - /// NEP-178 authorized account ID. - #[serde(skip_serializing_if = "Option::is_none")] - pub authorized_id: Option, - /// Additional burn information. - #[serde(skip_serializing_if = "Option::is_none")] - pub memo: Option, - } - - /// Token metadata update. - #[derive(Serialize, Debug, Clone)] - pub struct NftMetadataUpdateLog { - /// IDs of the updated tokens. - pub token_ids: Vec, - /// Additional update information. - #[serde(skip_serializing_if = "Option::is_none")] - pub memo: Option, - } - - /// Contract metadata update. - #[derive(Serialize, Debug, Clone)] - pub struct NftContractMetadataUpdateLog { - /// Additional update information. - #[serde(skip_serializing_if = "Option::is_none")] - pub memo: Option, - } -} - #[derive(BorshSerialize, BorshStorageKey)] enum StorageKey<'a> { TokenOwner(&'a str), } -/// Potential errors produced by various token manipulations. -pub mod error { - use near_sdk::AccountId; - use thiserror::Error; - - use super::TokenId; - - /// Occurs when trying to create a token ID that already exists. - /// Overwriting pre-existing token IDs is not allowed. - #[derive(Error, Clone, Debug)] - #[error("Token `{token_id}` already exists")] - pub struct TokenAlreadyExistsError { - /// The conflicting token ID. - pub token_id: TokenId, - } - - /// When attempting to interact with a non-existent token ID. - #[derive(Error, Clone, Debug)] - #[error("Token `{token_id}` does not exist")] - pub struct TokenDoesNotExistError { - /// The invalid token ID. - pub token_id: TokenId, - } - - /// Occurs when performing a checked operation that expects a token to be - /// owned by a particular account, but the token is _not_ owned by that - /// account. - #[derive(Error, Clone, Debug)] - #[error( - "Token `{token_id}` is owned by `{actual_owner_id}` instead of expected `{expected_owner_id}`", - )] - pub struct TokenNotOwnedByExpectedOwnerError { - /// The token was supposed to be owned by this account. - pub expected_owner_id: AccountId, - /// The token is actually owned by this account. - pub actual_owner_id: AccountId, - /// The ID of the token in question. - pub token_id: TokenId, - } - - /// Occurs when a particular account is not allowed to transfer a token (e.g. on behalf of another user). See: NEP-178. - #[derive(Error, Clone, Debug)] - #[error("Sender `{sender_id}` does not have permission to transfer token `{token_id}`")] - pub struct SenderNotApprovedError { - /// The unapproved sender. - pub sender_id: AccountId, - /// The ID of the token in question. - pub token_id: TokenId, - } - - /// Occurs when attempting to perform a transfer of a token from one - /// account to the same account. - #[derive(Error, Clone, Debug)] - #[error("Receiver must be different from current owner `{current_owner_id}` to transfer token `{token_id}`")] - pub struct TokenReceiverIsCurrentOwnerError { - /// The account ID of current owner of the token. - pub current_owner_id: AccountId, - /// The ID of the token in question. - pub token_id: TokenId, - } -} - /// Potential errors encountered when performing a burn operation. #[derive(Error, Clone, Debug)] pub enum Nep171BurnError { @@ -292,6 +170,11 @@ pub trait Nep171Controller { /// Returns the owner of a token, if it exists. fn token_owner(&self, token_id: &TokenId) -> Option; + + /// Loads the metadata associated with a token. + fn load_token>(&self, token_id: &TokenId) -> Option + where + Self: Sized; } /// Transfer metadata generic over both types of transfer (`nft_transfer` and @@ -512,94 +395,71 @@ impl Nep171Controller for T { fn token_owner(&self, token_id: &TokenId) -> Option { Self::slot_token_owner(token_id).read() } + + fn load_token>(&self, token_id: &TokenId) -> Option { + let mut metadata = std::collections::HashMap::new(); + L::load(self, token_id, &mut metadata).ok()?; + Some(Token { + token_id: token_id.clone(), + owner_id: self.token_owner(token_id)?, + extensions_metadata: metadata, + }) + } } /// Token information structure. -#[derive( - Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize, BorshSerialize, BorshDeserialize, -)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Token { /// Token ID. pub token_id: TokenId, /// Current owner of the token. pub owner_id: AccountId, + /// Metadata provided by extensions. + #[serde(flatten)] + pub extensions_metadata: std::collections::HashMap, } -impl Token { - /// Load token information from the contract. - pub fn load(contract: &impl Nep171Controller, token_id: TokenId) -> Option { - contract - .token_owner(&token_id) - .map(|owner_id| Self { token_id, owner_id }) - } +/// Trait for NFT extensions to load token metadata. +pub trait LoadTokenMetadata { + /// Load token metadata into `metadata`. + fn load( + contract: &C, + token_id: &TokenId, + metadata: &mut std::collections::HashMap, + ) -> Result<(), Box>; } -// separate module with re-export because ext_contract doesn't play well with #![warn(missing_docs)] -mod ext { - #![allow(missing_docs)] - - use std::collections::HashMap; - - use near_sdk::{ext_contract, AccountId, PromiseOrValue}; - - use super::{Token, TokenId}; - - /// Interface of contracts that implement NEP-171. - #[ext_contract(ext_nep171)] - pub trait Nep171 { - /// Transfer a token. - fn nft_transfer( - &mut self, - receiver_id: AccountId, - token_id: TokenId, - approval_id: Option, - memo: Option, - ); - - /// Transfer a token, and call [`Nep171Receiver::nft_on_transfer`] on the receiving account. - fn nft_transfer_call( - &mut self, - receiver_id: AccountId, - token_id: TokenId, - approval_id: Option, - memo: Option, - msg: String, - ) -> PromiseOrValue; - - /// Get individual token information. - fn nft_token(&self, token_id: TokenId) -> Option; +impl LoadTokenMetadata for () { + fn load( + _contract: &C, + _token_id: &TokenId, + _metadata: &mut std::collections::HashMap, + ) -> Result<(), Box> { + Ok(()) } +} - /// Original token contract follow-up to [`Nep171::nft_transfer_call`]. - #[ext_contract(ext_nep171_resolver)] - pub trait Nep171Resolver { - /// Final method call on the original token contract during an - /// [`Nep171::nft_transfer_call`] promise chain. - fn nft_resolve_transfer( - &mut self, - previous_owner_id: AccountId, - receiver_id: AccountId, - token_id: TokenId, - approved_account_ids: Option>, - ) -> bool; +impl> LoadTokenMetadata for (T,) { + fn load( + contract: &C, + token_id: &TokenId, + metadata: &mut std::collections::HashMap, + ) -> Result<(), Box> { + T::load(contract, token_id, metadata)?; + Ok(()) } +} - /// A contract that may be the recipient of an `nft_transfer_call` function - /// call. - #[ext_contract(ext_nep171_receiver)] - pub trait Nep171Receiver { - /// Function that is called in an `nft_transfer_call` promise chain. - /// Performs some action after receiving a non-fungible token. - /// - /// Returns `true` if token should be returned to `sender_id`. - fn nft_on_transfer( - &mut self, - sender_id: AccountId, - previous_owner_id: AccountId, - token_id: TokenId, - msg: String, - ) -> PromiseOrValue; +impl, U: LoadTokenMetadata> LoadTokenMetadata for (T, U) { + fn load( + contract: &C, + token_id: &TokenId, + metadata: &mut std::collections::HashMap, + ) -> Result<(), Box> { + T::load(contract, token_id, metadata)?; + U::load(contract, token_id, metadata)?; + Ok(()) } } -pub use ext::*; +// further variations are technically unnecessary: just use (T, (U, V)) or ((T, U), V) diff --git a/src/standard/nep177.rs b/src/standard/nep177.rs index 455f70e..973d2a9 100644 --- a/src/standard/nep177.rs +++ b/src/standard/nep177.rs @@ -1,6 +1,8 @@ //! NEP-177 non-fungible token contract metadata implementation. //! //! Reference: +use std::error::Error; + use near_sdk::{ borsh::{self, BorshDeserialize, BorshSerialize}, env, @@ -28,36 +30,6 @@ pub use ext::*; const CONTRACT_METADATA_NOT_INITIALIZED_ERROR: &str = "Contract metadata not initialized"; -/// Non-fungible token with metadata. -#[derive( - Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize, BorshSerialize, BorshDeserialize, -)] -#[serde(crate = "near_sdk::serde")] -pub struct Token { - /// The token ID. - pub token_id: TokenId, - /// The token owner. - pub owner_id: AccountId, - /// The token metadata. - pub metadata: TokenMetadata, -} - -impl Token { - /// Loads token metadata. - pub fn load( - contract: &(impl Nep171Controller + Nep177Controller), - token_id: TokenId, - ) -> Option { - let owner_id = contract.token_owner(&token_id)?; - let metadata = contract.token_metadata(&token_id)?; - Some(Self { - token_id, - owner_id, - metadata, - }) - } -} - /// Non-fungible token contract metadata. #[derive( Serialize, @@ -149,6 +121,29 @@ pub struct TokenMetadata { pub reference_hash: Option, } +/// Error returned when trying to load token metadata that does not exist. +#[derive(Error, Debug)] +#[error("Token metadata does not exist: {0}")] +pub struct TokenMetadataMissingError(pub TokenId); + +impl LoadTokenMetadata for TokenMetadata { + fn load( + contract: &C, + token_id: &TokenId, + metadata: &mut std::collections::HashMap, + ) -> Result<(), Box> { + metadata.insert( + "metadata".to_string(), + near_sdk::serde_json::to_value( + contract + .token_metadata(token_id) + .ok_or_else(|| TokenMetadataMissingError(token_id.to_string()))?, + )?, + ); + Ok(()) + } +} + #[derive(BorshSerialize, BorshStorageKey)] enum StorageKey<'a> { ContractMetadata, diff --git a/workspaces-tests/tests/non_fungible_token.rs b/workspaces-tests/tests/non_fungible_token.rs index cdb6726..4e58892 100644 --- a/workspaces-tests/tests/non_fungible_token.rs +++ b/workspaces-tests/tests/non_fungible_token.rs @@ -90,6 +90,7 @@ async fn create_and_mint() { Some(Token { token_id: "token_0".to_string(), owner_id: alice.id().parse().unwrap(), + extensions_metadata: Default::default(), }), ); assert_eq!( @@ -97,6 +98,7 @@ async fn create_and_mint() { Some(Token { token_id: "token_1".to_string(), owner_id: bob.id().parse().unwrap(), + extensions_metadata: Default::default(), }), ); assert_eq!( @@ -104,6 +106,7 @@ async fn create_and_mint() { Some(Token { token_id: "token_2".to_string(), owner_id: charlie.id().parse().unwrap(), + extensions_metadata: Default::default(), }), ); assert_eq!(token_3, None::); @@ -145,8 +148,8 @@ async fn create_and_mint_with_metadata() { nft_token(&contract, "token_3"), ); - fn token_meta(id: String) -> TokenMetadata { - TokenMetadata { + fn token_meta(id: String) -> near_sdk::serde_json::Value { + near_sdk::serde_json::to_value(TokenMetadata { title: Some(id), description: Some("description".to_string()), media: None, @@ -159,32 +162,36 @@ async fn create_and_mint_with_metadata() { extra: None, reference: None, reference_hash: None, - } + }) + .unwrap() } // Verify minted tokens assert_eq!( token_0, - Some(nep177::Token { + Some(Token { token_id: "token_0".to_string(), owner_id: alice.id().parse().unwrap(), - metadata: token_meta("token_0".to_string()), + extensions_metadata: [("metadata".to_string(), token_meta("token_0".to_string()))] + .into(), }), ); assert_eq!( token_1, - Some(nep177::Token { + Some(Token { token_id: "token_1".to_string(), owner_id: bob.id().parse().unwrap(), - metadata: token_meta("token_1".to_string()), + extensions_metadata: [("metadata".to_string(), token_meta("token_1".to_string()))] + .into(), }), ); assert_eq!( token_2, - Some(nep177::Token { + Some(Token { token_id: "token_2".to_string(), owner_id: charlie.id().parse().unwrap(), - metadata: token_meta("token_2".to_string()), + extensions_metadata: [("metadata".to_string(), token_meta("token_2".to_string()))] + .into(), }), ); assert_eq!(token_3, None::); @@ -237,6 +244,7 @@ async fn transfer_success() { Some(Token { token_id: "token_0".to_string(), owner_id: bob.id().parse().unwrap(), + extensions_metadata: Default::default(), }), ); assert_eq!( @@ -244,6 +252,7 @@ async fn transfer_success() { Some(Token { token_id: "token_1".to_string(), owner_id: bob.id().parse().unwrap(), + extensions_metadata: Default::default(), }), ); assert_eq!( @@ -251,6 +260,7 @@ async fn transfer_success() { Some(Token { token_id: "token_2".to_string(), owner_id: charlie.id().parse().unwrap(), + extensions_metadata: Default::default(), }), ); } @@ -419,6 +429,7 @@ async fn transfer_call_success() { Some(Token { token_id: "token_0".to_string(), owner_id: bob.id().parse().unwrap(), + extensions_metadata: Default::default(), }), ); } @@ -487,6 +498,7 @@ async fn transfer_call_return_success() { Some(Token { token_id: "token_0".to_string(), owner_id: alice.id().parse().unwrap(), + extensions_metadata: Default::default(), }), ); } @@ -555,6 +567,7 @@ async fn transfer_call_receiver_panic() { Some(Token { token_id: "token_0".to_string(), owner_id: alice.id().parse().unwrap(), + extensions_metadata: Default::default(), }), ); } @@ -628,6 +641,7 @@ async fn transfer_call_receiver_send_return() { Some(Token { token_id: "token_0".to_string(), owner_id: charlie.id().parse().unwrap(), + extensions_metadata: Default::default(), }), ); } From e5542beaf6332c94e14230188f1484c1dd748066 Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Tue, 1 Aug 2023 01:27:41 +0900 Subject: [PATCH 20/34] feat: nep178 --- macros/src/lib.rs | 4 +- macros/src/standard/nep171.rs | 76 ++-- macros/src/standard/non_fungible_token.rs | 6 + src/lib.rs | 3 + src/standard/mod.rs | 1 + src/standard/nep171/ext.rs | 4 +- src/standard/nep171/mod.rs | 93 +++-- src/standard/nep177.rs | 2 +- src/standard/nep178.rs | 339 ++++++++++++++++++ tests/macros/standard/nep171.rs | 16 +- .../src/bin/non_fungible_token.rs | 4 +- 11 files changed, 477 insertions(+), 71 deletions(-) create mode 100644 src/standard/nep178.rs diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 3e78ad3..91a3bde 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -156,9 +156,7 @@ pub fn derive_fungible_token(input: TokenStream) -> TokenStream { /// /// Fields: /// - `no_hooks`: Flag. Removes the requirement for the contract to implement [`near_sdk_contract_tools::standard::nep171::Nep171Hooks`]. -/// - `token_type`: specify the type of the token returned by `nft_token`. -/// - `token_loader`: A function that takes a reference to the contract, and -/// the desired token ID, and returns a value of type `token_type`. +/// - `token_type`: specify the token metadata loading extensions invoked by `nft_token`. #[proc_macro_derive(Nep171, attributes(nep171))] pub fn derive_nep171(input: TokenStream) -> TokenStream { make_derive(input, standard::nep171::expand) diff --git a/macros/src/standard/nep171.rs b/macros/src/standard/nep171.rs index 0baa05a..65177ab 100644 --- a/macros/src/standard/nep171.rs +++ b/macros/src/standard/nep171.rs @@ -10,6 +10,8 @@ use syn::Expr; pub struct Nep171Meta { pub storage_key: Option, pub no_hooks: Flag, + pub extension_hooks: Option, + pub check_external_transfer: Option, pub token_type: Option, pub generics: syn::Generics, @@ -26,6 +28,8 @@ pub fn expand(meta: Nep171Meta) -> Result { let Nep171Meta { storage_key, no_hooks, + extension_hooks, + check_external_transfer, token_type, generics, @@ -43,6 +47,12 @@ pub fn expand(meta: Nep171Meta) -> Result { quote! { () } }); + let check_external_transfer = check_external_transfer + .map(|check_external_transfer| quote! { #check_external_transfer }) + .unwrap_or_else(|| { + quote! { #me::standard::nep171::DefaultCheckExternalTransfer } + }); + let root = storage_key.map(|storage_key| { quote! { fn root() -> #me::slot::Slot<()> { @@ -51,15 +61,37 @@ pub fn expand(meta: Nep171Meta) -> Result { } }); + let extension_hooks_type = extension_hooks + .map(|extension_hooks| quote! { #extension_hooks }) + .unwrap_or_else(|| { + quote! { () } + }); + + let self_hooks_type = no_hooks + .is_present() + .not() + .then(|| { + quote! { + Self + } + }) + .unwrap_or_else(|| { + quote! { + () + } + }); + + let hooks_type = quote! { (#self_hooks_type, #extension_hooks_type) }; + let before_nft_transfer = no_hooks.is_present().not().then(|| { quote! { - let hook_state = >::before_nft_transfer(&self, &transfer); + let hook_state = <#hooks_type as #me::standard::nep171::Nep171Hook::<_, _>>::before_nft_transfer(&self, &transfer); } }); let after_nft_transfer = no_hooks.is_present().not().then(|| { quote! { - >::after_nft_transfer(self, &transfer, hook_state); + <#hooks_type as #me::standard::nep171::Nep171Hook::<_, _>>::after_nft_transfer(self, &transfer, hook_state); } }); @@ -146,16 +178,11 @@ pub fn expand(meta: Nep171Meta) -> Result { &mut self, receiver_id: #near_sdk::AccountId, token_id: #me::standard::nep171::TokenId, - approval_id: Option, + approval_id: Option, memo: Option, ) { use #me::standard::nep171::*; - #near_sdk::require!( - approval_id.is_none(), - APPROVAL_MANAGEMENT_NOT_SUPPORTED_MESSAGE, - ); - #near_sdk::assert_one_yocto(); let sender_id = #near_sdk::env::predecessor_account_id(); @@ -167,22 +194,15 @@ pub fn expand(meta: Nep171Meta) -> Result { owner_id: &sender_id, sender_id: &sender_id, receiver_id: &receiver_id, - approval_id: None, + approval_id: approval_id.clone(), memo: memo.as_deref(), msg: None, }; #before_nft_transfer - Nep171Controller::transfer( - self, - &token_ids, - sender_id.clone(), - sender_id.clone(), - receiver_id.clone(), - memo.clone(), - ) - .unwrap_or_else(|e| #near_sdk::env::panic_str(&e.to_string())); + Nep171Controller::external_transfer::<#check_external_transfer>(self, &transfer) + .unwrap_or_else(|e| #near_sdk::env::panic_str(&e.to_string())); #after_nft_transfer } @@ -192,17 +212,12 @@ pub fn expand(meta: Nep171Meta) -> Result { &mut self, receiver_id: #near_sdk::AccountId, token_id: #me::standard::nep171::TokenId, - approval_id: Option, + approval_id: Option, memo: Option, msg: String, ) -> #near_sdk::PromiseOrValue { use #me::standard::nep171::*; - #near_sdk::require!( - approval_id.is_none(), - APPROVAL_MANAGEMENT_NOT_SUPPORTED_MESSAGE, - ); - #near_sdk::assert_one_yocto(); #near_sdk::require!( @@ -219,22 +234,15 @@ pub fn expand(meta: Nep171Meta) -> Result { owner_id: &sender_id, sender_id: &sender_id, receiver_id: &receiver_id, - approval_id: None, + approval_id: approval_id.clone(), memo: memo.as_deref(), msg: Some(&msg), }; #before_nft_transfer - Nep171Controller::transfer( - self, - &token_ids, - sender_id.clone(), - sender_id.clone(), - receiver_id.clone(), - memo.clone(), - ) - .unwrap_or_else(|e| #near_sdk::env::panic_str(&e.to_string())); + Nep171Controller::external_transfer::<#check_external_transfer>(self, &transfer) + .unwrap_or_else(|e| #near_sdk::env::panic_str(&e.to_string())); #after_nft_transfer diff --git a/macros/src/standard/non_fungible_token.rs b/macros/src/standard/non_fungible_token.rs index 8cb177b..3983aa1 100644 --- a/macros/src/standard/non_fungible_token.rs +++ b/macros/src/standard/non_fungible_token.rs @@ -11,6 +11,8 @@ pub struct NonFungibleTokenMeta { // NEP-171 fields pub storage_key: Option, pub no_hooks: Flag, + pub extension_hooks: Option, + pub check_external_transfer: Option, // NEP-177 fields pub metadata_storage_key: Option, @@ -30,6 +32,8 @@ pub fn expand(meta: NonFungibleTokenMeta) -> Result let NonFungibleTokenMeta { storage_key, no_hooks, + extension_hooks, + check_external_transfer, metadata_storage_key, @@ -43,6 +47,8 @@ pub fn expand(meta: NonFungibleTokenMeta) -> Result let expand_nep171 = nep171::expand(nep171::Nep171Meta { storage_key, no_hooks, + extension_hooks, + check_external_transfer, token_type: Some(syn::parse_quote! { ( #me::standard::nep177::TokenMetadata ) }), diff --git a/src/lib.rs b/src/lib.rs index becbee6..03efe04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,8 @@ pub enum DefaultStorageKey { Nep171, /// Default storage key for [`standard::nep177::Nep177Controller::root`] Nep177, + /// Default storage key for [`standard::nep178::Nep177Controller::root`] + Nep178, /// Default storage key for [`owner::Owner::root`] Owner, /// Default storage key for [`pause::Pause::root`] @@ -29,6 +31,7 @@ impl IntoStorageKey for DefaultStorageKey { DefaultStorageKey::Nep141 => b"~$141".to_vec(), DefaultStorageKey::Nep171 => b"~$171".to_vec(), DefaultStorageKey::Nep177 => b"~$177".to_vec(), + DefaultStorageKey::Nep178 => b"~$178".to_vec(), DefaultStorageKey::Owner => b"~o".to_vec(), DefaultStorageKey::Pause => b"~p".to_vec(), DefaultStorageKey::Rbac => b"~r".to_vec(), diff --git a/src/standard/mod.rs b/src/standard/mod.rs index 9df090d..69cd9b2 100644 --- a/src/standard/mod.rs +++ b/src/standard/mod.rs @@ -4,4 +4,5 @@ pub mod nep141; pub mod nep148; pub mod nep171; pub mod nep177; +pub mod nep178; pub mod nep297; diff --git a/src/standard/nep171/ext.rs b/src/standard/nep171/ext.rs index 9659a52..c1b4dc2 100644 --- a/src/standard/nep171/ext.rs +++ b/src/standard/nep171/ext.rs @@ -14,7 +14,7 @@ pub trait Nep171 { &mut self, receiver_id: AccountId, token_id: TokenId, - approval_id: Option, + approval_id: Option, memo: Option, ); @@ -23,7 +23,7 @@ pub trait Nep171 { &mut self, receiver_id: AccountId, token_id: TokenId, - approval_id: Option, + approval_id: Option, memo: Option, msg: String, ) -> PromiseOrValue; diff --git a/src/standard/nep171/mod.rs b/src/standard/nep171/mod.rs index 37abc45..87bd6fb 100644 --- a/src/standard/nep171/mod.rs +++ b/src/standard/nep171/mod.rs @@ -28,9 +28,6 @@ pub const GAS_FOR_RESOLVE_TRANSFER: Gas = Gas(5_000_000_000_000); pub const GAS_FOR_NFT_TRANSFER_CALL: Gas = Gas(25_000_000_000_000 + GAS_FOR_RESOLVE_TRANSFER.0); /// Error message when insufficient gas is attached to function calls with a minimum attached gas requirement (i.e. those that produce a promise chain, perform cross-contract calls). pub const INSUFFICIENT_GAS_MESSAGE: &str = "More gas is required"; -/// Error message when the NEP-171 implementation does not also implement NEP-178. -pub const APPROVAL_MANAGEMENT_NOT_SUPPORTED_MESSAGE: &str = - "NEP-178: Approval Management is not supported"; /// NFT token IDs. pub type TokenId = String; @@ -88,6 +85,8 @@ pub enum Nep171TransferError { #[error(transparent)] TokenDoesNotExist(#[from] error::TokenDoesNotExistError), /// The token could not be transferred because the sender is not allowed to perform transfers of this token on behalf of its current owner. See: NEP-178. + /// + /// NOTE: If you only implement NEP-171, approval IDs will _not work_, and this error will always be returned whenever the sender is not the current owner. #[error(transparent)] SenderNotApproved(#[from] error::SenderNotApprovedError), /// The token could not be transferred because the token is being sent to the account that currently owns it. Reflexive transfers are not allowed. @@ -114,14 +113,12 @@ pub trait Nep171ControllerInternal { /// Non-public controller interface for NEP-171 implementations. pub trait Nep171Controller { /// Transfer a token from `sender_id` to `receiver_id`. Checks that the transfer is valid using [`Nep171Controller::check_transfer`] before performing the transfer. - fn transfer( + fn external_transfer>( &mut self, - token_ids: &[TokenId], - current_owner_id: AccountId, - sender_id: AccountId, - receiver_id: AccountId, - memo: Option, - ) -> Result<(), Nep171TransferError>; + transfer: &Nep171Transfer, + ) -> Result<(), Nep171TransferError> + where + Self: Sized; /// Check if a token transfer is valid without actually performing it. fn check_transfer( @@ -188,7 +185,7 @@ pub struct Nep171Transfer<'a> { /// Receiving account ID. pub receiver_id: &'a AccountId, /// Optional approval ID. - pub approval_id: Option, + pub approval_id: Option, /// Token ID. pub token_id: &'a TokenId, /// Optional memo string. @@ -197,40 +194,92 @@ pub struct Nep171Transfer<'a> { pub msg: Option<&'a str>, } +/// Different ways of checking if a transfer is valid. +pub trait CheckExternalTransfer { + /// Checks if a transfer is valid. + fn check_external_transfer( + contract: &C, + transfer: &Nep171Transfer, + ) -> Result<(), Nep171TransferError>; +} + +pub struct DefaultCheckExternalTransfer; + +impl CheckExternalTransfer for DefaultCheckExternalTransfer { + fn check_external_transfer( + contract: &T, + transfer: &Nep171Transfer, + ) -> Result<(), Nep171TransferError> { + contract.check_transfer( + &[transfer.token_id.to_string()], + transfer.owner_id, + transfer.sender_id, + transfer.receiver_id, + ) + } +} + /// Contracts may implement this trait to inject code into NEP-171 functions. /// /// `T` is an optional value for passing state between different lifecycle /// hooks. This may be useful for charging callers for storage usage, for /// example. -pub trait Nep171Hook { +pub trait Nep171Hook { + // TODO: Switch order of C, S generics /// Executed before a token transfer is conducted. /// /// May return an optional state value which will be passed along to the /// following `after_transfer`. /// /// MUST NOT PANIC. - fn before_nft_transfer(&self, transfer: &Nep171Transfer) -> T; + fn before_nft_transfer(contract: &C, transfer: &Nep171Transfer) -> S; /// Executed after a token transfer is conducted. /// /// Receives the state value returned by `before_transfer`. /// /// MUST NOT PANIC. - fn after_nft_transfer(&mut self, transfer: &Nep171Transfer, state: T); + fn after_nft_transfer(contract: &mut C, transfer: &Nep171Transfer, state: S); +} + +impl Nep171Hook for () { + fn before_nft_transfer(_contract: &C, _transfer: &Nep171Transfer) {} + + fn after_nft_transfer(_contract: &mut C, _transfer: &Nep171Transfer, _state: ()) {} +} + +impl Nep171Hook for (Handl0, Handl1) +where + Handl0: Nep171Hook, + Handl1: Nep171Hook, +{ + fn before_nft_transfer(contract: &Cont, transfer: &Nep171Transfer) -> (Stat0, Stat1) { + ( + Handl0::before_nft_transfer(contract, transfer), + Handl1::before_nft_transfer(contract, transfer), + ) + } + + fn after_nft_transfer(contract: &mut Cont, transfer: &Nep171Transfer, state: (Stat0, Stat1)) { + Handl0::after_nft_transfer(contract, transfer, state.0); + Handl1::after_nft_transfer(contract, transfer, state.1); + } } impl Nep171Controller for T { - fn transfer( + fn external_transfer>( &mut self, - token_ids: &[TokenId], - current_owner_id: AccountId, - sender_id: AccountId, - receiver_id: AccountId, - memo: Option, + transfer: &Nep171Transfer, ) -> Result<(), Nep171TransferError> { - match self.check_transfer(token_ids, ¤t_owner_id, &sender_id, &receiver_id) { + match Check::check_external_transfer(self, transfer) { Ok(()) => { - self.transfer_unchecked(token_ids, current_owner_id, sender_id, receiver_id, memo); + self.transfer_unchecked( + &[transfer.token_id.to_string()], + transfer.owner_id.clone(), + transfer.sender_id.clone(), + transfer.receiver_id.clone(), + transfer.memo.map(ToString::to_string), + ); Ok(()) } e => e, diff --git a/src/standard/nep177.rs b/src/standard/nep177.rs index 973d2a9..310f0b5 100644 --- a/src/standard/nep177.rs +++ b/src/standard/nep177.rs @@ -284,7 +284,7 @@ mod ext { use super::*; - #[near_sdk::ext_contract(ext_nep171)] + #[near_sdk::ext_contract(ext_nep177)] pub trait Nep177 { fn nft_metadata(&self) -> ContractMetadata; } diff --git a/src/standard/nep178.rs b/src/standard/nep178.rs new file mode 100644 index 0000000..50c18ad --- /dev/null +++ b/src/standard/nep178.rs @@ -0,0 +1,339 @@ +//! NEP-178 non-fungible token approval management implementation. +//! +//! Reference: +use std::{collections::HashMap, error::Error}; + +use near_sdk::{ + borsh::{self, BorshDeserialize, BorshSerialize}, + serde::*, + store::UnorderedMap, + AccountId, BorshStorageKey, +}; +use thiserror::Error; + +use crate::{slot::Slot, standard::nep171::*, DefaultStorageKey}; + +pub use ext::*; + +pub type ApprovalId = u32; +pub const MAX_APPROVALS: ApprovalId = 32; + +/// Non-fungible token metadata. +#[derive(Serialize, BorshSerialize, BorshDeserialize, Debug)] +#[serde(crate = "near_sdk::serde")] +pub struct TokenApprovals { + #[serde(skip)] + pub next_approval_id: ApprovalId, + + #[serde(flatten, serialize_with = "to_map")] + /// The list of approved accounts. + pub accounts: UnorderedMap, +} + +fn to_map( + accounts: &UnorderedMap, + s: S, +) -> Result { + s.collect_map(accounts.iter()) +} + +impl LoadTokenMetadata for TokenApprovals { + fn load( + contract: &C, + token_id: &TokenId, + metadata: &mut std::collections::HashMap, + ) -> Result<(), Box> { + metadata.insert( + "approved_account_ids".to_string(), + near_sdk::serde_json::to_value(contract.get_approvals_for(token_id))?, + ); + Ok(()) + } +} + +impl Nep171Hook for TokenApprovals { + fn before_nft_transfer(contract: &C, transfer: &Nep171Transfer) -> () { + todo!() + } + + fn after_nft_transfer(contract: &mut C, transfer: &Nep171Transfer, state: ()) {} +} + +impl CheckExternalTransfer for TokenApprovals { + fn check_external_transfer( + contract: &C, + transfer: &Nep171Transfer, + ) -> Result<(), Nep171TransferError> { + let normal_check = contract.check_transfer( + &[transfer.token_id.to_string()], + transfer.owner_id, + transfer.sender_id, + transfer.receiver_id, + ); + + match normal_check { + Ok(()) => Ok(()), + Err(e @ Nep171TransferError::SenderNotApproved(_)) => { + let approval_id = if let Some(id) = transfer.approval_id { + id + } else { + return Err(e); + }; + + let saved_approvals = contract.get_approvals_for(transfer.token_id); + + if saved_approvals.get(transfer.sender_id) == Some(&approval_id) { + Ok(()) + } else { + Err(e) + } + } + e => e, + } + } +} + +#[derive(BorshSerialize, BorshStorageKey)] +enum StorageKey<'a> { + TokenApprovals(&'a TokenId), + TokenApprovalsUnorderedMap(&'a TokenId), +} + +/// Internal functions for [`Nep178Controller`]. +pub trait Nep178ControllerInternal { + /// Storage root. + fn root() -> Slot<()> { + Slot::root(DefaultStorageKey::Nep178) + } + + /// Storage slot for token approvals. + fn slot_token_approvals(token_id: &TokenId) -> Slot { + Self::root().field(StorageKey::TokenApprovals(token_id)) + } + + /// Storage slot for token approvals `UnorderedMap`. + fn slot_token_approvals_unordered_map( + token_id: &TokenId, + ) -> Slot> { + Self::root().field(StorageKey::TokenApprovalsUnorderedMap(token_id)) + } +} + +#[derive(Error, Debug)] +pub enum Nep178ApproveError { + #[error("Account {account_id} is already approved for token {token_id}.")] + AccountAlreadyApproved { + token_id: TokenId, + account_id: AccountId, + }, + #[error( + "Too many approvals for token {token_id}, maximum is {}.", + MAX_APPROVALS + )] + TooManyApprovals { token_id: TokenId }, +} + +#[derive(Error, Debug)] +pub enum Nep178RevokeError { + #[error("Account {account_id} is not approved for token {token_id}")] + AccountNotApproved { + token_id: TokenId, + account_id: AccountId, + }, +} + +/// Functions for managing non-fungible tokens with attached metadata, NEP-178. +pub trait Nep178Controller { + /// Approve a token for transfer by a delegated account. + fn approve( + &mut self, + token_id: &TokenId, + account_id: &AccountId, + ) -> Result; + + /// Approve a token without checking if the account is already approved or + /// if it exceeds the maximum number of approvals. + fn approve_unchecked(&mut self, token_id: &TokenId, account_id: &AccountId) -> ApprovalId; + + /// Revoke approval for an account to transfer token. + fn revoke( + &mut self, + token_id: &TokenId, + account_id: &AccountId, + ) -> Result<(), Nep178RevokeError>; + + /// Revoke approval for an account to transfer token without checking if + /// the account is approved. + fn revoke_unchecked(&mut self, token_id: &TokenId, account_id: &AccountId); + + /// Revoke all approvals for a token. + fn revoke_all(&mut self, token_id: &TokenId); + + /// Get the approval ID for an account, if it is approved for a token. + fn get_approval_id_for(&self, token_id: &TokenId, account_id: &AccountId) + -> Option; + + /// Get the approvals for a token. + fn get_approvals_for(&self, token_id: &TokenId) -> HashMap; +} + +impl Nep178Controller for T { + fn approve_unchecked(&mut self, token_id: &TokenId, account_id: &AccountId) -> ApprovalId { + let mut slot = Self::slot_token_approvals(token_id); + let mut approvals = slot.read().unwrap_or_else(|| TokenApprovals { + next_approval_id: 0, + accounts: UnorderedMap::new(Self::slot_token_approvals_unordered_map(token_id)), + }); + let approval_id = approvals.next_approval_id; + approvals.accounts.insert(account_id.clone(), approval_id); + approvals.next_approval_id += 1; // overflow unrealistic + slot.write(&approvals); + + approval_id + } + + fn approve( + &mut self, + token_id: &TokenId, + account_id: &AccountId, + ) -> Result { + let mut slot = Self::slot_token_approvals(token_id); + let mut approvals = slot.read().unwrap_or_else(|| TokenApprovals { + next_approval_id: 0, + accounts: UnorderedMap::new(Self::slot_token_approvals_unordered_map(token_id)), + }); + + if approvals.accounts.len() >= MAX_APPROVALS { + return Err(Nep178ApproveError::TooManyApprovals { + token_id: token_id.clone(), + }); + } + + let approval_id = approvals.next_approval_id; + if approvals + .accounts + .insert(account_id.clone(), approval_id) + .is_some() + { + return Err(Nep178ApproveError::AccountAlreadyApproved { + token_id: token_id.clone(), + account_id: account_id.clone(), + }); + } + approvals.next_approval_id += 1; // overflow unrealistic + slot.write(&approvals); + + Ok(approval_id) + } + + fn revoke_unchecked(&mut self, token_id: &TokenId, account_id: &AccountId) { + let mut slot = Self::slot_token_approvals(token_id); + let mut approvals = match slot.read() { + Some(approvals) => approvals, + None => return, + }; + + let old = approvals.accounts.remove(account_id); + + if old.is_some() { + slot.write(&approvals); + } + } + + fn revoke( + &mut self, + token_id: &TokenId, + account_id: &AccountId, + ) -> Result<(), Nep178RevokeError> { + let mut slot = Self::slot_token_approvals(token_id); + let mut approvals = slot + .read() + .ok_or_else(|| Nep178RevokeError::AccountNotApproved { + token_id: token_id.clone(), + account_id: account_id.clone(), + })?; + + approvals + .accounts + .remove(account_id) + .ok_or(Nep178RevokeError::AccountNotApproved { + token_id: token_id.clone(), + account_id: account_id.clone(), + })?; + + slot.write(&approvals); + + Ok(()) + } + + fn revoke_all(&mut self, token_id: &TokenId) { + let mut slot = Self::slot_token_approvals(token_id); + let approvals = match slot.read() { + Some(approvals) => approvals, + None => return, + }; + + slot.write(&TokenApprovals { + next_approval_id: approvals.next_approval_id, + accounts: UnorderedMap::new(Self::slot_token_approvals_unordered_map(token_id)), + }); + } + + fn get_approval_id_for( + &self, + token_id: &TokenId, + account_id: &AccountId, + ) -> Option { + let slot = Self::slot_token_approvals(token_id); + let approvals = match slot.read() { + Some(approvals) => approvals, + None => return None, + }; + + approvals.accounts.get(account_id).copied() + } + + fn get_approvals_for(&self, token_id: &TokenId) -> HashMap { + let slot = Self::slot_token_approvals(token_id); + let approvals = match slot.read() { + Some(approvals) => approvals, + None => return HashMap::default(), + }; + + approvals + .accounts + .into_iter() + .map(|(k, v)| (k.clone(), *v)) + .collect() + } +} + +// separate module with re-export because ext_contract doesn't play well with #![warn(missing_docs)] +mod ext { + #![allow(missing_docs)] + + use near_sdk::PromiseOrValue; + + use super::*; + + #[near_sdk::ext_contract(ext_nep178)] + pub trait Nep178 { + fn nft_approve( + &mut self, + token_id: TokenId, + account_id: AccountId, + msg: Option, + ) -> PromiseOrValue<()>; + + fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId); + + fn nft_revoke_all(&mut self, token_id: TokenId); + + fn nft_is_approved( + &self, + token_id: TokenId, + approved_account_id: AccountId, + approval_id: Option, + ) -> bool; + } +} diff --git a/tests/macros/standard/nep171.rs b/tests/macros/standard/nep171.rs index 0f32107..21425aa 100644 --- a/tests/macros/standard/nep171.rs +++ b/tests/macros/standard/nep171.rs @@ -34,21 +34,23 @@ struct NonFungibleToken { pub after_nft_transfer_balance_record: store::Vector>, } -impl Nep171Hook> for NonFungibleToken { - fn before_nft_transfer(&self, transfer: &Nep171Transfer) -> Option { - let token = Nep171::nft_token(self, transfer.token_id.clone()); +impl Nep171Hook> for NonFungibleToken { + fn before_nft_transfer(contract: &Self, transfer: &Nep171Transfer) -> Option { + let token = Nep171::nft_token(contract, transfer.token_id.clone()); token.map(Into::into) } fn after_nft_transfer( - &mut self, + contract: &mut Self, transfer: &Nep171Transfer, before_nft_transfer: Option, ) { - let token = Nep171::nft_token(self, transfer.token_id.clone()); - self.before_nft_transfer_balance_record + let token = Nep171::nft_token(contract, transfer.token_id.clone()); + contract + .before_nft_transfer_balance_record .push(before_nft_transfer); - self.after_nft_transfer_balance_record + contract + .after_nft_transfer_balance_record .push(token.map(Into::into)); } } diff --git a/workspaces-tests/src/bin/non_fungible_token.rs b/workspaces-tests/src/bin/non_fungible_token.rs index c15efe0..234f27a 100644 --- a/workspaces-tests/src/bin/non_fungible_token.rs +++ b/workspaces-tests/src/bin/non_fungible_token.rs @@ -14,11 +14,11 @@ use near_sdk_contract_tools::{standard::nep171::*, Nep171}; pub struct Contract {} impl Nep171Hook for Contract { - fn before_nft_transfer(&self, transfer: &Nep171Transfer) { + fn before_nft_transfer(_contract: &Self, transfer: &Nep171Transfer) { log!("before_nft_transfer({})", transfer.token_id); } - fn after_nft_transfer(&mut self, transfer: &Nep171Transfer, _state: ()) { + fn after_nft_transfer(_contract: &mut Self, transfer: &Nep171Transfer, _state: ()) { log!("after_nft_transfer({})", transfer.token_id); } } From 7d59168bb12efdf578013dff283e665b8b9f631f Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Fri, 11 Aug 2023 17:57:47 +0900 Subject: [PATCH 21/34] chore: upgrade tests & organize libs --- .github/workflows/rust.yml | 8 +- Cargo.toml | 24 ++-- rust-toolchain.toml | 2 +- src/standard/nep171/mod.rs | 10 +- src/standard/nep178.rs | 2 +- tests/macros/standard/nep171.rs | 2 +- workspaces-tests-utils/Cargo.toml | 9 ++ workspaces-tests-utils/src/lib.rs | 57 ++++++++ workspaces-tests/Cargo.toml | 9 +- workspaces-tests/Makefile.toml | 2 +- ...ken_meta.rs => non_fungible_token_full.rs} | 13 +- ..._token.rs => non_fungible_token_nep171.rs} | 0 workspaces-tests/tests/non_fungible_token.rs | 129 ++++++++---------- 13 files changed, 169 insertions(+), 98 deletions(-) create mode 100644 workspaces-tests-utils/Cargo.toml create mode 100644 workspaces-tests-utils/src/lib.rs rename workspaces-tests/src/bin/{non_fungible_token_meta.rs => non_fungible_token_full.rs} (80%) rename workspaces-tests/src/bin/{non_fungible_token.rs => non_fungible_token_nep171.rs} (100%) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 85a99c4..52293d4 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.66 + toolchain: 1.69 components: rustfmt - name: Check formatting run: > @@ -30,7 +30,7 @@ jobs: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.66 + toolchain: 1.69 components: clippy - name: Run linter run: cargo clippy -- -D warnings @@ -41,7 +41,7 @@ jobs: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.66 + toolchain: 1.69 - name: Run unit and integration tests run: cargo test --workspace --exclude workspaces-tests workspaces-test: @@ -51,7 +51,7 @@ jobs: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.66 + toolchain: 1.69 targets: wasm32-unknown-unknown - name: Run workspaces tests run: > diff --git a/Cargo.toml b/Cargo.toml index d0dd7ec..16c0f47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,22 +12,30 @@ name = "near-sdk-contract-tools" repository = "https://github.com/NEARFoundation/near-sdk-contract-tools" version = "1.0.1" -[dependencies] -near-sdk = {version = "4.1.0", default-features = false} -near-sdk-contract-tools-macros = {version = "=1.0.1", path = "./macros"} +[workspace.dependencies] +near-sdk = { version = "4.1.1", default-features = false } +near-sdk-contract-tools-macros = { version = "=1.0.1", path = "./macros" } serde = "1.0.144" serde_json = "1.0.85" thiserror = "1.0.35" +[dependencies] +near-sdk.workspace = true +near-sdk-contract-tools-macros.workspace = true +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true + [dev-dependencies] -near-sdk = {version = "4.1.0", default-features = false, features = ["unit-testing", "legacy"]} +near-sdk = { workspace = true, default-features = false, features = [ + "unit-testing", + "legacy", +] } [features] unstable = ["near-sdk/unstable"] [workspace] -members = [ - ".", - "macros", - "workspaces-tests", +members = [".", "macros", "workspaces-tests", +"workspaces-tests-utils" ] diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 99c6e11..5eca3a9 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.66" +channel = "1.69" # https://github.com/near/nearcore/issues/9143 diff --git a/src/standard/nep171/mod.rs b/src/standard/nep171/mod.rs index 87bd6fb..2c3b233 100644 --- a/src/standard/nep171/mod.rs +++ b/src/standard/nep171/mod.rs @@ -224,7 +224,7 @@ impl CheckExternalTransfer for DefaultCheckExternalTrans /// `T` is an optional value for passing state between different lifecycle /// hooks. This may be useful for charging callers for storage usage, for /// example. -pub trait Nep171Hook { +pub trait Nep171Hook { // TODO: Switch order of C, S generics /// Executed before a token transfer is conducted. /// @@ -242,16 +242,16 @@ pub trait Nep171Hook { fn after_nft_transfer(contract: &mut C, transfer: &Nep171Transfer, state: S); } -impl Nep171Hook for () { +impl Nep171Hook<(), C> for () { fn before_nft_transfer(_contract: &C, _transfer: &Nep171Transfer) {} fn after_nft_transfer(_contract: &mut C, _transfer: &Nep171Transfer, _state: ()) {} } -impl Nep171Hook for (Handl0, Handl1) +impl Nep171Hook<(Stat0, Stat1), Cont> for (Handl0, Handl1) where - Handl0: Nep171Hook, - Handl1: Nep171Hook, + Handl0: Nep171Hook, + Handl1: Nep171Hook, { fn before_nft_transfer(contract: &Cont, transfer: &Nep171Transfer) -> (Stat0, Stat1) { ( diff --git a/src/standard/nep178.rs b/src/standard/nep178.rs index 50c18ad..c7b7daa 100644 --- a/src/standard/nep178.rs +++ b/src/standard/nep178.rs @@ -51,7 +51,7 @@ impl LoadTokenMetadata for TokenApprovals { } } -impl Nep171Hook for TokenApprovals { +impl Nep171Hook<(), C> for TokenApprovals { fn before_nft_transfer(contract: &C, transfer: &Nep171Transfer) -> () { todo!() } diff --git a/tests/macros/standard/nep171.rs b/tests/macros/standard/nep171.rs index 21425aa..d17f284 100644 --- a/tests/macros/standard/nep171.rs +++ b/tests/macros/standard/nep171.rs @@ -34,7 +34,7 @@ struct NonFungibleToken { pub after_nft_transfer_balance_record: store::Vector>, } -impl Nep171Hook> for NonFungibleToken { +impl Nep171Hook> for NonFungibleToken { fn before_nft_transfer(contract: &Self, transfer: &Nep171Transfer) -> Option { let token = Nep171::nft_token(contract, transfer.token_id.clone()); token.map(Into::into) diff --git a/workspaces-tests-utils/Cargo.toml b/workspaces-tests-utils/Cargo.toml new file mode 100644 index 0000000..2304f9f --- /dev/null +++ b/workspaces-tests-utils/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "workspaces-tests-utils" +version = "0.1.0" +edition = "2021" +publish = false + +[target.'cfg(not(windows))'.dependencies] +near-sdk.workspace = true +workspaces = "0.7" diff --git a/workspaces-tests-utils/src/lib.rs b/workspaces-tests-utils/src/lib.rs new file mode 100644 index 0000000..ef49adf --- /dev/null +++ b/workspaces-tests-utils/src/lib.rs @@ -0,0 +1,57 @@ +#![allow(missing_docs)] +#![cfg(not(windows))] + +use near_sdk::{serde::de::DeserializeOwned, serde_json::json}; +use workspaces::{result::ExecutionFinalResult, Account, Contract}; + +pub async fn nft_token(contract: &Contract, token_id: &str) -> Option { + contract + .view("nft_token") + .args_json(json!({ "token_id": token_id })) + .await + .unwrap() + .json::>() + .unwrap() +} + +pub struct Setup { + pub contract: Contract, + pub accounts: Vec, +} + +/// Setup for individual tests +pub async fn setup(wasm: &[u8], num_accounts: usize) -> Setup { + let worker = workspaces::sandbox().await.unwrap(); + + // Initialize contract + let contract = worker.dev_deploy(wasm).await.unwrap(); + contract.call("new").transact().await.unwrap().unwrap(); + + // Initialize user accounts + let mut accounts = vec![]; + for _ in 0..num_accounts { + accounts.push(worker.dev_create_account().await.unwrap()); + } + + Setup { contract, accounts } +} + +/// For dynamic should_panic messages +pub fn expect_execution_error(result: &ExecutionFinalResult, expected_error: impl AsRef) { + let failures = result.failures(); + + assert_eq!(failures.len(), 1); + + let actual_error_string = failures[0] + .clone() + .into_result() + .unwrap_err() + .into_inner() + .unwrap() + .to_string(); + + assert_eq!( + format!("Action #0: ExecutionError(\"{}\")", expected_error.as_ref()), + actual_error_string + ); +} diff --git a/workspaces-tests/Cargo.toml b/workspaces-tests/Cargo.toml index a6aa512..026d2a2 100644 --- a/workspaces-tests/Cargo.toml +++ b/workspaces-tests/Cargo.toml @@ -21,10 +21,10 @@ name = "fungible_token" name = "native_multisig" [[bin]] -name = "non_fungible_token" +name = "non_fungible_token_full" [[bin]] -name = "non_fungible_token_meta" +name = "non_fungible_token_nep171" [[bin]] name = "non_fungible_token_receiver" @@ -57,11 +57,11 @@ name = "upgrade_old_multisig" name = "upgrade_old_raw" [dependencies] -near-sdk = { version = "4.1.1", default-features = false } +near-sdk.workspace = true near-sdk-contract-tools = { path = "../", features = ["unstable"] } strum = "0.24.1" strum_macros = "0.24.3" -thiserror = "1.0.34" +thiserror.workspace = true [dev-dependencies] near-crypto = "0.15.0" @@ -69,3 +69,4 @@ tokio = "1.21.1" [target.'cfg(not(windows))'.dev-dependencies] workspaces = "0.7" +workspaces-tests-utils = { path = "../workspaces-tests-utils" } diff --git a/workspaces-tests/Makefile.toml b/workspaces-tests/Makefile.toml index 3062b23..98919a8 100644 --- a/workspaces-tests/Makefile.toml +++ b/workspaces-tests/Makefile.toml @@ -2,7 +2,7 @@ clear = true script = """ rustup target add wasm32-unknown-unknown -cargo build --target wasm32-unknown-unknown --release --all +cargo build --package workspaces-tests --target wasm32-unknown-unknown --release """ [tasks.test] diff --git a/workspaces-tests/src/bin/non_fungible_token_meta.rs b/workspaces-tests/src/bin/non_fungible_token_full.rs similarity index 80% rename from workspaces-tests/src/bin/non_fungible_token_meta.rs rename to workspaces-tests/src/bin/non_fungible_token_full.rs index 30826f6..eaaa854 100644 --- a/workspaces-tests/src/bin/non_fungible_token_meta.rs +++ b/workspaces-tests/src/bin/non_fungible_token_full.rs @@ -5,7 +5,7 @@ pub fn main() {} use near_sdk::{ borsh::{self, BorshDeserialize, BorshSerialize}, - env, near_bindgen, PanicOnDefault, + env, log, near_bindgen, PanicOnDefault, }; use near_sdk_contract_tools::{ standard::{nep171::*, nep177::*}, @@ -13,10 +13,19 @@ use near_sdk_contract_tools::{ }; #[derive(PanicOnDefault, BorshSerialize, BorshDeserialize, NonFungibleToken)] -#[non_fungible_token(no_hooks)] #[near_bindgen] pub struct Contract {} +impl Nep171Hook for Contract { + fn before_nft_transfer(_contract: &Self, transfer: &Nep171Transfer) { + log!("before_nft_transfer({})", transfer.token_id); + } + + fn after_nft_transfer(_contract: &mut Self, transfer: &Nep171Transfer, _state: ()) { + log!("after_nft_transfer({})", transfer.token_id); + } +} + #[near_bindgen] impl Contract { #[init] diff --git a/workspaces-tests/src/bin/non_fungible_token.rs b/workspaces-tests/src/bin/non_fungible_token_nep171.rs similarity index 100% rename from workspaces-tests/src/bin/non_fungible_token.rs rename to workspaces-tests/src/bin/non_fungible_token_nep171.rs diff --git a/workspaces-tests/tests/non_fungible_token.rs b/workspaces-tests/tests/non_fungible_token.rs index 4e58892..2a0bb4c 100644 --- a/workspaces-tests/tests/non_fungible_token.rs +++ b/workspaces-tests/tests/non_fungible_token.rs @@ -1,54 +1,23 @@ #![cfg(not(windows))] -use near_sdk::{serde::de::DeserializeOwned, serde_json::json}; +use near_sdk::serde_json::json; use near_sdk_contract_tools::standard::{ nep171::{event::NftTransferLog, Nep171Event, Token}, nep177::{self, TokenMetadata}, nep297::Event, }; -use workspaces::{operations::Function, result::ExecutionFinalResult, Account, Contract}; +use workspaces::operations::Function; +use workspaces_tests_utils::{expect_execution_error, nft_token, setup, Setup}; -const WASM: &[u8] = - include_bytes!("../../target/wasm32-unknown-unknown/release/non_fungible_token.wasm"); +const WASM_171_ONLY: &[u8] = + include_bytes!("../../target/wasm32-unknown-unknown/release/non_fungible_token_nep171.wasm"); -const WASM_177: &[u8] = - include_bytes!("../../target/wasm32-unknown-unknown/release/non_fungible_token_meta.wasm"); +const WASM_FULL: &[u8] = + include_bytes!("../../target/wasm32-unknown-unknown/release/non_fungible_token_full.wasm"); const RECEIVER_WASM: &[u8] = include_bytes!("../../target/wasm32-unknown-unknown/release/non_fungible_token_receiver.wasm"); -async fn nft_token(contract: &Contract, token_id: &str) -> Option { - contract - .view("nft_token") - .args_json(json!({ "token_id": token_id })) - .await - .unwrap() - .json::>() - .unwrap() -} - -struct Setup { - pub contract: Contract, - pub accounts: Vec, -} - -/// Setup for individual tests -async fn setup(wasm: &[u8], num_accounts: usize) -> Setup { - let worker = workspaces::sandbox().await.unwrap(); - - // Initialize contract - let contract = worker.dev_deploy(wasm).await.unwrap(); - contract.call("new").transact().await.unwrap().unwrap(); - - // Initialize user accounts - let mut accounts = vec![]; - for _ in 0..num_accounts { - accounts.push(worker.dev_create_account().await.unwrap()); - } - - Setup { contract, accounts } -} - async fn setup_balances( wasm: &[u8], num_accounts: usize, @@ -72,7 +41,7 @@ async fn setup_balances( #[tokio::test] async fn create_and_mint() { let Setup { contract, accounts } = - setup_balances(WASM, 3, |i| vec![format!("token_{i}")]).await; + setup_balances(WASM_171_ONLY, 3, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; let bob = &accounts[1]; let charlie = &accounts[2]; @@ -115,7 +84,7 @@ async fn create_and_mint() { #[tokio::test] async fn create_and_mint_with_metadata() { let Setup { contract, accounts } = - setup_balances(WASM_177, 3, |i| vec![format!("token_{i}")]).await; + setup_balances(WASM_FULL, 3, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; let bob = &accounts[1]; let charlie = &accounts[2]; @@ -200,7 +169,7 @@ async fn create_and_mint_with_metadata() { #[tokio::test] async fn transfer_success() { let Setup { contract, accounts } = - setup_balances(WASM, 3, |i| vec![format!("token_{i}")]).await; + setup_balances(WASM_171_ONLY, 3, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; let bob = &accounts[1]; let charlie = &accounts[2]; @@ -267,9 +236,19 @@ async fn transfer_success() { #[tokio::test] #[should_panic = "Smart contract panicked: Requires attached deposit of exactly 1 yoctoNEAR"] -async fn transfer_fail_no_deposit() { +async fn transfer_fail_no_deposit_full() { + transfer_fail_no_deposit(WASM_FULL).await; +} + +#[tokio::test] +#[should_panic = "Smart contract panicked: Requires attached deposit of exactly 1 yoctoNEAR"] +async fn transfer_fail_no_deposit_171() { + transfer_fail_no_deposit(WASM_171_ONLY).await; +} + +async fn transfer_fail_no_deposit(wasm: &[u8]) { let Setup { contract, accounts } = - setup_balances(WASM, 2, |i| vec![format!("token_{i}")]).await; + setup_balances(wasm, 2, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; let bob = &accounts[1]; @@ -287,9 +266,19 @@ async fn transfer_fail_no_deposit() { #[tokio::test] #[should_panic = "Smart contract panicked: Token `token_5` does not exist"] -async fn transfer_fail_token_dne() { +async fn transfer_fail_token_dne_full() { + transfer_fail_token_dne(WASM_FULL).await; +} + +#[tokio::test] +#[should_panic = "Smart contract panicked: Token `token_5` does not exist"] +async fn transfer_fail_token_dne_171() { + transfer_fail_token_dne(WASM_171_ONLY).await; +} + +async fn transfer_fail_token_dne(wasm: &[u8]) { let Setup { contract, accounts } = - setup_balances(WASM, 2, |i| vec![format!("token_{i}")]).await; + setup_balances(wasm, 2, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; let bob = &accounts[1]; @@ -306,30 +295,19 @@ async fn transfer_fail_token_dne() { .unwrap(); } -/// For dynamic should_panic messages -fn expect_execution_error(result: &ExecutionFinalResult, expected_error: impl AsRef) { - let failures = result.failures(); - - assert_eq!(failures.len(), 1); - - let actual_error_string = failures[0] - .clone() - .into_result() - .unwrap_err() - .into_inner() - .unwrap() - .to_string(); - - assert_eq!( - format!("Action #0: ExecutionError(\"{}\")", expected_error.as_ref()), - actual_error_string - ); +#[tokio::test] +async fn transfer_fail_not_owner_full() { + transfer_fail_not_owner(WASM_FULL).await; } #[tokio::test] -async fn transfer_fail_not_owner() { +async fn transfer_fail_not_owner_171() { + transfer_fail_not_owner(WASM_171_ONLY).await; +} + +async fn transfer_fail_not_owner(wasm: &[u8]) { let Setup { contract, accounts } = - setup_balances(WASM, 3, |i| vec![format!("token_{i}")]).await; + setup_balances(wasm, 3, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; let bob = &accounts[1]; let charlie = &accounts[2]; @@ -356,9 +334,18 @@ async fn transfer_fail_not_owner() { } #[tokio::test] -async fn transfer_fail_reflexive_transfer() { +async fn transfer_fail_reflexive_transfer_full() { + transfer_fail_reflexive_transfer(WASM_FULL).await; +} + +#[tokio::test] +async fn transfer_fail_reflexive_transfer_171() { + transfer_fail_reflexive_transfer(WASM_171_ONLY).await; +} + +async fn transfer_fail_reflexive_transfer(wasm: &[u8]) { let Setup { contract, accounts } = - setup_balances(WASM, 2, |i| vec![format!("token_{i}")]).await; + setup_balances(wasm, 2, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; let result = alice @@ -378,7 +365,7 @@ async fn transfer_fail_reflexive_transfer() { #[tokio::test] async fn transfer_call_success() { let Setup { contract, accounts } = - setup_balances(WASM, 2, |i| vec![format!("token_{i}")]).await; + setup_balances(WASM_171_ONLY, 2, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; let bob = &accounts[1]; @@ -437,7 +424,7 @@ async fn transfer_call_success() { #[tokio::test] async fn transfer_call_return_success() { let Setup { contract, accounts } = - setup_balances(WASM, 2, |i| vec![format!("token_{i}")]).await; + setup_balances(WASM_171_ONLY, 2, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; let bob = &accounts[1]; @@ -506,7 +493,7 @@ async fn transfer_call_return_success() { #[tokio::test] async fn transfer_call_receiver_panic() { let Setup { contract, accounts } = - setup_balances(WASM, 2, |i| vec![format!("token_{i}")]).await; + setup_balances(WASM_171_ONLY, 2, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; let bob = &accounts[1]; @@ -575,7 +562,7 @@ async fn transfer_call_receiver_panic() { #[tokio::test] async fn transfer_call_receiver_send_return() { let Setup { contract, accounts } = - setup_balances(WASM, 3, |i| vec![format!("token_{i}")]).await; + setup_balances(WASM_171_ONLY, 3, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; let bob = &accounts[1]; let charlie = &accounts[2]; From 2907ee018f5c2cbb5d7c0bcc57c8e83396ba4c5d Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Wed, 16 Aug 2023 19:15:34 +0900 Subject: [PATCH 22/34] feat: nep178 macro integration --- macros/src/lib.rs | 5 + macros/src/standard/mod.rs | 1 + macros/src/standard/nep171.rs | 49 ++--- macros/src/standard/nep178.rs | 181 ++++++++++++++++ macros/src/standard/non_fungible_token.rs | 42 ++-- src/standard/nep171/error.rs | 16 +- src/standard/nep171/mod.rs | 205 ++++++++++-------- src/standard/nep177.rs | 1 + src/standard/nep178.rs | 173 +++++++++++---- src/utils.rs | 8 + tests/macros/standard/nep171.rs | 47 +++- .../src/bin/non_fungible_token_full.rs | 60 ++++- workspaces-tests/tests/non_fungible_token.rs | 157 ++++++++++++-- 13 files changed, 743 insertions(+), 202 deletions(-) create mode 100644 macros/src/standard/nep178.rs diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 91a3bde..ada9d40 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -171,6 +171,11 @@ pub fn derive_nep177(input: TokenStream) -> TokenStream { make_derive(input, standard::nep177::expand) } +#[proc_macro_derive(Nep178, attributes(nep178))] +pub fn derive_nep178(input: TokenStream) -> TokenStream { + make_derive(input, standard::nep178::expand) +} + /// Implements all NFT functionality at once, like `#[derive(Nep171, Nep177)]`. #[proc_macro_derive(NonFungibleToken, attributes(non_fungible_token))] pub fn derive_non_fungible_token(input: TokenStream) -> TokenStream { diff --git a/macros/src/standard/mod.rs b/macros/src/standard/mod.rs index 65d6dab..c9e15eb 100644 --- a/macros/src/standard/mod.rs +++ b/macros/src/standard/mod.rs @@ -6,4 +6,5 @@ pub mod nep141; pub mod nep148; pub mod nep171; pub mod nep177; +pub mod nep178; pub mod nep297; diff --git a/macros/src/standard/nep171.rs b/macros/src/standard/nep171.rs index 65177ab..820cea4 100644 --- a/macros/src/standard/nep171.rs +++ b/macros/src/standard/nep171.rs @@ -97,6 +97,8 @@ pub fn expand(meta: Nep171Meta) -> Result { Ok(quote! { impl #imp #me::standard::nep171::Nep171ControllerInternal for #ident #ty #wher { + type CheckTransfer = #check_external_transfer; + #root } @@ -110,6 +112,8 @@ pub fn expand(meta: Nep171Meta) -> Result { token_id: #me::standard::nep171::TokenId, approved_account_ids: Option>, ) -> bool { + use #me::standard::nep171::*; + let _ = approved_account_ids; // #[near_bindgen] cares about parameter names #near_sdk::require!( @@ -128,26 +132,19 @@ pub fn expand(meta: Nep171Meta) -> Result { if should_revert { let token_ids = [token_id]; - let check_result = #me::standard::nep171::Nep171Controller::check_transfer( - self, - &token_ids, - &receiver_id, - &receiver_id, - &previous_owner_id, - ); + let transfer = Nep171Transfer { + token_id: &token_ids[0], + authorization: Nep171TransferAuthorization::Owner, + sender_id: &receiver_id, + receiver_id: &previous_owner_id, + memo: None, + msg: None, + }; - match check_result { - Ok(()) => { - let transfer = #me::standard::nep171::Nep171Transfer { - token_id: &token_ids[0], - owner_id: &receiver_id, - sender_id: &receiver_id, - receiver_id: &previous_owner_id, - approval_id: None, - memo: None, - msg: None, - }; + let check_result = ::CheckTransfer::check_external_transfer(self, &transfer); + match check_result { + Ok(_) => { #before_nft_transfer #me::standard::nep171::Nep171Controller::transfer_unchecked( @@ -189,19 +186,18 @@ pub fn expand(meta: Nep171Meta) -> Result { let token_ids = [token_id]; - let transfer = #me::standard::nep171::Nep171Transfer { + let transfer = Nep171Transfer { token_id: &token_ids[0], - owner_id: &sender_id, + authorization: approval_id.map(Nep171TransferAuthorization::ApprovalId).unwrap_or(Nep171TransferAuthorization::Owner), sender_id: &sender_id, receiver_id: &receiver_id, - approval_id: approval_id.clone(), memo: memo.as_deref(), msg: None, }; #before_nft_transfer - Nep171Controller::external_transfer::<#check_external_transfer>(self, &transfer) + ::external_transfer(self, &transfer) .unwrap_or_else(|e| #near_sdk::env::panic_str(&e.to_string())); #after_nft_transfer @@ -229,19 +225,18 @@ pub fn expand(meta: Nep171Meta) -> Result { let token_ids = [token_id]; - let transfer = #me::standard::nep171::Nep171Transfer { + let transfer = Nep171Transfer { token_id: &token_ids[0], - owner_id: &sender_id, + authorization: approval_id.map(Nep171TransferAuthorization::ApprovalId).unwrap_or(Nep171TransferAuthorization::Owner), sender_id: &sender_id, receiver_id: &receiver_id, - approval_id: approval_id.clone(), memo: memo.as_deref(), msg: Some(&msg), }; #before_nft_transfer - Nep171Controller::external_transfer::<#check_external_transfer>(self, &transfer) + ::external_transfer(self, &transfer) .unwrap_or_else(|e| #near_sdk::env::panic_str(&e.to_string())); #after_nft_transfer @@ -268,7 +263,7 @@ pub fn expand(meta: Nep171Meta) -> Result { &self, token_id: #me::standard::nep171::TokenId, ) -> Option<#me::standard::nep171::Token> { - #me::standard::nep171::Nep171Controller::load_token::<#token_type>(self, &token_id) + ::load_token::<#token_type>(self, &token_id) } } }) diff --git a/macros/src/standard/nep178.rs b/macros/src/standard/nep178.rs new file mode 100644 index 0000000..a9bf170 --- /dev/null +++ b/macros/src/standard/nep178.rs @@ -0,0 +1,181 @@ +use std::ops::Not; + +use darling::{util::Flag, FromDeriveInput}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::Expr; + +#[derive(Debug, FromDeriveInput)] +#[darling(attributes(nep178), supports(struct_named))] +pub struct Nep178Meta { + pub storage_key: Option, + pub no_hooks: Flag, + + pub generics: syn::Generics, + pub ident: syn::Ident, + + // crates + #[darling(rename = "crate", default = "crate::default_crate_name")] + pub me: syn::Path, + #[darling(default = "crate::default_near_sdk")] + pub near_sdk: syn::Path, +} + +pub fn expand(meta: Nep178Meta) -> Result { + let Nep178Meta { + storage_key, + no_hooks, + + generics, + ident, + + me, + near_sdk, + } = meta; + + let (imp, ty, wher) = generics.split_for_impl(); + + let root = storage_key.map(|storage_key| { + quote! { + fn root() -> #me::slot::Slot<()> { + #me::slot::Slot::root(#storage_key) + } + } + }); + + let before_nft_approve = no_hooks.is_present().not().then(|| { + quote! { + let hook_state = >::before_nft_approve(&self, &token_id, &account_id); + } + }); + + let after_nft_approve = no_hooks.is_present().not().then(|| { + quote! { + >::after_nft_approve(self, &token_id, &account_id, &approval_id, hook_state); + } + }); + + let before_nft_revoke = no_hooks.is_present().not().then(|| { + quote! { + let hook_state = >::before_nft_revoke(&self, &token_id, &account_id); + } + }); + + let after_nft_revoke = no_hooks.is_present().not().then(|| { + quote! { + >::after_nft_revoke(self, &token_id, &account_id, hook_state); + } + }); + + let before_nft_revoke_all = no_hooks.is_present().not().then(|| { + quote! { + let hook_state = >::before_nft_revoke_all(&self, &token_id); + } + }); + + let after_nft_revoke_all = no_hooks.is_present().not().then(|| { + quote! { + >::after_nft_revoke_all(self, &token_id, hook_state); + } + }); + + Ok(quote! { + impl #imp #me::standard::nep178::Nep178ControllerInternal for #ident #ty #wher { + #root + } + + #[#near_sdk::near_bindgen] + impl #imp #me::standard::nep178::Nep178 for #ident #ty #wher { + #[payable] + fn nft_approve( + &mut self, + token_id: #me::standard::nep171::TokenId, + account_id: #near_sdk::AccountId, + msg: Option, + ) -> #near_sdk::PromiseOrValue<()> { + #me::utils::assert_nonzero_deposit(); + + let predecessor = #near_sdk::env::predecessor_account_id(); + + #before_nft_approve + + let approval_id = #me::standard::nep178::Nep178Controller::approve( + self, + &token_id, + &predecessor, + &account_id, + ) + .unwrap_or_else(|e| #near_sdk::env::panic_str(&e.to_string())); + + #after_nft_approve + + msg.map_or(#near_sdk::PromiseOrValue::Value(()), |msg| { + #me::standard::nep178::ext_nep178_receiver::ext(account_id) + .nft_on_approve(token_id, predecessor, approval_id, msg) + .into() + }) + } + + #[payable] + fn nft_revoke( + &mut self, + token_id: #me::standard::nep171::TokenId, + account_id: #near_sdk::AccountId, + ) { + #near_sdk::assert_one_yocto(); + + let predecessor = #near_sdk::env::predecessor_account_id(); + + #before_nft_revoke + + #me::standard::nep178::Nep178Controller::revoke( + self, + &token_id, + &predecessor, + &account_id, + ) + .unwrap_or_else(|e| #near_sdk::env::panic_str(&e.to_string())); + + #after_nft_revoke + } + + #[payable] + fn nft_revoke_all(&mut self, token_id: #me::standard::nep171::TokenId) { + #near_sdk::assert_one_yocto(); + + let predecessor = #near_sdk::env::predecessor_account_id(); + + #before_nft_revoke_all + + #me::standard::nep178::Nep178Controller::revoke_all( + self, + &token_id, + &predecessor, + ) + .unwrap_or_else(|e| #near_sdk::env::panic_str(&e.to_string())); + + #after_nft_revoke_all + } + + fn nft_is_approved( + &self, + token_id: #me::standard::nep171::TokenId, + approved_account_id: #near_sdk::AccountId, + approval_id: Option<#me::standard::nep178::ApprovalId>, + ) -> bool { + match ( + #me::standard::nep178::Nep178Controller::get_approval_id_for( + self, + &token_id, + &approved_account_id, + ), + approval_id, + ) { + (Some(saved_approval_id), Some(provided_approval_id)) => saved_approval_id == provided_approval_id, + (Some(_), _) => true, + _ => false, + } + } + } + }) +} diff --git a/macros/src/standard/non_fungible_token.rs b/macros/src/standard/non_fungible_token.rs index 3983aa1..2c13dee 100644 --- a/macros/src/standard/non_fungible_token.rs +++ b/macros/src/standard/non_fungible_token.rs @@ -3,20 +3,22 @@ use proc_macro2::TokenStream; use quote::quote; use syn::Expr; -use super::{nep171, nep177}; +use super::{nep171, nep177, nep178}; #[derive(Debug, FromDeriveInput)] #[darling(attributes(non_fungible_token), supports(struct_named))] pub struct NonFungibleTokenMeta { // NEP-171 fields - pub storage_key: Option, - pub no_hooks: Flag, - pub extension_hooks: Option, - pub check_external_transfer: Option, + pub core_storage_key: Option, + pub no_core_hooks: Flag, // NEP-177 fields pub metadata_storage_key: Option, + // NEP-178 fields + pub approval_storage_key: Option, + pub no_approval_hooks: Flag, + // darling pub generics: syn::Generics, pub ident: syn::Ident, @@ -30,13 +32,14 @@ pub struct NonFungibleTokenMeta { pub fn expand(meta: NonFungibleTokenMeta) -> Result { let NonFungibleTokenMeta { - storage_key, - no_hooks, - extension_hooks, - check_external_transfer, + core_storage_key: storage_key, + no_core_hooks: no_hooks, metadata_storage_key, + approval_storage_key, + no_approval_hooks, + generics, ident, @@ -47,10 +50,12 @@ pub fn expand(meta: NonFungibleTokenMeta) -> Result let expand_nep171 = nep171::expand(nep171::Nep171Meta { storage_key, no_hooks, - extension_hooks, - check_external_transfer, + extension_hooks: Some(syn::parse_quote! { #me::standard::nep178::TokenApprovals }), + check_external_transfer: Some(syn::parse_quote! { #me::standard::nep178::TokenApprovals }), - token_type: Some(syn::parse_quote! { ( #me::standard::nep177::TokenMetadata ) }), + token_type: Some( + syn::parse_quote! { (#me::standard::nep177::TokenMetadata, #me::standard::nep178::TokenApprovals) }, + ), generics: generics.clone(), ident: ident.clone(), @@ -62,9 +67,18 @@ pub fn expand(meta: NonFungibleTokenMeta) -> Result let expand_nep177 = nep177::expand(nep177::Nep177Meta { storage_key: metadata_storage_key, + generics: generics.clone(), + ident: ident.clone(), + + me: me.clone(), + near_sdk: near_sdk.clone(), + }); + + let expand_nep178 = nep178::expand(nep178::Nep178Meta { + storage_key: approval_storage_key, + no_hooks: no_approval_hooks, generics, ident, - me, near_sdk, }); @@ -73,9 +87,11 @@ pub fn expand(meta: NonFungibleTokenMeta) -> Result let nep171 = e.handle(expand_nep171); let nep177 = e.handle(expand_nep177); + let nep178 = e.handle(expand_nep178); e.finish_with(quote! { #nep171 #nep177 + #nep178 }) } diff --git a/src/standard/nep171/error.rs b/src/standard/nep171/error.rs index ba6d562..246cdf8 100644 --- a/src/standard/nep171/error.rs +++ b/src/standard/nep171/error.rs @@ -26,24 +26,24 @@ pub struct TokenDoesNotExistError { /// owned by a particular account, but the token is _not_ owned by that /// account. #[derive(Error, Clone, Debug)] -#[error( - "Token `{token_id}` is owned by `{actual_owner_id}` instead of expected `{expected_owner_id}`" -)] +#[error("Token `{token_id}` is owned by `{owner_id}` instead of expected `{expected_owner_id}`")] pub struct TokenNotOwnedByExpectedOwnerError { /// The token was supposed to be owned by this account. pub expected_owner_id: AccountId, /// The token is actually owned by this account. - pub actual_owner_id: AccountId, + pub owner_id: AccountId, /// The ID of the token in question. pub token_id: TokenId, } /// Occurs when a particular account is not allowed to transfer a token (e.g. on behalf of another user). See: NEP-178. #[derive(Error, Clone, Debug)] -#[error("Sender `{sender_id}` does not have permission to transfer token `{token_id}`")] +#[error("Sender `{sender_id}` does not have permission to transfer token `{token_id}`, owned by `{owner_id}`")] pub struct SenderNotApprovedError { /// The unapproved sender. pub sender_id: AccountId, + /// The owner of the token. + pub owner_id: AccountId, /// The ID of the token in question. pub token_id: TokenId, } @@ -51,10 +51,12 @@ pub struct SenderNotApprovedError { /// Occurs when attempting to perform a transfer of a token from one /// account to the same account. #[derive(Error, Clone, Debug)] -#[error("Receiver must be different from current owner `{current_owner_id}` to transfer token `{token_id}`")] +#[error( + "Receiver must be different from current owner `{owner_id}` to transfer token `{token_id}`" +)] pub struct TokenReceiverIsCurrentOwnerError { /// The account ID of current owner of the token. - pub current_owner_id: AccountId, + pub owner_id: AccountId, /// The ID of the token in question. pub token_id: TokenId, } diff --git a/src/standard/nep171/mod.rs b/src/standard/nep171/mod.rs index 2c3b233..7a66929 100644 --- a/src/standard/nep171/mod.rs +++ b/src/standard/nep171/mod.rs @@ -99,6 +99,10 @@ pub enum Nep171TransferError { /// Internal (storage location) methods for implementors of [`Nep171Controller`]. pub trait Nep171ControllerInternal { + type CheckTransfer: CheckExternalTransfer + where + Self: Sized; + /// Root storage slot. fn root() -> Slot<()> { Slot::root(DefaultStorageKey::Nep171) @@ -112,22 +116,23 @@ pub trait Nep171ControllerInternal { /// Non-public controller interface for NEP-171 implementations. pub trait Nep171Controller { + type CheckTransfer: CheckExternalTransfer + where + Self: Sized; + /// Transfer a token from `sender_id` to `receiver_id`. Checks that the transfer is valid using [`Nep171Controller::check_transfer`] before performing the transfer. - fn external_transfer>( - &mut self, - transfer: &Nep171Transfer, - ) -> Result<(), Nep171TransferError> + fn external_transfer(&mut self, transfer: &Nep171Transfer) -> Result<(), Nep171TransferError> where Self: Sized; - /// Check if a token transfer is valid without actually performing it. - fn check_transfer( - &self, - token_ids: &[TokenId], - current_owner_id: &AccountId, - sender_id: &AccountId, - receiver_id: &AccountId, - ) -> Result<(), Nep171TransferError>; + // /// Check if a token transfer is valid without actually performing it. Returns the account ID of the current owner of the token. + // fn check_transfer( + // &self, + // token_ids: &[TokenId], + // authorization: Nep171TransferAuthorization, + // sender_id: &AccountId, + // receiver_id: &AccountId, + // ) -> Result; /// Performs a token transfer without running [`Nep171Controller::check_transfer`]. /// @@ -178,14 +183,12 @@ pub trait Nep171Controller { /// `nft_transfer_call`). #[derive(Serialize, BorshSerialize, PartialEq, Eq, Clone, Debug, Hash)] pub struct Nep171Transfer<'a> { - /// Current owner account ID. - pub owner_id: &'a AccountId, + /// Why is this sender allowed to perform this transfer? + pub authorization: Nep171TransferAuthorization, /// Sending account ID. pub sender_id: &'a AccountId, /// Receiving account ID. pub receiver_id: &'a AccountId, - /// Optional approval ID. - pub approval_id: Option, /// Token ID. pub token_id: &'a TokenId, /// Optional memo string. @@ -194,13 +197,19 @@ pub struct Nep171Transfer<'a> { pub msg: Option<&'a str>, } +#[derive(Serialize, BorshSerialize, PartialEq, Eq, Clone, Debug, Hash)] +pub enum Nep171TransferAuthorization { + Owner, + ApprovalId(u32), +} + /// Different ways of checking if a transfer is valid. pub trait CheckExternalTransfer { - /// Checks if a transfer is valid. + /// Checks if a transfer is valid. Returns the account ID of the current owner of the token. fn check_external_transfer( contract: &C, transfer: &Nep171Transfer, - ) -> Result<(), Nep171TransferError>; + ) -> Result; } pub struct DefaultCheckExternalTransfer; @@ -209,13 +218,43 @@ impl CheckExternalTransfer for DefaultCheckExternalTrans fn check_external_transfer( contract: &T, transfer: &Nep171Transfer, - ) -> Result<(), Nep171TransferError> { - contract.check_transfer( - &[transfer.token_id.to_string()], - transfer.owner_id, - transfer.sender_id, - transfer.receiver_id, - ) + ) -> Result { + let owner_id = contract.token_owner(transfer.token_id).ok_or_else(|| { + error::TokenDoesNotExistError { + token_id: transfer.token_id.clone(), + } + })?; + + match transfer.authorization { + Nep171TransferAuthorization::Owner => { + if transfer.sender_id != &owner_id { + return Err(error::TokenNotOwnedByExpectedOwnerError { + expected_owner_id: transfer.sender_id.clone(), + owner_id, + token_id: transfer.token_id.clone(), + } + .into()); + } + } + Nep171TransferAuthorization::ApprovalId(_) => { + return Err(error::SenderNotApprovedError { + owner_id, + sender_id: transfer.sender_id.clone(), + token_id: transfer.token_id.clone(), + } + .into()) + } + } + + if transfer.receiver_id == &owner_id { + return Err(error::TokenReceiverIsCurrentOwnerError { + owner_id, + token_id: transfer.token_id.clone(), + } + .into()); + } + + Ok(owner_id) } } @@ -225,7 +264,6 @@ impl CheckExternalTransfer for DefaultCheckExternalTrans /// hooks. This may be useful for charging callers for storage usage, for /// example. pub trait Nep171Hook { - // TODO: Switch order of C, S generics /// Executed before a token transfer is conducted. /// /// May return an optional state value which will be passed along to the @@ -267,68 +305,68 @@ where } impl Nep171Controller for T { - fn external_transfer>( - &mut self, - transfer: &Nep171Transfer, - ) -> Result<(), Nep171TransferError> { - match Check::check_external_transfer(self, transfer) { - Ok(()) => { + type CheckTransfer = ::CheckTransfer; + + fn external_transfer(&mut self, transfer: &Nep171Transfer) -> Result<(), Nep171TransferError> { + match Self::CheckTransfer::check_external_transfer(self, transfer) { + Ok(current_owner_id) => { self.transfer_unchecked( &[transfer.token_id.to_string()], - transfer.owner_id.clone(), + current_owner_id, transfer.sender_id.clone(), transfer.receiver_id.clone(), transfer.memo.map(ToString::to_string), ); Ok(()) } - e => e, + Err(e) => Err(e), } } - fn check_transfer( - &self, - token_ids: &[TokenId], - current_owner_id: &AccountId, - sender_id: &AccountId, - receiver_id: &AccountId, - ) -> Result<(), Nep171TransferError> { - for token_id in token_ids { - let slot = Self::slot_token_owner(token_id); - - let actual_current_owner_id = - slot.read().ok_or_else(|| error::TokenDoesNotExistError { - token_id: token_id.clone(), - })?; - - if current_owner_id != &actual_current_owner_id { - return Err(error::TokenNotOwnedByExpectedOwnerError { - expected_owner_id: current_owner_id.clone(), - actual_owner_id: actual_current_owner_id, - token_id: token_id.clone(), - } - .into()); - } - - // This version doesn't implement approval management - if sender_id != current_owner_id { - return Err(error::SenderNotApprovedError { - sender_id: sender_id.clone(), - token_id: token_id.clone(), - } - .into()); - } - - if receiver_id == current_owner_id { - return Err(error::TokenReceiverIsCurrentOwnerError { - current_owner_id: current_owner_id.clone(), - token_id: token_id.clone(), - } - .into()); - } - } - Ok(()) - } + // fn check_transfer( + // &self, + // token_ids: &[TokenId], + // authorization: Nep171TransferAuthorization, + // sender_id: &AccountId, + // receiver_id: &AccountId, + // ) -> Result { + // for token_id in token_ids { + // let slot = Self::slot_token_owner(token_id); + + // let owner_id = slot.read().ok_or_else(|| error::TokenDoesNotExistError { + // token_id: token_id.clone(), + // })?; + + // match authorization { + // Nep171TransferAuthorization::Owner => { + // if sender_id != &owner_id { + // return Err(error::TokenNotOwnedByExpectedOwnerError { + // expected_owner_id: sender_id.clone(), + // actual_owner_id: owner_id, + // token_id: token_id.clone(), + // } + // .into()); + // } + // } + // Nep171TransferAuthorization::ApprovalId(_) => { + // return Err(error::SenderNotApprovedError { + // sender_id: sender_id.clone(), + // token_id: token_id.clone(), + // } + // .into()) + // } + // } + + // if receiver_id == &owner_id { + // return Err(error::TokenReceiverIsCurrentOwnerError { + // current_owner_id: owner_id, + // token_id: token_id.clone(), + // } + // .into()); + // } + // } + // Ok(()) + // } fn transfer_unchecked( &mut self, @@ -405,7 +443,7 @@ impl Nep171Controller for T { if &actual_owner_id != current_owner_id { return Err(error::TokenNotOwnedByExpectedOwnerError { expected_owner_id: current_owner_id.clone(), - actual_owner_id, + owner_id: actual_owner_id, token_id: (*token_id).clone(), } .into()); @@ -488,17 +526,6 @@ impl LoadTokenMetadata for () { } } -impl> LoadTokenMetadata for (T,) { - fn load( - contract: &C, - token_id: &TokenId, - metadata: &mut std::collections::HashMap, - ) -> Result<(), Box> { - T::load(contract, token_id, metadata)?; - Ok(()) - } -} - impl, U: LoadTokenMetadata> LoadTokenMetadata for (T, U) { fn load( contract: &C, diff --git a/src/standard/nep177.rs b/src/standard/nep177.rs index 310f0b5..7caf41b 100644 --- a/src/standard/nep177.rs +++ b/src/standard/nep177.rs @@ -7,6 +7,7 @@ use near_sdk::{ borsh::{self, BorshDeserialize, BorshSerialize}, env, json_types::U64, + log, serde::*, AccountId, BorshStorageKey, }; diff --git a/src/standard/nep178.rs b/src/standard/nep178.rs index c7b7daa..0bf8b93 100644 --- a/src/standard/nep178.rs +++ b/src/standard/nep178.rs @@ -5,6 +5,7 @@ use std::{collections::HashMap, error::Error}; use near_sdk::{ borsh::{self, BorshDeserialize, BorshSerialize}, + log, serde::*, store::UnorderedMap, AccountId, BorshStorageKey, @@ -52,43 +53,37 @@ impl LoadTokenMetadata for TokenApprovals { } impl Nep171Hook<(), C> for TokenApprovals { - fn before_nft_transfer(contract: &C, transfer: &Nep171Transfer) -> () { - todo!() - } + fn before_nft_transfer(_contract: &C, _transfer: &Nep171Transfer) {} - fn after_nft_transfer(contract: &mut C, transfer: &Nep171Transfer, state: ()) {} + fn after_nft_transfer(contract: &mut C, transfer: &Nep171Transfer, _: ()) { + contract.revoke_all_unchecked(transfer.token_id); + } } impl CheckExternalTransfer for TokenApprovals { fn check_external_transfer( contract: &C, transfer: &Nep171Transfer, - ) -> Result<(), Nep171TransferError> { - let normal_check = contract.check_transfer( - &[transfer.token_id.to_string()], - transfer.owner_id, - transfer.sender_id, - transfer.receiver_id, - ); - - match normal_check { - Ok(()) => Ok(()), - Err(e @ Nep171TransferError::SenderNotApproved(_)) => { - let approval_id = if let Some(id) = transfer.approval_id { - id + ) -> Result { + let normal_check = + DefaultCheckExternalTransfer::check_external_transfer(contract, transfer); + + match (&transfer.authorization, normal_check) { + (_, Ok(current_owner_id)) => Ok(current_owner_id), + ( + Nep171TransferAuthorization::ApprovalId(approval_id), + Err(Nep171TransferError::SenderNotApproved(s)), + ) => { + let saved_approval = + contract.get_approval_id_for(transfer.token_id, transfer.sender_id); + + if saved_approval == Some(*approval_id) { + Ok(s.owner_id) } else { - return Err(e); - }; - - let saved_approvals = contract.get_approvals_for(transfer.token_id); - - if saved_approvals.get(transfer.sender_id) == Some(&approval_id) { - Ok(()) - } else { - Err(e) + Err(s.into()) } } - e => e, + (_, e) => e, } } } @@ -121,6 +116,11 @@ pub trait Nep178ControllerInternal { #[derive(Error, Debug)] pub enum Nep178ApproveError { + #[error("Account `{account_id}` is cannot create approvals for token `{token_id}`.")] + Unauthorized { + token_id: TokenId, + account_id: AccountId, + }, #[error("Account {account_id} is already approved for token {token_id}.")] AccountAlreadyApproved { token_id: TokenId, @@ -135,6 +135,11 @@ pub enum Nep178ApproveError { #[derive(Error, Debug)] pub enum Nep178RevokeError { + #[error("Account `{account_id}` is cannot revoke approvals for token `{token_id}`.")] + Unauthorized { + token_id: TokenId, + account_id: AccountId, + }, #[error("Account {account_id} is not approved for token {token_id}")] AccountNotApproved { token_id: TokenId, @@ -142,12 +147,22 @@ pub enum Nep178RevokeError { }, } +#[derive(Error, Debug)] +pub enum Nep178RevokeAllError { + #[error("Account `{account_id}` is cannot revoke approvals for token `{token_id}`.")] + Unauthorized { + token_id: TokenId, + account_id: AccountId, + }, +} + /// Functions for managing non-fungible tokens with attached metadata, NEP-178. pub trait Nep178Controller { /// Approve a token for transfer by a delegated account. fn approve( &mut self, token_id: &TokenId, + current_owner_id: &AccountId, account_id: &AccountId, ) -> Result; @@ -159,6 +174,7 @@ pub trait Nep178Controller { fn revoke( &mut self, token_id: &TokenId, + current_owner_id: &AccountId, account_id: &AccountId, ) -> Result<(), Nep178RevokeError>; @@ -167,7 +183,14 @@ pub trait Nep178Controller { fn revoke_unchecked(&mut self, token_id: &TokenId, account_id: &AccountId); /// Revoke all approvals for a token. - fn revoke_all(&mut self, token_id: &TokenId); + fn revoke_all( + &mut self, + token_id: &TokenId, + current_owner_id: &AccountId, + ) -> Result<(), Nep178RevokeAllError>; + + /// Revoke all approvals for a token without checking current owner. + fn revoke_all_unchecked(&mut self, token_id: &TokenId); /// Get the approval ID for an account, if it is approved for a token. fn get_approval_id_for(&self, token_id: &TokenId, account_id: &AccountId) @@ -195,8 +218,17 @@ impl Nep178Controller for T { fn approve( &mut self, token_id: &TokenId, + current_owner_id: &AccountId, account_id: &AccountId, ) -> Result { + // owner check + if self.token_owner(token_id).as_ref() != Some(current_owner_id) { + return Err(Nep178ApproveError::Unauthorized { + token_id: token_id.clone(), + account_id: account_id.clone(), + }); + } + let mut slot = Self::slot_token_approvals(token_id); let mut approvals = slot.read().unwrap_or_else(|| TokenApprovals { next_approval_id: 0, @@ -243,8 +275,17 @@ impl Nep178Controller for T { fn revoke( &mut self, token_id: &TokenId, + current_owner_id: &AccountId, account_id: &AccountId, ) -> Result<(), Nep178RevokeError> { + // owner check + if self.token_owner(token_id).as_ref() != Some(current_owner_id) { + return Err(Nep178RevokeError::Unauthorized { + token_id: token_id.clone(), + account_id: account_id.clone(), + }); + } + let mut slot = Self::slot_token_approvals(token_id); let mut approvals = slot .read() @@ -266,17 +307,32 @@ impl Nep178Controller for T { Ok(()) } - fn revoke_all(&mut self, token_id: &TokenId) { - let mut slot = Self::slot_token_approvals(token_id); - let approvals = match slot.read() { + fn revoke_all( + &mut self, + token_id: &TokenId, + current_owner_id: &AccountId, + ) -> Result<(), Nep178RevokeAllError> { + // owner check + if self.token_owner(token_id).as_ref() != Some(current_owner_id) { + return Err(Nep178RevokeAllError::Unauthorized { + token_id: token_id.clone(), + account_id: current_owner_id.clone(), + }); + } + + self.revoke_all_unchecked(token_id); + + Ok(()) + } + + fn revoke_all_unchecked(&mut self, token_id: &TokenId) { + let slot = Self::slot_token_approvals(token_id); + let mut approvals = match slot.read() { Some(approvals) => approvals, None => return, }; - slot.write(&TokenApprovals { - next_approval_id: approvals.next_approval_id, - accounts: UnorderedMap::new(Self::slot_token_approvals_unordered_map(token_id)), - }); + approvals.accounts.clear(); } fn get_approval_id_for( @@ -300,14 +356,40 @@ impl Nep178Controller for T { None => return HashMap::default(), }; - approvals - .accounts - .into_iter() - .map(|(k, v)| (k.clone(), *v)) - .collect() + // FIX: "Index out of bounds" error. This _should be_ correct... + // approvals + // .accounts + // .into_iter() + // .map(|(k, v)| (k.clone(), *v)) + // .collect() + + // This just compiles, but is incorrect. + let _ = approvals; + + Default::default() } } +pub trait Nep178Hook { + fn before_nft_approve(&self, token_id: &TokenId, account_id: &AccountId) -> AState; + + fn after_nft_approve( + &mut self, + token_id: &TokenId, + account_id: &AccountId, + approval_id: &ApprovalId, + state: AState, + ); + + fn before_nft_revoke(&self, token_id: &TokenId, account_id: &AccountId) -> RState; + + fn after_nft_revoke(&mut self, token_id: &TokenId, account_id: &AccountId, state: RState); + + fn before_nft_revoke_all(&self, token_id: &TokenId) -> RAState; + + fn after_nft_revoke_all(&mut self, token_id: &TokenId, state: RAState); +} + // separate module with re-export because ext_contract doesn't play well with #![warn(missing_docs)] mod ext { #![allow(missing_docs)] @@ -336,4 +418,15 @@ mod ext { approval_id: Option, ) -> bool; } + + #[near_sdk::ext_contract(ext_nep178_receiver)] + pub trait Nep178Receiver { + fn nft_on_approve( + &mut self, + token_id: TokenId, + owner_id: AccountId, + approval_id: ApprovalId, + msg: String, + ); + } } diff --git a/src/utils.rs b/src/utils.rs index 8d95c92..614194f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -78,6 +78,14 @@ pub fn apply_storage_fee_and_refund( } } +/// Asserts that the attached deposit is greater than zero. +pub fn assert_nonzero_deposit() { + require!( + env::attached_deposit() > 0, + "Attached deposit must be greater than zero" + ); +} + #[cfg(test)] mod tests { use super::prefix_key; diff --git a/tests/macros/standard/nep171.rs b/tests/macros/standard/nep171.rs index d17f284..0b71d84 100644 --- a/tests/macros/standard/nep171.rs +++ b/tests/macros/standard/nep171.rs @@ -2,7 +2,14 @@ use near_sdk::{ borsh::{self, BorshDeserialize, BorshSerialize}, env, near_bindgen, store, AccountId, }; -use near_sdk_contract_tools::{standard::nep171::*, Nep171}; +use near_sdk_contract_tools::{ + standard::{ + nep171::*, + nep177::{Nep177Controller, TokenMetadata}, + }, + Nep171, +}; +use near_sdk_contract_tools_macros::NonFungibleToken; #[derive(BorshDeserialize, BorshSerialize, Debug, Clone, PartialEq, PartialOrd)] struct TokenRecord { @@ -19,14 +26,48 @@ impl From for TokenRecord { } } -#[derive(Nep171, BorshDeserialize, BorshSerialize)] -#[nep171(no_hooks)] +#[derive(NonFungibleToken, BorshDeserialize, BorshSerialize)] +// #[nep171(no_hooks, token_type = "()")] +#[non_fungible_token(no_core_hooks, no_approval_hooks)] #[near_bindgen] struct NonFungibleTokenNoHooks { pub before_nft_transfer_balance_record: store::Vector>, pub after_nft_transfer_balance_record: store::Vector>, } +#[test] +fn t() { + let mut n = NonFungibleTokenNoHooks { + before_nft_transfer_balance_record: store::Vector::new(b"a"), + after_nft_transfer_balance_record: store::Vector::new(b"b"), + }; + + let token_id = "token1".to_string(); + + n.mint_with_metadata( + token_id.clone(), + "alice".parse().unwrap(), + TokenMetadata { + title: Some("Title".to_string()), + description: None, + media: None, + media_hash: None, + copies: None, + issued_at: None, + expires_at: None, + starts_at: None, + updated_at: None, + extra: None, + reference: None, + reference_hash: None, + }, + ) + .unwrap(); + + let nft_tok = n.nft_token(token_id); + dbg!(nft_tok); +} + #[derive(Nep171, BorshDeserialize, BorshSerialize)] #[near_bindgen] struct NonFungibleToken { diff --git a/workspaces-tests/src/bin/non_fungible_token_full.rs b/workspaces-tests/src/bin/non_fungible_token_full.rs index eaaa854..e989087 100644 --- a/workspaces-tests/src/bin/non_fungible_token_full.rs +++ b/workspaces-tests/src/bin/non_fungible_token_full.rs @@ -5,16 +5,50 @@ pub fn main() {} use near_sdk::{ borsh::{self, BorshDeserialize, BorshSerialize}, - env, log, near_bindgen, PanicOnDefault, + env, log, near_bindgen, store, AccountId, PanicOnDefault, }; use near_sdk_contract_tools::{ - standard::{nep171::*, nep177::*}, + standard::{nep171::*, nep177::*, nep178::*}, NonFungibleToken, }; #[derive(PanicOnDefault, BorshSerialize, BorshDeserialize, NonFungibleToken)] #[near_bindgen] -pub struct Contract {} +pub struct Contract { + dummy: store::UnorderedMap, +} + +impl Nep178Hook for Contract { + fn before_nft_approve(&self, token_id: &TokenId, _account_id: &AccountId) { + log!("before_nft_approve({})", token_id); + } + + fn after_nft_approve( + &mut self, + token_id: &TokenId, + _account_id: &AccountId, + _approval_id: &ApprovalId, + _state: (), + ) { + log!("after_nft_approve({})", token_id); + } + + fn before_nft_revoke(&self, token_id: &TokenId, _account_id: &AccountId) { + log!("before_nft_revoke({})", token_id); + } + + fn after_nft_revoke(&mut self, token_id: &TokenId, _account_id: &AccountId, _state: ()) { + log!("after_nft_revoke({})", token_id); + } + + fn before_nft_revoke_all(&self, token_id: &TokenId) { + log!("before_nft_revoke_all({})", token_id); + } + + fn after_nft_revoke_all(&mut self, token_id: &TokenId, _state: ()) { + log!("after_nft_revoke_all({})", token_id); + } +} impl Nep171Hook for Contract { fn before_nft_transfer(_contract: &Self, transfer: &Nep171Transfer) { @@ -26,11 +60,13 @@ impl Nep171Hook for Contract { } } -#[near_bindgen] +#[near_sdk::near_bindgen] impl Contract { #[init] pub fn new() -> Self { - let mut contract = Self {}; + let mut contract = Self { + dummy: store::UnorderedMap::new(b"z"), + }; contract.set_contract_metadata(ContractMetadata::new( "My NFT Smart Contract".to_string(), @@ -41,6 +77,20 @@ impl Contract { contract } + // TODO: Remove + pub fn dummy_insert(&mut self) { + let i = self.dummy.len(); + self.dummy.insert(i.to_string(), format!("value {}", i)); + } + + pub fn dummy_clear(&mut self) { + self.dummy.clear(); + } + + pub fn dummy_iter(&self) -> Vec<(&String, &String)> { + self.dummy.into_iter().collect() + } + pub fn mint(&mut self, token_ids: Vec) { let receiver = env::predecessor_account_id(); for token_id in token_ids { diff --git a/workspaces-tests/tests/non_fungible_token.rs b/workspaces-tests/tests/non_fungible_token.rs index 2a0bb4c..f5ac4a8 100644 --- a/workspaces-tests/tests/non_fungible_token.rs +++ b/workspaces-tests/tests/non_fungible_token.rs @@ -4,6 +4,7 @@ use near_sdk::serde_json::json; use near_sdk_contract_tools::standard::{ nep171::{event::NftTransferLog, Nep171Event, Token}, nep177::{self, TokenMetadata}, + nep178, nep297::Event, }; use workspaces::operations::Function; @@ -18,6 +19,24 @@ const WASM_FULL: &[u8] = const RECEIVER_WASM: &[u8] = include_bytes!("../../target/wasm32-unknown-unknown/release/non_fungible_token_receiver.wasm"); +fn token_meta(id: String) -> near_sdk::serde_json::Value { + near_sdk::serde_json::to_value(TokenMetadata { + title: Some(id), + description: Some("description".to_string()), + media: None, + media_hash: None, + copies: None, + issued_at: None, + expires_at: None, + starts_at: None, + updated_at: None, + extra: None, + reference: None, + reference_hash: None, + }) + .unwrap() +} + async fn setup_balances( wasm: &[u8], num_accounts: usize, @@ -117,24 +136,6 @@ async fn create_and_mint_with_metadata() { nft_token(&contract, "token_3"), ); - fn token_meta(id: String) -> near_sdk::serde_json::Value { - near_sdk::serde_json::to_value(TokenMetadata { - title: Some(id), - description: Some("description".to_string()), - media: None, - media_hash: None, - copies: None, - issued_at: None, - expires_at: None, - starts_at: None, - updated_at: None, - extra: None, - reference: None, - reference_hash: None, - }) - .unwrap() - } - // Verify minted tokens assert_eq!( token_0, @@ -632,3 +633,123 @@ async fn transfer_call_receiver_send_return() { }), ); } + +#[tokio::test] +async fn panic_on_new() { + let Setup { contract, accounts } = + setup_balances(WASM_FULL, 3, |i| vec![format!("token_{i}")]).await; + + contract + .call("dummy_insert") + .transact() + .await + .unwrap() + .unwrap(); + contract + .call("dummy_insert") + .transact() + .await + .unwrap() + .unwrap(); + contract + .call("dummy_clear") + .transact() + .await + .unwrap() + .unwrap(); + + let x = contract + .view("dummy_iter") + .await + .unwrap() + .json::>() + .unwrap(); + + dbg!(x); +} + +#[tokio::test] +async fn transfer_approval_success() { + let Setup { contract, accounts } = + setup_balances(WASM_FULL, 3, |i| vec![format!("token_{i}")]).await; + let alice = &accounts[0]; + let bob = &accounts[1]; + let charlie = &accounts[2]; + + alice + .call(contract.id(), "nft_approve") + .args_json(json!({ + "token_id": "token_0", + "account_id": bob.id(), + })) + .deposit(1) + .transact() + .await + .unwrap() + .unwrap(); + + let view_token = contract + .view("nft_token") + .args_json(json!({ + "token_id": "token_0", + })) + .await + .unwrap() + .json::() + .unwrap(); + + let expected_view_token = Token { + token_id: "token_0".into(), + owner_id: alice.id().parse().unwrap(), + extensions_metadata: [ + ("metadata".to_string(), token_meta("token_0".to_string())), + ( + "approved_account_ids".to_string(), + json!({ + bob.id().to_string(): 0, + }), + ), + ] + .into(), + }; + + // assert_eq!(view_token, expected_view_token); + + let is_approved = contract + .view("nft_is_approved") + .args_json(json!({ + "token_id": "token_0", + "approved_account_id": bob.id().to_string(), + })) + .await + .unwrap() + .json::() + .unwrap(); + + assert!(is_approved); + + bob.call(contract.id(), "nft_transfer") + .args_json(json!({ + "token_id": "token_0", + "approval_id": 0, + "receiver_id": charlie.id().to_string(), + })) + .deposit(1) + .transact() + .await + .unwrap() + .unwrap(); + + assert_eq!( + nft_token(&contract, "token_0").await, + Some(Token { + token_id: "token_0".to_string(), + owner_id: charlie.id().parse().unwrap(), + extensions_metadata: [ + ("metadata".to_string(), token_meta("token_0".to_string())), + ("approved_account_ids".to_string(), json!({})) + ] + .into(), + }), + ); +} From 4ca7b90c38ee7ec32392dcb362f9f9b423c433f4 Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Sat, 26 Aug 2023 11:59:55 +0200 Subject: [PATCH 23/34] feat: nep178 testing --- src/standard/nep177.rs | 1 - src/standard/nep178.rs | 42 +++------- .../src/bin/non_fungible_token_full.rs | 24 +----- workspaces-tests/tests/non_fungible_token.rs | 77 +++++++------------ 4 files changed, 41 insertions(+), 103 deletions(-) diff --git a/src/standard/nep177.rs b/src/standard/nep177.rs index 7caf41b..310f0b5 100644 --- a/src/standard/nep177.rs +++ b/src/standard/nep177.rs @@ -7,7 +7,6 @@ use near_sdk::{ borsh::{self, BorshDeserialize, BorshSerialize}, env, json_types::U64, - log, serde::*, AccountId, BorshStorageKey, }; diff --git a/src/standard/nep178.rs b/src/standard/nep178.rs index 0bf8b93..ec8de58 100644 --- a/src/standard/nep178.rs +++ b/src/standard/nep178.rs @@ -5,8 +5,6 @@ use std::{collections::HashMap, error::Error}; use near_sdk::{ borsh::{self, BorshDeserialize, BorshSerialize}, - log, - serde::*, store::UnorderedMap, AccountId, BorshStorageKey, }; @@ -20,24 +18,14 @@ pub type ApprovalId = u32; pub const MAX_APPROVALS: ApprovalId = 32; /// Non-fungible token metadata. -#[derive(Serialize, BorshSerialize, BorshDeserialize, Debug)] -#[serde(crate = "near_sdk::serde")] +#[derive(BorshSerialize, BorshDeserialize, Debug)] pub struct TokenApprovals { - #[serde(skip)] pub next_approval_id: ApprovalId, - #[serde(flatten, serialize_with = "to_map")] /// The list of approved accounts. pub accounts: UnorderedMap, } -fn to_map( - accounts: &UnorderedMap, - s: S, -) -> Result { - s.collect_map(accounts.iter()) -} - impl LoadTokenMetadata for TokenApprovals { fn load( contract: &C, @@ -326,13 +314,16 @@ impl Nep178Controller for T { } fn revoke_all_unchecked(&mut self, token_id: &TokenId) { - let slot = Self::slot_token_approvals(token_id); + let mut slot = Self::slot_token_approvals(token_id); let mut approvals = match slot.read() { Some(approvals) => approvals, None => return, }; - approvals.accounts.clear(); + if !approvals.accounts.is_empty() { + approvals.accounts.clear(); + slot.write(&approvals); + } } fn get_approval_id_for( @@ -341,10 +332,7 @@ impl Nep178Controller for T { account_id: &AccountId, ) -> Option { let slot = Self::slot_token_approvals(token_id); - let approvals = match slot.read() { - Some(approvals) => approvals, - None => return None, - }; + let approvals = slot.read()?; approvals.accounts.get(account_id).copied() } @@ -356,17 +344,11 @@ impl Nep178Controller for T { None => return HashMap::default(), }; - // FIX: "Index out of bounds" error. This _should be_ correct... - // approvals - // .accounts - // .into_iter() - // .map(|(k, v)| (k.clone(), *v)) - // .collect() - - // This just compiles, but is incorrect. - let _ = approvals; - - Default::default() + approvals + .accounts + .into_iter() + .map(|(k, v)| (k.clone(), *v)) + .collect() } } diff --git a/workspaces-tests/src/bin/non_fungible_token_full.rs b/workspaces-tests/src/bin/non_fungible_token_full.rs index e989087..39be3f8 100644 --- a/workspaces-tests/src/bin/non_fungible_token_full.rs +++ b/workspaces-tests/src/bin/non_fungible_token_full.rs @@ -5,7 +5,7 @@ pub fn main() {} use near_sdk::{ borsh::{self, BorshDeserialize, BorshSerialize}, - env, log, near_bindgen, store, AccountId, PanicOnDefault, + env, log, near_bindgen, AccountId, PanicOnDefault, }; use near_sdk_contract_tools::{ standard::{nep171::*, nep177::*, nep178::*}, @@ -14,9 +14,7 @@ use near_sdk_contract_tools::{ #[derive(PanicOnDefault, BorshSerialize, BorshDeserialize, NonFungibleToken)] #[near_bindgen] -pub struct Contract { - dummy: store::UnorderedMap, -} +pub struct Contract {} impl Nep178Hook for Contract { fn before_nft_approve(&self, token_id: &TokenId, _account_id: &AccountId) { @@ -64,9 +62,7 @@ impl Nep171Hook for Contract { impl Contract { #[init] pub fn new() -> Self { - let mut contract = Self { - dummy: store::UnorderedMap::new(b"z"), - }; + let mut contract = Self {}; contract.set_contract_metadata(ContractMetadata::new( "My NFT Smart Contract".to_string(), @@ -77,20 +73,6 @@ impl Contract { contract } - // TODO: Remove - pub fn dummy_insert(&mut self) { - let i = self.dummy.len(); - self.dummy.insert(i.to_string(), format!("value {}", i)); - } - - pub fn dummy_clear(&mut self) { - self.dummy.clear(); - } - - pub fn dummy_iter(&self) -> Vec<(&String, &String)> { - self.dummy.into_iter().collect() - } - pub fn mint(&mut self, token_ids: Vec) { let receiver = env::predecessor_account_id(); for token_id in token_ids { diff --git a/workspaces-tests/tests/non_fungible_token.rs b/workspaces-tests/tests/non_fungible_token.rs index f5ac4a8..c8b0a01 100644 --- a/workspaces-tests/tests/non_fungible_token.rs +++ b/workspaces-tests/tests/non_fungible_token.rs @@ -4,7 +4,6 @@ use near_sdk::serde_json::json; use near_sdk_contract_tools::standard::{ nep171::{event::NftTransferLog, Nep171Event, Token}, nep177::{self, TokenMetadata}, - nep178, nep297::Event, }; use workspaces::operations::Function; @@ -142,8 +141,14 @@ async fn create_and_mint_with_metadata() { Some(Token { token_id: "token_0".to_string(), owner_id: alice.id().parse().unwrap(), - extensions_metadata: [("metadata".to_string(), token_meta("token_0".to_string()))] - .into(), + extensions_metadata: [ + ("metadata".to_string(), token_meta("token_0".to_string())), + ( + "approved_account_ids".to_string(), + near_sdk::serde_json::json!({}), + ) + ] + .into(), }), ); assert_eq!( @@ -151,8 +156,14 @@ async fn create_and_mint_with_metadata() { Some(Token { token_id: "token_1".to_string(), owner_id: bob.id().parse().unwrap(), - extensions_metadata: [("metadata".to_string(), token_meta("token_1".to_string()))] - .into(), + extensions_metadata: [ + ("metadata".to_string(), token_meta("token_1".to_string())), + ( + "approved_account_ids".to_string(), + near_sdk::serde_json::json!({}), + ) + ] + .into(), }), ); assert_eq!( @@ -160,8 +171,14 @@ async fn create_and_mint_with_metadata() { Some(Token { token_id: "token_2".to_string(), owner_id: charlie.id().parse().unwrap(), - extensions_metadata: [("metadata".to_string(), token_meta("token_2".to_string()))] - .into(), + extensions_metadata: [ + ("metadata".to_string(), token_meta("token_2".to_string())), + ( + "approved_account_ids".to_string(), + near_sdk::serde_json::json!({}), + ) + ] + .into(), }), ); assert_eq!(token_3, None::); @@ -634,40 +651,6 @@ async fn transfer_call_receiver_send_return() { ); } -#[tokio::test] -async fn panic_on_new() { - let Setup { contract, accounts } = - setup_balances(WASM_FULL, 3, |i| vec![format!("token_{i}")]).await; - - contract - .call("dummy_insert") - .transact() - .await - .unwrap() - .unwrap(); - contract - .call("dummy_insert") - .transact() - .await - .unwrap() - .unwrap(); - contract - .call("dummy_clear") - .transact() - .await - .unwrap() - .unwrap(); - - let x = contract - .view("dummy_iter") - .await - .unwrap() - .json::>() - .unwrap(); - - dbg!(x); -} - #[tokio::test] async fn transfer_approval_success() { let Setup { contract, accounts } = @@ -688,15 +671,7 @@ async fn transfer_approval_success() { .unwrap() .unwrap(); - let view_token = contract - .view("nft_token") - .args_json(json!({ - "token_id": "token_0", - })) - .await - .unwrap() - .json::() - .unwrap(); + let view_token = nft_token::(&contract, "token_0").await; let expected_view_token = Token { token_id: "token_0".into(), @@ -713,7 +688,7 @@ async fn transfer_approval_success() { .into(), }; - // assert_eq!(view_token, expected_view_token); + assert_eq!(view_token, Some(expected_view_token)); let is_approved = contract .view("nft_is_approved") From 6824eb4ab9abbbbf959e3d8224c635fe4f5b2525 Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Sat, 26 Aug 2023 12:32:06 +0200 Subject: [PATCH 24/34] chore: docs --- macros/src/lib.rs | 4 ++ macros/src/standard/nep171.rs | 4 +- src/standard/nep171/mod.rs | 15 +++++-- src/standard/nep178.rs | 42 +++++++++++++++++++- tests/macros/standard/nep171.rs | 1 - workspaces-tests/tests/non_fungible_token.rs | 15 ++----- 6 files changed, 61 insertions(+), 20 deletions(-) diff --git a/macros/src/lib.rs b/macros/src/lib.rs index ada9d40..9ecc4f0 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -171,6 +171,10 @@ pub fn derive_nep177(input: TokenStream) -> TokenStream { make_derive(input, standard::nep177::expand) } +/// Adds NEP-178 non-fungible token approvals functionality to a contract. +/// +/// The storage key prefix for the fields can be optionally specified (default: +/// `"~$178"`) using `#[nep178(storage_key = "")]`. #[proc_macro_derive(Nep178, attributes(nep178))] pub fn derive_nep178(input: TokenStream) -> TokenStream { make_derive(input, standard::nep178::expand) diff --git a/macros/src/standard/nep171.rs b/macros/src/standard/nep171.rs index 820cea4..0527b98 100644 --- a/macros/src/standard/nep171.rs +++ b/macros/src/standard/nep171.rs @@ -97,7 +97,7 @@ pub fn expand(meta: Nep171Meta) -> Result { Ok(quote! { impl #imp #me::standard::nep171::Nep171ControllerInternal for #ident #ty #wher { - type CheckTransfer = #check_external_transfer; + type CheckExternalTransfer = #check_external_transfer; #root } @@ -141,7 +141,7 @@ pub fn expand(meta: Nep171Meta) -> Result { msg: None, }; - let check_result = ::CheckTransfer::check_external_transfer(self, &transfer); + let check_result = ::CheckExternalTransfer::check_external_transfer(self, &transfer); match check_result { Ok(_) => { diff --git a/src/standard/nep171/mod.rs b/src/standard/nep171/mod.rs index 7a66929..2f04e5e 100644 --- a/src/standard/nep171/mod.rs +++ b/src/standard/nep171/mod.rs @@ -99,7 +99,8 @@ pub enum Nep171TransferError { /// Internal (storage location) methods for implementors of [`Nep171Controller`]. pub trait Nep171ControllerInternal { - type CheckTransfer: CheckExternalTransfer + /// Invoked during an external transfer. + type CheckExternalTransfer: CheckExternalTransfer where Self: Sized; @@ -116,7 +117,8 @@ pub trait Nep171ControllerInternal { /// Non-public controller interface for NEP-171 implementations. pub trait Nep171Controller { - type CheckTransfer: CheckExternalTransfer + /// Invoked during an external transfer. + type CheckExternalTransfer: CheckExternalTransfer where Self: Sized; @@ -197,9 +199,12 @@ pub struct Nep171Transfer<'a> { pub msg: Option<&'a str>, } +/// Authorization for a transfer. #[derive(Serialize, BorshSerialize, PartialEq, Eq, Clone, Debug, Hash)] pub enum Nep171TransferAuthorization { + /// The sender is the owner of the token. Owner, + /// The sender holds a valid approval ID for the token. ApprovalId(u32), } @@ -212,6 +217,8 @@ pub trait CheckExternalTransfer { ) -> Result; } +/// Default external transfer checker. Only allows transfers by the owner of a +/// token. Does not support approval IDs. pub struct DefaultCheckExternalTransfer; impl CheckExternalTransfer for DefaultCheckExternalTransfer { @@ -305,10 +312,10 @@ where } impl Nep171Controller for T { - type CheckTransfer = ::CheckTransfer; + type CheckExternalTransfer = ::CheckExternalTransfer; fn external_transfer(&mut self, transfer: &Nep171Transfer) -> Result<(), Nep171TransferError> { - match Self::CheckTransfer::check_external_transfer(self, transfer) { + match Self::CheckExternalTransfer::check_external_transfer(self, transfer) { Ok(current_owner_id) => { self.transfer_unchecked( &[transfer.token_id.to_string()], diff --git a/src/standard/nep178.rs b/src/standard/nep178.rs index ec8de58..93c4b9a 100644 --- a/src/standard/nep178.rs +++ b/src/standard/nep178.rs @@ -14,12 +14,15 @@ use crate::{slot::Slot, standard::nep171::*, DefaultStorageKey}; pub use ext::*; +/// Type for approval IDs. pub type ApprovalId = u32; +/// Maximum number of approvals per token. pub const MAX_APPROVALS: ApprovalId = 32; /// Non-fungible token metadata. #[derive(BorshSerialize, BorshDeserialize, Debug)] pub struct TokenApprovals { + /// The next approval ID to use. Only incremented. pub next_approval_id: ApprovalId, /// The list of approved accounts. @@ -102,44 +105,66 @@ pub trait Nep178ControllerInternal { } } +/// Errors that can occur when managing non-fungible token approvals. #[derive(Error, Debug)] pub enum Nep178ApproveError { + /// The account is not authorized to approve the token. #[error("Account `{account_id}` is cannot create approvals for token `{token_id}`.")] Unauthorized { + /// The token ID. token_id: TokenId, + /// The unauthorized account ID. account_id: AccountId, }, + /// The account is already approved for the token. #[error("Account {account_id} is already approved for token {token_id}.")] AccountAlreadyApproved { + /// The token ID. token_id: TokenId, + /// The account ID that has already been approved. account_id: AccountId, }, + /// The token has too many approvals. #[error( "Too many approvals for token {token_id}, maximum is {}.", MAX_APPROVALS )] - TooManyApprovals { token_id: TokenId }, + TooManyApprovals { + /// The token ID. + token_id: TokenId, + }, } +/// Errors that can occur when revoking non-fungible token approvals. #[derive(Error, Debug)] pub enum Nep178RevokeError { + /// The account is not authorized to revoke approvals for the token. #[error("Account `{account_id}` is cannot revoke approvals for token `{token_id}`.")] Unauthorized { + /// The token ID. token_id: TokenId, + /// The unauthorized account ID. account_id: AccountId, }, + /// The account is not approved for the token. #[error("Account {account_id} is not approved for token {token_id}")] AccountNotApproved { + /// The token ID. token_id: TokenId, + /// The account ID that is not approved. account_id: AccountId, }, } +/// Errors that can occur when revoking all approvals for a non-fungible token. #[derive(Error, Debug)] pub enum Nep178RevokeAllError { + /// The account is not authorized to revoke approvals for the token. #[error("Account `{account_id}` is cannot revoke approvals for token `{token_id}`.")] Unauthorized { + /// The token ID. token_id: TokenId, + /// The unauthorized account ID. account_id: AccountId, }, } @@ -352,9 +377,12 @@ impl Nep178Controller for T { } } +/// Hooks for NEP-178. pub trait Nep178Hook { + /// Called before a token is approved for transfer. fn before_nft_approve(&self, token_id: &TokenId, account_id: &AccountId) -> AState; + /// Called after a token is approved for transfer. fn after_nft_approve( &mut self, token_id: &TokenId, @@ -363,12 +391,16 @@ pub trait Nep178Hook { state: AState, ); + /// Called before a token approval is revoked. fn before_nft_revoke(&self, token_id: &TokenId, account_id: &AccountId) -> RState; + /// Called after a token approval is revoked. fn after_nft_revoke(&mut self, token_id: &TokenId, account_id: &AccountId, state: RState); + /// Called before all approvals for a token are revoked. fn before_nft_revoke_all(&self, token_id: &TokenId) -> RAState; + /// Called after all approvals for a token are revoked. fn after_nft_revoke_all(&mut self, token_id: &TokenId, state: RAState); } @@ -380,6 +412,9 @@ mod ext { use super::*; + /// NEP-178 external interface. + /// + /// See for more details. #[near_sdk::ext_contract(ext_nep178)] pub trait Nep178 { fn nft_approve( @@ -401,6 +436,11 @@ mod ext { ) -> bool; } + /// NEP-178 receiver interface. + /// + /// Respond to notification that contract has been granted approval for a token. + /// + /// See for more details. #[near_sdk::ext_contract(ext_nep178_receiver)] pub trait Nep178Receiver { fn nft_on_approve( diff --git a/tests/macros/standard/nep171.rs b/tests/macros/standard/nep171.rs index 0b71d84..ea88d9b 100644 --- a/tests/macros/standard/nep171.rs +++ b/tests/macros/standard/nep171.rs @@ -27,7 +27,6 @@ impl From for TokenRecord { } #[derive(NonFungibleToken, BorshDeserialize, BorshSerialize)] -// #[nep171(no_hooks, token_type = "()")] #[non_fungible_token(no_core_hooks, no_approval_hooks)] #[near_bindgen] struct NonFungibleTokenNoHooks { diff --git a/workspaces-tests/tests/non_fungible_token.rs b/workspaces-tests/tests/non_fungible_token.rs index c8b0a01..88e5b8a 100644 --- a/workspaces-tests/tests/non_fungible_token.rs +++ b/workspaces-tests/tests/non_fungible_token.rs @@ -143,10 +143,7 @@ async fn create_and_mint_with_metadata() { owner_id: alice.id().parse().unwrap(), extensions_metadata: [ ("metadata".to_string(), token_meta("token_0".to_string())), - ( - "approved_account_ids".to_string(), - near_sdk::serde_json::json!({}), - ) + ("approved_account_ids".to_string(), json!({}),) ] .into(), }), @@ -158,10 +155,7 @@ async fn create_and_mint_with_metadata() { owner_id: bob.id().parse().unwrap(), extensions_metadata: [ ("metadata".to_string(), token_meta("token_1".to_string())), - ( - "approved_account_ids".to_string(), - near_sdk::serde_json::json!({}), - ) + ("approved_account_ids".to_string(), json!({}),) ] .into(), }), @@ -173,10 +167,7 @@ async fn create_and_mint_with_metadata() { owner_id: charlie.id().parse().unwrap(), extensions_metadata: [ ("metadata".to_string(), token_meta("token_2".to_string())), - ( - "approved_account_ids".to_string(), - near_sdk::serde_json::json!({}), - ) + ("approved_account_ids".to_string(), json!({}),) ] .into(), }), From 1df7e0274f0bad520b3ba6a08e470cb638896de5 Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Sat, 26 Aug 2023 15:36:33 +0200 Subject: [PATCH 25/34] chore: more tests and examples in docs --- macros/src/standard/nep171.rs | 3 +- src/standard/mod.rs | 2 +- src/standard/nep171/error.rs | 6 +- src/standard/nep171/mod.rs | 104 ++++---- src/standard/nep178.rs | 2 +- tests/macros/standard/mod.rs | 1 + tests/macros/standard/non_fungible_token.rs | 31 +++ workspaces-tests/tests/non_fungible_token.rs | 248 ++++++++++++++++++- 8 files changed, 341 insertions(+), 56 deletions(-) create mode 100644 tests/macros/standard/non_fungible_token.rs diff --git a/macros/src/standard/nep171.rs b/macros/src/standard/nep171.rs index 0527b98..3ee0084 100644 --- a/macros/src/standard/nep171.rs +++ b/macros/src/standard/nep171.rs @@ -98,6 +98,7 @@ pub fn expand(meta: Nep171Meta) -> Result { Ok(quote! { impl #imp #me::standard::nep171::Nep171ControllerInternal for #ident #ty #wher { type CheckExternalTransfer = #check_external_transfer; + type LoadTokenMetadata = #token_type; #root } @@ -263,7 +264,7 @@ pub fn expand(meta: Nep171Meta) -> Result { &self, token_id: #me::standard::nep171::TokenId, ) -> Option<#me::standard::nep171::Token> { - ::load_token::<#token_type>(self, &token_id) + ::load_token(self, &token_id) } } }) diff --git a/src/standard/mod.rs b/src/standard/mod.rs index 69cd9b2..ff78de2 100644 --- a/src/standard/mod.rs +++ b/src/standard/mod.rs @@ -1,4 +1,4 @@ -//! Implementations of NEP standards +//! Implementations of NEP standards. pub mod nep141; pub mod nep148; diff --git a/src/standard/nep171/error.rs b/src/standard/nep171/error.rs index 246cdf8..f164617 100644 --- a/src/standard/nep171/error.rs +++ b/src/standard/nep171/error.rs @@ -3,6 +3,8 @@ use near_sdk::AccountId; use thiserror::Error; +use crate::standard::nep178::ApprovalId; + use super::TokenId; /// Occurs when trying to create a token ID that already exists. @@ -38,7 +40,7 @@ pub struct TokenNotOwnedByExpectedOwnerError { /// Occurs when a particular account is not allowed to transfer a token (e.g. on behalf of another user). See: NEP-178. #[derive(Error, Clone, Debug)] -#[error("Sender `{sender_id}` does not have permission to transfer token `{token_id}`, owned by `{owner_id}`")] +#[error("Sender `{sender_id}` does not have permission to transfer token `{token_id}`, owned by `{owner_id}`, with approval ID {approval_id}")] pub struct SenderNotApprovedError { /// The unapproved sender. pub sender_id: AccountId, @@ -46,6 +48,8 @@ pub struct SenderNotApprovedError { pub owner_id: AccountId, /// The ID of the token in question. pub token_id: TokenId, + /// The approval ID that the sender tried to use to transfer the token. + pub approval_id: ApprovalId, } /// Occurs when attempting to perform a transfer of a token from one diff --git a/src/standard/nep171/mod.rs b/src/standard/nep171/mod.rs index 2f04e5e..199b041 100644 --- a/src/standard/nep171/mod.rs +++ b/src/standard/nep171/mod.rs @@ -1,6 +1,45 @@ //! NEP-171 non-fungible token core implementation. //! //! Reference: +//! +//! # Usage +//! +//! It is recommended to use the [`near_sdk_contract_tools::Nep171`] derive macro or the [`near_sdk_contract_tools::NonFungibleToken`] macro to implement NEP-171 with this crate. +//! +//! ## Basic implementation with no transfer hooks +//! +//! ```rust +//! use near_sdk_contract_tools::{Nep171, standard::nep171::*}; +//! use near_sdk::{*, borsh::{self, *}}; +//! +//! #[derive(BorshSerialize, BorshDeserialize, PanicOnDefault, Nep171)] +//! #[nep171(no_hooks)] +//! #[near_bindgen] +//! pub struct Contract {} +//! ``` +//! +//! ## Basic implementation with transfer hooks +//! +//! ```rust +//! use near_sdk_contract_tools::{Nep171, standard::nep171::*}; +//! use near_sdk::{*, borsh::{self, *}}; +//! +//! #[derive(BorshSerialize, BorshDeserialize, PanicOnDefault, Nep171)] +//! #[near_bindgen] +//! pub struct Contract { +//! transfers: u32, +//! } +//! +//! impl Nep171Hook for Contract { +//! fn before_nft_transfer(_contract: &Self, transfer: &Nep171Transfer) { +//! log!("{} is transferring {} to {}", transfer.sender_id, transfer.token_id, transfer.receiver_id); +//! } +//! +//! fn after_nft_transfer(contract: &mut Self, _transfer: &Nep171Transfer, _: ()) { +//! contract.transfers += 1; +//! } +//! } +//! ``` use std::error::Error; @@ -104,6 +143,10 @@ pub trait Nep171ControllerInternal { where Self: Sized; + type LoadTokenMetadata: LoadTokenMetadata + where + Self: Sized; + /// Root storage slot. fn root() -> Slot<()> { Slot::root(DefaultStorageKey::Nep171) @@ -122,6 +165,10 @@ pub trait Nep171Controller { where Self: Sized; + type LoadTokenMetadata: LoadTokenMetadata + where + Self: Sized; + /// Transfer a token from `sender_id` to `receiver_id`. Checks that the transfer is valid using [`Nep171Controller::check_transfer`] before performing the transfer. fn external_transfer(&mut self, transfer: &Nep171Transfer) -> Result<(), Nep171TransferError> where @@ -176,9 +223,7 @@ pub trait Nep171Controller { fn token_owner(&self, token_id: &TokenId) -> Option; /// Loads the metadata associated with a token. - fn load_token>(&self, token_id: &TokenId) -> Option - where - Self: Sized; + fn load_token(&self, token_id: &TokenId) -> Option; } /// Transfer metadata generic over both types of transfer (`nft_transfer` and @@ -243,11 +288,12 @@ impl CheckExternalTransfer for DefaultCheckExternalTrans .into()); } } - Nep171TransferAuthorization::ApprovalId(_) => { + Nep171TransferAuthorization::ApprovalId(approval_id) => { return Err(error::SenderNotApprovedError { owner_id, sender_id: transfer.sender_id.clone(), token_id: transfer.token_id.clone(), + approval_id, } .into()) } @@ -313,6 +359,7 @@ where impl Nep171Controller for T { type CheckExternalTransfer = ::CheckExternalTransfer; + type LoadTokenMetadata = ::LoadTokenMetadata; fn external_transfer(&mut self, transfer: &Nep171Transfer) -> Result<(), Nep171TransferError> { match Self::CheckExternalTransfer::check_external_transfer(self, transfer) { @@ -330,51 +377,6 @@ impl Nep171Controller for T { } } - // fn check_transfer( - // &self, - // token_ids: &[TokenId], - // authorization: Nep171TransferAuthorization, - // sender_id: &AccountId, - // receiver_id: &AccountId, - // ) -> Result { - // for token_id in token_ids { - // let slot = Self::slot_token_owner(token_id); - - // let owner_id = slot.read().ok_or_else(|| error::TokenDoesNotExistError { - // token_id: token_id.clone(), - // })?; - - // match authorization { - // Nep171TransferAuthorization::Owner => { - // if sender_id != &owner_id { - // return Err(error::TokenNotOwnedByExpectedOwnerError { - // expected_owner_id: sender_id.clone(), - // actual_owner_id: owner_id, - // token_id: token_id.clone(), - // } - // .into()); - // } - // } - // Nep171TransferAuthorization::ApprovalId(_) => { - // return Err(error::SenderNotApprovedError { - // sender_id: sender_id.clone(), - // token_id: token_id.clone(), - // } - // .into()) - // } - // } - - // if receiver_id == &owner_id { - // return Err(error::TokenReceiverIsCurrentOwnerError { - // current_owner_id: owner_id, - // token_id: token_id.clone(), - // } - // .into()); - // } - // } - // Ok(()) - // } - fn transfer_unchecked( &mut self, token_ids: &[TokenId], @@ -490,9 +492,9 @@ impl Nep171Controller for T { Self::slot_token_owner(token_id).read() } - fn load_token>(&self, token_id: &TokenId) -> Option { + fn load_token(&self, token_id: &TokenId) -> Option { let mut metadata = std::collections::HashMap::new(); - L::load(self, token_id, &mut metadata).ok()?; + Self::LoadTokenMetadata::load(self, token_id, &mut metadata).ok()?; Some(Token { token_id: token_id.clone(), owner_id: self.token_owner(token_id)?, diff --git a/src/standard/nep178.rs b/src/standard/nep178.rs index 93c4b9a..ec702ad 100644 --- a/src/standard/nep178.rs +++ b/src/standard/nep178.rs @@ -109,7 +109,7 @@ pub trait Nep178ControllerInternal { #[derive(Error, Debug)] pub enum Nep178ApproveError { /// The account is not authorized to approve the token. - #[error("Account `{account_id}` is cannot create approvals for token `{token_id}`.")] + #[error("Account `{account_id}` cannot create approvals for token `{token_id}`.")] Unauthorized { /// The token ID. token_id: TokenId, diff --git a/tests/macros/standard/mod.rs b/tests/macros/standard/mod.rs index 6685e67..5001351 100644 --- a/tests/macros/standard/mod.rs +++ b/tests/macros/standard/mod.rs @@ -2,3 +2,4 @@ pub mod fungible_token; pub mod nep141; pub mod nep148; pub mod nep171; +pub mod non_fungible_token; diff --git a/tests/macros/standard/non_fungible_token.rs b/tests/macros/standard/non_fungible_token.rs new file mode 100644 index 0000000..1c99781 --- /dev/null +++ b/tests/macros/standard/non_fungible_token.rs @@ -0,0 +1,31 @@ +use near_sdk::{ + borsh::{self, *}, + *, +}; +use near_sdk_contract_tools::{standard::nep171::*, Nep171}; + +#[derive(BorshSerialize, BorshDeserialize, PanicOnDefault, Nep171)] +#[near_bindgen] +pub struct Contract { + transfers: u32, +} + +impl Nep171Hook for Contract { + fn before_nft_transfer(_contract: &Self, transfer: &Nep171Transfer) { + log!( + "{} is transferring {} to {}", + transfer.sender_id, + transfer.token_id, + transfer.receiver_id, + ); + } + + fn after_nft_transfer(contract: &mut Self, _transfer: &Nep171Transfer, _: ()) { + contract.transfers += 1; + } +} + +#[derive(BorshSerialize, BorshDeserialize, PanicOnDefault, Nep171)] +#[nep171(no_hooks)] +#[near_bindgen] +pub struct Contract1 {} diff --git a/workspaces-tests/tests/non_fungible_token.rs b/workspaces-tests/tests/non_fungible_token.rs index 88e5b8a..baf3fc6 100644 --- a/workspaces-tests/tests/non_fungible_token.rs +++ b/workspaces-tests/tests/non_fungible_token.rs @@ -2,10 +2,12 @@ use near_sdk::serde_json::json; use near_sdk_contract_tools::standard::{ - nep171::{event::NftTransferLog, Nep171Event, Token}, + nep171::{self, event::NftTransferLog, Nep171Event, Token}, nep177::{self, TokenMetadata}, + nep178, nep297::Event, }; +use tokio::task::JoinSet; use workspaces::operations::Function; use workspaces_tests_utils::{expect_execution_error, nft_token, setup, Setup}; @@ -719,3 +721,247 @@ async fn transfer_approval_success() { }), ); } + +#[tokio::test] +async fn transfer_approval_unapproved_fail() { + let Setup { contract, accounts } = + setup_balances(WASM_FULL, 4, |i| vec![format!("token_{i}")]).await; + let alice = &accounts[0]; + let bob = &accounts[1]; + let charlie = &accounts[2]; + let debbie = &accounts[3]; + + alice + .call(contract.id(), "nft_approve") + .args_json(json!({ + "token_id": "token_0", + "account_id": debbie.id(), + })) + .deposit(1) + .transact() + .await + .unwrap() + .unwrap(); + + let is_approved = contract + .view("nft_is_approved") + .args_json(json!({ + "token_id": "token_0", + "approved_account_id": bob.id().to_string(), + })) + .await + .unwrap() + .json::() + .unwrap(); + + assert!(!is_approved); + + let result = bob + .call(contract.id(), "nft_transfer") + .args_json(json!({ + "token_id": "token_0", + "approval_id": 0, + "receiver_id": charlie.id().to_string(), + })) + .deposit(1) + .transact() + .await + .unwrap(); + + let expected_error_message = format!( + "Smart contract panicked: {}", + nep171::error::SenderNotApprovedError { + owner_id: alice.id().parse().unwrap(), + sender_id: bob.id().parse().unwrap(), + token_id: "token_0".to_string(), + approval_id: 0, + } + ); + + expect_execution_error(&result, expected_error_message); +} + +#[tokio::test] +#[should_panic = "Attached deposit must be greater than zero"] +async fn transfer_approval_no_deposit_fail() { + let Setup { contract, accounts } = + setup_balances(WASM_FULL, 2, |i| vec![format!("token_{i}")]).await; + let alice = &accounts[0]; + let bob = &accounts[1]; + + alice + .call(contract.id(), "nft_approve") + .args_json(json!({ + "token_id": "token_0", + "account_id": bob.id(), + })) + .transact() + .await + .unwrap() + .unwrap(); +} + +#[tokio::test] +async fn transfer_approval_double_approval_fail() { + let Setup { contract, accounts } = + setup_balances(WASM_FULL, 2, |i| vec![format!("token_{i}")]).await; + let alice = &accounts[0]; + let bob = &accounts[1]; + + alice + .call(contract.id(), "nft_approve") + .args_json(json!({ + "token_id": "token_0", + "account_id": bob.id(), + })) + .deposit(1) + .transact() + .await + .unwrap() + .unwrap(); + + let result = alice + .call(contract.id(), "nft_approve") + .args_json(json!({ + "token_id": "token_0", + "account_id": bob.id(), + })) + .deposit(1) + .transact() + .await + .unwrap(); + + let expected_error = format!( + "Smart contract panicked: {}", + nep178::Nep178ApproveError::AccountAlreadyApproved { + account_id: bob.id().parse().unwrap(), + token_id: "token_0".to_string(), + }, + ); + + expect_execution_error(&result, expected_error); +} + +#[tokio::test] +async fn transfer_approval_unauthorized_approval_fail() { + let Setup { contract, accounts } = + setup_balances(WASM_FULL, 2, |i| vec![format!("token_{i}")]).await; + let _alice = &accounts[0]; + let bob = &accounts[1]; + + let result = bob + .call(contract.id(), "nft_approve") + .args_json(json!({ + "token_id": "token_0", + "account_id": bob.id(), + })) + .deposit(1) + .transact() + .await + .unwrap(); + + let expected_error = format!( + "Smart contract panicked: {}", + nep178::Nep178ApproveError::Unauthorized { + account_id: bob.id().parse().unwrap(), + token_id: "token_0".to_string(), + }, + ); + + expect_execution_error(&result, expected_error); +} + +#[tokio::test] +async fn transfer_approval_too_many_approvals_fail() { + let Setup { contract, accounts } = + setup_balances(WASM_FULL, 2, |i| vec![format!("token_{i}")]).await; + let alice = &accounts[0]; + let bob = &accounts[1]; + + let mut set = JoinSet::new(); + + for i in 0..32 { + let contract = contract.clone(); + let alice = alice.clone(); + set.spawn(async move { + alice + .call(contract.id(), "nft_approve") + .args_json(json!({ + "token_id": "token_0", + "account_id": format!("account_{}", i), + })) + .deposit(1) + .transact() + .await + .unwrap() + .unwrap(); + }); + } + + while (set.join_next().await).is_some() {} + + let result = alice + .call(contract.id(), "nft_approve") + .args_json(json!({ + "token_id": "token_0", + "account_id": bob.id(), + })) + .deposit(1) + .transact() + .await + .unwrap(); + + let expected_error = format!( + "Smart contract panicked: {}", + nep178::Nep178ApproveError::TooManyApprovals { + token_id: "token_0".to_string(), + }, + ); + + expect_execution_error(&result, expected_error); +} + +#[tokio::test] +async fn transfer_approval_approved_but_wrong_approval_id_fail() { + let Setup { contract, accounts } = + setup_balances(WASM_FULL, 3, |i| vec![format!("token_{i}")]).await; + let alice = &accounts[0]; + let bob = &accounts[1]; + let charlie = &accounts[2]; + + alice + .call(contract.id(), "nft_approve") + .args_json(json!({ + "token_id": "token_0", + "account_id": bob.id(), + })) + .deposit(1) + .transact() + .await + .unwrap() + .unwrap(); + + let result = bob + .call(contract.id(), "nft_transfer") + .args_json(json!({ + "token_id": "token_0", + "approval_id": 1, + "receiver_id": charlie.id().to_string(), + })) + .deposit(1) + .transact() + .await + .unwrap(); + + let expected_error = format!( + "Smart contract panicked: {}", + nep171::Nep171TransferError::SenderNotApproved(nep171::error::SenderNotApprovedError { + sender_id: bob.id().parse().unwrap(), + owner_id: alice.id().parse().unwrap(), + token_id: "token_0".to_string(), + approval_id: 1, + }), + ); + + expect_execution_error(&result, expected_error); +} From 7ba549f5e5e3b508cb13b252909c87a774e9d32e Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Tue, 29 Aug 2023 00:07:55 +0900 Subject: [PATCH 26/34] chore: lots of docs --- Cargo.toml | 8 +- macros/src/lib.rs | 12 +-- macros/src/standard/nep171.rs | 10 +-- macros/src/standard/non_fungible_token.rs | 2 +- src/lib.rs | 17 ++-- src/slot.rs | 3 - src/standard/nep171/mod.rs | 57 ++++++------- src/standard/nep177.rs | 81 +++++++++++++++++++ src/upgrade/mod.rs | 5 +- tests/macros/standard/mod.rs | 1 - .../hooks.rs} | 13 +-- .../standard/nep171/manual_integration.rs | 72 +++++++++++++++++ .../standard/{nep171.rs => nep171/mod.rs} | 10 ++- tests/macros/standard/nep171/no_hooks.rs | 28 +++++++ .../standard/nep171/non_fungible_token.rs | 60 ++++++++++++++ 15 files changed, 307 insertions(+), 72 deletions(-) rename tests/macros/standard/{non_fungible_token.rs => nep171/hooks.rs} (72%) create mode 100644 tests/macros/standard/nep171/manual_integration.rs rename tests/macros/standard/{nep171.rs => nep171/mod.rs} (97%) create mode 100644 tests/macros/standard/nep171/no_hooks.rs create mode 100644 tests/macros/standard/nep171/non_fungible_token.rs diff --git a/Cargo.toml b/Cargo.toml index 16c0f47..1afeb89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,9 @@ near-sdk = { workspace = true, default-features = false, features = [ [features] unstable = ["near-sdk/unstable"] +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + [workspace] -members = [".", "macros", "workspaces-tests", -"workspaces-tests-utils" -] +members = [".", "macros", "workspaces-tests", "workspaces-tests-utils"] diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 9ecc4f0..f49a540 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -110,7 +110,7 @@ pub fn derive_rbac(input: TokenStream) -> TokenStream { /// Adds NEP-141 fungible token core functionality to a contract. Exposes /// `ft_*` functions to the public blockchain, implements internal controller -/// and receiver functionality (see: [`near_sdk_contract_tools::standard::nep141`]). +/// and receiver functionality. /// /// The storage key prefix for the fields can be optionally specified (default: /// `"~$141"`) using `#[nep141(storage_key = "")]`. @@ -149,14 +149,16 @@ pub fn derive_fungible_token(input: TokenStream) -> TokenStream { /// Adds NEP-171 non-fungible token core functionality to a contract. Exposes /// `nft_*` functions to the public blockchain, implements internal controller -/// and receiver functionality (see: [`near_sdk_contract_tools::standard::nep171`]). +/// and receiver functionality. /// /// The storage key prefix for the fields can be optionally specified (default: /// `"~$171"`) using `#[nep171(storage_key = "")]`. /// /// Fields: -/// - `no_hooks`: Flag. Removes the requirement for the contract to implement [`near_sdk_contract_tools::standard::nep171::Nep171Hooks`]. -/// - `token_type`: specify the token metadata loading extensions invoked by `nft_token`. +/// - `no_hooks`: Flag. Removes the requirement for the contract to implement +/// transfer hooks. +/// - `token_data`: specify the token metadata loading extensions invoked by +/// `nft_token`. #[proc_macro_derive(Nep171, attributes(nep171))] pub fn derive_nep171(input: TokenStream) -> TokenStream { make_derive(input, standard::nep171::expand) @@ -180,7 +182,7 @@ pub fn derive_nep178(input: TokenStream) -> TokenStream { make_derive(input, standard::nep178::expand) } -/// Implements all NFT functionality at once, like `#[derive(Nep171, Nep177)]`. +/// Implements all NFT functionality at once, like `#[derive(Nep171, Nep177, Nep178)]`. #[proc_macro_derive(NonFungibleToken, attributes(non_fungible_token))] pub fn derive_non_fungible_token(input: TokenStream) -> TokenStream { make_derive(input, standard::non_fungible_token::expand) diff --git a/macros/src/standard/nep171.rs b/macros/src/standard/nep171.rs index 3ee0084..7664d15 100644 --- a/macros/src/standard/nep171.rs +++ b/macros/src/standard/nep171.rs @@ -12,7 +12,7 @@ pub struct Nep171Meta { pub no_hooks: Flag, pub extension_hooks: Option, pub check_external_transfer: Option, - pub token_type: Option, + pub token_data: Option, pub generics: syn::Generics, pub ident: syn::Ident, @@ -30,7 +30,7 @@ pub fn expand(meta: Nep171Meta) -> Result { no_hooks, extension_hooks, check_external_transfer, - token_type, + token_data, generics, ident, @@ -41,8 +41,8 @@ pub fn expand(meta: Nep171Meta) -> Result { let (imp, ty, wher) = generics.split_for_impl(); - let token_type = token_type - .map(|token_type| quote! { #token_type }) + let token_data = token_data + .map(|token_data| quote! { #token_data }) .unwrap_or_else(|| { quote! { () } }); @@ -98,7 +98,7 @@ pub fn expand(meta: Nep171Meta) -> Result { Ok(quote! { impl #imp #me::standard::nep171::Nep171ControllerInternal for #ident #ty #wher { type CheckExternalTransfer = #check_external_transfer; - type LoadTokenMetadata = #token_type; + type LoadTokenMetadata = #token_data; #root } diff --git a/macros/src/standard/non_fungible_token.rs b/macros/src/standard/non_fungible_token.rs index 2c13dee..df63f2c 100644 --- a/macros/src/standard/non_fungible_token.rs +++ b/macros/src/standard/non_fungible_token.rs @@ -53,7 +53,7 @@ pub fn expand(meta: NonFungibleTokenMeta) -> Result extension_hooks: Some(syn::parse_quote! { #me::standard::nep178::TokenApprovals }), check_external_transfer: Some(syn::parse_quote! { #me::standard::nep178::TokenApprovals }), - token_type: Some( + token_data: Some( syn::parse_quote! { (#me::standard::nep177::TokenMetadata, #me::standard::nep178::TokenApprovals) }, ), diff --git a/src/lib.rs b/src/lib.rs index 03efe04..66746cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ #![doc = include_str!("../README.md")] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] use near_sdk::IntoStorageKey; pub use near_sdk_contract_tools_macros::*; @@ -6,21 +7,21 @@ pub use near_sdk_contract_tools_macros::*; /// Default storage keys used by various traits' `root()` functions. #[derive(Clone, Debug)] pub enum DefaultStorageKey { - /// Default storage key for [`approval::ApprovalManager::root`] + /// Default storage key for [`approval::ApprovalManagerInternal::root`]. ApprovalManager, - /// Default storage key for [`standard::nep141::Nep141Controller::root`] + /// Default storage key for [`standard::nep141::Nep141ControllerInternal::root`]. Nep141, - /// Default storage key for [`standard::nep171::Nep171Controller::root`] + /// Default storage key for [`standard::nep171::Nep171ControllerInternal::root`]. Nep171, - /// Default storage key for [`standard::nep177::Nep177Controller::root`] + /// Default storage key for [`standard::nep177::Nep177ControllerInternal::root`]. Nep177, - /// Default storage key for [`standard::nep178::Nep177Controller::root`] + /// Default storage key for [`standard::nep178::Nep178ControllerInternal::root`]. Nep178, - /// Default storage key for [`owner::Owner::root`] + /// Default storage key for [`owner::OwnerInternal::root`]. Owner, - /// Default storage key for [`pause::Pause::root`] + /// Default storage key for [`pause::PauseInternal::root`]. Pause, - /// Default storage key for [`rbac::Rbac::root`] + /// Default storage key for [`rbac::RbacInternal::root`]. Rbac, } diff --git a/src/slot.rs b/src/slot.rs index 577e1ba..18c3f53 100644 --- a/src/slot.rs +++ b/src/slot.rs @@ -101,9 +101,6 @@ impl Slot { /// If the given value is `Some(T)`, writes `T` to storage. Otherwise, /// removes the key from storage. - /// - /// Use of this method makes the slot function similarly to - /// [`near_sdk::collections::LazyOption`]. pub fn set(&mut self, value: Option<&T>) -> bool { match value { Some(value) => self.write(value), diff --git a/src/standard/nep171/mod.rs b/src/standard/nep171/mod.rs index 199b041..1697f73 100644 --- a/src/standard/nep171/mod.rs +++ b/src/standard/nep171/mod.rs @@ -4,41 +4,37 @@ //! //! # Usage //! -//! It is recommended to use the [`near_sdk_contract_tools::Nep171`] derive macro or the [`near_sdk_contract_tools::NonFungibleToken`] macro to implement NEP-171 with this crate. +//! It is recommended to use the [`near_sdk_contract_tools_macros::Nep171`] +//! derive macro or the [`near_sdk_contract_tools_macros::NonFungibleToken`] +//! macro to implement NEP-171 with this crate. //! //! ## Basic implementation with no transfer hooks //! //! ```rust -//! use near_sdk_contract_tools::{Nep171, standard::nep171::*}; -//! use near_sdk::{*, borsh::{self, *}}; -//! -//! #[derive(BorshSerialize, BorshDeserialize, PanicOnDefault, Nep171)] -//! #[nep171(no_hooks)] -//! #[near_bindgen] -//! pub struct Contract {} +#![doc = include_str!("../../../tests/macros/standard/nep171/no_hooks.rs")] //! ``` //! //! ## Basic implementation with transfer hooks //! //! ```rust -//! use near_sdk_contract_tools::{Nep171, standard::nep171::*}; -//! use near_sdk::{*, borsh::{self, *}}; +#![doc = include_str!("../../../tests/macros/standard/nep171/hooks.rs")] +//! ``` +//! +//! ## Using the `NonFungibleToken` derive macro for partially-automatic integration with other utilities +//! +//! The `NonFungibleToken` derive macro automatically wires up all of the NFT-related standards' implementations (NEP-171, NEP-177, NEP-178) for you. //! -//! #[derive(BorshSerialize, BorshDeserialize, PanicOnDefault, Nep171)] -//! #[near_bindgen] -//! pub struct Contract { -//! transfers: u32, -//! } +//! ```rust +#![doc = include_str!("../../../tests/macros/standard/nep171/non_fungible_token.rs")] +//! ``` //! -//! impl Nep171Hook for Contract { -//! fn before_nft_transfer(_contract: &Self, transfer: &Nep171Transfer) { -//! log!("{} is transferring {} to {}", transfer.sender_id, transfer.token_id, transfer.receiver_id); -//! } +//! ## Manual integration with other utilities //! -//! fn after_nft_transfer(contract: &mut Self, _transfer: &Nep171Transfer, _: ()) { -//! contract.transfers += 1; -//! } -//! } +//! Note: NFT-related utilities are automatically integrated with each other +//! when using the [`near_sdk_contract_tools_macros::NonFungibleToken`] derive +//! macro. +//! ```rust +#![doc = include_str!("../../../tests/macros/standard/nep171/manual_integration.rs")] //! ``` use std::error::Error; @@ -143,6 +139,7 @@ pub trait Nep171ControllerInternal { where Self: Sized; + /// Load additional token data into [`Token::extensions_metadata`]. type LoadTokenMetadata: LoadTokenMetadata where Self: Sized; @@ -165,25 +162,17 @@ pub trait Nep171Controller { where Self: Sized; + /// Load additional token data into [`Token::extensions_metadata`]. type LoadTokenMetadata: LoadTokenMetadata where Self: Sized; - /// Transfer a token from `sender_id` to `receiver_id`. Checks that the transfer is valid using [`Nep171Controller::check_transfer`] before performing the transfer. + /// Transfer a token from `sender_id` to `receiver_id`. Checks that the transfer is valid using [`CheckExternalTransfer::check_external_transfer`] before performing the transfer. fn external_transfer(&mut self, transfer: &Nep171Transfer) -> Result<(), Nep171TransferError> where Self: Sized; - // /// Check if a token transfer is valid without actually performing it. Returns the account ID of the current owner of the token. - // fn check_transfer( - // &self, - // token_ids: &[TokenId], - // authorization: Nep171TransferAuthorization, - // sender_id: &AccountId, - // receiver_id: &AccountId, - // ) -> Result; - - /// Performs a token transfer without running [`Nep171Controller::check_transfer`]. + /// Performs a token transfer without running [`CheckExternalTransfer::check_external_transfer`]. /// /// # Warning /// diff --git a/src/standard/nep177.rs b/src/standard/nep177.rs index 310f0b5..e508d1c 100644 --- a/src/standard/nep177.rs +++ b/src/standard/nep177.rs @@ -92,6 +92,7 @@ impl ContractMetadata { Eq, PartialOrd, Ord, + Default, )] #[serde(crate = "near_sdk::serde")] pub struct TokenMetadata { @@ -121,6 +122,86 @@ pub struct TokenMetadata { pub reference_hash: Option, } +// Builder pattern for TokenMetadata. +impl TokenMetadata { + /// Create a new `TokenMetadata` with all fields set to `None`. + pub fn new() -> Self { + Self::default() + } + + /// Set the title. + pub fn title(mut self, title: impl Into) -> Self { + self.title = Some(title.into()); + self + } + + /// Set the description. + pub fn description(mut self, description: impl Into) -> Self { + self.description = Some(description.into()); + self + } + + /// Set the media. + pub fn media(mut self, media: impl Into) -> Self { + self.media = Some(media.into()); + self + } + + /// Set the media hash. + pub fn media_hash(mut self, media_hash: impl Into) -> Self { + self.media_hash = Some(media_hash.into()); + self + } + + /// Set the copies. + pub fn copies(mut self, copies: impl Into) -> Self { + self.copies = Some(copies.into()); + self + } + + /// Set the time the token was issued. + pub fn issued_at(mut self, issued_at: impl Into) -> Self { + self.issued_at = Some(issued_at.into()); + self + } + + /// Set the time the token expires. + pub fn expires_at(mut self, expires_at: impl Into) -> Self { + self.expires_at = Some(expires_at.into()); + self + } + + /// Set the time the token starts being valid. + pub fn starts_at(mut self, starts_at: impl Into) -> Self { + self.starts_at = Some(starts_at.into()); + self + } + + /// Set the time the token was last updated. + pub fn updated_at(mut self, updated_at: impl Into) -> Self { + self.updated_at = Some(updated_at.into()); + self + } + + /// Set the extra data. + pub fn extra(mut self, extra: impl Into) -> Self { + self.extra = Some(extra.into()); + self + } + + /// Set the reference. + pub fn reference(mut self, reference: impl Into) -> Self { + self.reference = Some(reference.into()); + self + } + + /// Set the reference hash. + pub fn reference_hash(mut self, reference_hash: impl Into) -> Self { + self.reference_hash = Some(reference_hash.into()); + self + } +} + /// Error returned when trying to load token metadata that does not exist. #[derive(Error, Debug)] #[error("Token metadata does not exist: {0}")] diff --git a/src/upgrade/mod.rs b/src/upgrade/mod.rs index da17c2d..0c5b57d 100644 --- a/src/upgrade/mod.rs +++ b/src/upgrade/mod.rs @@ -14,7 +14,10 @@ //! migrated. This behaviour can be changed by providing a //! custom [`PostUpgrade`]. //! -//! The [`raw`] module is included mostly for legacy / compatibility reasons, +//! The +#![cfg_attr(feature = "unstable", doc = "[`raw`]")] +#![cfg_attr(not(feature = "unstable"), doc = "`raw` (feature: `unstable`)")] +//! module is included mostly for legacy / compatibility reasons, //! and for the niche efficiency use-case, since it allows for the most //! efficient binary serialization (though only by a little). However, it is //! more difficult to use and has more sharp edges. diff --git a/tests/macros/standard/mod.rs b/tests/macros/standard/mod.rs index 5001351..6685e67 100644 --- a/tests/macros/standard/mod.rs +++ b/tests/macros/standard/mod.rs @@ -2,4 +2,3 @@ pub mod fungible_token; pub mod nep141; pub mod nep148; pub mod nep171; -pub mod non_fungible_token; diff --git a/tests/macros/standard/non_fungible_token.rs b/tests/macros/standard/nep171/hooks.rs similarity index 72% rename from tests/macros/standard/non_fungible_token.rs rename to tests/macros/standard/nep171/hooks.rs index 1c99781..b613e12 100644 --- a/tests/macros/standard/non_fungible_token.rs +++ b/tests/macros/standard/nep171/hooks.rs @@ -1,13 +1,13 @@ use near_sdk::{ - borsh::{self, *}, - *, + borsh::{self, BorshDeserialize, BorshSerialize}, + log, near_bindgen, PanicOnDefault, }; use near_sdk_contract_tools::{standard::nep171::*, Nep171}; #[derive(BorshSerialize, BorshDeserialize, PanicOnDefault, Nep171)] #[near_bindgen] pub struct Contract { - transfers: u32, + transfer_count: u32, } impl Nep171Hook for Contract { @@ -21,11 +21,6 @@ impl Nep171Hook for Contract { } fn after_nft_transfer(contract: &mut Self, _transfer: &Nep171Transfer, _: ()) { - contract.transfers += 1; + contract.transfer_count += 1; } } - -#[derive(BorshSerialize, BorshDeserialize, PanicOnDefault, Nep171)] -#[nep171(no_hooks)] -#[near_bindgen] -pub struct Contract1 {} diff --git a/tests/macros/standard/nep171/manual_integration.rs b/tests/macros/standard/nep171/manual_integration.rs new file mode 100644 index 0000000..38a6b9a --- /dev/null +++ b/tests/macros/standard/nep171/manual_integration.rs @@ -0,0 +1,72 @@ +use near_sdk::{ + borsh::{self, BorshDeserialize, BorshSerialize}, + env, near_bindgen, PanicOnDefault, +}; +use near_sdk_contract_tools::{ + owner::Owner, + pause::Pause, + standard::{ + nep171::*, + nep177::{self, Nep177Controller}, + nep178, + }, + Nep171, Nep177, Nep178, Owner, Pause, +}; + +#[derive( + BorshSerialize, BorshDeserialize, PanicOnDefault, Nep171, Nep177, Nep178, Pause, Owner, +)] +#[nep171( + extension_hooks = "nep178::TokenApprovals", + check_external_transfer = "nep178::TokenApprovals", + token_data = "(nep177::TokenMetadata, nep178::TokenApprovals)" +)] +#[nep178(no_hooks)] +#[near_bindgen] +pub struct Contract { + next_token_id: u32, +} + +impl Nep171Hook for Contract { + fn before_nft_transfer(_contract: &Self, _transfer: &Nep171Transfer) { + Self::require_unpaused(); + } + + fn after_nft_transfer(_contract: &mut Self, _transfer: &Nep171Transfer, _: ()) {} +} + +#[near_bindgen] +impl Contract { + #[init] + pub fn new() -> Self { + let mut contract = Self { next_token_id: 0 }; + + contract.set_contract_metadata(nep177::ContractMetadata::new( + "My NFT".to_string(), + "MYNFT".to_string(), + None, + )); + + Owner::init(&mut contract, &env::predecessor_account_id()); + + contract + } + + pub fn mint(&mut self) -> TokenId { + Self::require_unpaused(); + + let token_id = format!("token_{}", self.next_token_id); + self.next_token_id += 1; + Nep177Controller::mint_with_metadata( + self, + token_id.clone(), + env::predecessor_account_id(), + nep177::TokenMetadata::new() + .title(format!("Token {token_id}")) + .description(format!("This is token {token_id}.")), + ) + .unwrap_or_else(|e| env::panic_str(&format!("Minting failed: {e}"))); + + token_id + } +} diff --git a/tests/macros/standard/nep171.rs b/tests/macros/standard/nep171/mod.rs similarity index 97% rename from tests/macros/standard/nep171.rs rename to tests/macros/standard/nep171/mod.rs index ea88d9b..5098d73 100644 --- a/tests/macros/standard/nep171.rs +++ b/tests/macros/standard/nep171/mod.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use near_sdk::{ borsh::{self, BorshDeserialize, BorshSerialize}, env, near_bindgen, store, AccountId, @@ -7,9 +9,13 @@ use near_sdk_contract_tools::{ nep171::*, nep177::{Nep177Controller, TokenMetadata}, }, - Nep171, + Nep171, NonFungibleToken, }; -use near_sdk_contract_tools_macros::NonFungibleToken; + +mod hooks; +mod manual_integration; +mod no_hooks; +mod non_fungible_token; #[derive(BorshDeserialize, BorshSerialize, Debug, Clone, PartialEq, PartialOrd)] struct TokenRecord { diff --git a/tests/macros/standard/nep171/no_hooks.rs b/tests/macros/standard/nep171/no_hooks.rs new file mode 100644 index 0000000..3342e45 --- /dev/null +++ b/tests/macros/standard/nep171/no_hooks.rs @@ -0,0 +1,28 @@ +use near_sdk::{ + borsh::{self, BorshDeserialize, BorshSerialize}, + env, near_bindgen, PanicOnDefault, +}; +use near_sdk_contract_tools::{standard::nep171::*, Nep171}; + +#[derive(BorshSerialize, BorshDeserialize, PanicOnDefault, Nep171)] +#[nep171(no_hooks)] +#[near_bindgen] +pub struct Contract { + pub next_token_id: u32, +} + +#[near_bindgen] +impl Contract { + pub fn mint(&mut self) -> TokenId { + let token_id = format!("token_{}", self.next_token_id); + self.next_token_id += 1; + Nep171Controller::mint( + self, + &[token_id.clone()], + &env::predecessor_account_id(), + None, + ) + .unwrap_or_else(|e| env::panic_str(&format!("Minting failed: {e}"))); + token_id + } +} diff --git a/tests/macros/standard/nep171/non_fungible_token.rs b/tests/macros/standard/nep171/non_fungible_token.rs new file mode 100644 index 0000000..cf9af1c --- /dev/null +++ b/tests/macros/standard/nep171/non_fungible_token.rs @@ -0,0 +1,60 @@ +use near_sdk::{ + borsh::{self, BorshDeserialize, BorshSerialize}, + env, near_bindgen, PanicOnDefault, +}; +use near_sdk_contract_tools::{ + owner::Owner, + pause::Pause, + standard::{nep171::*, nep177::*}, + NonFungibleToken, Owner, Pause, +}; + +#[derive(BorshSerialize, BorshDeserialize, PanicOnDefault, NonFungibleToken, Pause, Owner)] +#[non_fungible_token(no_approval_hooks)] +#[near_bindgen] +pub struct Contract { + next_token_id: u32, +} + +impl Nep171Hook for Contract { + fn before_nft_transfer(_contract: &Self, _transfer: &Nep171Transfer) { + Self::require_unpaused(); + } + + fn after_nft_transfer(_contract: &mut Self, _transfer: &Nep171Transfer, _: ()) {} +} + +#[near_bindgen] +impl Contract { + #[init] + pub fn new() -> Self { + let mut contract = Self { next_token_id: 0 }; + + contract.set_contract_metadata(ContractMetadata::new( + "My NFT".to_string(), + "MYNFT".to_string(), + None, + )); + + Owner::init(&mut contract, &env::predecessor_account_id()); + + contract + } + + pub fn mint(&mut self) -> TokenId { + Self::require_unpaused(); + + let token_id = format!("token_{}", self.next_token_id); + self.next_token_id += 1; + self.mint_with_metadata( + token_id.clone(), + env::predecessor_account_id(), + TokenMetadata::new() + .title(format!("Token {token_id}")) + .description(format!("This is token {token_id}.")), + ) + .unwrap_or_else(|e| env::panic_str(&format!("Minting failed: {e}"))); + + token_id + } +} From 9854111858ae69aee74a7a8c5146cbdc70d61562 Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Thu, 7 Sep 2023 21:59:12 +0900 Subject: [PATCH 27/34] feat: switch hook state to associated type --- macros/src/standard/nep171.rs | 53 +----- src/lib.rs | 3 + src/standard/mod.rs | 1 + src/standard/nep171/mod.rs | 164 +++++++++++++----- src/standard/nep178.rs | 14 +- src/standard/nep181.rs | 69 ++++++++ tests/macros/standard/nep171/hooks.rs | 4 +- .../standard/nep171/manual_integration.rs | 2 + tests/macros/standard/nep171/mod.rs | 4 +- .../standard/nep171/non_fungible_token.rs | 2 + .../src/bin/non_fungible_token_full.rs | 2 + .../src/bin/non_fungible_token_nep171.rs | 2 + 12 files changed, 220 insertions(+), 100 deletions(-) create mode 100644 src/standard/nep181.rs diff --git a/macros/src/standard/nep171.rs b/macros/src/standard/nep171.rs index 7664d15..00180a2 100644 --- a/macros/src/standard/nep171.rs +++ b/macros/src/standard/nep171.rs @@ -83,20 +83,9 @@ pub fn expand(meta: Nep171Meta) -> Result { let hooks_type = quote! { (#self_hooks_type, #extension_hooks_type) }; - let before_nft_transfer = no_hooks.is_present().not().then(|| { - quote! { - let hook_state = <#hooks_type as #me::standard::nep171::Nep171Hook::<_, _>>::before_nft_transfer(&self, &transfer); - } - }); - - let after_nft_transfer = no_hooks.is_present().not().then(|| { - quote! { - <#hooks_type as #me::standard::nep171::Nep171Hook::<_, _>>::after_nft_transfer(self, &transfer, hook_state); - } - }); - Ok(quote! { impl #imp #me::standard::nep171::Nep171ControllerInternal for #ident #ty #wher { + type Hook = #hooks_type; type CheckExternalTransfer = #check_external_transfer; type LoadTokenMetadata = #token_data; @@ -136,33 +125,15 @@ pub fn expand(meta: Nep171Meta) -> Result { let transfer = Nep171Transfer { token_id: &token_ids[0], authorization: Nep171TransferAuthorization::Owner, - sender_id: &receiver_id, + sender_id: Some(&receiver_id), receiver_id: &previous_owner_id, memo: None, msg: None, + revert: true, }; - let check_result = ::CheckExternalTransfer::check_external_transfer(self, &transfer); - - match check_result { - Ok(_) => { - #before_nft_transfer - - #me::standard::nep171::Nep171Controller::transfer_unchecked( - self, - &token_ids, - receiver_id.clone(), - receiver_id.clone(), - previous_owner_id.clone(), - None, - ); - - #after_nft_transfer - - false - }, - Err(_) => true, - } + ::external_transfer(self, &transfer) + .is_err() } else { true } @@ -190,18 +161,15 @@ pub fn expand(meta: Nep171Meta) -> Result { let transfer = Nep171Transfer { token_id: &token_ids[0], authorization: approval_id.map(Nep171TransferAuthorization::ApprovalId).unwrap_or(Nep171TransferAuthorization::Owner), - sender_id: &sender_id, + sender_id: Some(&sender_id), receiver_id: &receiver_id, memo: memo.as_deref(), msg: None, + revert: false, }; - #before_nft_transfer - ::external_transfer(self, &transfer) .unwrap_or_else(|e| #near_sdk::env::panic_str(&e.to_string())); - - #after_nft_transfer } #[payable] @@ -229,19 +197,16 @@ pub fn expand(meta: Nep171Meta) -> Result { let transfer = Nep171Transfer { token_id: &token_ids[0], authorization: approval_id.map(Nep171TransferAuthorization::ApprovalId).unwrap_or(Nep171TransferAuthorization::Owner), - sender_id: &sender_id, + sender_id: Some(&sender_id), receiver_id: &receiver_id, memo: memo.as_deref(), msg: Some(&msg), + revert: false, }; - #before_nft_transfer - ::external_transfer(self, &transfer) .unwrap_or_else(|e| #near_sdk::env::panic_str(&e.to_string())); - #after_nft_transfer - let [token_id] = token_ids; ext_nep171_receiver::ext(receiver_id.clone()) diff --git a/src/lib.rs b/src/lib.rs index 66746cf..f014a84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,8 @@ pub enum DefaultStorageKey { Nep177, /// Default storage key for [`standard::nep178::Nep178ControllerInternal::root`]. Nep178, + /// Default storage key for [`standard::nep181::Nep181ControllerInternal::root`]. + Nep181, /// Default storage key for [`owner::OwnerInternal::root`]. Owner, /// Default storage key for [`pause::PauseInternal::root`]. @@ -33,6 +35,7 @@ impl IntoStorageKey for DefaultStorageKey { DefaultStorageKey::Nep171 => b"~$171".to_vec(), DefaultStorageKey::Nep177 => b"~$177".to_vec(), DefaultStorageKey::Nep178 => b"~$178".to_vec(), + DefaultStorageKey::Nep181 => b"~$181".to_vec(), DefaultStorageKey::Owner => b"~o".to_vec(), DefaultStorageKey::Pause => b"~p".to_vec(), DefaultStorageKey::Rbac => b"~r".to_vec(), diff --git a/src/standard/mod.rs b/src/standard/mod.rs index ff78de2..faaac74 100644 --- a/src/standard/mod.rs +++ b/src/standard/mod.rs @@ -5,4 +5,5 @@ pub mod nep148; pub mod nep171; pub mod nep177; pub mod nep178; +pub mod nep181; pub mod nep297; diff --git a/src/standard/nep171/mod.rs b/src/standard/nep171/mod.rs index 1697f73..d82bd2b 100644 --- a/src/standard/nep171/mod.rs +++ b/src/standard/nep171/mod.rs @@ -134,6 +134,10 @@ pub enum Nep171TransferError { /// Internal (storage location) methods for implementors of [`Nep171Controller`]. pub trait Nep171ControllerInternal { + type Hook: Nep171Hook + where + Self: Sized; + /// Invoked during an external transfer. type CheckExternalTransfer: CheckExternalTransfer where @@ -157,6 +161,10 @@ pub trait Nep171ControllerInternal { /// Non-public controller interface for NEP-171 implementations. pub trait Nep171Controller { + type Hook: Nep171Hook + where + Self: Sized; + /// Invoked during an external transfer. type CheckExternalTransfer: CheckExternalTransfer where @@ -197,6 +205,13 @@ pub trait Nep171Controller { memo: Option, ) -> Result<(), Nep171MintError>; + fn mint_unchecked( + &mut self, + token_ids: &[TokenId], + new_owner_id: &AccountId, + memo: Option, + ); + /// Burns tokens `token_ids` owned by `current_owner_id`. fn burn( &mut self, @@ -221,8 +236,8 @@ pub trait Nep171Controller { pub struct Nep171Transfer<'a> { /// Why is this sender allowed to perform this transfer? pub authorization: Nep171TransferAuthorization, - /// Sending account ID. - pub sender_id: &'a AccountId, + /// Sending account ID. `None` when minting. + pub sender_id: Option<&'a AccountId>, /// Receiving account ID. pub receiver_id: &'a AccountId, /// Token ID. @@ -231,6 +246,8 @@ pub struct Nep171Transfer<'a> { pub memo: Option<&'a str>, /// Message passed to contract located at `receiver_id` in the case of `nft_transfer_call`. pub msg: Option<&'a str>, + /// `true` if the transfer is a revert for a `nft_transfer_call`. + pub revert: bool, } /// Authorization for a transfer. @@ -266,26 +283,29 @@ impl CheckExternalTransfer for DefaultCheckExternalTrans } })?; - match transfer.authorization { - Nep171TransferAuthorization::Owner => { - if transfer.sender_id != &owner_id { - return Err(error::TokenNotOwnedByExpectedOwnerError { - expected_owner_id: transfer.sender_id.clone(), + if let Some(sender_id) = transfer.sender_id { + // authorizations are only relevent when not minting + match transfer.authorization { + Nep171TransferAuthorization::Owner => { + if transfer.sender_id != Some(&owner_id) { + return Err(error::TokenNotOwnedByExpectedOwnerError { + expected_owner_id: sender_id.clone(), + owner_id, + token_id: transfer.token_id.clone(), + } + .into()); + } + } + Nep171TransferAuthorization::ApprovalId(approval_id) => { + return Err(error::SenderNotApprovedError { owner_id, + sender_id: sender_id.clone(), token_id: transfer.token_id.clone(), + approval_id, } - .into()); + .into()) } } - Nep171TransferAuthorization::ApprovalId(approval_id) => { - return Err(error::SenderNotApprovedError { - owner_id, - sender_id: transfer.sender_id.clone(), - token_id: transfer.token_id.clone(), - approval_id, - } - .into()) - } } if transfer.receiver_id == &owner_id { @@ -305,61 +325,100 @@ impl CheckExternalTransfer for DefaultCheckExternalTrans /// `T` is an optional value for passing state between different lifecycle /// hooks. This may be useful for charging callers for storage usage, for /// example. -pub trait Nep171Hook { +pub trait Nep171Hook { + type NftTransferState; + /// Executed before a token transfer is conducted. /// /// May return an optional state value which will be passed along to the /// following `after_transfer`. /// - /// MUST NOT PANIC. - fn before_nft_transfer(contract: &C, transfer: &Nep171Transfer) -> S; + /// MUST NOT PANIC if the transfer is a revert. + fn before_nft_transfer(contract: &C, transfer: &Nep171Transfer) -> Self::NftTransferState; /// Executed after a token transfer is conducted. /// /// Receives the state value returned by `before_transfer`. /// - /// MUST NOT PANIC. - fn after_nft_transfer(contract: &mut C, transfer: &Nep171Transfer, state: S); + /// MUST NOT PANIC if the transfer is a revert. + fn after_nft_transfer( + contract: &mut C, + transfer: &Nep171Transfer, + state: Self::NftTransferState, + ); + + // fn before_mint(contract: &C, + // token_ids: &[TokenId], + // new_owner_id: &AccountId, + // memo: Option, + // ) {} } -impl Nep171Hook<(), C> for () { +impl Nep171Hook for () { + type NftTransferState = (); + fn before_nft_transfer(_contract: &C, _transfer: &Nep171Transfer) {} fn after_nft_transfer(_contract: &mut C, _transfer: &Nep171Transfer, _state: ()) {} } -impl Nep171Hook<(Stat0, Stat1), Cont> for (Handl0, Handl1) +impl Nep171Hook for (Handl0, Handl1) where - Handl0: Nep171Hook, - Handl1: Nep171Hook, + Handl0: Nep171Hook, + Handl1: Nep171Hook, { - fn before_nft_transfer(contract: &Cont, transfer: &Nep171Transfer) -> (Stat0, Stat1) { + type NftTransferState = (Handl0::NftTransferState, Handl1::NftTransferState); + + fn before_nft_transfer( + contract: &Cont, + transfer: &Nep171Transfer, + ) -> (Handl0::NftTransferState, Handl1::NftTransferState) { ( Handl0::before_nft_transfer(contract, transfer), Handl1::before_nft_transfer(contract, transfer), ) } - fn after_nft_transfer(contract: &mut Cont, transfer: &Nep171Transfer, state: (Stat0, Stat1)) { + fn after_nft_transfer( + contract: &mut Cont, + transfer: &Nep171Transfer, + state: (Handl0::NftTransferState, Handl1::NftTransferState), + ) { Handl0::after_nft_transfer(contract, transfer, state.0); Handl1::after_nft_transfer(contract, transfer, state.1); } } impl Nep171Controller for T { + type Hook = ::Hook; type CheckExternalTransfer = ::CheckExternalTransfer; type LoadTokenMetadata = ::LoadTokenMetadata; fn external_transfer(&mut self, transfer: &Nep171Transfer) -> Result<(), Nep171TransferError> { match Self::CheckExternalTransfer::check_external_transfer(self, transfer) { Ok(current_owner_id) => { - self.transfer_unchecked( - &[transfer.token_id.to_string()], - current_owner_id, - transfer.sender_id.clone(), - transfer.receiver_id.clone(), - transfer.memo.map(ToString::to_string), - ); + let state = ::Hook::before_nft_transfer(self, transfer); + + if let Some(sender_id) = transfer.sender_id { + // transfer + self.transfer_unchecked( + &[transfer.token_id.to_string()], + current_owner_id, + sender_id.clone(), + transfer.receiver_id.clone(), + transfer.memo.map(ToString::to_string), + ); + } else { + // mint + self.mint_unchecked( + &[transfer.token_id.to_string()], + transfer.receiver_id, + transfer.memo.map(ToString::to_string), + ) + } + + ::Hook::after_nft_transfer(self, transfer, state); + Ok(()) } Err(e) => Err(e), @@ -391,24 +450,14 @@ impl Nep171Controller for T { } } - fn mint( + fn mint_unchecked( &mut self, token_ids: &[TokenId], new_owner_id: &AccountId, memo: Option, - ) -> Result<(), Nep171MintError> { + ) { if token_ids.is_empty() { - return Ok(()); - } - - for token_id in token_ids { - let slot = Self::slot_token_owner(token_id); - if slot.exists() { - return Err(error::TokenAlreadyExistsError { - token_id: token_id.to_string(), - } - .into()); - } + return; } Nep171Event::NftMint(vec![event::NftMintLog { @@ -422,6 +471,25 @@ impl Nep171Controller for T { let mut slot = Self::slot_token_owner(token_id); slot.write(new_owner_id); }); + } + + fn mint( + &mut self, + token_ids: &[TokenId], + new_owner_id: &AccountId, + memo: Option, + ) -> Result<(), Nep171MintError> { + for token_id in token_ids { + let slot = Self::slot_token_owner(token_id); + if slot.exists() { + return Err(error::TokenAlreadyExistsError { + token_id: token_id.to_string(), + } + .into()); + } + } + + self.mint_unchecked(token_ids, new_owner_id, memo); Ok(()) } diff --git a/src/standard/nep178.rs b/src/standard/nep178.rs index ec702ad..1da74fa 100644 --- a/src/standard/nep178.rs +++ b/src/standard/nep178.rs @@ -43,7 +43,9 @@ impl LoadTokenMetadata for TokenApprovals { } } -impl Nep171Hook<(), C> for TokenApprovals { +impl Nep171Hook for TokenApprovals { + type NftTransferState = (); + fn before_nft_transfer(_contract: &C, _transfer: &Nep171Transfer) {} fn after_nft_transfer(contract: &mut C, transfer: &Nep171Transfer, _: ()) { @@ -59,14 +61,14 @@ impl CheckExternalTransfer for TokenA let normal_check = DefaultCheckExternalTransfer::check_external_transfer(contract, transfer); - match (&transfer.authorization, normal_check) { - (_, Ok(current_owner_id)) => Ok(current_owner_id), + match (&transfer.authorization, &transfer.sender_id, normal_check) { + (_, _, r @ Ok(_)) => r, ( Nep171TransferAuthorization::ApprovalId(approval_id), + Some(sender_id), Err(Nep171TransferError::SenderNotApproved(s)), ) => { - let saved_approval = - contract.get_approval_id_for(transfer.token_id, transfer.sender_id); + let saved_approval = contract.get_approval_id_for(transfer.token_id, sender_id); if saved_approval == Some(*approval_id) { Ok(s.owner_id) @@ -74,7 +76,7 @@ impl CheckExternalTransfer for TokenA Err(s.into()) } } - (_, e) => e, + (_, _, e @ Err(_)) => e, } } } diff --git a/src/standard/nep181.rs b/src/standard/nep181.rs new file mode 100644 index 0000000..7c1614a --- /dev/null +++ b/src/standard/nep181.rs @@ -0,0 +1,69 @@ +//! NEP-181 non-fungible token contract metadata implementation. +//! +//! Reference: +use std::error::Error; + +use near_sdk::{ + borsh::{self, BorshDeserialize, BorshSerialize}, + env, + json_types::U64, + serde::*, + AccountId, BorshStorageKey, +}; +use thiserror::Error; + +use crate::{ + slot::Slot, + standard::{ + nep171::{ + self, + error::TokenDoesNotExistError, + event::{NftContractMetadataUpdateLog, NftMetadataUpdateLog}, + *, + }, + nep297::Event, + }, + DefaultStorageKey, +}; + +pub use ext::*; + +#[derive(BorshSerialize, BorshStorageKey)] +enum StorageKey { + X, +} + +/// Internal functions for [`Nep181Controller`]. +pub trait Nep181ControllerInternal { + /// Storage root. + fn root() -> Slot<()> { + Slot::root(DefaultStorageKey::Nep181) + } +} + +/// Functions for managing non-fungible tokens with attached metadata, NEP-181. +pub trait Nep181Controller {} + +impl Nep181Controller for T {} + +// separate module with re-export because ext_contract doesn't play well with #![warn(missing_docs)] +mod ext { + #![allow(missing_docs)] + + use near_sdk::json_types::U128; + + use super::*; + + #[near_sdk::ext_contract(ext_nep181)] + pub trait Nep181 { + fn nft_total_supply(&self) -> U128; + fn nft_tokens(&self, from_index: Option, limit: Option) -> Vec; + fn nft_supply_for_owner(&self, account_id: AccountId) -> U128; + fn nft_tokens_for_owner( + &self, + account_id: AccountId, + from_index: Option, + limit: Option, + ) -> Vec; + } +} diff --git a/tests/macros/standard/nep171/hooks.rs b/tests/macros/standard/nep171/hooks.rs index b613e12..8c7cfa5 100644 --- a/tests/macros/standard/nep171/hooks.rs +++ b/tests/macros/standard/nep171/hooks.rs @@ -11,9 +11,11 @@ pub struct Contract { } impl Nep171Hook for Contract { + type NftTransferState = (); + fn before_nft_transfer(_contract: &Self, transfer: &Nep171Transfer) { log!( - "{} is transferring {} to {}", + "{:?} is transferring {} to {}", transfer.sender_id, transfer.token_id, transfer.receiver_id, diff --git a/tests/macros/standard/nep171/manual_integration.rs b/tests/macros/standard/nep171/manual_integration.rs index 38a6b9a..6340d9f 100644 --- a/tests/macros/standard/nep171/manual_integration.rs +++ b/tests/macros/standard/nep171/manual_integration.rs @@ -28,6 +28,8 @@ pub struct Contract { } impl Nep171Hook for Contract { + type NftTransferState = (); + fn before_nft_transfer(_contract: &Self, _transfer: &Nep171Transfer) { Self::require_unpaused(); } diff --git a/tests/macros/standard/nep171/mod.rs b/tests/macros/standard/nep171/mod.rs index 5098d73..3a9dec1 100644 --- a/tests/macros/standard/nep171/mod.rs +++ b/tests/macros/standard/nep171/mod.rs @@ -80,7 +80,9 @@ struct NonFungibleToken { pub after_nft_transfer_balance_record: store::Vector>, } -impl Nep171Hook> for NonFungibleToken { +impl Nep171Hook for NonFungibleToken { + type NftTransferState = Option; + fn before_nft_transfer(contract: &Self, transfer: &Nep171Transfer) -> Option { let token = Nep171::nft_token(contract, transfer.token_id.clone()); token.map(Into::into) diff --git a/tests/macros/standard/nep171/non_fungible_token.rs b/tests/macros/standard/nep171/non_fungible_token.rs index cf9af1c..322ea9d 100644 --- a/tests/macros/standard/nep171/non_fungible_token.rs +++ b/tests/macros/standard/nep171/non_fungible_token.rs @@ -17,6 +17,8 @@ pub struct Contract { } impl Nep171Hook for Contract { + type NftTransferState = (); + fn before_nft_transfer(_contract: &Self, _transfer: &Nep171Transfer) { Self::require_unpaused(); } diff --git a/workspaces-tests/src/bin/non_fungible_token_full.rs b/workspaces-tests/src/bin/non_fungible_token_full.rs index 39be3f8..bc82163 100644 --- a/workspaces-tests/src/bin/non_fungible_token_full.rs +++ b/workspaces-tests/src/bin/non_fungible_token_full.rs @@ -49,6 +49,8 @@ impl Nep178Hook for Contract { } impl Nep171Hook for Contract { + type NftTransferState = (); + fn before_nft_transfer(_contract: &Self, transfer: &Nep171Transfer) { log!("before_nft_transfer({})", transfer.token_id); } diff --git a/workspaces-tests/src/bin/non_fungible_token_nep171.rs b/workspaces-tests/src/bin/non_fungible_token_nep171.rs index 234f27a..c22ea96 100644 --- a/workspaces-tests/src/bin/non_fungible_token_nep171.rs +++ b/workspaces-tests/src/bin/non_fungible_token_nep171.rs @@ -14,6 +14,8 @@ use near_sdk_contract_tools::{standard::nep171::*, Nep171}; pub struct Contract {} impl Nep171Hook for Contract { + type NftTransferState = (); + fn before_nft_transfer(_contract: &Self, transfer: &Nep171Transfer) { log!("before_nft_transfer({})", transfer.token_id); } From 1f72173f7490f38101a24ea2db6958bca060a39d Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Wed, 13 Sep 2023 02:36:01 +0900 Subject: [PATCH 28/34] feat: nep181 internals done, improved hooks --- macros/src/standard/nep171.rs | 6 +- src/standard/nep171/mod.rs | 180 ++++++++++++++---- src/standard/nep178.rs | 27 ++- src/standard/nep181.rs | 174 ++++++++++++++--- tests/macros/standard/nep171/hooks.rs | 10 +- .../standard/nep171/manual_integration.rs | 8 +- tests/macros/standard/nep171/mod.rs | 11 ++ .../standard/nep171/non_fungible_token.rs | 8 +- .../src/bin/non_fungible_token_full.rs | 8 +- .../src/bin/non_fungible_token_nep171.rs | 8 +- 10 files changed, 339 insertions(+), 101 deletions(-) diff --git a/macros/src/standard/nep171.rs b/macros/src/standard/nep171.rs index 00180a2..5220ec5 100644 --- a/macros/src/standard/nep171.rs +++ b/macros/src/standard/nep171.rs @@ -125,7 +125,7 @@ pub fn expand(meta: Nep171Meta) -> Result { let transfer = Nep171Transfer { token_id: &token_ids[0], authorization: Nep171TransferAuthorization::Owner, - sender_id: Some(&receiver_id), + sender_id: &receiver_id, receiver_id: &previous_owner_id, memo: None, msg: None, @@ -161,7 +161,7 @@ pub fn expand(meta: Nep171Meta) -> Result { let transfer = Nep171Transfer { token_id: &token_ids[0], authorization: approval_id.map(Nep171TransferAuthorization::ApprovalId).unwrap_or(Nep171TransferAuthorization::Owner), - sender_id: Some(&sender_id), + sender_id: &sender_id, receiver_id: &receiver_id, memo: memo.as_deref(), msg: None, @@ -197,7 +197,7 @@ pub fn expand(meta: Nep171Meta) -> Result { let transfer = Nep171Transfer { token_id: &token_ids[0], authorization: approval_id.map(Nep171TransferAuthorization::ApprovalId).unwrap_or(Nep171TransferAuthorization::Owner), - sender_id: Some(&sender_id), + sender_id: &sender_id, receiver_id: &receiver_id, memo: memo.as_deref(), msg: Some(&msg), diff --git a/src/standard/nep171/mod.rs b/src/standard/nep171/mod.rs index d82bd2b..3d38aa0 100644 --- a/src/standard/nep171/mod.rs +++ b/src/standard/nep171/mod.rs @@ -236,8 +236,8 @@ pub trait Nep171Controller { pub struct Nep171Transfer<'a> { /// Why is this sender allowed to perform this transfer? pub authorization: Nep171TransferAuthorization, - /// Sending account ID. `None` when minting. - pub sender_id: Option<&'a AccountId>, + /// Sending account ID. + pub sender_id: &'a AccountId, /// Receiving account ID. pub receiver_id: &'a AccountId, /// Token ID. @@ -283,29 +283,26 @@ impl CheckExternalTransfer for DefaultCheckExternalTrans } })?; - if let Some(sender_id) = transfer.sender_id { - // authorizations are only relevent when not minting - match transfer.authorization { - Nep171TransferAuthorization::Owner => { - if transfer.sender_id != Some(&owner_id) { - return Err(error::TokenNotOwnedByExpectedOwnerError { - expected_owner_id: sender_id.clone(), - owner_id, - token_id: transfer.token_id.clone(), - } - .into()); - } - } - Nep171TransferAuthorization::ApprovalId(approval_id) => { - return Err(error::SenderNotApprovedError { + match transfer.authorization { + Nep171TransferAuthorization::Owner => { + if transfer.sender_id != &owner_id { + return Err(error::TokenNotOwnedByExpectedOwnerError { + expected_owner_id: transfer.sender_id.clone(), owner_id, - sender_id: sender_id.clone(), token_id: transfer.token_id.clone(), - approval_id, } - .into()) + .into()); } } + Nep171TransferAuthorization::ApprovalId(approval_id) => { + return Err(error::SenderNotApprovedError { + owner_id, + sender_id: transfer.sender_id.clone(), + token_id: transfer.token_id.clone(), + approval_id, + } + .into()) + } } if transfer.receiver_id == &owner_id { @@ -327,6 +324,8 @@ impl CheckExternalTransfer for DefaultCheckExternalTrans /// example. pub trait Nep171Hook { type NftTransferState; + type MintState; + type BurnState; /// Executed before a token transfer is conducted. /// @@ -347,19 +346,51 @@ pub trait Nep171Hook { state: Self::NftTransferState, ); - // fn before_mint(contract: &C, - // token_ids: &[TokenId], - // new_owner_id: &AccountId, - // memo: Option, - // ) {} + fn before_mint(contract: &C, token_id: &TokenId, owner_id: &AccountId) -> Self::MintState; + fn after_mint( + contract: &mut C, + token_id: &TokenId, + owner_id: &AccountId, + state: Self::MintState, + ); + + fn before_burn(contract: &C, token_id: &TokenId, owner_id: &AccountId) -> Self::BurnState; + fn after_burn( + contract: &mut C, + token_id: &TokenId, + owner_id: &AccountId, + state: Self::BurnState, + ); } impl Nep171Hook for () { type NftTransferState = (); + type MintState = (); + type BurnState = (); fn before_nft_transfer(_contract: &C, _transfer: &Nep171Transfer) {} fn after_nft_transfer(_contract: &mut C, _transfer: &Nep171Transfer, _state: ()) {} + + fn before_mint(_contract: &C, _token_id: &TokenId, _owner_id: &AccountId) -> Self::MintState {} + + fn after_mint( + _contract: &mut C, + _token_id: &TokenId, + _owner_id: &AccountId, + _state: Self::MintState, + ) { + } + + fn before_burn(_contract: &C, _token_id: &TokenId, _owner_id: &AccountId) -> Self::BurnState {} + + fn after_burn( + _contract: &mut C, + _token_id: &TokenId, + _owner_id: &AccountId, + _state: Self::BurnState, + ) { + } } impl Nep171Hook for (Handl0, Handl1) @@ -367,7 +398,26 @@ where Handl0: Nep171Hook, Handl1: Nep171Hook, { + type MintState = (Handl0::MintState, Handl1::MintState); type NftTransferState = (Handl0::NftTransferState, Handl1::NftTransferState); + type BurnState = (Handl0::BurnState, Handl1::BurnState); + + fn before_mint(contract: &Cont, token_id: &TokenId, owner_id: &AccountId) -> Self::MintState { + ( + Handl0::before_mint(contract, token_id, owner_id), + Handl1::before_mint(contract, token_id, owner_id), + ) + } + + fn after_mint( + contract: &mut Cont, + token_id: &TokenId, + owner_id: &AccountId, + state: Self::MintState, + ) { + Handl0::after_mint(contract, token_id, owner_id, state.0); + Handl1::after_mint(contract, token_id, owner_id, state.1); + } fn before_nft_transfer( contract: &Cont, @@ -387,6 +437,62 @@ where Handl0::after_nft_transfer(contract, transfer, state.0); Handl1::after_nft_transfer(contract, transfer, state.1); } + + fn before_burn(contract: &Cont, token_id: &TokenId, owner_id: &AccountId) -> Self::BurnState { + ( + Handl0::before_burn(contract, token_id, owner_id), + Handl1::before_burn(contract, token_id, owner_id), + ) + } + + fn after_burn( + contract: &mut Cont, + token_id: &TokenId, + owner_id: &AccountId, + state: Self::BurnState, + ) { + Handl0::after_burn(contract, token_id, owner_id, state.0); + Handl1::after_burn(contract, token_id, owner_id, state.1); + } +} + +pub trait SimpleNep171Hook { + fn before_mint(&self, _token_id: &TokenId, _owner_id: &AccountId) {} + fn after_mint(&mut self, _token_id: &TokenId, _owner_id: &AccountId) {} + fn before_nft_transfer(&self, _transfer: &Nep171Transfer) {} + fn after_nft_transfer(&mut self, _transfer: &Nep171Transfer) {} + fn before_burn(&self, _token_id: &TokenId, _owner_id: &AccountId) {} + fn after_burn(&mut self, _token_id: &TokenId, _owner_id: &AccountId) {} +} + +impl Nep171Hook for T { + type MintState = (); + type NftTransferState = (); + type BurnState = (); + + fn before_mint(contract: &Self, token_id: &TokenId, owner_id: &AccountId) { + SimpleNep171Hook::before_mint(contract, token_id, owner_id); + } + + fn after_mint(contract: &mut Self, token_id: &TokenId, owner_id: &AccountId, _: ()) { + SimpleNep171Hook::after_burn(contract, token_id, owner_id); + } + + fn before_nft_transfer(contract: &T, transfer: &Nep171Transfer) { + SimpleNep171Hook::before_nft_transfer(contract, transfer); + } + + fn after_nft_transfer(contract: &mut T, transfer: &Nep171Transfer, _: ()) { + SimpleNep171Hook::after_nft_transfer(contract, transfer); + } + + fn before_burn(contract: &T, token_id: &TokenId, owner_id: &AccountId) { + SimpleNep171Hook::before_burn(contract, token_id, owner_id); + } + + fn after_burn(contract: &mut T, token_id: &TokenId, owner_id: &AccountId, _: ()) { + SimpleNep171Hook::after_burn(contract, token_id, owner_id); + } } impl Nep171Controller for T { @@ -399,23 +505,13 @@ impl Nep171Controller for T { Ok(current_owner_id) => { let state = ::Hook::before_nft_transfer(self, transfer); - if let Some(sender_id) = transfer.sender_id { - // transfer - self.transfer_unchecked( - &[transfer.token_id.to_string()], - current_owner_id, - sender_id.clone(), - transfer.receiver_id.clone(), - transfer.memo.map(ToString::to_string), - ); - } else { - // mint - self.mint_unchecked( - &[transfer.token_id.to_string()], - transfer.receiver_id, - transfer.memo.map(ToString::to_string), - ) - } + self.transfer_unchecked( + &[transfer.token_id.to_string()], + current_owner_id, + transfer.sender_id.clone(), + transfer.receiver_id.clone(), + transfer.memo.map(ToString::to_string), + ); ::Hook::after_nft_transfer(self, transfer, state); diff --git a/src/standard/nep178.rs b/src/standard/nep178.rs index 1da74fa..2c68f7e 100644 --- a/src/standard/nep178.rs +++ b/src/standard/nep178.rs @@ -44,13 +44,30 @@ impl LoadTokenMetadata for TokenApprovals { } impl Nep171Hook for TokenApprovals { + type MintState = (); type NftTransferState = (); + type BurnState = (); + + fn before_mint(_contract: &C, _token_id: &TokenId, owner_id: &AccountId) {} + + fn after_mint(_contract: &mut C, _token_id: &TokenId, owner_id: &AccountId, _: ()) {} fn before_nft_transfer(_contract: &C, _transfer: &Nep171Transfer) {} fn after_nft_transfer(contract: &mut C, transfer: &Nep171Transfer, _: ()) { contract.revoke_all_unchecked(transfer.token_id); } + + fn before_burn(contract: &C, token_id: &TokenId, owner_id: &AccountId) {} + + fn after_burn( + contract: &mut C, + token_id: &TokenId, + owner_id: &AccountId, + state: Self::BurnState, + ) { + contract.revoke_all_unchecked(token_id); + } } impl CheckExternalTransfer for TokenApprovals { @@ -61,14 +78,14 @@ impl CheckExternalTransfer for TokenA let normal_check = DefaultCheckExternalTransfer::check_external_transfer(contract, transfer); - match (&transfer.authorization, &transfer.sender_id, normal_check) { - (_, _, r @ Ok(_)) => r, + match (&transfer.authorization, normal_check) { + (_, r @ Ok(_)) => r, ( Nep171TransferAuthorization::ApprovalId(approval_id), - Some(sender_id), Err(Nep171TransferError::SenderNotApproved(s)), ) => { - let saved_approval = contract.get_approval_id_for(transfer.token_id, sender_id); + let saved_approval = + contract.get_approval_id_for(transfer.token_id, transfer.sender_id); if saved_approval == Some(*approval_id) { Ok(s.owner_id) @@ -76,7 +93,7 @@ impl CheckExternalTransfer for TokenA Err(s.into()) } } - (_, _, e @ Err(_)) => e, + (_, e @ Err(_)) => e, } } } diff --git a/src/standard/nep181.rs b/src/standard/nep181.rs index 7c1614a..dcc51f5 100644 --- a/src/standard/nep181.rs +++ b/src/standard/nep181.rs @@ -1,36 +1,60 @@ //! NEP-181 non-fungible token contract metadata implementation. //! //! Reference: -use std::error::Error; +use std::borrow::Cow; use near_sdk::{ - borsh::{self, BorshDeserialize, BorshSerialize}, + borsh::{self, BorshSerialize}, env, - json_types::U64, - serde::*, + store::UnorderedSet, AccountId, BorshStorageKey, }; -use thiserror::Error; - -use crate::{ - slot::Slot, - standard::{ - nep171::{ - self, - error::TokenDoesNotExistError, - event::{NftContractMetadataUpdateLog, NftMetadataUpdateLog}, - *, - }, - nep297::Event, - }, - DefaultStorageKey, -}; + +use crate::{slot::Slot, standard::nep171::*, DefaultStorageKey}; pub use ext::*; +pub struct TokenEnumeration {} + +impl Nep171Hook for TokenEnumeration { + type MintState = (); + type NftTransferState = (); + type BurnState = (); + + fn before_mint(_contract: &C, _token_id: &TokenId, _owner_id: &AccountId) {} + + fn after_mint(contract: &mut C, token_id: &TokenId, owner_id: &AccountId, _: ()) { + contract.add_token_to_enumeration(token_id.clone(), owner_id); + } + + fn before_nft_transfer(_contract: &C, _transfer: &Nep171Transfer) {} + + fn after_nft_transfer(contract: &mut C, transfer: &Nep171Transfer, _: ()) { + let owner_id = match transfer.authorization { + Nep171TransferAuthorization::Owner => Cow::Borrowed(transfer.sender_id), + Nep171TransferAuthorization::ApprovalId(_) => Cow::Owned(contract.token_owner(transfer.token_id).unwrap_or_else(|| { + env::panic_str(&format!("Inconsistent state: Enumeration reconciliation should only run after a token has been transferred, but token {} does not exist.", transfer.token_id)) + })), + }; + + contract.transfer_token_enumeration( + transfer.token_id.clone(), + owner_id.as_ref(), + transfer.receiver_id, + ); + } + + fn before_burn(_contract: &C, _token_id: &TokenId, _owner_id: &AccountId) {} + + fn after_burn(contract: &mut C, token_id: &TokenId, owner_id: &AccountId, _: ()) { + contract.remove_token_from_enumeration(token_id, owner_id); + } +} + #[derive(BorshSerialize, BorshStorageKey)] -enum StorageKey { - X, +enum StorageKey<'a> { + Tokens, + OwnerTokens(&'a AccountId), } /// Internal functions for [`Nep181Controller`]. @@ -39,12 +63,116 @@ pub trait Nep181ControllerInternal { fn root() -> Slot<()> { Slot::root(DefaultStorageKey::Nep181) } + + fn slot_tokens() -> Slot> { + Self::root().field(StorageKey::Tokens) + } + + fn slot_owner_tokens(owner_id: &AccountId) -> Slot> { + Self::root().field(StorageKey::OwnerTokens(owner_id)) + } } /// Functions for managing non-fungible tokens with attached metadata, NEP-181. -pub trait Nep181Controller {} +pub trait Nep181Controller { + fn add_token_to_enumeration(&mut self, token_id: TokenId, owner_id: &AccountId); + fn remove_token_from_enumeration(&mut self, token_id: &TokenId, owner_id: &AccountId); + fn transfer_token_enumeration( + &mut self, + token_id: TokenId, + from_owner_id: &AccountId, + to_owner_id: &AccountId, + ); + fn total_enumerated_tokens(&self) -> u128; + fn with_tokens(&self, f: impl FnOnce(&UnorderedSet) -> T) -> T; + fn with_tokens_for_owner( + &self, + owner_id: &AccountId, + f: impl FnOnce(&UnorderedSet) -> T, + ) -> T; +} + +impl Nep181Controller for T { + fn add_token_to_enumeration(&mut self, token_id: TokenId, owner_id: &AccountId) { + let mut all_tokens_slot = Self::slot_tokens(); + let mut all_tokens = all_tokens_slot + .read() + .unwrap_or_else(|| UnorderedSet::new(StorageKey::Tokens)); -impl Nep181Controller for T {} + all_tokens.insert(token_id.clone()); + + all_tokens_slot.write(&all_tokens); + + let mut owner_tokens_slot = Self::slot_owner_tokens(owner_id); + let mut owner_tokens = owner_tokens_slot + .read() + .unwrap_or_else(|| UnorderedSet::new(StorageKey::OwnerTokens(owner_id))); + + owner_tokens.insert(token_id); + + owner_tokens_slot.write(&owner_tokens); + } + + fn remove_token_from_enumeration(&mut self, token_id: &TokenId, owner_id: &AccountId) { + let mut all_tokens_slot = Self::slot_tokens(); + if let Some(mut all_tokens) = all_tokens_slot.read() { + all_tokens.remove(token_id); + all_tokens_slot.write(&all_tokens); + } + + let mut owner_tokens_slot = Self::slot_owner_tokens(owner_id); + if let Some(mut owner_tokens) = owner_tokens_slot.read() { + owner_tokens.remove(token_id); + owner_tokens_slot.write(&owner_tokens); + } + } + + fn transfer_token_enumeration( + &mut self, + token_id: TokenId, + from_owner_id: &AccountId, + to_owner_id: &AccountId, + ) { + let mut from_owner_tokens_slot = Self::slot_owner_tokens(from_owner_id); + if let Some(mut from_owner_tokens) = from_owner_tokens_slot.read() { + from_owner_tokens.remove(&token_id); + from_owner_tokens_slot.write(&from_owner_tokens); + } + + let mut to_owner_tokens_slot = Self::slot_owner_tokens(to_owner_id); + let mut to_owner_tokens = to_owner_tokens_slot + .read() + .unwrap_or_else(|| UnorderedSet::new(StorageKey::OwnerTokens(to_owner_id))); + + to_owner_tokens.insert(token_id); + + to_owner_tokens_slot.write(&to_owner_tokens); + } + + fn total_enumerated_tokens(&self) -> u128 { + Self::slot_tokens() + .read() + .map(|tokens| tokens.len()) + .unwrap_or_default() + .into() + } + + fn with_tokens(&self, f: impl FnOnce(&UnorderedSet) -> U) -> U { + f(&Self::slot_tokens() + .read() + .unwrap_or_else(|| UnorderedSet::new(StorageKey::Tokens))) + } + + fn with_tokens_for_owner( + &self, + owner_id: &AccountId, + f: impl FnOnce(&UnorderedSet) -> U, + ) -> U { + f(&Self::slot_owner_tokens(owner_id) + .read() + .unwrap_or_else(|| UnorderedSet::new(StorageKey::OwnerTokens(owner_id)))) + } +} // separate module with re-export because ext_contract doesn't play well with #![warn(missing_docs)] mod ext { diff --git a/tests/macros/standard/nep171/hooks.rs b/tests/macros/standard/nep171/hooks.rs index 8c7cfa5..4c3d2e2 100644 --- a/tests/macros/standard/nep171/hooks.rs +++ b/tests/macros/standard/nep171/hooks.rs @@ -10,10 +10,8 @@ pub struct Contract { transfer_count: u32, } -impl Nep171Hook for Contract { - type NftTransferState = (); - - fn before_nft_transfer(_contract: &Self, transfer: &Nep171Transfer) { +impl SimpleNep171Hook for Contract { + fn before_nft_transfer(&self, transfer: &Nep171Transfer) { log!( "{:?} is transferring {} to {}", transfer.sender_id, @@ -22,7 +20,7 @@ impl Nep171Hook for Contract { ); } - fn after_nft_transfer(contract: &mut Self, _transfer: &Nep171Transfer, _: ()) { - contract.transfer_count += 1; + fn after_nft_transfer(&mut self, _transfer: &Nep171Transfer) { + self.transfer_count += 1; } } diff --git a/tests/macros/standard/nep171/manual_integration.rs b/tests/macros/standard/nep171/manual_integration.rs index 6340d9f..eec7e75 100644 --- a/tests/macros/standard/nep171/manual_integration.rs +++ b/tests/macros/standard/nep171/manual_integration.rs @@ -27,14 +27,10 @@ pub struct Contract { next_token_id: u32, } -impl Nep171Hook for Contract { - type NftTransferState = (); - - fn before_nft_transfer(_contract: &Self, _transfer: &Nep171Transfer) { +impl SimpleNep171Hook for Contract { + fn before_nft_transfer(&self, _transfer: &Nep171Transfer) { Self::require_unpaused(); } - - fn after_nft_transfer(_contract: &mut Self, _transfer: &Nep171Transfer, _: ()) {} } #[near_bindgen] diff --git a/tests/macros/standard/nep171/mod.rs b/tests/macros/standard/nep171/mod.rs index 3a9dec1..6da8755 100644 --- a/tests/macros/standard/nep171/mod.rs +++ b/tests/macros/standard/nep171/mod.rs @@ -101,6 +101,17 @@ impl Nep171Hook for NonFungibleToken { .after_nft_transfer_balance_record .push(token.map(Into::into)); } + + type MintState = (); + type BurnState = (); + + fn before_mint(contract: &Self, token_id: &TokenId, owner_id: &AccountId) {} + + fn after_mint(contract: &mut Self, token_id: &TokenId, owner_id: &AccountId, _: ()) {} + + fn before_burn(contract: &Self, token_id: &TokenId, owner_id: &AccountId) {} + + fn after_burn(contract: &mut Self, token_id: &TokenId, owner_id: &AccountId, _: ()) {} } #[near_bindgen] diff --git a/tests/macros/standard/nep171/non_fungible_token.rs b/tests/macros/standard/nep171/non_fungible_token.rs index 322ea9d..4368a00 100644 --- a/tests/macros/standard/nep171/non_fungible_token.rs +++ b/tests/macros/standard/nep171/non_fungible_token.rs @@ -16,14 +16,10 @@ pub struct Contract { next_token_id: u32, } -impl Nep171Hook for Contract { - type NftTransferState = (); - - fn before_nft_transfer(_contract: &Self, _transfer: &Nep171Transfer) { +impl SimpleNep171Hook for Contract { + fn before_nft_transfer(&self, _transfer: &Nep171Transfer) { Self::require_unpaused(); } - - fn after_nft_transfer(_contract: &mut Self, _transfer: &Nep171Transfer, _: ()) {} } #[near_bindgen] diff --git a/workspaces-tests/src/bin/non_fungible_token_full.rs b/workspaces-tests/src/bin/non_fungible_token_full.rs index bc82163..34854a1 100644 --- a/workspaces-tests/src/bin/non_fungible_token_full.rs +++ b/workspaces-tests/src/bin/non_fungible_token_full.rs @@ -48,14 +48,12 @@ impl Nep178Hook for Contract { } } -impl Nep171Hook for Contract { - type NftTransferState = (); - - fn before_nft_transfer(_contract: &Self, transfer: &Nep171Transfer) { +impl SimpleNep171Hook for Contract { + fn before_nft_transfer(&self, transfer: &Nep171Transfer) { log!("before_nft_transfer({})", transfer.token_id); } - fn after_nft_transfer(_contract: &mut Self, transfer: &Nep171Transfer, _state: ()) { + fn after_nft_transfer(&mut self, transfer: &Nep171Transfer) { log!("after_nft_transfer({})", transfer.token_id); } } diff --git a/workspaces-tests/src/bin/non_fungible_token_nep171.rs b/workspaces-tests/src/bin/non_fungible_token_nep171.rs index c22ea96..0aee4cb 100644 --- a/workspaces-tests/src/bin/non_fungible_token_nep171.rs +++ b/workspaces-tests/src/bin/non_fungible_token_nep171.rs @@ -13,14 +13,12 @@ use near_sdk_contract_tools::{standard::nep171::*, Nep171}; #[near_bindgen] pub struct Contract {} -impl Nep171Hook for Contract { - type NftTransferState = (); - - fn before_nft_transfer(_contract: &Self, transfer: &Nep171Transfer) { +impl SimpleNep171Hook for Contract { + fn before_nft_transfer(&self, transfer: &Nep171Transfer) { log!("before_nft_transfer({})", transfer.token_id); } - fn after_nft_transfer(_contract: &mut Self, transfer: &Nep171Transfer, _state: ()) { + fn after_nft_transfer(&mut self, transfer: &Nep171Transfer) { log!("after_nft_transfer({})", transfer.token_id); } } From 768512ca324b4189db8b103deb4418964a469235 Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Wed, 13 Sep 2023 04:09:59 +0900 Subject: [PATCH 29/34] feat: nep181 macro, sanity test --- macros/src/standard/mod.rs | 1 + macros/src/standard/nep181.rs | 116 ++++++++++++++++++ macros/src/standard/non_fungible_token.rs | 21 +++- src/standard/nep171/mod.rs | 90 +++++++++----- src/standard/nep178.rs | 17 ++- src/standard/nep181.rs | 20 +-- tests/macros/standard/nep171/mod.rs | 11 +- .../src/bin/non_fungible_token_full.rs | 2 +- workspaces-tests/tests/non_fungible_token.rs | 29 ++++- 9 files changed, 248 insertions(+), 59 deletions(-) create mode 100644 macros/src/standard/nep181.rs diff --git a/macros/src/standard/mod.rs b/macros/src/standard/mod.rs index c9e15eb..4a95ca5 100644 --- a/macros/src/standard/mod.rs +++ b/macros/src/standard/mod.rs @@ -7,4 +7,5 @@ pub mod nep148; pub mod nep171; pub mod nep177; pub mod nep178; +pub mod nep181; pub mod nep297; diff --git a/macros/src/standard/nep181.rs b/macros/src/standard/nep181.rs new file mode 100644 index 0000000..98355d2 --- /dev/null +++ b/macros/src/standard/nep181.rs @@ -0,0 +1,116 @@ +use darling::FromDeriveInput; +use proc_macro2::TokenStream; +use quote::quote; +use syn::Expr; + +#[derive(Debug, FromDeriveInput)] +#[darling(attributes(nep181), supports(struct_named))] +pub struct Nep181Meta { + pub storage_key: Option, + + pub generics: syn::Generics, + pub ident: syn::Ident, + + // crates + #[darling(rename = "crate", default = "crate::default_crate_name")] + pub me: syn::Path, + #[darling(default = "crate::default_near_sdk")] + pub near_sdk: syn::Path, +} + +pub fn expand(meta: Nep181Meta) -> Result { + let Nep181Meta { + storage_key, + + generics, + ident, + + me, + near_sdk, + } = meta; + + let (imp, ty, wher) = generics.split_for_impl(); + + let root = storage_key.map(|storage_key| { + quote! { + fn root() -> #me::slot::Slot<()> { + #me::slot::Slot::root(#storage_key) + } + } + }); + + Ok(quote! { + impl #imp #me::standard::nep181::Nep181ControllerInternal for #ident #ty #wher { + #root + } + + #[#near_sdk::near_bindgen] + impl #imp #me::standard::nep181::Nep181 for #ident #ty #wher { + fn nft_total_supply(&self) -> #near_sdk::json_types::U128 { + #me::standard::nep181::Nep181Controller::total_enumerated_tokens(self) + .into() + } + + fn nft_tokens( + &self, + from_index: Option<#near_sdk::json_types::U128>, + limit: Option, + ) -> Vec { + use #me::standard::{ + nep171::Nep171Controller, nep181::Nep181Controller, + }; + + Nep181Controller::with_tokens(self, |tokens| { + let from_index = from_index.map_or(0, |i| i.0 as usize); + let it = tokens + .iter() + .skip(from_index) + .map(|token_id| Nep171Controller::load_token(self, token_id).unwrap_or_else(|| { + #near_sdk::env::panic_str(&format!("Inconsistent state: Token `{}` is in the enumeration set but its metadata could not be loaded.", token_id)) + })); + + if let Some(limit) = limit { + it.take(limit as usize).collect() + } else { + it.collect() + } + }) + } + + fn nft_supply_for_owner(&self, account_id: #near_sdk::AccountId) -> #near_sdk::json_types::U128 { + #me::standard::nep181::Nep181Controller::with_tokens_for_owner( + self, + &account_id, + |tokens| (tokens.len() as u128).into(), + ) + } + + fn nft_tokens_for_owner( + &self, + account_id: #near_sdk::AccountId, + from_index: Option<#near_sdk::json_types::U128>, + limit: Option, + ) -> Vec { + use #me::standard::{ + nep171::Nep171Controller, nep181::Nep181Controller, + }; + + Nep181Controller::with_tokens_for_owner(self, &account_id, |tokens| { + let from_index = from_index.map_or(0, |i| i.0 as usize); + let it = tokens + .iter() + .skip(from_index) + .map(|token_id| Nep171Controller::load_token(self, token_id).unwrap_or_else(|| { + #near_sdk::env::panic_str(&format!("Inconsistent state: Token `{}` is in the enumeration set but its metadata could not be loaded.", token_id)) + })); + + if let Some(limit) = limit { + it.take(limit as usize).collect() + } else { + it.collect() + } + }) + } + } + }) +} diff --git a/macros/src/standard/non_fungible_token.rs b/macros/src/standard/non_fungible_token.rs index df63f2c..552ab0d 100644 --- a/macros/src/standard/non_fungible_token.rs +++ b/macros/src/standard/non_fungible_token.rs @@ -3,7 +3,7 @@ use proc_macro2::TokenStream; use quote::quote; use syn::Expr; -use super::{nep171, nep177, nep178}; +use super::{nep171, nep177, nep178, nep181}; #[derive(Debug, FromDeriveInput)] #[darling(attributes(non_fungible_token), supports(struct_named))] @@ -19,6 +19,9 @@ pub struct NonFungibleTokenMeta { pub approval_storage_key: Option, pub no_approval_hooks: Flag, + // NEP-181 fields + pub enumeration_storage_key: Option, + // darling pub generics: syn::Generics, pub ident: syn::Ident, @@ -40,6 +43,8 @@ pub fn expand(meta: NonFungibleTokenMeta) -> Result approval_storage_key, no_approval_hooks, + enumeration_storage_key, + generics, ident, @@ -50,7 +55,9 @@ pub fn expand(meta: NonFungibleTokenMeta) -> Result let expand_nep171 = nep171::expand(nep171::Nep171Meta { storage_key, no_hooks, - extension_hooks: Some(syn::parse_quote! { #me::standard::nep178::TokenApprovals }), + extension_hooks: Some( + syn::parse_quote! { (#me::standard::nep178::TokenApprovals, #me::standard::nep181::TokenEnumeration) }, + ), check_external_transfer: Some(syn::parse_quote! { #me::standard::nep178::TokenApprovals }), token_data: Some( @@ -77,6 +84,14 @@ pub fn expand(meta: NonFungibleTokenMeta) -> Result let expand_nep178 = nep178::expand(nep178::Nep178Meta { storage_key: approval_storage_key, no_hooks: no_approval_hooks, + generics: generics.clone(), + ident: ident.clone(), + me: me.clone(), + near_sdk: near_sdk.clone(), + }); + + let expand_nep181 = nep181::expand(nep181::Nep181Meta { + storage_key: enumeration_storage_key, generics, ident, me, @@ -88,10 +103,12 @@ pub fn expand(meta: NonFungibleTokenMeta) -> Result let nep171 = e.handle(expand_nep171); let nep177 = e.handle(expand_nep177); let nep178 = e.handle(expand_nep178); + let nep181 = e.handle(expand_nep181); e.finish_with(quote! { #nep171 #nep177 #nep178 + #nep181 }) } diff --git a/src/standard/nep171/mod.rs b/src/standard/nep171/mod.rs index 3d38aa0..d4104e8 100644 --- a/src/standard/nep171/mod.rs +++ b/src/standard/nep171/mod.rs @@ -346,18 +346,18 @@ pub trait Nep171Hook { state: Self::NftTransferState, ); - fn before_mint(contract: &C, token_id: &TokenId, owner_id: &AccountId) -> Self::MintState; + fn before_mint(contract: &C, token_ids: &[TokenId], owner_id: &AccountId) -> Self::MintState; fn after_mint( contract: &mut C, - token_id: &TokenId, + token_ids: &[TokenId], owner_id: &AccountId, state: Self::MintState, ); - fn before_burn(contract: &C, token_id: &TokenId, owner_id: &AccountId) -> Self::BurnState; + fn before_burn(contract: &C, token_ids: &[TokenId], owner_id: &AccountId) -> Self::BurnState; fn after_burn( contract: &mut C, - token_id: &TokenId, + token_ids: &[TokenId], owner_id: &AccountId, state: Self::BurnState, ); @@ -372,21 +372,31 @@ impl Nep171Hook for () { fn after_nft_transfer(_contract: &mut C, _transfer: &Nep171Transfer, _state: ()) {} - fn before_mint(_contract: &C, _token_id: &TokenId, _owner_id: &AccountId) -> Self::MintState {} + fn before_mint( + _contract: &C, + _token_ids: &[TokenId], + _owner_id: &AccountId, + ) -> Self::MintState { + } fn after_mint( _contract: &mut C, - _token_id: &TokenId, + _token_ids: &[TokenId], _owner_id: &AccountId, _state: Self::MintState, ) { } - fn before_burn(_contract: &C, _token_id: &TokenId, _owner_id: &AccountId) -> Self::BurnState {} + fn before_burn( + _contract: &C, + _token_ids: &[TokenId], + _owner_id: &AccountId, + ) -> Self::BurnState { + } fn after_burn( _contract: &mut C, - _token_id: &TokenId, + _token_ids: &[TokenId], _owner_id: &AccountId, _state: Self::BurnState, ) { @@ -402,21 +412,25 @@ where type NftTransferState = (Handl0::NftTransferState, Handl1::NftTransferState); type BurnState = (Handl0::BurnState, Handl1::BurnState); - fn before_mint(contract: &Cont, token_id: &TokenId, owner_id: &AccountId) -> Self::MintState { + fn before_mint( + contract: &Cont, + token_ids: &[TokenId], + owner_id: &AccountId, + ) -> Self::MintState { ( - Handl0::before_mint(contract, token_id, owner_id), - Handl1::before_mint(contract, token_id, owner_id), + Handl0::before_mint(contract, token_ids, owner_id), + Handl1::before_mint(contract, token_ids, owner_id), ) } fn after_mint( contract: &mut Cont, - token_id: &TokenId, + token_ids: &[TokenId], owner_id: &AccountId, state: Self::MintState, ) { - Handl0::after_mint(contract, token_id, owner_id, state.0); - Handl1::after_mint(contract, token_id, owner_id, state.1); + Handl0::after_mint(contract, token_ids, owner_id, state.0); + Handl1::after_mint(contract, token_ids, owner_id, state.1); } fn before_nft_transfer( @@ -438,31 +452,35 @@ where Handl1::after_nft_transfer(contract, transfer, state.1); } - fn before_burn(contract: &Cont, token_id: &TokenId, owner_id: &AccountId) -> Self::BurnState { + fn before_burn( + contract: &Cont, + token_ids: &[TokenId], + owner_id: &AccountId, + ) -> Self::BurnState { ( - Handl0::before_burn(contract, token_id, owner_id), - Handl1::before_burn(contract, token_id, owner_id), + Handl0::before_burn(contract, token_ids, owner_id), + Handl1::before_burn(contract, token_ids, owner_id), ) } fn after_burn( contract: &mut Cont, - token_id: &TokenId, + token_ids: &[TokenId], owner_id: &AccountId, state: Self::BurnState, ) { - Handl0::after_burn(contract, token_id, owner_id, state.0); - Handl1::after_burn(contract, token_id, owner_id, state.1); + Handl0::after_burn(contract, token_ids, owner_id, state.0); + Handl1::after_burn(contract, token_ids, owner_id, state.1); } } pub trait SimpleNep171Hook { - fn before_mint(&self, _token_id: &TokenId, _owner_id: &AccountId) {} - fn after_mint(&mut self, _token_id: &TokenId, _owner_id: &AccountId) {} + fn before_mint(&self, _token_ids: &[TokenId], _owner_id: &AccountId) {} + fn after_mint(&mut self, _token_ids: &[TokenId], _owner_id: &AccountId) {} fn before_nft_transfer(&self, _transfer: &Nep171Transfer) {} fn after_nft_transfer(&mut self, _transfer: &Nep171Transfer) {} - fn before_burn(&self, _token_id: &TokenId, _owner_id: &AccountId) {} - fn after_burn(&mut self, _token_id: &TokenId, _owner_id: &AccountId) {} + fn before_burn(&self, _token_ids: &[TokenId], _owner_id: &AccountId) {} + fn after_burn(&mut self, _token_ids: &[TokenId], _owner_id: &AccountId) {} } impl Nep171Hook for T { @@ -470,12 +488,12 @@ impl Nep171Hook for T { type NftTransferState = (); type BurnState = (); - fn before_mint(contract: &Self, token_id: &TokenId, owner_id: &AccountId) { - SimpleNep171Hook::before_mint(contract, token_id, owner_id); + fn before_mint(contract: &Self, token_ids: &[TokenId], owner_id: &AccountId) { + SimpleNep171Hook::before_mint(contract, token_ids, owner_id); } - fn after_mint(contract: &mut Self, token_id: &TokenId, owner_id: &AccountId, _: ()) { - SimpleNep171Hook::after_burn(contract, token_id, owner_id); + fn after_mint(contract: &mut Self, token_ids: &[TokenId], owner_id: &AccountId, _: ()) { + SimpleNep171Hook::after_burn(contract, token_ids, owner_id); } fn before_nft_transfer(contract: &T, transfer: &Nep171Transfer) { @@ -486,12 +504,12 @@ impl Nep171Hook for T { SimpleNep171Hook::after_nft_transfer(contract, transfer); } - fn before_burn(contract: &T, token_id: &TokenId, owner_id: &AccountId) { - SimpleNep171Hook::before_burn(contract, token_id, owner_id); + fn before_burn(contract: &T, token_ids: &[TokenId], owner_id: &AccountId) { + SimpleNep171Hook::before_burn(contract, token_ids, owner_id); } - fn after_burn(contract: &mut T, token_id: &TokenId, owner_id: &AccountId, _: ()) { - SimpleNep171Hook::after_burn(contract, token_id, owner_id); + fn after_burn(contract: &mut T, token_ids: &[TokenId], owner_id: &AccountId, _: ()) { + SimpleNep171Hook::after_burn(contract, token_ids, owner_id); } } @@ -585,8 +603,12 @@ impl Nep171Controller for T { } } + let state = Self::Hook::before_mint(self, token_ids, new_owner_id); + self.mint_unchecked(token_ids, new_owner_id, memo); + Self::Hook::after_mint(self, token_ids, new_owner_id, state); + Ok(()) } @@ -618,8 +640,12 @@ impl Nep171Controller for T { } } + let state = Self::Hook::before_burn(self, token_ids, current_owner_id); + self.burn_unchecked(token_ids); + Self::Hook::after_burn(self, token_ids, current_owner_id, state); + Nep171Event::NftBurn(vec![event::NftBurnLog { token_ids: token_ids.iter().map(ToString::to_string).collect(), owner_id: current_owner_id.clone(), diff --git a/src/standard/nep178.rs b/src/standard/nep178.rs index 2c68f7e..50d6416 100644 --- a/src/standard/nep178.rs +++ b/src/standard/nep178.rs @@ -48,9 +48,9 @@ impl Nep171Hook for TokenApprovals { type NftTransferState = (); type BurnState = (); - fn before_mint(_contract: &C, _token_id: &TokenId, owner_id: &AccountId) {} + fn before_mint(_contract: &C, _token_ids: &[TokenId], _owner_id: &AccountId) {} - fn after_mint(_contract: &mut C, _token_id: &TokenId, owner_id: &AccountId, _: ()) {} + fn after_mint(_contract: &mut C, _token_ids: &[TokenId], _owner_id: &AccountId, _: ()) {} fn before_nft_transfer(_contract: &C, _transfer: &Nep171Transfer) {} @@ -58,15 +58,12 @@ impl Nep171Hook for TokenApprovals { contract.revoke_all_unchecked(transfer.token_id); } - fn before_burn(contract: &C, token_id: &TokenId, owner_id: &AccountId) {} + fn before_burn(_contract: &C, _token_ids: &[TokenId], _owner_id: &AccountId) {} - fn after_burn( - contract: &mut C, - token_id: &TokenId, - owner_id: &AccountId, - state: Self::BurnState, - ) { - contract.revoke_all_unchecked(token_id); + fn after_burn(contract: &mut C, token_ids: &[TokenId], _owner_id: &AccountId, _: ()) { + for token_id in token_ids { + contract.revoke_all_unchecked(token_id); + } } } diff --git a/src/standard/nep181.rs b/src/standard/nep181.rs index dcc51f5..07bc182 100644 --- a/src/standard/nep181.rs +++ b/src/standard/nep181.rs @@ -14,17 +14,19 @@ use crate::{slot::Slot, standard::nep171::*, DefaultStorageKey}; pub use ext::*; -pub struct TokenEnumeration {} +pub struct TokenEnumeration; impl Nep171Hook for TokenEnumeration { type MintState = (); type NftTransferState = (); type BurnState = (); - fn before_mint(_contract: &C, _token_id: &TokenId, _owner_id: &AccountId) {} + fn before_mint(_contract: &C, _token_ids: &[TokenId], _owner_id: &AccountId) {} - fn after_mint(contract: &mut C, token_id: &TokenId, owner_id: &AccountId, _: ()) { - contract.add_token_to_enumeration(token_id.clone(), owner_id); + fn after_mint(contract: &mut C, token_ids: &[TokenId], owner_id: &AccountId, _: ()) { + for token_id in token_ids { + contract.add_token_to_enumeration(token_id.clone(), owner_id); + } } fn before_nft_transfer(_contract: &C, _transfer: &Nep171Transfer) {} @@ -44,10 +46,12 @@ impl Nep171Hook for TokenEnumeration ); } - fn before_burn(_contract: &C, _token_id: &TokenId, _owner_id: &AccountId) {} + fn before_burn(_contract: &C, _token_ids: &[TokenId], _owner_id: &AccountId) {} - fn after_burn(contract: &mut C, token_id: &TokenId, owner_id: &AccountId, _: ()) { - contract.remove_token_from_enumeration(token_id, owner_id); + fn after_burn(contract: &mut C, token_ids: &[TokenId], owner_id: &AccountId, _: ()) { + for token_id in token_ids { + contract.remove_token_from_enumeration(token_id, owner_id); + } } } @@ -75,7 +79,7 @@ pub trait Nep181ControllerInternal { /// Functions for managing non-fungible tokens with attached metadata, NEP-181. pub trait Nep181Controller { - fn add_token_to_enumeration(&mut self, token_id: TokenId, owner_id: &AccountId); + fn add_token_to_enumeration(&mut self, token_id: TokenId, owner_id: &AccountId); // TODO: token_id should be an array of TokenIds, same for other fns fn remove_token_from_enumeration(&mut self, token_id: &TokenId, owner_id: &AccountId); fn transfer_token_enumeration( &mut self, diff --git a/tests/macros/standard/nep171/mod.rs b/tests/macros/standard/nep171/mod.rs index 6da8755..1da09a4 100644 --- a/tests/macros/standard/nep171/mod.rs +++ b/tests/macros/standard/nep171/mod.rs @@ -103,15 +103,16 @@ impl Nep171Hook for NonFungibleToken { } type MintState = (); - type BurnState = (); - fn before_mint(contract: &Self, token_id: &TokenId, owner_id: &AccountId) {} + fn before_mint(_contract: &Self, _token_ids: &[TokenId], _owner_id: &AccountId) {} + + fn after_mint(_contract: &mut Self, _token_ids: &[TokenId], _owner_id: &AccountId, _: ()) {} - fn after_mint(contract: &mut Self, token_id: &TokenId, owner_id: &AccountId, _: ()) {} + type BurnState = (); - fn before_burn(contract: &Self, token_id: &TokenId, owner_id: &AccountId) {} + fn before_burn(_contract: &Self, _token_ids: &[TokenId], _owner_id: &AccountId) {} - fn after_burn(contract: &mut Self, token_id: &TokenId, owner_id: &AccountId, _: ()) {} + fn after_burn(_contract: &mut Self, _token_ids: &[TokenId], _owner_id: &AccountId, _: ()) {} } #[near_bindgen] diff --git a/workspaces-tests/src/bin/non_fungible_token_full.rs b/workspaces-tests/src/bin/non_fungible_token_full.rs index 34854a1..01d2711 100644 --- a/workspaces-tests/src/bin/non_fungible_token_full.rs +++ b/workspaces-tests/src/bin/non_fungible_token_full.rs @@ -8,7 +8,7 @@ use near_sdk::{ env, log, near_bindgen, AccountId, PanicOnDefault, }; use near_sdk_contract_tools::{ - standard::{nep171::*, nep177::*, nep178::*}, + standard::{nep171::*, nep177::*, nep178::*, nep181::*}, NonFungibleToken, }; diff --git a/workspaces-tests/tests/non_fungible_token.rs b/workspaces-tests/tests/non_fungible_token.rs index baf3fc6..b807151 100644 --- a/workspaces-tests/tests/non_fungible_token.rs +++ b/workspaces-tests/tests/non_fungible_token.rs @@ -1,5 +1,7 @@ #![cfg(not(windows))] +use std::collections::{HashMap, HashSet}; + use near_sdk::serde_json::json; use near_sdk_contract_tools::standard::{ nep171::{self, event::NftTransferLog, Nep171Event, Token}, @@ -102,7 +104,7 @@ async fn create_and_mint() { } #[tokio::test] -async fn create_and_mint_with_metadata() { +async fn create_and_mint_with_metadata_and_enumeration() { let Setup { contract, accounts } = setup_balances(WASM_FULL, 3, |i| vec![format!("token_{i}")]).await; let alice = &accounts[0]; @@ -175,6 +177,31 @@ async fn create_and_mint_with_metadata() { }), ); assert_eq!(token_3, None::); + + // indeterminate order, so hashmap for equality instead of vec + let all_tokens_enumeration = contract + .view("nft_tokens") + // .args_json(json!({ "from_index": 0, "limit": 100 })) + .args_json(json!({})) + .await + .unwrap() + .json::>() + .unwrap() + .into_iter() + .map(|token| (token.token_id.clone(), token)) + .collect::>(); + + assert_eq!( + all_tokens_enumeration, + [ + token_0.clone().unwrap(), + token_1.clone().unwrap(), + token_2.clone().unwrap(), + ] + .into_iter() + .map(|token| (token.token_id.clone(), token)) + .collect::>(), + ); } #[tokio::test] From 37801005937cb2c48e2c641444bbf7e993548cf9 Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Wed, 13 Sep 2023 04:21:11 +0900 Subject: [PATCH 30/34] chore: qol nft module --- macros/src/lib.rs | 11 ++++++++++- src/lib.rs | 8 ++++++++ tests/macros/standard/nep171/hooks.rs | 2 +- tests/macros/standard/nep171/manual_integration.rs | 8 ++++---- tests/macros/standard/nep171/no_hooks.rs | 2 +- tests/macros/standard/nep171/non_fungible_token.rs | 7 +------ workspaces-tests/src/bin/non_fungible_token_full.rs | 5 +---- workspaces-tests/tests/non_fungible_token.rs | 2 +- 8 files changed, 27 insertions(+), 18 deletions(-) diff --git a/macros/src/lib.rs b/macros/src/lib.rs index f49a540..ca3b25d 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -182,7 +182,16 @@ pub fn derive_nep178(input: TokenStream) -> TokenStream { make_derive(input, standard::nep178::expand) } -/// Implements all NFT functionality at once, like `#[derive(Nep171, Nep177, Nep178)]`. +/// Adds NEP-181 non-fungible token enumeration functionality to a contract. +/// +/// The storage key prefix for the fields can be optionally specified (default: +/// `"~$181"`) using `#[nep181(storage_key = "")]`. +#[proc_macro_derive(Nep181, attributes(nep181))] +pub fn derive_nep181(input: TokenStream) -> TokenStream { + make_derive(input, standard::nep181::expand) +} + +/// Implements all NFT functionality at once, like `#[derive(Nep171, Nep177, Nep178, Nep181)]`. #[proc_macro_derive(NonFungibleToken, attributes(non_fungible_token))] pub fn derive_non_fungible_token(input: TokenStream) -> TokenStream { make_derive(input, standard::non_fungible_token::expand) diff --git a/src/lib.rs b/src/lib.rs index f014a84..3e81438 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,3 +53,11 @@ pub mod rbac; pub mod slot; pub mod upgrade; pub mod utils; + +/// Re-exports of the NFT standard traits. +pub mod nft { + pub use crate::{ + standard::{nep171::*, nep177::*, nep178::*, nep181::*}, + Nep171, Nep177, Nep178, Nep181, NonFungibleToken, + }; +} diff --git a/tests/macros/standard/nep171/hooks.rs b/tests/macros/standard/nep171/hooks.rs index 4c3d2e2..83215b3 100644 --- a/tests/macros/standard/nep171/hooks.rs +++ b/tests/macros/standard/nep171/hooks.rs @@ -2,7 +2,7 @@ use near_sdk::{ borsh::{self, BorshDeserialize, BorshSerialize}, log, near_bindgen, PanicOnDefault, }; -use near_sdk_contract_tools::{standard::nep171::*, Nep171}; +use near_sdk_contract_tools::nft::*; #[derive(BorshSerialize, BorshDeserialize, PanicOnDefault, Nep171)] #[near_bindgen] diff --git a/tests/macros/standard/nep171/manual_integration.rs b/tests/macros/standard/nep171/manual_integration.rs index eec7e75..817ce96 100644 --- a/tests/macros/standard/nep171/manual_integration.rs +++ b/tests/macros/standard/nep171/manual_integration.rs @@ -8,16 +8,16 @@ use near_sdk_contract_tools::{ standard::{ nep171::*, nep177::{self, Nep177Controller}, - nep178, + nep178, nep181, }, - Nep171, Nep177, Nep178, Owner, Pause, + Nep171, Nep177, Nep178, Nep181, Owner, Pause, }; #[derive( - BorshSerialize, BorshDeserialize, PanicOnDefault, Nep171, Nep177, Nep178, Pause, Owner, + BorshSerialize, BorshDeserialize, PanicOnDefault, Nep171, Nep177, Nep178, Nep181, Pause, Owner, )] #[nep171( - extension_hooks = "nep178::TokenApprovals", + extension_hooks = "(nep178::TokenApprovals, nep181::TokenEnumeration)", check_external_transfer = "nep178::TokenApprovals", token_data = "(nep177::TokenMetadata, nep178::TokenApprovals)" )] diff --git a/tests/macros/standard/nep171/no_hooks.rs b/tests/macros/standard/nep171/no_hooks.rs index 3342e45..de6e490 100644 --- a/tests/macros/standard/nep171/no_hooks.rs +++ b/tests/macros/standard/nep171/no_hooks.rs @@ -2,7 +2,7 @@ use near_sdk::{ borsh::{self, BorshDeserialize, BorshSerialize}, env, near_bindgen, PanicOnDefault, }; -use near_sdk_contract_tools::{standard::nep171::*, Nep171}; +use near_sdk_contract_tools::nft::*; #[derive(BorshSerialize, BorshDeserialize, PanicOnDefault, Nep171)] #[nep171(no_hooks)] diff --git a/tests/macros/standard/nep171/non_fungible_token.rs b/tests/macros/standard/nep171/non_fungible_token.rs index 4368a00..72fcb5e 100644 --- a/tests/macros/standard/nep171/non_fungible_token.rs +++ b/tests/macros/standard/nep171/non_fungible_token.rs @@ -2,12 +2,7 @@ use near_sdk::{ borsh::{self, BorshDeserialize, BorshSerialize}, env, near_bindgen, PanicOnDefault, }; -use near_sdk_contract_tools::{ - owner::Owner, - pause::Pause, - standard::{nep171::*, nep177::*}, - NonFungibleToken, Owner, Pause, -}; +use near_sdk_contract_tools::{nft::*, owner::Owner, pause::Pause, Owner, Pause}; #[derive(BorshSerialize, BorshDeserialize, PanicOnDefault, NonFungibleToken, Pause, Owner)] #[non_fungible_token(no_approval_hooks)] diff --git a/workspaces-tests/src/bin/non_fungible_token_full.rs b/workspaces-tests/src/bin/non_fungible_token_full.rs index 01d2711..b3f83ab 100644 --- a/workspaces-tests/src/bin/non_fungible_token_full.rs +++ b/workspaces-tests/src/bin/non_fungible_token_full.rs @@ -7,10 +7,7 @@ use near_sdk::{ borsh::{self, BorshDeserialize, BorshSerialize}, env, log, near_bindgen, AccountId, PanicOnDefault, }; -use near_sdk_contract_tools::{ - standard::{nep171::*, nep177::*, nep178::*, nep181::*}, - NonFungibleToken, -}; +use near_sdk_contract_tools::{nft::*, NonFungibleToken}; #[derive(PanicOnDefault, BorshSerialize, BorshDeserialize, NonFungibleToken)] #[near_bindgen] diff --git a/workspaces-tests/tests/non_fungible_token.rs b/workspaces-tests/tests/non_fungible_token.rs index b807151..627a018 100644 --- a/workspaces-tests/tests/non_fungible_token.rs +++ b/workspaces-tests/tests/non_fungible_token.rs @@ -1,6 +1,6 @@ #![cfg(not(windows))] -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use near_sdk::serde_json::json; use near_sdk_contract_tools::standard::{ From 17afaf0b09b2a63a813f13b0e5954bba6205ff33 Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Thu, 14 Sep 2023 02:35:55 +0900 Subject: [PATCH 31/34] feat: docs --- src/standard/nep171/mod.rs | 44 ++++++++++++++++++----- src/standard/nep181.rs | 72 ++++++++++++++++++++++++++++---------- 2 files changed, 89 insertions(+), 27 deletions(-) diff --git a/src/standard/nep171/mod.rs b/src/standard/nep171/mod.rs index d4104e8..78172a7 100644 --- a/src/standard/nep171/mod.rs +++ b/src/standard/nep171/mod.rs @@ -134,6 +134,7 @@ pub enum Nep171TransferError { /// Internal (storage location) methods for implementors of [`Nep171Controller`]. pub trait Nep171ControllerInternal { + /// Various lifecycle hooks for NEP-171 tokens. type Hook: Nep171Hook where Self: Sized; @@ -161,6 +162,7 @@ pub trait Nep171ControllerInternal { /// Non-public controller interface for NEP-171 implementations. pub trait Nep171Controller { + /// Various lifecycle hooks for NEP-171 tokens. type Hook: Nep171Hook where Self: Sized; @@ -175,7 +177,10 @@ pub trait Nep171Controller { where Self: Sized; - /// Transfer a token from `sender_id` to `receiver_id`. Checks that the transfer is valid using [`CheckExternalTransfer::check_external_transfer`] before performing the transfer. + /// Transfer a token from `sender_id` to `receiver_id`, as for an external + /// call to `nft_transfer`. Checks that the transfer is valid using + /// [`CheckExternalTransfer::check_external_transfer`] before performing + /// the transfer. Runs relevant hooks. fn external_transfer(&mut self, transfer: &Nep171Transfer) -> Result<(), Nep171TransferError> where Self: Sized; @@ -184,7 +189,10 @@ pub trait Nep171Controller { /// /// # Warning /// - /// This function performs _no checks_. It is up to the caller to ensure that the transfer is valid. Possible unintended effects of invalid transfers include: + /// This function performs _no checks_. It is up to the caller to ensure + /// that the transfer is valid. Possible unintended effects of invalid + /// transfers include: + /// /// - Transferring a token "from" an account that does not own it. /// - Creating token IDs that did not previously exist. /// - Transferring a token to the account that already owns it. @@ -197,7 +205,7 @@ pub trait Nep171Controller { memo: Option, ); - /// Mints a new token `token_id` to `owner_id`. + /// Mints a new token `token_id` to `owner_id`. Runs relevant hooks. fn mint( &mut self, token_ids: &[TokenId], @@ -205,6 +213,8 @@ pub trait Nep171Controller { memo: Option, ) -> Result<(), Nep171MintError>; + /// Mints a new token `token_id` to `owner_id` without checking if the + /// token already exists. Does not run hooks. fn mint_unchecked( &mut self, token_ids: &[TokenId], @@ -212,7 +222,7 @@ pub trait Nep171Controller { memo: Option, ); - /// Burns tokens `token_ids` owned by `current_owner_id`. + /// Burns tokens `token_ids` owned by `current_owner_id`. Runs relevant hooks. fn burn( &mut self, token_ids: &[TokenId], @@ -220,7 +230,7 @@ pub trait Nep171Controller { memo: Option, ) -> Result<(), Nep171BurnError>; - /// Burns tokens `token_ids` without checking the owners. + /// Burns tokens `token_ids` without checking the owners. Does not run hooks. fn burn_unchecked(&mut self, token_ids: &[TokenId]) -> bool; /// Returns the owner of a token, if it exists. @@ -261,7 +271,8 @@ pub enum Nep171TransferAuthorization { /// Different ways of checking if a transfer is valid. pub trait CheckExternalTransfer { - /// Checks if a transfer is valid. Returns the account ID of the current owner of the token. + /// Checks if a transfer is valid. Returns the account ID of the current + /// owner of the token. fn check_external_transfer( contract: &C, transfer: &Nep171Transfer, @@ -323,21 +334,24 @@ impl CheckExternalTransfer for DefaultCheckExternalTrans /// hooks. This may be useful for charging callers for storage usage, for /// example. pub trait Nep171Hook { + /// State value passed from `before_nft_transfer` to `after_nft_transfer`. type NftTransferState; + /// State value passed from `before_mint` to `after_mint`. type MintState; + /// State value passed from `before_burn` to `after_burn`. type BurnState; /// Executed before a token transfer is conducted. /// /// May return an optional state value which will be passed along to the - /// following `after_transfer`. + /// following `after_nft_transfer`. /// /// MUST NOT PANIC if the transfer is a revert. fn before_nft_transfer(contract: &C, transfer: &Nep171Transfer) -> Self::NftTransferState; /// Executed after a token transfer is conducted. /// - /// Receives the state value returned by `before_transfer`. + /// Receives the state value returned by `before_nft_transfer`. /// /// MUST NOT PANIC if the transfer is a revert. fn after_nft_transfer( @@ -346,7 +360,9 @@ pub trait Nep171Hook { state: Self::NftTransferState, ); + /// Executed before a token is minted. fn before_mint(contract: &C, token_ids: &[TokenId], owner_id: &AccountId) -> Self::MintState; + /// Executed after a token is minted. fn after_mint( contract: &mut C, token_ids: &[TokenId], @@ -354,7 +370,9 @@ pub trait Nep171Hook { state: Self::MintState, ); + /// Executed before a token is burned. fn before_burn(contract: &C, token_ids: &[TokenId], owner_id: &AccountId) -> Self::BurnState; + /// Executed after a token is burned. fn after_burn( contract: &mut C, token_ids: &[TokenId], @@ -474,12 +492,22 @@ where } } +/// Alternative to [`Nep171Hook`] for implementing NEP-171 hooks. Implementing +/// the full [`Nep171Hook`] trait is sometimes inconvenient, so this trait +/// provides a simpler interface. There is a blanket implementation of +/// [`Nep171Hook`] for all types that implement this trait. pub trait SimpleNep171Hook { + /// Executed before a token is minted. fn before_mint(&self, _token_ids: &[TokenId], _owner_id: &AccountId) {} + /// Executed after a token is minted. fn after_mint(&mut self, _token_ids: &[TokenId], _owner_id: &AccountId) {} + /// Executed before a token transfer is conducted. fn before_nft_transfer(&self, _transfer: &Nep171Transfer) {} + /// Executed after a token transfer is conducted. fn after_nft_transfer(&mut self, _transfer: &Nep171Transfer) {} + /// Executed before a token is burned. fn before_burn(&self, _token_ids: &[TokenId], _owner_id: &AccountId) {} + /// Executed after a token is burned. fn after_burn(&mut self, _token_ids: &[TokenId], _owner_id: &AccountId) {} } diff --git a/src/standard/nep181.rs b/src/standard/nep181.rs index 07bc182..ff1fe50 100644 --- a/src/standard/nep181.rs +++ b/src/standard/nep181.rs @@ -14,6 +14,7 @@ use crate::{slot::Slot, standard::nep171::*, DefaultStorageKey}; pub use ext::*; +/// Extension hook for [`Nep171Controller`]. pub struct TokenEnumeration; impl Nep171Hook for TokenEnumeration { @@ -24,9 +25,7 @@ impl Nep171Hook for TokenEnumeration fn before_mint(_contract: &C, _token_ids: &[TokenId], _owner_id: &AccountId) {} fn after_mint(contract: &mut C, token_ids: &[TokenId], owner_id: &AccountId, _: ()) { - for token_id in token_ids { - contract.add_token_to_enumeration(token_id.clone(), owner_id); - } + contract.add_tokens_to_enumeration(token_ids, owner_id); } fn before_nft_transfer(_contract: &C, _transfer: &Nep171Transfer) {} @@ -40,7 +39,7 @@ impl Nep171Hook for TokenEnumeration }; contract.transfer_token_enumeration( - transfer.token_id.clone(), + std::array::from_ref(transfer.token_id), owner_id.as_ref(), transfer.receiver_id, ); @@ -49,9 +48,7 @@ impl Nep171Hook for TokenEnumeration fn before_burn(_contract: &C, _token_ids: &[TokenId], _owner_id: &AccountId) {} fn after_burn(contract: &mut C, token_ids: &[TokenId], owner_id: &AccountId, _: ()) { - for token_id in token_ids { - contract.remove_token_from_enumeration(token_id, owner_id); - } + contract.remove_tokens_from_enumeration(token_ids, owner_id); } } @@ -68,10 +65,12 @@ pub trait Nep181ControllerInternal { Slot::root(DefaultStorageKey::Nep181) } + /// Storage slot for all tokens. fn slot_tokens() -> Slot> { Self::root().field(StorageKey::Tokens) } + /// Storage slot for tokens owned by an account. fn slot_owner_tokens(owner_id: &AccountId) -> Slot> { Self::root().field(StorageKey::OwnerTokens(owner_id)) } @@ -79,16 +78,45 @@ pub trait Nep181ControllerInternal { /// Functions for managing non-fungible tokens with attached metadata, NEP-181. pub trait Nep181Controller { - fn add_token_to_enumeration(&mut self, token_id: TokenId, owner_id: &AccountId); // TODO: token_id should be an array of TokenIds, same for other fns - fn remove_token_from_enumeration(&mut self, token_id: &TokenId, owner_id: &AccountId); + /// Add tokens to enumeration. + /// + /// # Warning + /// + /// Does not perform consistency checks. May cause inconsistent state if + /// the same token ID is added to the enumeration multiple times. + fn add_tokens_to_enumeration(&mut self, token_ids: &[TokenId], owner_id: &AccountId); + + /// Remove tokens from enumeration. + /// + /// # Warning + /// + /// Does not perform consistency checks. May cause inconsistent state if + /// any of the token IDs are not currently enumerated (owned) by `owner_id`. + fn remove_tokens_from_enumeration(&mut self, token_ids: &[TokenId], owner_id: &AccountId); + + /// Transfer tokens between owners. + /// + /// # Warning + /// + /// Does not perform consistency checks. May cause inconsistent state if + /// any of the token IDs are not currently enumerated (owned) by + /// `from_owner_id`, or have not previously been added to enumeration via + /// [`Nep181Controller::add_tokens_to_enumeration`]. fn transfer_token_enumeration( &mut self, - token_id: TokenId, + token_ids: &[TokenId], from_owner_id: &AccountId, to_owner_id: &AccountId, ); + + /// Total number of tokens in enumeration. fn total_enumerated_tokens(&self) -> u128; + + /// Execute a function with a reference to the set of all tokens. fn with_tokens(&self, f: impl FnOnce(&UnorderedSet) -> T) -> T; + + /// Execute a function with a reference to the set of tokens owned by an + /// account. fn with_tokens_for_owner( &self, owner_id: &AccountId, @@ -97,13 +125,13 @@ pub trait Nep181Controller { } impl Nep181Controller for T { - fn add_token_to_enumeration(&mut self, token_id: TokenId, owner_id: &AccountId) { + fn add_tokens_to_enumeration(&mut self, token_ids: &[TokenId], owner_id: &AccountId) { let mut all_tokens_slot = Self::slot_tokens(); let mut all_tokens = all_tokens_slot .read() .unwrap_or_else(|| UnorderedSet::new(StorageKey::Tokens)); - all_tokens.insert(token_id.clone()); + all_tokens.extend(token_ids.iter().cloned()); all_tokens_slot.write(&all_tokens); @@ -112,34 +140,40 @@ impl Nep181Controller for T { .read() .unwrap_or_else(|| UnorderedSet::new(StorageKey::OwnerTokens(owner_id))); - owner_tokens.insert(token_id); + owner_tokens.extend(token_ids.iter().cloned()); owner_tokens_slot.write(&owner_tokens); } - fn remove_token_from_enumeration(&mut self, token_id: &TokenId, owner_id: &AccountId) { + fn remove_tokens_from_enumeration(&mut self, token_ids: &[TokenId], owner_id: &AccountId) { let mut all_tokens_slot = Self::slot_tokens(); if let Some(mut all_tokens) = all_tokens_slot.read() { - all_tokens.remove(token_id); + for token_id in token_ids { + all_tokens.remove(token_id); + } all_tokens_slot.write(&all_tokens); } let mut owner_tokens_slot = Self::slot_owner_tokens(owner_id); if let Some(mut owner_tokens) = owner_tokens_slot.read() { - owner_tokens.remove(token_id); + for token_id in token_ids { + owner_tokens.remove(token_id); + } owner_tokens_slot.write(&owner_tokens); } } fn transfer_token_enumeration( &mut self, - token_id: TokenId, + token_ids: &[TokenId], from_owner_id: &AccountId, to_owner_id: &AccountId, ) { let mut from_owner_tokens_slot = Self::slot_owner_tokens(from_owner_id); if let Some(mut from_owner_tokens) = from_owner_tokens_slot.read() { - from_owner_tokens.remove(&token_id); + for token_id in token_ids { + from_owner_tokens.remove(token_id); + } from_owner_tokens_slot.write(&from_owner_tokens); } @@ -148,7 +182,7 @@ impl Nep181Controller for T { .read() .unwrap_or_else(|| UnorderedSet::new(StorageKey::OwnerTokens(to_owner_id))); - to_owner_tokens.insert(token_id); + to_owner_tokens.extend(token_ids.iter().cloned()); to_owner_tokens_slot.write(&to_owner_tokens); } From 21370a82607e1d678e4b35c77fcceff2dfbce475 Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Thu, 14 Sep 2023 02:54:48 +0900 Subject: [PATCH 32/34] feat: further enumeration tests --- workspaces-tests/tests/non_fungible_token.rs | 94 +++++++++++++++++--- 1 file changed, 82 insertions(+), 12 deletions(-) diff --git a/workspaces-tests/tests/non_fungible_token.rs b/workspaces-tests/tests/non_fungible_token.rs index 627a018..a988709 100644 --- a/workspaces-tests/tests/non_fungible_token.rs +++ b/workspaces-tests/tests/non_fungible_token.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; -use near_sdk::serde_json::json; +use near_sdk::{json_types::U128, serde_json::json}; use near_sdk_contract_tools::standard::{ nep171::{self, event::NftTransferLog, Nep171Event, Token}, nep177::{self, TokenMetadata}, @@ -179,17 +179,65 @@ async fn create_and_mint_with_metadata_and_enumeration() { assert_eq!(token_3, None::); // indeterminate order, so hashmap for equality instead of vec - let all_tokens_enumeration = contract - .view("nft_tokens") - // .args_json(json!({ "from_index": 0, "limit": 100 })) - .args_json(json!({})) - .await - .unwrap() - .json::>() - .unwrap() - .into_iter() - .map(|token| (token.token_id.clone(), token)) - .collect::>(); + let ( + all_tokens_enumeration, + all_tokens_enumeration_limit, + alice_supply, + alice_tokens_all, + alice_tokens_offset, + ) = tokio::join!( + async { + contract + .view("nft_tokens") + .args_json(json!({})) + .await + .unwrap() + .json::>() + .unwrap() + .into_iter() + .map(|token| (token.token_id.clone(), token)) + .collect::>() + }, + async { + contract + .view("nft_tokens") + .args_json(json!({ "from_index": "0", "limit": 100 })) + .await + .unwrap() + .json::>() + .unwrap() + .into_iter() + .map(|token| (token.token_id.clone(), token)) + .collect::>() + }, + async { + contract + .view("nft_supply_for_owner") + .args_json(json!({ "account_id": alice.id() })) + .await + .unwrap() + .json::() + .unwrap() + }, + async { + contract + .view("nft_tokens_for_owner") + .args_json(json!({ "account_id": alice.id(), "limit": 100 })) + .await + .unwrap() + .json::>() + .unwrap() + }, + async { + contract + .view("nft_tokens_for_owner") + .args_json(json!({ "account_id": alice.id(), "from_index": "1" })) + .await + .unwrap() + .json::>() + .unwrap() + }, + ); assert_eq!( all_tokens_enumeration, @@ -202,6 +250,28 @@ async fn create_and_mint_with_metadata_and_enumeration() { .map(|token| (token.token_id.clone(), token)) .collect::>(), ); + + assert_eq!( + all_tokens_enumeration, all_tokens_enumeration_limit, + "only 3 tokens in circulation, so limit:100 should be the same as unlimited" + ); + + assert_eq!( + alice_supply.0, 1, + "alice has one token, so alice's supply (balance) should be 1" + ); + + assert_eq!( + alice_tokens_all, + vec![token_0.clone().unwrap()], + "alice has one token, so it should be returned in the list of all of alice's tokens" + ); + + assert_eq!( + alice_tokens_offset, + vec![], + "alice only has one token so an offset:1 should return empty" + ); } #[tokio::test] From 56cd76201cd8736282252a8e9e8dd0bdf5b2d748 Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Thu, 14 Sep 2023 03:10:44 +0900 Subject: [PATCH 33/34] feat: final (?) tests for enumeration --- workspaces-tests/tests/non_fungible_token.rs | 16 ++++++++++++++++ workspaces-tests/tests/upgrade.rs | 1 + 2 files changed, 17 insertions(+) diff --git a/workspaces-tests/tests/non_fungible_token.rs b/workspaces-tests/tests/non_fungible_token.rs index a988709..84f2c29 100644 --- a/workspaces-tests/tests/non_fungible_token.rs +++ b/workspaces-tests/tests/non_fungible_token.rs @@ -185,6 +185,7 @@ async fn create_and_mint_with_metadata_and_enumeration() { alice_supply, alice_tokens_all, alice_tokens_offset, + nonexistent_account_tokens, ) = tokio::join!( async { contract @@ -237,6 +238,15 @@ async fn create_and_mint_with_metadata_and_enumeration() { .json::>() .unwrap() }, + async { + contract + .view("nft_tokens_for_owner") + .args_json(json!({ "account_id": "0000000000000000000000000000000000000000000000000000000000000000", "from_index": "1" })) + .await + .unwrap() + .json::>() + .unwrap() + }, ); assert_eq!( @@ -272,6 +282,12 @@ async fn create_and_mint_with_metadata_and_enumeration() { vec![], "alice only has one token so an offset:1 should return empty" ); + + assert_eq!( + nonexistent_account_tokens, + vec![], + "nonexistent account should return empty", + ); } #[tokio::test] diff --git a/workspaces-tests/tests/upgrade.rs b/workspaces-tests/tests/upgrade.rs index bacf6a5..3726eb1 100644 --- a/workspaces-tests/tests/upgrade.rs +++ b/workspaces-tests/tests/upgrade.rs @@ -114,6 +114,7 @@ async fn upgrade_borsh() { } #[tokio::test] +#[ignore] async fn upgrade_jsonbase64() { // For some reason this test fails only on GitHub Actions due to a running-out-of-gas error. if std::env::var_os("GITHUB_ACTIONS").is_some() { From 27bc4b8a4543bd82a36339601df6d0b525ed5f2b Mon Sep 17 00:00:00 2001 From: Jacob Lindahl Date: Thu, 14 Sep 2023 03:15:04 +0900 Subject: [PATCH 34/34] chore: update readme to mention nep-171 and related --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b4dee45..fc2a94a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ This package is a collection of common tools and patterns in NEAR smart contract - Pause (derive macro available) - Derive macro for [NEP-297 events](https://nomicon.io/Standards/EventsFormat) - Derive macro for [NEP-141](https://nomicon.io/Standards/Tokens/FungibleToken/Core) (and [NEP-148](https://nomicon.io/Standards/Tokens/FungibleToken/Metadata)) fungible tokens +- Derive macro for [NEP-171](https://nomicon.io/Standards/NonFungibleToken/NonFungibleToken) non-fungible tokens, and extensions [NEP-177](https://nomicon.io/Standards/Tokens/NonFungibleToken/Metadata), [NEP-178](https://nomicon.io/Standards/Tokens/NonFungibleToken/ApprovalManagement), and [NEP-181](https://nomicon.io/Standards/Tokens/NonFungibleToken/Enumeration). Not to be confused with [`near-contract-standards`](https://crates.io/crates/near-contract-standards), which contains official implementations of standardized NEPs. This crate is intended to be a complement to `near-contract-standards`.