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 49 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
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ jobs:
toolchain: ${{ matrix.toolchain }}
default: true
target: wasm32-unknown-unknown
- name: downgrade `anstyle`,`anstyle-parse`,`clap`,`clap_lex` crates to support older Rust toolchain
- name: downgrade `anstyle`,`anstyle-parse`,`anstyle-query`,`clap`,`clap_lex` crates to support older Rust toolchain
if: matrix.toolchain == '1.69.0'
run: |
cargo update -p anstyle --precise 1.0.2
cargo update -p anstyle-query --precise 1.0.0
cargo update -p anstyle-parse --precise 0.2.1
cargo update -p clap --precise 4.3.24
cargo update -p clap_lex --precise 0.5.0
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added
- **BREAKING** Added [NEP-199 - Non-Fungible Token Royalties and Payouts](https://github.com/willemneal/NEPs/blob/18828873648eff1a2e8464db234aefd70918b3e0/neps/nep-0199.md) implementation. This
will need a state migration in case of upgrade for any contracts using NFT of previous versions due to an extra field.

## [5.0.0-alpha.1](https://github.com/near/near-sdk-rs/compare/4.1.1...near-sdk-v5.0.0-alpha.1) - 2023-11-18

### Added
Expand Down
111 changes: 110 additions & 1 deletion examples/non-fungible-token/nft/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ use near_contract_standards::non_fungible_token::enumeration::NonFungibleTokenEn
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_sdk::borsh::{BorshDeserialize, BorshSerialize};
use near_sdk::collections::LazyOption;
Expand Down Expand Up @@ -51,6 +52,7 @@ enum StorageKey {
TokenMetadata,
Enumeration,
Approval,
Royalties,
}

#[near_bindgen]
Expand Down Expand Up @@ -84,6 +86,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 @@ -214,6 +217,34 @@ impl NonFungibleTokenEnumeration for Contract {
}
}

#[near_bindgen]
impl NonFungibleTokenPayout for Contract {
#[allow(unused_variables)]
fn nft_payout(&self, token_id: String, balance: U128, max_len_payout: Option<u32>) -> Payout {
self.tokens.nft_payout(token_id, balance, max_len_payout)
}

#[payable]
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 {
self.tokens.nft_transfer_payout(
receiver_id,
token_id,
approval_id,
memo,
balance,
max_len_payout,
)
}
}

#[near_bindgen]
impl NonFungibleTokenMetadataProvider for Contract {
fn nft_metadata(&self) -> NFTContractMetadata {
Expand Down Expand Up @@ -331,6 +362,84 @@ mod tests {
}
}

#[test]
fn test_transfer_payout() {
let mut context = get_context(accounts(0));
testing_env!(context.build());
let mut contract = Contract::new_default_meta(accounts(0).into());

testing_env!(context
.storage_usage(env::storage_usage())
.attached_deposit(MINT_STORAGE_COST)
.predecessor_account_id(accounts(0))
.build());
let token_id = "0".to_string();
contract.nft_mint(token_id.clone(), accounts(0), sample_token_metadata());

testing_env!(context
.storage_usage(env::storage_usage())
.attached_deposit(NearToken::from_yoctonear(1))
.predecessor_account_id(accounts(0))
.build());

let payout = contract.nft_transfer_payout(
accounts(1),
token_id.clone(),
None,
None,
U128::from(1),
None,
);

let mut expected_payout = HashMap::new();
expected_payout.insert(accounts(0), U128::from(1));

assert_eq!(payout.payout, expected_payout);

testing_env!(context
.storage_usage(env::storage_usage())
.account_balance(env::account_balance())
.is_view(true)
.attached_deposit(NearToken::from_near(0))
.build());

if let Some(token) = contract.nft_token(token_id.clone()) {
assert_eq!(token.token_id, token_id);
assert_eq!(token.owner_id, accounts(1));
assert_eq!(token.metadata.unwrap(), sample_token_metadata());
assert_eq!(token.approved_account_ids.unwrap(), HashMap::new());
} else {
panic!("token not correctly created, or not found by nft_token");
}
}

#[test]
fn test_payout() {
let mut context = get_context(accounts(0));
testing_env!(context.build());
let mut contract = Contract::new_default_meta(accounts(0).into());

testing_env!(context
.storage_usage(env::storage_usage())
.attached_deposit(MINT_STORAGE_COST)
.predecessor_account_id(accounts(0))
.build());
let token_id = "0".to_string();
contract.nft_mint(token_id.clone(), accounts(0), sample_token_metadata());

testing_env!(context
.storage_usage(env::storage_usage())
.predecessor_account_id(accounts(0))
.build());

let payout = contract.nft_payout(token_id, U128::from(1), None);

let mut expected_payout = HashMap::new();
expected_payout.insert(accounts(0), U128::from(1));

assert_eq!(payout.payout, expected_payout);
}

#[test]
fn test_approve() {
let mut context = get_context(accounts(0));
Expand Down
1 change: 1 addition & 0 deletions examples/non-fungible-token/tests/workspaces/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod test_approval;
mod test_core;
mod test_enumeration;
mod test_payout;
mod utils;
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ async fn simulate_transfer_call_receiver_panics_and_nft_resolve_transfer_produce
.deposit(ONE_YOCTO)
.transact()
.await?;

assert!(res.is_failure());

// Prints no logs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,8 @@ async fn simulate_enum_nft_supply_for_owner() -> anyhow::Result<()> {
let (nft_contract, alice, _, _) = init(&worker).await?;

// Get number from account with no NFTs
let owner_num_tokens: U128 = nft_contract
.call("nft_supply_for_owner")
.args_json((alice.id(),))
.view()
.await?
.json()?;
let owner_num_tokens: U128 =
nft_contract.call("nft_supply_for_owner").args_json((alice.id(),)).view().await?.json()?;
assert_eq!(owner_num_tokens, U128::from(0));

let owner_num_tokens: U128 = nft_contract
Expand Down
61 changes: 61 additions & 0 deletions examples/non-fungible-token/tests/workspaces/test_payout.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use crate::utils::{init, TOKEN_ID};
use near_contract_standards::non_fungible_token::Token;
use near_sdk::json_types::U128;
use near_sdk::NearToken;

const ONE_YOCTO: NearToken = NearToken::from_yoctonear(1);

#[tokio::test]
async fn simulate_payout() -> anyhow::Result<()> {
let worker = near_workspaces::sandbox().await?;
let (nft_contract, alice, _, _) = init(&worker).await?;

let res = nft_contract
.call("nft_payout")
.args_json((TOKEN_ID, U128::from(1), Option::<u32>::None))
.max_gas()
.transact()
.await?;
assert!(res.is_success());

// A single NFT transfer event should have been logged:
assert_eq!(res.logs().len(), 0);

Ok(())
}

#[tokio::test]
async fn nft_transfer_payout() -> anyhow::Result<()> {
let worker = near_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>()?;
assert_eq!(token.owner_id.to_string(), nft_contract.id().to_string());

let res = nft_contract
.call("nft_transfer_payout")
.args_json((
alice.id(),
TOKEN_ID,
Option::<u64>::None,
Some("simple transfer".to_string()),
U128::from(1),
Option::<u32>::None,
))
.max_gas()
.deposit(ONE_YOCTO)
.transact()
.await?;

assert!(res.is_success());

// 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>()?;
assert_eq!(token.owner_id.to_string(), alice.id().to_string());

Ok(())
}
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::{BorshDeserialize, BorshSerialize};
Expand Down Expand Up @@ -49,6 +50,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 @@ -58,18 +60,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 @@ -89,6 +93,7 @@ impl NonFungibleToken {
tokens_per_owner: enumeration_prefix.map(LookupMap::new),
approvals_by_id,
next_approval_id_by_id,
royalties: royalties_prefix.map(|prefix| Royalties::new(prefix)),
};
this.measure_min_token_storage_cost();
this
Expand Down
4 changes: 4 additions & 0 deletions near-contract-standards/src/non_fungible_token/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ mod macros;
/// Metadata traits and implementation according to the [NFT enumeration standard](https://nomicon.io/Standards/NonFungibleToken/Metadata.html).
/// This covers both the contract metadata and the individual token metadata.
pub mod metadata;

pub mod payout;

/// The Token struct for the non-fungible token.
mod token;
pub use self::token::{Token, TokenId};
Expand All @@ -26,5 +29,6 @@ pub use self::approval::NonFungibleTokenApproval;
pub use self::core::NonFungibleToken;
pub use self::core::NonFungibleTokenResolver;
pub use self::enumeration::NonFungibleTokenEnumeration;
pub use self::payout::NonFungibleTokenPayout;

pub mod events;
Loading
Loading