Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(near-contract-standards): NEP-199 - Non-Fungible Token Royalties and Payouts #1077

Draft
wants to merge 50 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
8a1c2da
payout
ymc182 Apr 4, 2022
da97136
d
ymc182 Mar 26, 2022
34c576f
included the sim
ymc182 Apr 4, 2022
7b58745
sim
ymc182 Apr 4, 2022
ca930d7
remove macros, add examples and docs
ruseinov Aug 20, 2023
0406030
add royalties storage prefix
ruseinov Aug 20, 2023
bd9c9fe
Merge branch 'master' into feat/payout
ruseinov Aug 20, 2023
c8bd137
examples
ruseinov Aug 20, 2023
1e62834
Merge branch 'master' into feat/payout
ruseinov Aug 25, 2023
8e5e777
remove near-sdk-sim
ruseinov Sep 10, 2023
14ac453
Merge branch 'master' into feat/payout
frol Sep 12, 2023
5da6f89
Merge branch 'master' into feat/payout
frol Sep 22, 2023
9470437
Merge branch 'master' into feat/payout
ruseinov Sep 26, 2023
98aeceb
update CHANGELOG.md
ruseinov Sep 26, 2023
3d7a4ad
fix test
ruseinov Sep 26, 2023
562b284
fixes and tests
ruseinov Sep 26, 2023
933c029
more tests
ruseinov Sep 26, 2023
2f2779d
better comment
ruseinov Sep 26, 2023
a5c67f7
Merge branch 'master' into feat/payout
ruseinov Sep 28, 2023
6f07b52
return the missing check
ruseinov Sep 29, 2023
e4da342
fix ci
ruseinov Oct 1, 2023
743eb06
Merge branch 'master' into feat/payout
ruseinov Oct 2, 2023
b3716bf
Merge branch 'master' into feat/payout
ruseinov Oct 7, 2023
d76ff5d
fix review comments
ruseinov Oct 7, 2023
f3b3594
use TreeMap for rolayties storage
ruseinov Oct 7, 2023
815a9ea
apply review suggestions
ruseinov Oct 13, 2023
d809519
Merge branch 'master' into feat/payout
ruseinov Oct 13, 2023
c2e990e
fixes
ruseinov Oct 13, 2023
c3d8b67
fixes
ruseinov Oct 13, 2023
ee59a72
fixes
ruseinov Oct 14, 2023
5ec54b3
fix
ruseinov Oct 14, 2023
71dc47b
Merge branch 'master' into feat/payout
ruseinov Oct 24, 2023
b7a0a22
add tests
ruseinov Oct 22, 2023
5c20528
fix lint
ruseinov Oct 24, 2023
2db048b
fix nits
ruseinov Oct 28, 2023
279a7ee
simplify royalties and more tests
ruseinov Nov 3, 2023
369faaa
add comment
ruseinov Nov 3, 2023
e080566
Merge branch 'master' into feat/payout
ruseinov Nov 3, 2023
9e01a09
validate apply_percent
ruseinov Nov 3, 2023
423bdc9
fix
ruseinov Nov 3, 2023
30175c3
Merge branch 'master' into feat/payout
ruseinov Nov 9, 2023
5829c77
fix tests
ruseinov Nov 10, 2023
b59fc92
Merge branch 'master' into feat/payout
ruseinov Nov 24, 2023
312f4ac
fix payouts
ruseinov Nov 24, 2023
f2e709c
fix NearToken
ruseinov Nov 24, 2023
f2e3e08
Merge branch 'master' into feat/payout
ruseinov Dec 11, 2023
66ccf2c
fix workflow name
ruseinov Dec 12, 2023
1fa86f7
fix
ruseinov Dec 12, 2023
5a574b4
fix compilation
ruseinov Dec 13, 2023
7745888
Merge branch 'master' into feat/payout
ruseinov Jan 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
members = [
"near-sdk",
"near-sdk-macros",
# "near-sdk-sim",
frol marked this conversation as resolved.
Show resolved Hide resolved
"near-contract-standards",
"near-sys",
]
Expand Down
99 changes: 84 additions & 15 deletions examples/non-fungible-token/nft/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,25 @@ NOTES:
- To prevent the deployed contract from being modified or deleted, it should not have any access
keys on its account.
*/
use std::collections::HashMap;
use near_contract_standards::non_fungible_token::approval::NonFungibleTokenApproval;
use near_contract_standards::non_fungible_token::core::{
NonFungibleTokenCore, NonFungibleTokenResolver,
};
use near_contract_standards::non_fungible_token::enumeration::NonFungibleTokenEnumeration;
use near_contract_standards::non_fungible_token::metadata::{
NFTContractMetadata, NonFungibleTokenMetadataProvider, TokenMetadata, NFT_METADATA_SPEC,
};
use near_contract_standards::non_fungible_token::NonFungibleToken;
use near_contract_standards::non_fungible_token::payout::Payout;
use near_contract_standards::non_fungible_token::{NonFungibleToken, NonFungibleTokenPayout};
use near_contract_standards::non_fungible_token::{Token, TokenId};
use near_contract_standards::non_fungible_token::approval::NonFungibleTokenApproval;
use near_contract_standards::non_fungible_token::core::{NonFungibleTokenCore, NonFungibleTokenResolver};
use near_contract_standards::non_fungible_token::enumeration::NonFungibleTokenEnumeration;
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::collections::LazyOption;
use near_sdk::json_types::U128;
use near_sdk::{
env, near_bindgen, require, AccountId, BorshStorageKey, PanicOnDefault, Promise, PromiseOrValue,
assert_one_yocto, env, near_bindgen, require, AccountId, BorshStorageKey, PanicOnDefault,
Promise, PromiseOrValue,
};
use near_sdk::json_types::U128;
use std::collections::HashMap;

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
Expand All @@ -47,6 +51,7 @@ enum StorageKey {
TokenMetadata,
Enumeration,
Approval,
Royalties,
}

#[near_bindgen]
Expand Down Expand Up @@ -80,6 +85,7 @@ impl Contract {
Some(StorageKey::TokenMetadata),
Some(StorageKey::Enumeration),
Some(StorageKey::Approval),
Some(StorageKey::Royalties),
),
metadata: LazyOption::new(StorageKey::Metadata, Some(&metadata)),
}
Expand Down Expand Up @@ -108,12 +114,25 @@ impl Contract {
#[near_bindgen]
impl NonFungibleTokenCore for Contract {
#[payable]
fn nft_transfer(&mut self, receiver_id: AccountId, token_id: TokenId, approval_id: Option<u64>, memo: Option<String>) {
fn nft_transfer(
&mut self,
receiver_id: AccountId,
token_id: TokenId,
approval_id: Option<u64>,
memo: Option<String>,
) {
self.tokens.nft_transfer(receiver_id, token_id, approval_id, memo);
}

#[payable]
fn nft_transfer_call(&mut self, receiver_id: AccountId, token_id: TokenId, approval_id: Option<u64>, memo: Option<String>, msg: String) -> PromiseOrValue<bool> {
fn nft_transfer_call(
&mut self,
receiver_id: AccountId,
token_id: TokenId,
approval_id: Option<u64>,
memo: Option<String>,
msg: String,
) -> PromiseOrValue<bool> {
self.tokens.nft_transfer_call(receiver_id, token_id, approval_id, memo, msg)
}

Expand All @@ -125,15 +144,31 @@ impl NonFungibleTokenCore for Contract {
#[near_bindgen]
impl NonFungibleTokenResolver for Contract {
#[private]
fn nft_resolve_transfer(&mut self, previous_owner_id: AccountId, receiver_id: AccountId, token_id: TokenId, approved_account_ids: Option<HashMap<AccountId, u64>>) -> bool {
self.tokens.nft_resolve_transfer(previous_owner_id, receiver_id, token_id, approved_account_ids)
fn nft_resolve_transfer(
&mut self,
previous_owner_id: AccountId,
receiver_id: AccountId,
token_id: TokenId,
approved_account_ids: Option<HashMap<AccountId, u64>>,
) -> bool {
self.tokens.nft_resolve_transfer(
previous_owner_id,
receiver_id,
token_id,
approved_account_ids,
)
}
}

#[near_bindgen]
impl NonFungibleTokenApproval for Contract {
#[payable]
fn nft_approve(&mut self, token_id: TokenId, account_id: AccountId, msg: Option<String>) -> Option<Promise> {
fn nft_approve(
&mut self,
token_id: TokenId,
account_id: AccountId,
msg: Option<String>,
) -> Option<Promise> {
self.tokens.nft_approve(token_id, account_id, msg)
}

Expand All @@ -145,10 +180,14 @@ impl NonFungibleTokenApproval for Contract {
#[payable]
fn nft_revoke_all(&mut self, token_id: TokenId) {
self.tokens.nft_revoke_all(token_id);

}

fn nft_is_approved(&self, token_id: TokenId, approved_account_id: AccountId, approval_id: Option<u64>) -> bool {
fn nft_is_approved(
&self,
token_id: TokenId,
approved_account_id: AccountId,
approval_id: Option<u64>,
) -> bool {
self.tokens.nft_is_approved(token_id, approved_account_id, approval_id)
}
}
Expand All @@ -167,11 +206,41 @@ impl NonFungibleTokenEnumeration for Contract {
self.tokens.nft_supply_for_owner(account_id)
}

fn nft_tokens_for_owner(&self, account_id: AccountId, from_index: Option<U128>, limit: Option<u64>) -> Vec<Token> {
fn nft_tokens_for_owner(
&self,
account_id: AccountId,
from_index: Option<U128>,
limit: Option<u64>,
) -> Vec<Token> {
self.tokens.nft_tokens_for_owner(account_id, from_index, limit)
}
}

impl NonFungibleTokenPayout for Contract {
fn nft_payout(&self, token_id: String, balance: U128, max_len_payout: Option<u32>) -> Payout {
let owner_id = self.tokens.owner_by_id.get(&token_id).expect("No such token_id");
self.tokens
.royalties
.as_ref()
.map_or(Payout::default(), |r| r.create_payout(balance.0, &owner_id))
}

fn nft_transfer_payout(
&mut self,
receiver_id: AccountId,
token_id: String,
approval_id: Option<u64>,
memo: Option<String>,
balance: U128,
max_len_payout: Option<u32>,
) -> Payout {
assert_one_yocto();
let payout = self.nft_payout(token_id.clone(), balance, max_len_payout);
self.nft_transfer(receiver_id, token_id, approval_id, memo);
payout
}
}

#[near_bindgen]
impl NonFungibleTokenMetadataProvider for Contract {
fn nft_metadata(&self) -> NFTContractMetadata {
Expand Down
80 changes: 20 additions & 60 deletions examples/non-fungible-token/tests/workspaces/test_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,13 @@ async fn simulate_simple_transfer() -> anyhow::Result<()> {
let worker = workspaces::sandbox().await?;
let (nft_contract, alice, _, _) = init(&worker).await?;

let token = nft_contract
.call("nft_token")
.args_json((TOKEN_ID,))
.view()
.await?
.json::<Token>()?;
let token =
nft_contract.call("nft_token").args_json((TOKEN_ID,)).view().await?.json::<Token>()?;
assert_eq!(token.owner_id.to_string(), nft_contract.id().to_string());

let res = nft_contract
.call("nft_transfer")
.args_json((
alice.id(),
TOKEN_ID,
Option::<u64>::None,
Some("simple transfer".to_string()),
))
.args_json((alice.id(), TOKEN_ID, Option::<u64>::None, Some("simple transfer".to_string())))
.max_gas()
.deposit(ONE_YOCTO)
.transact()
Expand All @@ -33,12 +24,8 @@ async fn simulate_simple_transfer() -> anyhow::Result<()> {
// A single NFT transfer event should have been logged:
assert_eq!(res.logs().len(), 1);

let token = nft_contract
.call("nft_token")
.args_json((TOKEN_ID,))
.view()
.await?
.json::<Token>()?;
let token =
nft_contract.call("nft_token").args_json((TOKEN_ID,)).view().await?.json::<Token>()?;
assert_eq!(token.owner_id.to_string(), alice.id().to_string());

Ok(())
Expand All @@ -64,12 +51,8 @@ async fn simulate_transfer_call_fast_return_to_sender() -> anyhow::Result<()> {
.await?;
assert!(res.is_success());

let token = nft_contract
.call("nft_token")
.args_json((TOKEN_ID,))
.view()
.await?
.json::<Token>()?;
let token =
nft_contract.call("nft_token").args_json((TOKEN_ID,)).view().await?.json::<Token>()?;
assert_eq!(token.owner_id.to_string(), nft_contract.id().to_string());

Ok(())
Expand All @@ -95,12 +78,8 @@ async fn simulate_transfer_call_slow_return_to_sender() -> anyhow::Result<()> {
.await?;
assert!(res.is_success());

let token = nft_contract
.call("nft_token")
.args_json((TOKEN_ID,))
.view()
.await?
.json::<Token>()?;
let token =
nft_contract.call("nft_token").args_json((TOKEN_ID,)).view().await?.json::<Token>()?;
assert_eq!(token.owner_id.to_string(), nft_contract.id().to_string());

Ok(())
Expand All @@ -127,12 +106,8 @@ async fn simulate_transfer_call_fast_keep_with_sender() -> anyhow::Result<()> {
assert!(res.is_success());
assert_eq!(res.logs().len(), 2);

let token = nft_contract
.call("nft_token")
.args_json((TOKEN_ID,))
.view()
.await?
.json::<Token>()?;
let token =
nft_contract.call("nft_token").args_json((TOKEN_ID,)).view().await?.json::<Token>()?;
assert_eq!(token.owner_id.to_string(), token_receiver_contract.id().to_string());

Ok(())
Expand All @@ -158,12 +133,8 @@ async fn simulate_transfer_call_slow_keep_with_sender() -> anyhow::Result<()> {
.await?;
assert!(res.is_success());

let token = nft_contract
.call("nft_token")
.args_json((TOKEN_ID,))
.view()
.await?
.json::<Token>()?;
let token =
nft_contract.call("nft_token").args_json((TOKEN_ID,)).view().await?.json::<Token>()?;
assert_eq!(token.owner_id.to_string(), token_receiver_contract.id().to_string());

Ok(())
Expand Down Expand Up @@ -192,12 +163,8 @@ async fn simulate_transfer_call_receiver_panics() -> anyhow::Result<()> {
// Prints final logs
assert_eq!(res.logs().len(), 3);

let token = nft_contract
.call("nft_token")
.args_json((TOKEN_ID,))
.view()
.await?
.json::<Token>()?;
let token =
nft_contract.call("nft_token").args_json((TOKEN_ID,)).view().await?.json::<Token>()?;
assert_eq!(token.owner_id.to_string(), nft_contract.id().to_string());

Ok(())
Expand All @@ -222,17 +189,14 @@ async fn simulate_transfer_call_receiver_panics_and_nft_resolve_transfer_produce
.deposit(ONE_YOCTO)
.transact()
.await?;

assert!(res.is_failure());

// Prints no logs
assert_eq!(res.logs().len(), 0);

let token = nft_contract
.call("nft_token")
.args_json((TOKEN_ID,))
.view()
.await?
.json::<Token>()?;
let token =
nft_contract.call("nft_token").args_json((TOKEN_ID,)).view().await?.json::<Token>()?;
assert_eq!(token.owner_id.to_string(), nft_contract.id().to_string());

Ok(())
Expand All @@ -256,12 +220,8 @@ async fn simulate_simple_transfer_no_logs_on_failure() -> anyhow::Result<()> {
// Prints no logs
assert_eq!(res.logs().len(), 0);

let token = nft_contract
.call("nft_token")
.args_json((TOKEN_ID,))
.view()
.await?
.json::<Token>()?;
let token =
nft_contract.call("nft_token").args_json((TOKEN_ID,)).view().await?.json::<Token>()?;
assert_eq!(token.owner_id.to_string(), nft_contract.id().to_string());

Ok(())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::non_fungible_token::core::resolver::ext_nft_resolver;
use crate::non_fungible_token::core::NonFungibleTokenCore;
use crate::non_fungible_token::events::{NftMint, NftTransfer};
use crate::non_fungible_token::metadata::TokenMetadata;
use crate::non_fungible_token::payout::Royalties;
use crate::non_fungible_token::token::{Token, TokenId};
use crate::non_fungible_token::utils::{refund_approved_account_ids, refund_deposit_to_account};
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
Expand Down Expand Up @@ -47,6 +48,7 @@ pub struct NonFungibleToken {
// required by approval extension
pub approvals_by_id: Option<LookupMap<TokenId, HashMap<AccountId, u64>>>,
pub next_approval_id_by_id: Option<LookupMap<TokenId, u64>>,
pub royalties: Option<Royalties>,
frol marked this conversation as resolved.
Show resolved Hide resolved
}

#[derive(BorshStorageKey, BorshSerialize)]
Expand All @@ -55,18 +57,20 @@ pub enum StorageKey {
}

impl NonFungibleToken {
pub fn new<Q, R, S, T>(
pub fn new<Q, R, S, T, Y>(
owner_by_id_prefix: Q,
owner_id: AccountId,
token_metadata_prefix: Option<R>,
enumeration_prefix: Option<S>,
approval_prefix: Option<T>,
royalties_prefix: Option<Y>,
) -> Self
where
Q: IntoStorageKey,
R: IntoStorageKey,
S: IntoStorageKey,
T: IntoStorageKey,
Y: IntoStorageKey,
{
let (approvals_by_id, next_approval_id_by_id) = if let Some(prefix) = approval_prefix {
let prefix: Vec<u8> = prefix.into_storage_key();
Expand All @@ -86,6 +90,7 @@ impl NonFungibleToken {
tokens_per_owner: enumeration_prefix.map(LookupMap::new),
approvals_by_id,
next_approval_id_by_id,
royalties: royalties_prefix.map(Royalties::new),
};
this.measure_min_token_storage_cost();
this
Expand Down Expand Up @@ -391,7 +396,6 @@ impl NonFungibleTokenCore for NonFungibleToken {
msg: String,
) -> PromiseOrValue<bool> {
assert_one_yocto();
require!(env::prepaid_gas() > GAS_FOR_NFT_TRANSFER_CALL, "More gas is required");
Copy link
Collaborator

@frol frol Sep 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this explicit check users may see "gas exceed" error while their function call consumed way less than they attached (it will fail on an attempt to attach gas to the cross-contract call, so this function may have 10Tgas attached, 1TGas used, and fail with "gas exceeded" error as it fails to make a cross-contract call that needs 100Tgas). Though, I would make the error message even more explicit with something like: "nft_transfer_call requires at least X Tgas to cover the gas for nft_on_transfer cross-contract call"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I see what I've missed.

let sender_id = env::predecessor_account_id();
let (old_owner, old_approvals) =
self.internal_transfer(&sender_id, &receiver_id, &token_id, approval_id, memo);
Expand Down
Loading
Loading