├── tests ├── main.rs ├── tests │ ├── mod.rs │ ├── enumeration.rs │ ├── core.rs │ └── approval.rs ├── contracts │ ├── token-receiver │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ └── approval-receiver │ │ ├── Cargo.toml │ │ └── src │ │ └── lib.rs └── common │ └── mod.rs ├── .gitattributes ├── rust-toolchain.toml ├── .github └── workflows │ ├── tests.yml │ └── add-to-devrel.yml ├── Cargo.toml ├── .gitignore ├── LICENSE ├── README.md ├── LICENSE-APACHE └── src └── lib.rs /tests/main.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | mod tests; 3 | -------------------------------------------------------------------------------- /tests/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod approval; 2 | mod enumeration; 3 | mod core; 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | package-lock.json linguist-generated=true -diff 2 | yarn.lock linguist-generated=true -diff 3 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = ["rustfmt"] 4 | targets = ["wasm32-unknown-unknown"] 5 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: push 3 | jobs: 4 | tests: 5 | strategy: 6 | matrix: 7 | platform: [ubuntu-latest, macos-latest] 8 | runs-on: ${{ matrix.platform }} 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Install and test modules 12 | run: | 13 | cargo test 14 | -------------------------------------------------------------------------------- /tests/contracts/token-receiver/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "token-receiver" 3 | version = "0.0.1" 4 | authors = ["Near Inc "] 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | near-sdk = "5.5.0" 12 | near-contract-standards = "5.5.0" 13 | 14 | [dev-dependencies] 15 | near-sdk = { version = "5.5.0", features = ["unit-testing"] } -------------------------------------------------------------------------------- /tests/contracts/approval-receiver/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "approval-receiver" 3 | version = "0.0.1" 4 | authors = ["Near Inc "] 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | near-sdk = "5.5.0" 12 | near-contract-standards = "5.5.0" 13 | 14 | [dev-dependencies] 15 | near-sdk = { version = "5.5.0", features = ["unit-testing"] } 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "non-fungible-token" 3 | version = "1.1.0" 4 | authors = ["Near Inc "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | near-sdk = "5.5.0" 12 | near-contract-standards = "5.5.0" 13 | 14 | [dev-dependencies] 15 | near-sdk = { version = "5.5.0", features = ["unit-testing"] } 16 | near-workspaces = { version = "0.14.1", features = ["unstable"] } 17 | anyhow = "1.0" 18 | tokio = { version = "1.41.0", features = ["full"] } -------------------------------------------------------------------------------- /.github/workflows/add-to-devrel.yml: -------------------------------------------------------------------------------- 1 | name: 'Add to DevRel Project' 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - reopened 8 | pull_request_target: 9 | types: 10 | - opened 11 | - reopened 12 | 13 | jobs: 14 | add-to-project: 15 | name: Add issue/PR to project 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/add-to-project@v1.0.0 19 | with: 20 | # add to DevRel Project #117 21 | project-url: https://github.com/orgs/near/projects/117 22 | github-token: ${{ secrets.PROJECT_GH_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | # Developer note: near.gitignore will be renamed to .gitignore upon project creation 3 | # dependencies 4 | package-lock.json 5 | **/node_modules 6 | /.pnp 7 | .pnp.js 8 | **/out 9 | *.lock 10 | 11 | #keys 12 | **/neardev 13 | 14 | # testing 15 | /coverage 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | .env.local 23 | .env.development.local 24 | .env.test.local 25 | .env.production.local 26 | .testnet 27 | 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | **/target 33 | *.wasm -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 NEAR Inc 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use near_contract_standards::non_fungible_token::metadata::TokenMetadata; 2 | use near_contract_standards::non_fungible_token::TokenId; 3 | 4 | use near_sdk::serde_json::json; 5 | use near_sdk::AccountId; 6 | use near_workspaces::types::NearToken; 7 | use near_workspaces::{Account, Contract}; 8 | 9 | pub async fn mint_nft( 10 | minter: &Account, 11 | contract_id: &AccountId, 12 | token_id: TokenId, 13 | token_owner_id: &AccountId, 14 | ) -> anyhow::Result<()> { 15 | let token_metadata = TokenMetadata { 16 | title: Some(format!("Title for {token_id}")), 17 | description: Some(format!("Description for {token_id}")), 18 | media: None, 19 | media_hash: None, 20 | copies: Some(1u64), 21 | issued_at: None, 22 | expires_at: None, 23 | starts_at: None, 24 | updated_at: None, 25 | extra: None, 26 | reference: None, 27 | reference_hash: None, 28 | }; 29 | let res = minter 30 | .call(contract_id, "nft_mint") 31 | .args_json(json!({"token_id": token_id, "token_owner_id": token_owner_id, "token_metadata": token_metadata})) 32 | .max_gas() 33 | .deposit(NearToken::from_millinear(7)) 34 | .transact() 35 | .await?; 36 | assert!(res.is_success()); 37 | 38 | Ok(()) 39 | } 40 | 41 | pub async fn init_nft_contract(contract: &Contract) -> anyhow::Result<()> { 42 | let res = contract 43 | .call("new_default_meta") 44 | .args_json((contract.id(),)) 45 | .max_gas() 46 | .transact() 47 | .await?; 48 | assert!(res.is_success()); 49 | 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /tests/contracts/approval-receiver/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | A stub contract that implements nft_on_approve for e2e testing nft_approve. 3 | */ 4 | use near_contract_standards::non_fungible_token::approval::NonFungibleTokenApprovalReceiver; 5 | use near_contract_standards::non_fungible_token::TokenId; 6 | use near_sdk::{env, log, near, require, AccountId, Gas, PromiseOrValue}; 7 | 8 | /// It is estimated that we need to attach 5 TGas for the code execution and 5 TGas for cross-contract call 9 | const GAS_FOR_NFT_ON_APPROVE: Gas = Gas::from_tgas(10); 10 | 11 | #[near(contract_state)] 12 | #[derive(Default)] 13 | pub struct ApprovalReceiver {} 14 | 15 | // Have to repeat the same trait for our own implementation. 16 | pub trait ValueReturnTrait { 17 | fn ok_go(&self, msg: String) -> PromiseOrValue; 18 | } 19 | 20 | #[near] 21 | impl NonFungibleTokenApprovalReceiver for ApprovalReceiver { 22 | /// Could do anything useful to the approval-receiving contract, such as store the given 23 | /// approval_id for use later when calling the NFT contract. Can also return whatever it wants, 24 | /// maybe after further promise calls. This one simulates "return anything" behavior only. 25 | /// Supports the following `msg` patterns: 26 | /// * "return-now" - immediately return `"cool"` 27 | /// * anything else - return the given `msg` after one more cross-contract call 28 | fn nft_on_approve( 29 | &mut self, 30 | token_id: TokenId, 31 | owner_id: AccountId, 32 | approval_id: u64, 33 | msg: String, 34 | ) -> PromiseOrValue { 35 | // Verifying that we were called by non-fungible token contract that we expect. 36 | log!( 37 | "in nft_on_approve; sender_id={}, previous_owner_id={}, token_id={}, msg={}", 38 | &token_id, 39 | &owner_id, 40 | &approval_id, 41 | msg 42 | ); 43 | match msg.as_str() { 44 | "return-now" => PromiseOrValue::Value("cool".to_string()), 45 | _ => { 46 | let prepaid_gas = env::prepaid_gas(); 47 | let account_id = env::current_account_id(); 48 | Self::ext(account_id) 49 | .with_static_gas(prepaid_gas.saturating_sub(GAS_FOR_NFT_ON_APPROVE)) 50 | .ok_go(msg) 51 | .into() 52 | } 53 | } 54 | } 55 | } 56 | 57 | #[near] 58 | impl ValueReturnTrait for ApprovalReceiver { 59 | fn ok_go(&self, msg: String) -> PromiseOrValue { 60 | log!("in ok_go, msg={}", msg); 61 | PromiseOrValue::Value(msg) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/contracts/token-receiver/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | A stub contract that implements nft_on_transfer for simulation testing nft_transfer_call. 3 | */ 4 | use near_contract_standards::non_fungible_token::core::NonFungibleTokenReceiver; 5 | use near_contract_standards::non_fungible_token::TokenId; 6 | use near_sdk::{env, log, near, require, AccountId, Gas, PromiseOrValue}; 7 | 8 | /// It is estimated that we need to attach 5 TGas for the code execution and 5 TGas for cross-contract call 9 | const GAS_FOR_NFT_ON_TRANSFER: Gas = Gas::from_tgas(10); 10 | 11 | #[near(contract_state)] 12 | #[derive(Default)] 13 | pub struct TokenReceiver {} 14 | 15 | // Have to repeat the same trait for our own implementation. 16 | pub trait ValueReturnTrait { 17 | fn ok_go(&self, return_it: bool) -> PromiseOrValue; 18 | } 19 | 20 | #[near] 21 | impl NonFungibleTokenReceiver for TokenReceiver { 22 | /// Returns true if token should be returned to `sender_id` 23 | /// Four supported `msg`s: 24 | /// * "return-it-now" - immediately return `true` 25 | /// * "keep-it-now" - immediately return `false` 26 | /// * "return-it-later" - make cross-contract call which resolves with `true` 27 | /// * "keep-it-later" - make cross-contract call which resolves with `false` 28 | /// Otherwise panics, which should also return token to `sender_id` 29 | fn nft_on_transfer( 30 | &mut self, 31 | sender_id: AccountId, 32 | previous_owner_id: AccountId, 33 | token_id: TokenId, 34 | msg: String, 35 | ) -> PromiseOrValue { 36 | // Verifying that we were called by non-fungible token contract that we expect. 37 | log!( 38 | "in nft_on_transfer; sender_id={}, previous_owner_id={}, token_id={}, msg={}", 39 | &sender_id, 40 | &previous_owner_id, 41 | &token_id, 42 | msg 43 | ); 44 | match msg.as_str() { 45 | "return-it-now" => PromiseOrValue::Value(true), 46 | "return-it-later" => { 47 | let prepaid_gas = env::prepaid_gas(); 48 | let account_id = env::current_account_id(); 49 | Self::ext(account_id) 50 | .with_static_gas(prepaid_gas.saturating_sub(GAS_FOR_NFT_ON_TRANSFER)) 51 | .ok_go(true) 52 | .into() 53 | } 54 | "keep-it-now" => PromiseOrValue::Value(false), 55 | "keep-it-later" => { 56 | let prepaid_gas = env::prepaid_gas(); 57 | let account_id = env::current_account_id(); 58 | Self::ext(account_id) 59 | .with_static_gas(prepaid_gas.saturating_sub(GAS_FOR_NFT_ON_TRANSFER)) 60 | .ok_go(false) 61 | .into() 62 | } 63 | _ => env::panic_str("unsupported msg"), 64 | } 65 | } 66 | } 67 | 68 | #[near] 69 | impl ValueReturnTrait for TokenReceiver { 70 | fn ok_go(&self, return_it: bool) -> PromiseOrValue { 71 | log!("in ok_go, return_it={}", return_it); 72 | PromiseOrValue::Value(return_it) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Non-fungible Token (NFT) Example 🖼️ 2 | 3 | [![](https://img.shields.io/badge/⋈%20Examples-Basics-green)](https://docs.near.org/tutorials/welcome) 4 | [![](https://img.shields.io/badge/Contract-Rust-red)](contract-rs) 5 | 6 | This repository contains an example implementation of a [non-fungible token] contract in Rust which uses [near-contract-standards] and workspaces-rs tests. 7 | 8 | [non-fungible token]: https://nomicon.io/Standards/NonFungibleToken/README.html 9 | [near-contract-standards]: https://github.com/near/near-sdk-rs/tree/master/near-contract-standards 10 | [near-workspaces-rs]: https://github.com/near/near-workspaces-rs 11 | 12 | >**Note**: If you'd like to learn how to create an NFT contract from scratch that explores every aspect of the [NEP-171](https://github.com/near/NEPs/blob/master/neps/nep-0171.md) standard including an NFT marketplace, check out the NFT [Zero to Hero Tutorial](https://docs.near.org/tutorials/nfts/introduction). 13 | 14 |
15 | 16 | ## How to Build Locally? 17 | 18 | Install [`cargo-near`](https://github.com/near/cargo-near) and run: 19 | 20 | ```bash 21 | cargo near build 22 | ``` 23 | 24 | > Note: to avoid issues, be sure to update your Rust compiler with `rustup update stable` 25 | 26 | ## How to Test Locally? 27 | 28 | ```bash 29 | cargo test 30 | ``` 31 | 32 | ## How to Deploy? 33 | 34 | To deploy manually, install [`cargo-near`](https://github.com/near/cargo-near) and run: 35 | 36 | ```bash 37 | # Create a new account 38 | cargo near create-dev-account 39 | 40 | # Deploy the contract on it 41 | cargo near deploy 42 | 43 | # Initialize the contract 44 | near call new_default_meta '{"owner_id": ""}' --accountId 45 | ``` 46 | 47 | ## Basic methods 48 | ```bash 49 | # View metadata 50 | near view nft_metadata 51 | 52 | # Mint a NFT 53 | near call nft_mint '{"token_id": "0", "token_owner_id": "", "token_metadata": { "title": "Olympus Mons", "description": "Tallest mountain in charted solar system", "media": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/00/Olympus_Mons_alt.jpg/1024px-Olympus_Mons_alt.jpg", "copies": 1}}' --accountId --deposit 0.1 54 | 55 | # View tokens for owner 56 | near view nft_tokens_for_owner '{"account_id": ""}' 57 | 58 | # Transfer a NFT 59 | near call nft_transfer '{"token_id": "0", "receiver_id": "", "memo": "transfer ownership"}' --accountId --depositYocto 1 60 | ``` 61 | 62 | ## Useful Links 63 | 64 | - [cargo-near](https://github.com/near/cargo-near) - NEAR smart contract development toolkit for Rust 65 | - [near CLI](https://near.cli.rs) - Iteract with NEAR blockchain from command line 66 | - [NEAR Rust SDK Documentation](https://docs.near.org/sdk/rust/introduction) 67 | - [NEAR Documentation](https://docs.near.org) 68 | - [NFT Zero to Hero Tutorial](https://docs.near.org/tutorials/nfts/introduction) 69 | - [NEAR StackOverflow](https://stackoverflow.com/questions/tagged/nearprotocol) 70 | - [NEAR Discord](https://near.chat) 71 | - [NEAR Telegram Developers Community Group](https://t.me/neardev) 72 | - NEAR DevHub: [Telegram](https://t.me/neardevhub), [Twitter](https://twitter.com/neardevhub) 73 | -------------------------------------------------------------------------------- /tests/tests/enumeration.rs: -------------------------------------------------------------------------------- 1 | use crate::common; 2 | use near_contract_standards::non_fungible_token::Token; 3 | use near_sdk::json_types::U128; 4 | use near_workspaces::{network::Sandbox, Worker}; 5 | 6 | #[tokio::test] 7 | async fn enumeration() -> anyhow::Result<()> { 8 | let nft_wasm = near_workspaces::compile_project(".").await.unwrap(); 9 | let worker = near_workspaces::sandbox().await?; 10 | 11 | let enum_total_supply = test_enum_total_supply(&worker, &nft_wasm); 12 | let enum_nft_tokens = test_enum_nft_tokens(&worker, &nft_wasm); 13 | let enum_nft_supply_for_owner = test_enum_nft_supply_for_owner(&worker, &nft_wasm); 14 | let enum_nft_tokens_for_owner = test_enum_nft_tokens_for_owner(&worker, &nft_wasm); 15 | 16 | enum_total_supply.await?; 17 | enum_nft_tokens.await?; 18 | enum_nft_supply_for_owner.await?; 19 | enum_nft_tokens_for_owner.await?; 20 | 21 | Ok(()) 22 | } 23 | 24 | async fn test_enum_total_supply( 25 | worker: &Worker, 26 | nft_wasm: &Vec, 27 | ) -> anyhow::Result<()> { 28 | let nft_contract = worker.dev_deploy(&nft_wasm).await?; 29 | common::init_nft_contract(&nft_contract).await?; 30 | 31 | common::mint_nft( 32 | &nft_contract.as_account(), 33 | nft_contract.id(), 34 | "id-0".into(), 35 | nft_contract.id(), 36 | ) 37 | .await?; 38 | 39 | common::mint_nft( 40 | &nft_contract.as_account(), 41 | nft_contract.id(), 42 | "id-1".into(), 43 | nft_contract.id(), 44 | ) 45 | .await?; 46 | 47 | common::mint_nft( 48 | &nft_contract.as_account(), 49 | nft_contract.id(), 50 | "id-2".into(), 51 | nft_contract.id(), 52 | ) 53 | .await?; 54 | 55 | let total_supply: U128 = nft_contract.call("nft_total_supply").view().await?.json()?; 56 | assert_eq!(total_supply, U128::from(3)); 57 | 58 | Ok(()) 59 | } 60 | 61 | async fn test_enum_nft_tokens(worker: &Worker, nft_wasm: &Vec) -> anyhow::Result<()> { 62 | let nft_contract = worker.dev_deploy(&nft_wasm).await?; 63 | common::init_nft_contract(&nft_contract).await?; 64 | 65 | common::mint_nft( 66 | &nft_contract.as_account(), 67 | nft_contract.id(), 68 | "id-0".into(), 69 | nft_contract.id(), 70 | ) 71 | .await?; 72 | 73 | common::mint_nft( 74 | &nft_contract.as_account(), 75 | nft_contract.id(), 76 | "id-1".into(), 77 | nft_contract.id(), 78 | ) 79 | .await?; 80 | 81 | common::mint_nft( 82 | &nft_contract.as_account(), 83 | nft_contract.id(), 84 | "id-2".into(), 85 | nft_contract.id(), 86 | ) 87 | .await?; 88 | 89 | common::mint_nft( 90 | &nft_contract.as_account(), 91 | nft_contract.id(), 92 | "id-3".into(), 93 | nft_contract.id(), 94 | ) 95 | .await?; 96 | 97 | // No optional args should return all 98 | let mut tokens: Vec = nft_contract 99 | .call("nft_tokens") 100 | .args_json((Option::::None, Option::::None)) 101 | .view() 102 | .await? 103 | .json()?; 104 | assert_eq!(tokens.len(), 4); 105 | 106 | // Start at "1", with no limit arg 107 | tokens = nft_contract 108 | .call("nft_tokens") 109 | .args_json((Some(U128::from(1)), Option::::None)) 110 | .view() 111 | .await? 112 | .json()?; 113 | assert_eq!(tokens.len(), 3); 114 | assert_eq!(tokens.get(0).unwrap().token_id, "id-1".to_string()); 115 | assert_eq!(tokens.get(1).unwrap().token_id, "id-2".to_string()); 116 | assert_eq!(tokens.get(2).unwrap().token_id, "id-3".to_string()); 117 | 118 | // Start at "2", with limit 1 119 | tokens = nft_contract 120 | .call("nft_tokens") 121 | .args_json((Some(U128::from(2)), Some(1u64))) 122 | .view() 123 | .await? 124 | .json()?; 125 | assert_eq!(tokens.len(), 1); 126 | assert_eq!(tokens.get(0).unwrap().token_id, "id-2".to_string()); 127 | 128 | // Don't specify from_index, but limit 2 129 | tokens = nft_contract 130 | .call("nft_tokens") 131 | .args_json((Option::::None, Some(2u64))) 132 | .view() 133 | .await? 134 | .json()?; 135 | assert_eq!(tokens.len(), 2); 136 | assert_eq!(tokens.get(0).unwrap().token_id, "id-0".to_string()); 137 | assert_eq!(tokens.get(1).unwrap().token_id, "id-1".to_string()); 138 | 139 | Ok(()) 140 | } 141 | 142 | async fn test_enum_nft_supply_for_owner( 143 | worker: &Worker, 144 | nft_wasm: &Vec, 145 | ) -> anyhow::Result<()> { 146 | let nft_contract = worker.dev_deploy(&nft_wasm).await?; 147 | common::init_nft_contract(&nft_contract).await?; 148 | 149 | let alice = worker.dev_create_account().await?; 150 | 151 | // Get number from account with no NFTs 152 | let owner_num_tokens: U128 = nft_contract 153 | .call("nft_supply_for_owner") 154 | .args_json((alice.id(),)) 155 | .view() 156 | .await? 157 | .json()?; 158 | assert_eq!(owner_num_tokens, U128::from(0)); 159 | 160 | let owner_num_tokens: U128 = nft_contract 161 | .call("nft_supply_for_owner") 162 | .args_json((nft_contract.id(),)) 163 | .view() 164 | .await? 165 | .json()?; 166 | assert_eq!(owner_num_tokens, U128::from(0)); 167 | 168 | common::mint_nft(&nft_contract.as_account(), nft_contract.id(), "id-0".into(), nft_contract.id()).await?; 169 | 170 | common::mint_nft( 171 | &nft_contract.as_account(), 172 | nft_contract.id(), 173 | "id-1".into(), 174 | nft_contract.id(), 175 | ) 176 | .await?; 177 | 178 | common::mint_nft( 179 | &nft_contract.as_account(), 180 | nft_contract.id(), 181 | "id-2".into(), 182 | alice.id(), 183 | ) 184 | .await?; 185 | 186 | let owner_num_tokens: U128 = nft_contract 187 | .call("nft_supply_for_owner") 188 | .args_json((nft_contract.id(),)) 189 | .view() 190 | .await? 191 | .json()?; 192 | assert_eq!(owner_num_tokens, U128::from(2)); 193 | 194 | let alice_num_tokens: U128 = nft_contract 195 | .call("nft_supply_for_owner") 196 | .args_json((alice.id(),)) 197 | .view() 198 | .await? 199 | .json()?; 200 | assert_eq!(alice_num_tokens, U128::from(1)); 201 | 202 | Ok(()) 203 | } 204 | 205 | async fn test_enum_nft_tokens_for_owner( 206 | worker: &Worker, 207 | nft_wasm: &Vec, 208 | ) -> anyhow::Result<()> { 209 | let nft_contract = worker.dev_deploy(&nft_wasm).await?; 210 | common::init_nft_contract(&nft_contract).await?; 211 | 212 | let alice = worker.dev_create_account().await?; 213 | 214 | common::mint_nft( 215 | &nft_contract.as_account(), 216 | nft_contract.id(), 217 | "id-0".into(), 218 | nft_contract.id(), 219 | ) 220 | .await?; 221 | 222 | common::mint_nft( 223 | &nft_contract.as_account(), 224 | nft_contract.id(), 225 | "id-1".into(), 226 | nft_contract.id(), 227 | ) 228 | .await?; 229 | 230 | common::mint_nft( 231 | &nft_contract.as_account(), 232 | nft_contract.id(), 233 | "id-2".into(), 234 | nft_contract.id(), 235 | ) 236 | .await?; 237 | 238 | common::mint_nft( 239 | &nft_contract.as_account(), 240 | nft_contract.id(), 241 | "id-3".into(), 242 | nft_contract.id(), 243 | ) 244 | .await?; 245 | 246 | // Get tokens from account with no NFTs 247 | let owner_tokens: Vec = nft_contract 248 | .call("nft_tokens_for_owner") 249 | .args_json((alice.id(), Option::::None, Option::::None)) 250 | .view() 251 | .await? 252 | .json()?; 253 | assert_eq!(owner_tokens.len(), 0); 254 | 255 | // Get tokens with no optional args 256 | let owner_tokens: Vec = nft_contract 257 | .call("nft_tokens_for_owner") 258 | .args_json((nft_contract.id(), Option::::None, Option::::None)) 259 | .view() 260 | .await? 261 | .json()?; 262 | assert_eq!(owner_tokens.len(), 4); 263 | 264 | // With from_index and no limit 265 | let owner_tokens: Vec = nft_contract 266 | .call("nft_tokens_for_owner") 267 | .args_json((nft_contract.id(), Some(U128::from(2)), Option::::None)) 268 | .view() 269 | .await? 270 | .json()?; 271 | assert_eq!(owner_tokens.len(), 2); 272 | assert_eq!(owner_tokens.get(0).unwrap().token_id, "id-2".to_string()); 273 | assert_eq!(owner_tokens.get(1).unwrap().token_id, "id-3".to_string()); 274 | 275 | // With from_index and limit 1 276 | let owner_tokens: Vec = nft_contract 277 | .call("nft_tokens_for_owner") 278 | .args_json((nft_contract.id(), Some(U128::from(1)), Some(1u64))) 279 | .view() 280 | .await? 281 | .json()?; 282 | assert_eq!(owner_tokens.len(), 1); 283 | assert_eq!(owner_tokens.get(0).unwrap().token_id, "id-1".to_string()); 284 | 285 | // No from_index but limit 3 286 | let owner_tokens: Vec = nft_contract 287 | .call("nft_tokens_for_owner") 288 | .args_json((nft_contract.id(), Option::::None, Some(3u64))) 289 | .view() 290 | .await? 291 | .json()?; 292 | assert_eq!(owner_tokens.len(), 3); 293 | assert_eq!(owner_tokens.get(0).unwrap().token_id, "id-0".to_string()); 294 | assert_eq!(owner_tokens.get(1).unwrap().token_id, "id-1".to_string()); 295 | assert_eq!(owner_tokens.get(2).unwrap().token_id, "id-2".to_string()); 296 | 297 | Ok(()) 298 | } 299 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /tests/tests/core.rs: -------------------------------------------------------------------------------- 1 | use crate::common; 2 | use near_contract_standards::non_fungible_token::Token; 3 | 4 | use near_workspaces::{network::Sandbox, types::NearToken, Worker}; 5 | 6 | const ONE_YOCTO: NearToken = NearToken::from_yoctonear(1); 7 | const TOKEN_ID: &str = "id-0"; 8 | 9 | #[tokio::test] 10 | async fn core() -> anyhow::Result<()> { 11 | let nft_wasm = near_workspaces::compile_project(".").await.unwrap(); 12 | let token_receiver_wasm = near_workspaces::compile_project("./tests/contracts/token-receiver") 13 | .await 14 | .unwrap(); 15 | 16 | let worker: near_workspaces::Worker = 17 | near_workspaces::sandbox().await?; 18 | 19 | let simple_transfer = test_simple_transfer(&worker, &nft_wasm); 20 | let transfer_call_fast_return_to_sender = test_transfer_call_fast_return_to_sender( 21 | &worker, 22 | &nft_wasm, 23 | &token_receiver_wasm, 24 | ); 25 | let transfer_call_slow_return_to_sender = test_transfer_call_slow_return_to_sender( 26 | &worker, 27 | &nft_wasm, 28 | &token_receiver_wasm, 29 | ); 30 | let transfer_call_fast_keep_with_sender = test_transfer_call_fast_keep_with_sender( 31 | &worker, 32 | &nft_wasm, 33 | &token_receiver_wasm, 34 | ); 35 | let transfer_call_slow_keep_with_sender = test_transfer_call_slow_keep_with_sender( 36 | &worker, 37 | &nft_wasm, 38 | &token_receiver_wasm, 39 | ); 40 | let transfer_call_receiver_panics = test_transfer_call_receiver_panics( 41 | &worker, 42 | &nft_wasm, 43 | &token_receiver_wasm, 44 | ); 45 | let transfer_call_receiver_panics_and_nft_resolve_transfer_produces_no_log_if_not_enough_gas = test_transfer_call_receiver_panics_and_nft_resolve_transfer_produces_no_log_if_not_enough_gas(&worker, &nft_wasm, &token_receiver_wasm); 46 | let simple_transfer_no_logs_on_failure = test_simple_transfer_no_logs_on_failure( 47 | &worker, 48 | &nft_wasm, 49 | ); 50 | 51 | simple_transfer.await?; 52 | transfer_call_fast_return_to_sender.await?; 53 | transfer_call_slow_return_to_sender.await?; 54 | transfer_call_fast_keep_with_sender.await?; 55 | transfer_call_slow_keep_with_sender.await?; 56 | transfer_call_receiver_panics.await?; 57 | transfer_call_receiver_panics_and_nft_resolve_transfer_produces_no_log_if_not_enough_gas 58 | .await?; 59 | simple_transfer_no_logs_on_failure.await?; 60 | 61 | Ok(()) 62 | } 63 | 64 | async fn test_simple_transfer(worker: &Worker, nft_wasm: &Vec) -> anyhow::Result<()> { 65 | let alice = worker.dev_create_account().await?; 66 | let nft_contract = worker.dev_deploy(&nft_wasm).await?; 67 | common::init_nft_contract(&nft_contract).await?; 68 | 69 | common::mint_nft( 70 | nft_contract.as_account(), 71 | nft_contract.id(), 72 | TOKEN_ID.into(), 73 | nft_contract.id(), 74 | ) 75 | .await?; 76 | 77 | let token = nft_contract 78 | .call("nft_token") 79 | .args_json((TOKEN_ID,)) 80 | .view() 81 | .await? 82 | .json::()?; 83 | assert_eq!(token.owner_id.to_string(), nft_contract.id().to_string()); 84 | 85 | let res = nft_contract 86 | .call("nft_transfer") 87 | .args_json(( 88 | alice.id(), 89 | TOKEN_ID, 90 | Option::::None, 91 | Some("simple transfer".to_string()), 92 | )) 93 | .max_gas() 94 | .deposit(ONE_YOCTO) 95 | .transact() 96 | .await?; 97 | assert!(res.is_success()); 98 | 99 | // A single NFT transfer event should have been logged: 100 | assert_eq!(res.logs().len(), 1); 101 | 102 | let token = nft_contract 103 | .call("nft_token") 104 | .args_json((TOKEN_ID,)) 105 | .view() 106 | .await? 107 | .json::()?; 108 | assert_eq!(token.owner_id.to_string(), alice.id().to_string()); 109 | 110 | Ok(()) 111 | } 112 | 113 | async fn test_transfer_call_fast_return_to_sender( 114 | worker: &Worker, 115 | nft_wasm: &Vec, 116 | token_receiver_wasm: &Vec, 117 | ) -> anyhow::Result<()> { 118 | let nft_contract = worker.dev_deploy(&nft_wasm).await?; 119 | let token_receiver_contract = worker.dev_deploy(&token_receiver_wasm).await?; 120 | 121 | common::init_nft_contract(&nft_contract).await?; 122 | common::mint_nft( 123 | nft_contract.as_account(), 124 | nft_contract.id(), 125 | TOKEN_ID.into(), 126 | nft_contract.id(), 127 | ) 128 | .await?; 129 | 130 | let res = nft_contract 131 | .call("nft_transfer_call") 132 | .args_json(( 133 | token_receiver_contract.id(), 134 | TOKEN_ID, 135 | Option::::None, 136 | Some("transfer & call"), 137 | "return-it-now", 138 | )) 139 | .max_gas() 140 | .deposit(ONE_YOCTO) 141 | .transact() 142 | .await?; 143 | assert!(res.is_success()); 144 | 145 | let token = nft_contract 146 | .call("nft_token") 147 | .args_json((TOKEN_ID,)) 148 | .view() 149 | .await? 150 | .json::()?; 151 | assert_eq!(token.owner_id.to_string(), nft_contract.id().to_string()); 152 | 153 | Ok(()) 154 | } 155 | 156 | async fn test_transfer_call_slow_return_to_sender( 157 | worker: &Worker, 158 | nft_wasm: &Vec, 159 | token_receiver_wasm: &Vec, 160 | ) -> anyhow::Result<()> { 161 | let nft_contract = worker.dev_deploy(&nft_wasm).await?; 162 | let token_receiver_contract = worker.dev_deploy(&token_receiver_wasm).await?; 163 | 164 | common::init_nft_contract(&nft_contract).await?; 165 | common::mint_nft( 166 | nft_contract.as_account(), 167 | nft_contract.id(), 168 | TOKEN_ID.into(), 169 | nft_contract.id(), 170 | ) 171 | .await?; 172 | 173 | let res = nft_contract 174 | .call("nft_transfer_call") 175 | .args_json(( 176 | token_receiver_contract.id(), 177 | TOKEN_ID, 178 | Option::::None, 179 | Some("transfer & call"), 180 | "return-it-later", 181 | )) 182 | .max_gas() 183 | .deposit(ONE_YOCTO) 184 | .transact() 185 | .await?; 186 | assert!(res.is_success()); 187 | 188 | let token = nft_contract 189 | .call("nft_token") 190 | .args_json((TOKEN_ID,)) 191 | .view() 192 | .await? 193 | .json::()?; 194 | assert_eq!(token.owner_id.to_string(), nft_contract.id().to_string()); 195 | 196 | Ok(()) 197 | } 198 | 199 | async fn test_transfer_call_fast_keep_with_sender( 200 | worker: &Worker, 201 | nft_wasm: &Vec, 202 | token_receiver_wasm: &Vec, 203 | ) -> anyhow::Result<()> { 204 | let nft_contract = worker.dev_deploy(&nft_wasm).await?; 205 | let token_receiver_contract = worker.dev_deploy(&token_receiver_wasm).await?; 206 | 207 | common::init_nft_contract(&nft_contract).await?; 208 | common::mint_nft( 209 | nft_contract.as_account(), 210 | nft_contract.id(), 211 | TOKEN_ID.into(), 212 | nft_contract.id(), 213 | ) 214 | .await?; 215 | 216 | let res = nft_contract 217 | .call("nft_transfer_call") 218 | .args_json(( 219 | token_receiver_contract.id(), 220 | TOKEN_ID, 221 | Option::::None, 222 | Some("transfer & call"), 223 | "keep-it-now", 224 | )) 225 | .max_gas() 226 | .deposit(ONE_YOCTO) 227 | .transact() 228 | .await?; 229 | assert!(res.is_success()); 230 | assert_eq!(res.logs().len(), 2); 231 | 232 | let token = nft_contract 233 | .call("nft_token") 234 | .args_json((TOKEN_ID,)) 235 | .view() 236 | .await? 237 | .json::()?; 238 | assert_eq!( 239 | token.owner_id.to_string(), 240 | token_receiver_contract.id().to_string() 241 | ); 242 | 243 | Ok(()) 244 | } 245 | 246 | async fn test_transfer_call_slow_keep_with_sender( 247 | worker: &Worker, 248 | nft_wasm: &Vec, 249 | token_receiver_wasm: &Vec, 250 | ) -> anyhow::Result<()> { 251 | let nft_contract = worker.dev_deploy(&nft_wasm).await?; 252 | let token_receiver_contract = worker.dev_deploy(&token_receiver_wasm).await?; 253 | 254 | common::init_nft_contract(&nft_contract).await?; 255 | common::mint_nft( 256 | nft_contract.as_account(), 257 | nft_contract.id(), 258 | TOKEN_ID.into(), 259 | nft_contract.id(), 260 | ) 261 | .await?; 262 | 263 | let res = nft_contract 264 | .call("nft_transfer_call") 265 | .args_json(( 266 | token_receiver_contract.id(), 267 | TOKEN_ID, 268 | Option::::None, 269 | Some("transfer & call"), 270 | "keep-it-later", 271 | )) 272 | .max_gas() 273 | .deposit(ONE_YOCTO) 274 | .transact() 275 | .await?; 276 | assert!(res.is_success()); 277 | 278 | let token = nft_contract 279 | .call("nft_token") 280 | .args_json((TOKEN_ID,)) 281 | .view() 282 | .await? 283 | .json::()?; 284 | assert_eq!( 285 | token.owner_id.to_string(), 286 | token_receiver_contract.id().to_string() 287 | ); 288 | 289 | Ok(()) 290 | } 291 | 292 | async fn test_transfer_call_receiver_panics( 293 | worker: &Worker, 294 | nft_wasm: &Vec, 295 | token_receiver_wasm: &Vec, 296 | ) -> anyhow::Result<()> { 297 | let nft_contract = worker.dev_deploy(&nft_wasm).await?; 298 | let token_receiver_contract = worker.dev_deploy(&token_receiver_wasm).await?; 299 | 300 | common::init_nft_contract(&nft_contract).await?; 301 | common::mint_nft( 302 | nft_contract.as_account(), 303 | nft_contract.id(), 304 | TOKEN_ID.into(), 305 | nft_contract.id(), 306 | ) 307 | .await?; 308 | 309 | let res = nft_contract 310 | .call("nft_transfer_call") 311 | .args_json(( 312 | token_receiver_contract.id(), 313 | TOKEN_ID, 314 | Option::::None, 315 | Some("transfer & call"), 316 | "incorrect message", 317 | )) 318 | .gas(near_sdk::Gas::from_gas(35_000_000_000_000 + 1)) 319 | .deposit(ONE_YOCTO) 320 | .transact() 321 | .await?; 322 | assert!(res.is_success()); 323 | 324 | // Prints final logs 325 | assert_eq!(res.logs().len(), 3); 326 | 327 | let token = nft_contract 328 | .call("nft_token") 329 | .args_json((TOKEN_ID,)) 330 | .view() 331 | .await? 332 | .json::()?; 333 | assert_eq!(token.owner_id.to_string(), nft_contract.id().to_string()); 334 | 335 | Ok(()) 336 | } 337 | 338 | async fn test_transfer_call_receiver_panics_and_nft_resolve_transfer_produces_no_log_if_not_enough_gas( 339 | worker: &Worker, 340 | nft_wasm: &Vec, 341 | token_receiver_wasm: &Vec, 342 | ) -> anyhow::Result<()> { 343 | let nft_contract = worker.dev_deploy(&nft_wasm).await?; 344 | let token_receiver_contract = worker.dev_deploy(&token_receiver_wasm).await?; 345 | 346 | common::init_nft_contract(&nft_contract).await?; 347 | common::mint_nft( 348 | nft_contract.as_account(), 349 | nft_contract.id(), 350 | TOKEN_ID.into(), 351 | nft_contract.id(), 352 | ) 353 | .await?; 354 | 355 | let res = nft_contract 356 | .call("nft_transfer_call") 357 | .args_json(( 358 | token_receiver_contract.id(), 359 | TOKEN_ID, 360 | Option::::None, 361 | Some("transfer & call"), 362 | "incorrect message", 363 | )) 364 | .gas(near_sdk::Gas::from_tgas(30)) 365 | .deposit(ONE_YOCTO) 366 | .transact() 367 | .await?; 368 | assert!(res.is_failure()); 369 | 370 | // Prints no logs 371 | assert_eq!(res.logs().len(), 0); 372 | 373 | let token = nft_contract 374 | .call("nft_token") 375 | .args_json((TOKEN_ID,)) 376 | .view() 377 | .await? 378 | .json::()?; 379 | assert_eq!(token.owner_id.to_string(), nft_contract.id().to_string()); 380 | 381 | Ok(()) 382 | } 383 | 384 | async fn test_simple_transfer_no_logs_on_failure( 385 | worker: &Worker, 386 | nft_wasm: &Vec, 387 | ) -> anyhow::Result<()> { 388 | let nft_contract = worker.dev_deploy(&nft_wasm).await?; 389 | 390 | common::init_nft_contract(&nft_contract).await?; 391 | common::mint_nft( 392 | nft_contract.as_account(), 393 | nft_contract.id(), 394 | TOKEN_ID.into(), 395 | nft_contract.id(), 396 | ) 397 | .await?; 398 | 399 | 400 | let res = nft_contract 401 | .call("nft_transfer") 402 | // transfer to the current owner should fail and not print log 403 | .args_json(( 404 | nft_contract.id(), 405 | TOKEN_ID, 406 | Option::::None, 407 | Some("simple transfer"), 408 | )) 409 | .gas(near_sdk::Gas::from_tgas(200)) 410 | .deposit(ONE_YOCTO) 411 | .transact() 412 | .await?; 413 | assert!(res.is_failure()); 414 | 415 | // Prints no logs 416 | assert_eq!(res.logs().len(), 0); 417 | 418 | let token = nft_contract 419 | .call("nft_token") 420 | .args_json((TOKEN_ID,)) 421 | .view() 422 | .await? 423 | .json::()?; 424 | assert_eq!(token.owner_id.to_string(), nft_contract.id().to_string()); 425 | 426 | Ok(()) 427 | } 428 | -------------------------------------------------------------------------------- /tests/tests/approval.rs: -------------------------------------------------------------------------------- 1 | use crate::common; 2 | 3 | use near_contract_standards::non_fungible_token::Token; 4 | use near_workspaces::network::Sandbox; 5 | use near_workspaces::Worker; 6 | use near_workspaces::{types::NearToken, AccountId}; 7 | 8 | use std::collections::HashMap; 9 | use std::convert::TryFrom; 10 | 11 | pub const TOKEN_ID: &str = "0"; 12 | 13 | const ONE_NEAR: NearToken = NearToken::from_near(1); 14 | const ONE_YOCTO: NearToken = NearToken::from_yoctonear(1); 15 | 16 | #[tokio::test] 17 | async fn approval() -> anyhow::Result<()> { 18 | let nft_wasm = near_workspaces::compile_project(".").await.unwrap(); 19 | let token_receiver_wasm = near_workspaces::compile_project("./tests/contracts/token-receiver") 20 | .await 21 | .unwrap(); 22 | let approval_receiver_wasm = 23 | near_workspaces::compile_project("./tests/contracts/approval-receiver") 24 | .await 25 | .unwrap(); 26 | let worker: near_workspaces::Worker = 27 | near_workspaces::sandbox().await?; 28 | 29 | let simple_approval = test_simple_approve(&worker, &nft_wasm, &token_receiver_wasm); 30 | let approval_with_call = test_approval_with_call(&worker, &nft_wasm, &approval_receiver_wasm); 31 | let approved_account_transfers_token = 32 | test_approved_account_transfers_token(&worker, &nft_wasm); 33 | let revoke = test_revoke(&worker, &nft_wasm, &token_receiver_wasm); 34 | let revoke_all = test_revoke_all(&worker, &nft_wasm, &token_receiver_wasm); 35 | 36 | // make sure they all pass 37 | simple_approval.await?; 38 | approval_with_call.await?; 39 | approved_account_transfers_token.await?; 40 | revoke.await?; 41 | revoke_all.await?; 42 | 43 | Ok(()) 44 | } 45 | 46 | pub async fn test_simple_approve( 47 | worker: &Worker, 48 | nft_wasm: &Vec, 49 | token_receiver_wasm: &Vec, 50 | ) -> anyhow::Result<()> { 51 | let nft_contract = worker.dev_deploy(&nft_wasm).await?; 52 | common::init_nft_contract(&nft_contract).await?; 53 | common::mint_nft( 54 | nft_contract.as_account(), 55 | nft_contract.id(), 56 | TOKEN_ID.into(), 57 | nft_contract.id(), 58 | ) 59 | .await?; 60 | 61 | let token_receiver_contract = worker.dev_deploy(&token_receiver_wasm).await?; 62 | let alice = worker.dev_create_account().await?; 63 | 64 | // root approves alice 65 | let res = nft_contract 66 | .call("nft_approve") 67 | .args_json((TOKEN_ID, alice.id(), Option::::None)) 68 | .max_gas() 69 | .deposit(NearToken::from_yoctonear(510000000000000000000)) 70 | .transact() 71 | .await?; 72 | assert!(res.is_success()); 73 | 74 | // check nft_is_approved, don't provide approval_id 75 | let alice_approved = nft_contract 76 | .call("nft_is_approved") 77 | .args_json((TOKEN_ID, alice.id(), Option::::None)) 78 | .view() 79 | .await? 80 | .json::()?; 81 | assert!(alice_approved); 82 | 83 | // check nft_is_approved, with approval_id=1 84 | let alice_approval_id_is_1 = nft_contract 85 | .call("nft_is_approved") 86 | .args_json((TOKEN_ID, alice.id(), Some(1u64))) 87 | .view() 88 | .await? 89 | .json::()?; 90 | assert!(alice_approval_id_is_1); 91 | 92 | // check nft_is_approved, with approval_id=2 93 | let alice_approval_id_is_2 = nft_contract 94 | .call("nft_is_approved") 95 | .args_json(&(TOKEN_ID, alice.id(), Some(2u64))) 96 | .view() 97 | .await? 98 | .json::()?; 99 | assert!(!alice_approval_id_is_2); 100 | 101 | // alternatively, one could check the data returned by nft_token 102 | let token = nft_contract 103 | .call("nft_token") 104 | .args_json((TOKEN_ID,)) 105 | .view() 106 | .await? 107 | .json::()?; 108 | let mut expected_approvals: HashMap = HashMap::new(); 109 | expected_approvals.insert(AccountId::try_from(alice.id().to_string())?, 1); 110 | assert_eq!(token.approved_account_ids.unwrap(), expected_approvals); 111 | 112 | // root approves alice again, which changes the approval_id and doesn't require as much deposit 113 | let res = nft_contract 114 | .call("nft_approve") 115 | .args_json((TOKEN_ID, alice.id(), Option::::None)) 116 | .max_gas() 117 | .deposit(ONE_NEAR) 118 | .transact() 119 | .await?; 120 | assert!(res.is_success()); 121 | 122 | let alice_approval_id_is_2 = nft_contract 123 | .call("nft_is_approved") 124 | .args_json((TOKEN_ID, alice.id(), Some(2u64))) 125 | .view() 126 | .await? 127 | .json::()?; 128 | assert!(alice_approval_id_is_2); 129 | 130 | // approving another account gives different approval_id 131 | let res = nft_contract 132 | .call("nft_approve") 133 | .args_json(( 134 | TOKEN_ID, 135 | token_receiver_contract.id(), 136 | Option::::None, 137 | )) 138 | .max_gas() 139 | .deposit(NearToken::from_yoctonear(510000000000000000000)) 140 | .transact() 141 | .await?; 142 | assert!(res.is_success()); 143 | 144 | let token_receiver_approval_id_is_3 = nft_contract 145 | .call("nft_is_approved") 146 | .args_json((TOKEN_ID, token_receiver_contract.id(), Some(3u64))) 147 | .view() 148 | .await? 149 | .json::()?; 150 | assert!(token_receiver_approval_id_is_3); 151 | 152 | Ok(()) 153 | } 154 | 155 | pub async fn test_approval_with_call( 156 | worker: &Worker, 157 | nft_wasm: &Vec, 158 | approval_receiver_wasm: &Vec, 159 | ) -> anyhow::Result<()> { 160 | let nft_contract = worker.dev_deploy(&nft_wasm).await?; 161 | common::init_nft_contract(&nft_contract).await?; 162 | common::mint_nft( 163 | nft_contract.as_account(), 164 | nft_contract.id(), 165 | TOKEN_ID.into(), 166 | nft_contract.id(), 167 | ) 168 | .await?; 169 | 170 | let approval_receiver_contract = worker.dev_deploy(&approval_receiver_wasm).await?; 171 | 172 | let res = nft_contract 173 | .call("nft_approve") 174 | .args_json(( 175 | TOKEN_ID, 176 | approval_receiver_contract.id(), 177 | Some("return-now".to_string()), 178 | )) 179 | .max_gas() 180 | .deposit(NearToken::from_yoctonear(450000000000000000000)) 181 | .transact() 182 | .await?; 183 | assert_eq!(res.json::()?, "cool".to_string()); 184 | 185 | // Approve again; will set different approval_id (ignored by approval_receiver). 186 | // The approval_receiver implementation will return given `msg` after subsequent promise call, 187 | // if given something other than "return-now". 188 | let msg = "hahaha".to_string(); 189 | let res = nft_contract 190 | .call("nft_approve") 191 | .args_json((TOKEN_ID, approval_receiver_contract.id(), Some(msg.clone()))) 192 | .max_gas() 193 | .deposit(ONE_YOCTO) 194 | .transact() 195 | .await?; 196 | assert_eq!(res.json::()?, msg); 197 | 198 | Ok(()) 199 | } 200 | 201 | pub async fn test_approved_account_transfers_token( 202 | worker: &Worker, 203 | nft_wasm: &Vec, 204 | ) -> anyhow::Result<()> { 205 | let nft_contract = worker.dev_deploy(&nft_wasm).await?; 206 | common::init_nft_contract(&nft_contract).await?; 207 | common::mint_nft( 208 | nft_contract.as_account(), 209 | nft_contract.id(), 210 | TOKEN_ID.into(), 211 | nft_contract.id(), 212 | ) 213 | .await?; 214 | 215 | let alice = worker.dev_create_account().await?; 216 | 217 | // root approves alice 218 | let res = nft_contract 219 | .call("nft_approve") 220 | .args_json((TOKEN_ID, alice.id(), Option::::None)) 221 | .max_gas() 222 | .deposit(NearToken::from_yoctonear(510000000000000000000)) 223 | .transact() 224 | .await?; 225 | assert!(res.is_success()); 226 | 227 | // alice sends to self 228 | let res = alice 229 | .call(nft_contract.id(), "nft_transfer") 230 | .args_json(( 231 | alice.id(), 232 | TOKEN_ID, 233 | Some(1u64), 234 | Some("gotcha! bahahaha".to_string()), 235 | )) 236 | .max_gas() 237 | .deposit(ONE_YOCTO) 238 | .transact() 239 | .await?; 240 | assert!(res.is_success()); 241 | 242 | // token now owned by alice 243 | let token = nft_contract 244 | .call("nft_token") 245 | .args_json((TOKEN_ID,)) 246 | .view() 247 | .await? 248 | .json::()?; 249 | assert_eq!(token.owner_id.to_string(), alice.id().to_string()); 250 | 251 | Ok(()) 252 | } 253 | 254 | pub async fn test_revoke( 255 | worker: &Worker, 256 | nft_wasm: &Vec, 257 | token_receiver_wasm: &Vec, 258 | ) -> anyhow::Result<()> { 259 | let nft_contract = worker.dev_deploy(&nft_wasm).await?; 260 | common::init_nft_contract(&nft_contract).await?; 261 | common::mint_nft( 262 | nft_contract.as_account(), 263 | nft_contract.id(), 264 | TOKEN_ID.into(), 265 | nft_contract.id(), 266 | ) 267 | .await?; 268 | 269 | let token_receiver_contract = worker.dev_deploy(&token_receiver_wasm).await?; 270 | let alice = worker.dev_create_account().await?; 271 | 272 | // root approves alice 273 | let res = nft_contract 274 | .call("nft_approve") 275 | .args_json((TOKEN_ID, alice.id(), Option::::None)) 276 | .max_gas() 277 | .deposit(NearToken::from_yoctonear(510000000000000000000)) 278 | .transact() 279 | .await?; 280 | assert!(res.is_success()); 281 | 282 | // root approves token_receiver 283 | let res = nft_contract 284 | .call("nft_approve") 285 | .args_json(( 286 | TOKEN_ID, 287 | token_receiver_contract.id(), 288 | Option::::None, 289 | )) 290 | .max_gas() 291 | .deposit(NearToken::from_yoctonear(450000000000000000000)) 292 | .transact() 293 | .await?; 294 | assert!(res.is_success()); 295 | 296 | // root revokes alice 297 | let res = nft_contract 298 | .call("nft_revoke") 299 | .args_json((TOKEN_ID, alice.id())) 300 | .max_gas() 301 | .deposit(ONE_YOCTO) 302 | .transact() 303 | .await?; 304 | assert!(res.is_success()); 305 | 306 | // alice is revoked... 307 | let alice_approved = nft_contract 308 | .call("nft_is_approved") 309 | .args_json((TOKEN_ID, alice.id(), Some(3u64))) 310 | .view() 311 | .await? 312 | .json::()?; 313 | assert!(!alice_approved); 314 | 315 | // but token_receiver is still approved 316 | let token_receiver_approved = nft_contract 317 | .call("nft_is_approved") 318 | .args_json((TOKEN_ID, token_receiver_contract.id(), Option::::None)) 319 | .view() 320 | .await? 321 | .json::()?; 322 | assert!(token_receiver_approved); 323 | 324 | // root revokes token_receiver 325 | let res = nft_contract 326 | .call("nft_revoke") 327 | .args_json((TOKEN_ID, token_receiver_contract.id())) 328 | .max_gas() 329 | .deposit(ONE_YOCTO) 330 | .transact() 331 | .await?; 332 | assert!(res.is_success()); 333 | 334 | // alice is still revoked... 335 | let alice_approved = nft_contract 336 | .call("nft_is_approved") 337 | .args_json((TOKEN_ID, alice.id(), Some(3u64))) 338 | .view() 339 | .await? 340 | .json::()?; 341 | assert!(!alice_approved); 342 | 343 | // ...and now so is token_receiver 344 | let token_receiver_approved = nft_contract 345 | .call("nft_is_approved") 346 | .args_json((TOKEN_ID, token_receiver_contract.id(), Option::::None)) 347 | .view() 348 | .await? 349 | .json::()?; 350 | assert!(!token_receiver_approved); 351 | 352 | // alice tries to send it to self and fails 353 | let res = alice 354 | .call(nft_contract.id(), "nft_transfer") 355 | .args_json(( 356 | alice.id(), 357 | TOKEN_ID, 358 | Some(1u64), 359 | Some("gotcha! bahahaha".to_string()), 360 | )) 361 | .max_gas() 362 | .deposit(ONE_YOCTO) 363 | .transact() 364 | .await?; 365 | assert!(res.is_failure()); 366 | 367 | Ok(()) 368 | } 369 | 370 | pub async fn test_revoke_all( 371 | worker: &Worker, 372 | nft_wasm: &Vec, 373 | token_receiver_wasm: &Vec, 374 | ) -> anyhow::Result<()> { 375 | let nft_contract = worker.dev_deploy(&nft_wasm).await?; 376 | common::init_nft_contract(&nft_contract).await?; 377 | common::mint_nft( 378 | nft_contract.as_account(), 379 | nft_contract.id(), 380 | TOKEN_ID.into(), 381 | nft_contract.id(), 382 | ) 383 | .await?; 384 | 385 | let token_receiver_contract = worker.dev_deploy(&token_receiver_wasm).await?; 386 | let alice = worker.dev_create_account().await?; 387 | 388 | // root approves alice 389 | let res = nft_contract 390 | .call("nft_approve") 391 | .args_json((TOKEN_ID, alice.id(), Option::::None)) 392 | .max_gas() 393 | .deposit(NearToken::from_yoctonear(510000000000000000000)) 394 | .transact() 395 | .await?; 396 | assert!(res.is_success()); 397 | 398 | // root approves token_receiver 399 | let res = nft_contract 400 | .call("nft_approve") 401 | .args_json(( 402 | TOKEN_ID, 403 | token_receiver_contract.id(), 404 | Option::::None, 405 | )) 406 | .max_gas() 407 | .deposit(NearToken::from_yoctonear(450000000000000000000)) 408 | .transact() 409 | .await?; 410 | assert!(res.is_success()); 411 | 412 | // root revokes all 413 | let res = nft_contract 414 | .call("nft_revoke_all") 415 | .args_json((TOKEN_ID,)) 416 | .max_gas() 417 | .deposit(ONE_YOCTO) 418 | .transact() 419 | .await?; 420 | assert!(res.is_success()); 421 | 422 | // alice is revoked... 423 | let alice_approved = nft_contract 424 | .call("nft_is_approved") 425 | .args_json((TOKEN_ID, alice.id(), Some(3u64))) 426 | .view() 427 | .await? 428 | .json::()?; 429 | assert!(!alice_approved); 430 | 431 | // and so is token_receiver 432 | let token_receiver_approved = nft_contract 433 | .call("nft_is_approved") 434 | .args_json((TOKEN_ID, token_receiver_contract.id(), Option::::None)) 435 | .view() 436 | .await? 437 | .json::()?; 438 | assert!(!token_receiver_approved); 439 | 440 | // alice tries to send it to self and fails 441 | let res = alice 442 | .call(nft_contract.id(), "nft_transfer") 443 | .args_json(( 444 | alice.id(), 445 | TOKEN_ID, 446 | Some(1u64), 447 | Some("gotcha! bahahaha".to_string()), 448 | )) 449 | .max_gas() 450 | .deposit(ONE_YOCTO) 451 | .transact() 452 | .await?; 453 | assert!(res.is_failure()); 454 | 455 | // so does token_receiver 456 | let res = token_receiver_contract 457 | .as_account() 458 | .call(nft_contract.id(), "nft_transfer") 459 | .args_json(( 460 | alice.id(), 461 | TOKEN_ID, 462 | Some(1u64), 463 | Some("gotcha! bahahaha".to_string()), 464 | )) 465 | .max_gas() 466 | .deposit(ONE_YOCTO) 467 | .transact() 468 | .await?; 469 | assert!(res.is_failure()); 470 | 471 | Ok(()) 472 | } 473 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Non-Fungible Token implementation with JSON serialization. 3 | NOTES: 4 | - The maximum balance value is limited by U128 (2**128 - 1). 5 | - JSON calls should pass U128 as a base-10 string. E.g. "100". 6 | - The contract optimizes the inner trie structure by hashing account IDs. It will prevent some 7 | abuse of deep tries. Shouldn't be an issue, once NEAR clients implement full hashing of keys. 8 | - The contract tracks the change in storage before and after the call. If the storage increases, 9 | the contract requires the caller of the contract to attach enough deposit to the function call 10 | to cover the storage cost. 11 | This is done to prevent a denial of service attack on the contract by taking all available storage. 12 | If the storage decreases, the contract will issue a refund for the cost of the released storage. 13 | The unused tokens from the attached deposit are also refunded, so it's safe to 14 | attach more deposit than required. 15 | - To prevent the deployed contract from being modified or deleted, it should not have any access 16 | keys on its account. 17 | */ 18 | use near_contract_standards::non_fungible_token::approval::NonFungibleTokenApproval; 19 | use near_contract_standards::non_fungible_token::core::{ 20 | NonFungibleTokenCore, NonFungibleTokenResolver, 21 | }; 22 | use near_contract_standards::non_fungible_token::enumeration::NonFungibleTokenEnumeration; 23 | use near_contract_standards::non_fungible_token::metadata::{ 24 | NFTContractMetadata, NonFungibleTokenMetadataProvider, TokenMetadata, NFT_METADATA_SPEC, 25 | }; 26 | use near_contract_standards::non_fungible_token::NonFungibleToken; 27 | use near_contract_standards::non_fungible_token::{Token, TokenId}; 28 | use near_sdk::collections::LazyOption; 29 | use near_sdk::json_types::U128; 30 | use near_sdk::{ 31 | env, near, require, AccountId, BorshStorageKey, PanicOnDefault, Promise, PromiseOrValue, 32 | }; 33 | use std::collections::HashMap; 34 | 35 | #[derive(PanicOnDefault)] 36 | #[near(contract_state)] 37 | pub struct Contract { 38 | tokens: NonFungibleToken, 39 | metadata: LazyOption, 40 | } 41 | 42 | const DATA_IMAGE_SVG_NEAR_ICON: &str = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 288 288'%3E%3Cg id='l' data-name='l'%3E%3Cpath d='M187.58,79.81l-30.1,44.69a3.2,3.2,0,0,0,4.75,4.2L191.86,103a1.2,1.2,0,0,1,2,.91v80.46a1.2,1.2,0,0,1-2.12.77L102.18,77.93A15.35,15.35,0,0,0,90.47,72.5H87.34A15.34,15.34,0,0,0,72,87.84V201.16A15.34,15.34,0,0,0,87.34,216.5h0a15.35,15.35,0,0,0,13.08-7.31l30.1-44.69a3.2,3.2,0,0,0-4.75-4.2L96.14,186a1.2,1.2,0,0,1-2-.91V104.61a1.2,1.2,0,0,1,2.12-.77l89.55,107.23a15.35,15.35,0,0,0,11.71,5.43h3.13A15.34,15.34,0,0,0,216,201.16V87.84A15.34,15.34,0,0,0,200.66,72.5h0A15.35,15.35,0,0,0,187.58,79.81Z'/%3E%3C/g%3E%3C/svg%3E"; 43 | 44 | #[derive(BorshStorageKey)] 45 | #[near] 46 | enum StorageKey { 47 | NonFungibleToken, 48 | Metadata, 49 | TokenMetadata, 50 | Enumeration, 51 | Approval, 52 | } 53 | 54 | #[near] 55 | impl Contract { 56 | /// Initializes the contract owned by `owner_id` with 57 | /// default metadata (for example purposes only). 58 | #[init] 59 | pub fn new_default_meta(owner_id: AccountId) -> Self { 60 | Self::new( 61 | owner_id, 62 | NFTContractMetadata { 63 | spec: NFT_METADATA_SPEC.to_string(), 64 | name: "Example NEAR non-fungible token".to_string(), 65 | symbol: "EXAMPLE".to_string(), 66 | icon: Some(DATA_IMAGE_SVG_NEAR_ICON.to_string()), 67 | base_uri: None, 68 | reference: None, 69 | reference_hash: None, 70 | }, 71 | ) 72 | } 73 | 74 | #[init] 75 | pub fn new(owner_id: AccountId, metadata: NFTContractMetadata) -> Self { 76 | require!(!env::state_exists(), "Already initialized"); 77 | metadata.assert_valid(); 78 | Self { 79 | tokens: NonFungibleToken::new( 80 | StorageKey::NonFungibleToken, 81 | owner_id, 82 | Some(StorageKey::TokenMetadata), 83 | Some(StorageKey::Enumeration), 84 | Some(StorageKey::Approval), 85 | ), 86 | metadata: LazyOption::new(StorageKey::Metadata, Some(&metadata)), 87 | } 88 | } 89 | 90 | /// Mint a new token with ID=`token_id` belonging to `token_owner_id`. 91 | /// 92 | /// Since this example implements metadata, it also requires per-token metadata to be provided 93 | /// in this call. `self.tokens.mint` will also require it to be Some, since 94 | /// `StorageKey::TokenMetadata` was provided at initialization. 95 | /// 96 | #[payable] 97 | pub fn nft_mint( 98 | &mut self, 99 | token_id: TokenId, 100 | token_owner_id: AccountId, 101 | token_metadata: TokenMetadata, 102 | ) -> Token { 103 | assert_eq!( 104 | env::predecessor_account_id(), 105 | self.tokens.owner_id, 106 | "Unauthorized" 107 | ); 108 | self.tokens 109 | .internal_mint(token_id, token_owner_id, Some(token_metadata)) 110 | } 111 | } 112 | 113 | #[near] 114 | impl NonFungibleTokenCore for Contract { 115 | #[payable] 116 | fn nft_transfer( 117 | &mut self, 118 | receiver_id: AccountId, 119 | token_id: TokenId, 120 | approval_id: Option, 121 | memo: Option, 122 | ) { 123 | self.tokens 124 | .nft_transfer(receiver_id, token_id, approval_id, memo); 125 | } 126 | 127 | #[payable] 128 | fn nft_transfer_call( 129 | &mut self, 130 | receiver_id: AccountId, 131 | token_id: TokenId, 132 | approval_id: Option, 133 | memo: Option, 134 | msg: String, 135 | ) -> PromiseOrValue { 136 | self.tokens 137 | .nft_transfer_call(receiver_id, token_id, approval_id, memo, msg) 138 | } 139 | 140 | fn nft_token(&self, token_id: TokenId) -> Option { 141 | self.tokens.nft_token(token_id) 142 | } 143 | } 144 | 145 | #[near] 146 | impl NonFungibleTokenResolver for Contract { 147 | #[private] 148 | fn nft_resolve_transfer( 149 | &mut self, 150 | previous_owner_id: AccountId, 151 | receiver_id: AccountId, 152 | token_id: TokenId, 153 | approved_account_ids: Option>, 154 | ) -> bool { 155 | self.tokens.nft_resolve_transfer( 156 | previous_owner_id, 157 | receiver_id, 158 | token_id, 159 | approved_account_ids, 160 | ) 161 | } 162 | } 163 | 164 | #[near] 165 | impl NonFungibleTokenApproval for Contract { 166 | #[payable] 167 | fn nft_approve( 168 | &mut self, 169 | token_id: TokenId, 170 | account_id: AccountId, 171 | msg: Option, 172 | ) -> Option { 173 | self.tokens.nft_approve(token_id, account_id, msg) 174 | } 175 | 176 | #[payable] 177 | fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId) { 178 | self.tokens.nft_revoke(token_id, account_id); 179 | } 180 | 181 | #[payable] 182 | fn nft_revoke_all(&mut self, token_id: TokenId) { 183 | self.tokens.nft_revoke_all(token_id); 184 | } 185 | 186 | fn nft_is_approved( 187 | &self, 188 | token_id: TokenId, 189 | approved_account_id: AccountId, 190 | approval_id: Option, 191 | ) -> bool { 192 | self.tokens 193 | .nft_is_approved(token_id, approved_account_id, approval_id) 194 | } 195 | } 196 | 197 | #[near] 198 | impl NonFungibleTokenEnumeration for Contract { 199 | fn nft_total_supply(&self) -> U128 { 200 | self.tokens.nft_total_supply() 201 | } 202 | 203 | fn nft_tokens(&self, from_index: Option, limit: Option) -> Vec { 204 | self.tokens.nft_tokens(from_index, limit) 205 | } 206 | 207 | fn nft_supply_for_owner(&self, account_id: AccountId) -> U128 { 208 | self.tokens.nft_supply_for_owner(account_id) 209 | } 210 | 211 | fn nft_tokens_for_owner( 212 | &self, 213 | account_id: AccountId, 214 | from_index: Option, 215 | limit: Option, 216 | ) -> Vec { 217 | self.tokens 218 | .nft_tokens_for_owner(account_id, from_index, limit) 219 | } 220 | } 221 | 222 | #[near] 223 | impl NonFungibleTokenMetadataProvider for Contract { 224 | fn nft_metadata(&self) -> NFTContractMetadata { 225 | self.metadata.get().unwrap() 226 | } 227 | } 228 | 229 | #[cfg(all(test, not(target_arch = "wasm32")))] 230 | mod tests { 231 | use near_sdk::test_utils::{accounts, VMContextBuilder}; 232 | use near_sdk::{testing_env, NearToken}; 233 | use std::collections::HashMap; 234 | 235 | use super::*; 236 | 237 | const ZERO_NEAR: NearToken = NearToken::from_yoctonear(0); 238 | const ONE_YOCTONEAR: NearToken = NearToken::from_yoctonear(1); 239 | const MINT_STORAGE_COST: NearToken = NearToken::from_yoctonear(5870000000000000000000); 240 | const APPROVE_STORAGE_COST: NearToken = NearToken::from_yoctonear(150000000000000000000); 241 | 242 | fn get_context(predecessor_account_id: AccountId) -> VMContextBuilder { 243 | let mut builder = VMContextBuilder::new(); 244 | builder 245 | .current_account_id(accounts(0)) 246 | .signer_account_id(predecessor_account_id.clone()) 247 | .predecessor_account_id(predecessor_account_id); 248 | builder 249 | } 250 | 251 | fn sample_token_metadata() -> TokenMetadata { 252 | TokenMetadata { 253 | title: Some("Olympus Mons".into()), 254 | description: Some("The tallest mountain in the charted solar system".into()), 255 | media: None, 256 | media_hash: None, 257 | copies: Some(1u64), 258 | issued_at: None, 259 | expires_at: None, 260 | starts_at: None, 261 | updated_at: None, 262 | extra: None, 263 | reference: None, 264 | reference_hash: None, 265 | } 266 | } 267 | 268 | #[test] 269 | fn test_new() { 270 | let mut context = get_context(accounts(1)); 271 | testing_env!(context.build()); 272 | let contract = Contract::new_default_meta(accounts(1).into()); 273 | testing_env!(context.is_view(true).build()); 274 | assert_eq!(contract.nft_token("1".to_string()), None); 275 | } 276 | 277 | #[test] 278 | #[should_panic(expected = "The contract is not initialized")] 279 | fn test_default() { 280 | let context = get_context(accounts(1)); 281 | testing_env!(context.build()); 282 | let _contract = Contract::default(); 283 | } 284 | 285 | #[test] 286 | fn test_mint() { 287 | let mut context = get_context(accounts(0)); 288 | testing_env!(context.build()); 289 | let mut contract = Contract::new_default_meta(accounts(0).into()); 290 | 291 | testing_env!(context 292 | .storage_usage(env::storage_usage()) 293 | .attached_deposit(MINT_STORAGE_COST) 294 | .predecessor_account_id(accounts(0)) 295 | .build()); 296 | 297 | let token_id = "0".to_string(); 298 | let token = contract.nft_mint(token_id.clone(), accounts(0), sample_token_metadata()); 299 | assert_eq!(token.token_id, token_id); 300 | assert_eq!(token.owner_id, accounts(0)); 301 | assert_eq!(token.metadata.unwrap(), sample_token_metadata()); 302 | assert_eq!(token.approved_account_ids.unwrap(), HashMap::new()); 303 | } 304 | 305 | #[test] 306 | fn test_transfer() { 307 | let mut context = get_context(accounts(0)); 308 | testing_env!(context.build()); 309 | let mut contract = Contract::new_default_meta(accounts(0).into()); 310 | 311 | testing_env!(context 312 | .storage_usage(env::storage_usage()) 313 | .attached_deposit(MINT_STORAGE_COST) 314 | .predecessor_account_id(accounts(0)) 315 | .build()); 316 | let token_id = "0".to_string(); 317 | contract.nft_mint(token_id.clone(), accounts(0), sample_token_metadata()); 318 | 319 | testing_env!(context 320 | .storage_usage(env::storage_usage()) 321 | .attached_deposit(ONE_YOCTONEAR) 322 | .predecessor_account_id(accounts(0)) 323 | .build()); 324 | contract.nft_transfer(accounts(1), token_id.clone(), None, None); 325 | 326 | testing_env!(context 327 | .storage_usage(env::storage_usage()) 328 | .account_balance(env::account_balance()) 329 | .is_view(true) 330 | .attached_deposit(ZERO_NEAR) 331 | .build()); 332 | if let Some(token) = contract.nft_token(token_id.clone()) { 333 | assert_eq!(token.token_id, token_id); 334 | assert_eq!(token.owner_id, accounts(1)); 335 | assert_eq!(token.metadata.unwrap(), sample_token_metadata()); 336 | assert_eq!(token.approved_account_ids.unwrap(), HashMap::new()); 337 | } else { 338 | panic!("token not correctly created, or not found by nft_token"); 339 | } 340 | } 341 | 342 | #[test] 343 | fn test_approve() { 344 | let mut context = get_context(accounts(0)); 345 | testing_env!(context.build()); 346 | let mut contract = Contract::new_default_meta(accounts(0).into()); 347 | 348 | testing_env!(context 349 | .storage_usage(env::storage_usage()) 350 | .attached_deposit(MINT_STORAGE_COST) 351 | .predecessor_account_id(accounts(0)) 352 | .build()); 353 | let token_id = "0".to_string(); 354 | contract.nft_mint(token_id.clone(), accounts(0), sample_token_metadata()); 355 | 356 | // alice approves bob 357 | testing_env!(context 358 | .storage_usage(env::storage_usage()) 359 | .attached_deposit(APPROVE_STORAGE_COST) 360 | .predecessor_account_id(accounts(0)) 361 | .build()); 362 | contract.nft_approve(token_id.clone(), accounts(1), None); 363 | 364 | testing_env!(context 365 | .storage_usage(env::storage_usage()) 366 | .account_balance(env::account_balance()) 367 | .is_view(true) 368 | .attached_deposit(ZERO_NEAR) 369 | .build()); 370 | assert!(contract.nft_is_approved(token_id.clone(), accounts(1), Some(1))); 371 | } 372 | 373 | #[test] 374 | fn test_revoke() { 375 | let mut context = get_context(accounts(0)); 376 | testing_env!(context.build()); 377 | let mut contract = Contract::new_default_meta(accounts(0).into()); 378 | 379 | testing_env!(context 380 | .storage_usage(env::storage_usage()) 381 | .attached_deposit(MINT_STORAGE_COST) 382 | .predecessor_account_id(accounts(0)) 383 | .build()); 384 | let token_id = "0".to_string(); 385 | contract.nft_mint(token_id.clone(), accounts(0), sample_token_metadata()); 386 | 387 | // alice approves bob 388 | testing_env!(context 389 | .storage_usage(env::storage_usage()) 390 | .attached_deposit(APPROVE_STORAGE_COST) 391 | .predecessor_account_id(accounts(0)) 392 | .build()); 393 | contract.nft_approve(token_id.clone(), accounts(1), None); 394 | 395 | // alice revokes bob 396 | testing_env!(context 397 | .storage_usage(env::storage_usage()) 398 | .attached_deposit(ONE_YOCTONEAR) 399 | .predecessor_account_id(accounts(0)) 400 | .build()); 401 | contract.nft_revoke(token_id.clone(), accounts(1)); 402 | testing_env!(context 403 | .storage_usage(env::storage_usage()) 404 | .account_balance(env::account_balance()) 405 | .is_view(true) 406 | .attached_deposit(ZERO_NEAR) 407 | .build()); 408 | assert!(!contract.nft_is_approved(token_id.clone(), accounts(1), None)); 409 | } 410 | 411 | #[test] 412 | fn test_revoke_all() { 413 | let mut context = get_context(accounts(0)); 414 | testing_env!(context.build()); 415 | let mut contract = Contract::new_default_meta(accounts(0).into()); 416 | 417 | testing_env!(context 418 | .storage_usage(env::storage_usage()) 419 | .attached_deposit(MINT_STORAGE_COST) 420 | .predecessor_account_id(accounts(0)) 421 | .build()); 422 | let token_id = "0".to_string(); 423 | contract.nft_mint(token_id.clone(), accounts(0), sample_token_metadata()); 424 | 425 | // alice approves bob 426 | testing_env!(context 427 | .storage_usage(env::storage_usage()) 428 | .attached_deposit(APPROVE_STORAGE_COST) 429 | .predecessor_account_id(accounts(0)) 430 | .build()); 431 | contract.nft_approve(token_id.clone(), accounts(1), None); 432 | 433 | // alice revokes bob 434 | testing_env!(context 435 | .storage_usage(env::storage_usage()) 436 | .attached_deposit(ONE_YOCTONEAR) 437 | .predecessor_account_id(accounts(0)) 438 | .build()); 439 | contract.nft_revoke_all(token_id.clone()); 440 | testing_env!(context 441 | .storage_usage(env::storage_usage()) 442 | .account_balance(env::account_balance()) 443 | .is_view(true) 444 | .attached_deposit(ZERO_NEAR) 445 | .build()); 446 | assert!(!contract.nft_is_approved(token_id.clone(), accounts(1), Some(1))); 447 | } 448 | } 449 | --------------------------------------------------------------------------------