├── .github └── workflows │ ├── add-to-devrel.yml │ └── tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── integration-tests ├── Cargo.toml ├── rust-toolchain.toml └── src │ ├── helpers.rs │ └── tests.rs ├── market-contract ├── Cargo.toml ├── README.md └── src │ ├── external.rs │ ├── internal.rs │ ├── lib.rs │ ├── nft_callbacks.rs │ ├── sale.rs │ ├── sale_views.rs │ └── tests.rs ├── nft-contract-approval ├── Cargo.toml ├── README.md └── src │ ├── approval.rs │ ├── enumeration.rs │ ├── events.rs │ ├── internal.rs │ ├── lib.rs │ ├── metadata.rs │ ├── mint.rs │ ├── nft_core.rs │ └── royalty.rs ├── nft-contract-basic ├── Cargo.toml ├── README.md └── src │ ├── approval.rs │ ├── enumeration.rs │ ├── internal.rs │ ├── lib.rs │ ├── metadata.rs │ ├── mint.rs │ ├── nft_core.rs │ └── royalty.rs ├── nft-contract-events ├── Cargo.toml ├── README.md └── src │ ├── approval.rs │ ├── enumeration.rs │ ├── events.rs │ ├── internal.rs │ ├── lib.rs │ ├── metadata.rs │ ├── mint.rs │ ├── nft_core.rs │ └── royalty.rs ├── nft-contract-royalty ├── Cargo.toml ├── README.md └── src │ ├── approval.rs │ ├── enumeration.rs │ ├── events.rs │ ├── internal.rs │ ├── lib.rs │ ├── metadata.rs │ ├── mint.rs │ ├── nft_core.rs │ └── royalty.rs ├── nft-contract-skeleton ├── Cargo.toml ├── README.md └── src │ ├── approval.rs │ ├── enumeration.rs │ ├── events.rs │ ├── lib.rs │ ├── metadata.rs │ ├── mint.rs │ ├── nft_core.rs │ └── royalty.rs └── nft-series ├── Cargo.toml ├── README.md ├── rust-toolchain.toml └── src ├── approval.rs ├── enumeration.rs ├── events.rs ├── internal.rs ├── lib.rs ├── metadata.rs ├── nft_core.rs ├── owner.rs ├── royalty.rs └── series.rs /.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 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | repository_dispatch: 4 | types: [tests-report] 5 | push: 6 | jobs: 7 | unit-tests: 8 | name: Unit tests 9 | strategy: 10 | matrix: 11 | platform: [ubuntu-latest] # , windows-latest, macos-latest] 12 | runs-on: ${{ matrix.platform }} 13 | env: 14 | RUST_BACKTRACE: 1 15 | steps: 16 | - name: Checkout branch 17 | uses: actions/checkout@v4 18 | - name: Run integration test 19 | run: cd integration-tests && cargo run --example integration-tests 20 | - name: Market tests 21 | run: cd market-contract && cargo test 22 | - name: Contract Basics tests 23 | run: cd nft-contract-basic && cargo test 24 | - name: Events tests 25 | run: cd nft-contract-events && cargo test 26 | - name: Royalty tests 27 | run: cd nft-contract-royalty && cargo test 28 | - name: Skeleton tests 29 | run: cd nft-contract-skeleton && cargo test 30 | - name: NFT series tests 31 | run: cd nft-series && cargo test 32 | -------------------------------------------------------------------------------- /.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 | **/node_modules 5 | /neardev 6 | /.pnp 7 | .pnp.js 8 | **/*.lock 9 | **/package-lock.json 10 | 11 | #cache 12 | .cache 13 | .parcel-cache 14 | 15 | #contracts 16 | target 17 | notes 18 | 19 | # testing 20 | /coverage 21 | 22 | # production 23 | /dist 24 | 25 | # misc 26 | .DS_Store 27 | .vscode 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | npm-debug.log* 34 | yarn-debug.log* 35 | yarn-error.log* 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Examples from NEAR, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NEAR NFT-Tutorial 2 | 3 | [![](https://img.shields.io/github/workflow/status/near-examples/nft-tutorial/Tests/main?label=Tests)](https://github.com/near-examples/nft-tutorial/actions/workflows/tests.yml) 4 | 5 | Welcome to NEAR's NFT tutorial, where we will help you parse the details around NEAR's [NEP-171 standard](https://nomicon.io/Standards/NonFungibleToken/Core.html) (Non-Fungible Token Standard), and show you how to build your own NFT smart contract from the ground up, improving your understanding about the NFT standard along the way. 6 | 7 | ## Prerequisites 8 | 9 | * [NEAR Wallet Account](https://testnet.mynearwallet.com/) 10 | * [Rust Toolchain](https://docs.near.org/develop/prerequisites) 11 | * [NEAR-CLI](https://docs.near.org/tools/near-cli#setup) 12 | * [yarn](https://classic.yarnpkg.com/en/docs/install#mac-stable) 13 | 14 | ## Tutorial Stages 15 | 16 | Each branch you will find in this repo corresponds to various stages of this tutorial with a partially completed contract at each stage. You are welcome to start from any stage you want to learn the most about. 17 | 18 | 19 | 20 | | Branch | Docs Tutorial | Description | 21 | | ------------- | ------------------------------------------------------------------------------------------------ | ----------- | 22 | | 1.skeleton | [Contract Architecture](https://docs.near.org/tutorials/nfts/skeleton) | You'll learn the basic architecture of the NFT smart contract, and you'll compile this skeleton code with the Rust toolchain. | 23 | | 2.minting | [Minting](https://docs.near.org/tutorials/nfts/minting) |Here you'll flesh out the skeleton so the smart contract can mint a non-fungible token | 24 | | 3.enumeration | [Enumeration](https://docs.near.org/tutorials/nfts/enumeration) | Here you'll find different enumeration methods that can be used to return the smart contract's states. | 25 | | 4.core | [Core](https://docs.near.org/tutorials/nfts/core) | In this tutorial you'll extend the NFT contract using the core standard, which will allow you to transfer non-fungible tokens. | 26 | | 5.approval | [Approval](https://docs.near.org/tutorials/nfts/approvals) | Here you'll expand the contract allowing other accounts to transfer NFTs on your behalf. | 27 | | 6.royalty | [Royalty](https://docs.near.org/tutorials/nfts/royalty) |Here you'll add the ability for non-fungible tokens to have royalties. This will allow people to get a percentage of the purchase price when an NFT is purchased. | 28 | | 7.events | [Events](https://docs.near.org/tutorials/nfts/events) | This allows indexers to know what functions are being called and make it easier and more reliable to keep track of information that can be used to populate the collectibles tab in the wallet for example. (tutorial docs have yet to be implemented ) | 29 | | 8.marketplace | [Marketplace](https://docs.near.org/tutorials/nfts/marketplace) | Get a comprehensive understanding of how an NFT marketplace contract can be built on NEAR through this tutorial. This contract facilitates the buying and selling of non-fungible tokens with $NEAR. | 30 | 31 | 32 | The tutorial series also contains a very helpful section on [**Upgrading Smart Contracts**](https://docs.near.org/tutorials/nfts/upgrade-contract). Definitely go and check it out as this is a common pain point. 33 | 34 | # Quick-Start 35 | 36 | If you want to see the full completed contract go ahead and clone and build this repo using 37 | 38 | ```=bash 39 | git clone https://github.com/near-examples/nft-tutorial.git 40 | cd nft-tutorial 41 | git switch 6.royalty 42 | yarn build 43 | ``` 44 | 45 | Now that you've cloned and built the contract we can try a few things. 46 | 47 | ## Mint An NFT 48 | 49 | Once you've created your near wallet go ahead and login to your wallet with your cli and follow the on-screen prompts 50 | 51 | ```=bash 52 | near login 53 | ``` 54 | 55 | Once your logged in you have to deploy the contract. Make a subaccount with the name of your choosing 56 | 57 | ```=bash 58 | near create-account nft-example.your-account.testnet --masterAccount your-account.testnet --initialBalance 10 59 | ``` 60 | 61 | After you've created your sub account deploy the contract to that sub account, set this variable to your sub account name 62 | 63 | ```=bash 64 | NFT_CONTRACT_ID=nft-example.your-account.testnet 65 | 66 | MAIN_ACCOUNT=your-account.testnet 67 | ``` 68 | 69 | Verify your new variable has the correct value 70 | ```=bash 71 | echo $NFT_CONTRACT_ID 72 | 73 | echo $MAIN_ACCOUNT 74 | ``` 75 | 76 | 77 | ### Deploy Your Contract 78 | ```=bash 79 | near deploy --accountId $NFT_CONTRACT_ID --wasmFile out/main.wasm 80 | ``` 81 | 82 | ### Initialize Your Contract 83 | 84 | ```=bash 85 | near call $NFT_CONTRACT_ID new_default_meta '{"owner_id": "'$NFT_CONTRACT_ID'"}' --accountId $NFT_CONTRACT_ID 86 | ``` 87 | 88 | ### View Contracts Meta Data 89 | 90 | ```=bash 91 | near view $NFT_CONTRACT_ID nft_metadata 92 | ``` 93 | ### Minting Token 94 | 95 | ```bash= 96 | near call $NFT_CONTRACT_ID nft_mint '{"token_id": "token-1", "token_metadata": {"title": "My Non Fungible Team Token", "description": "The Team Most Certainly Goes :)", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "token_owner_id": "'$MAIN_ACCOUNT'"}' --accountId $MAIN_ACCOUNT --amount 0.1 97 | ``` 98 | 99 | After you've minted the token go to wallet.testnet.near.org to `your-account.testnet` and look in the collections tab and check out your new sample NFT! 100 | 101 | 102 | 103 | ## View NFT Information 104 | 105 | After you've minted your NFT you can make a view call to get a response containing the `token_id` `owner_id` and the `metadata` 106 | 107 | ```bash= 108 | near view $NFT_CONTRACT_ID nft_token '{"token_id": "token-1"}' 109 | ``` 110 | 111 | ## Transfering NFTs 112 | 113 | To transfer an NFT go ahead and make another [testnet wallet account](https://wallet.testnet.near.org). 114 | 115 | Then run the following 116 | ```bash= 117 | MAIN_ACCOUNT_2=your-second-wallet-account.testnet 118 | ``` 119 | 120 | Verify the correct variable names with this 121 | 122 | ```=bash 123 | echo $NFT_CONTRACT_ID 124 | 125 | echo $MAIN_ACCOUNT 126 | 127 | echo $MAIN_ACCOUNT_2 128 | ``` 129 | 130 | To initiate the transfer.. 131 | 132 | ```bash= 133 | near call $NFT_CONTRACT_ID nft_transfer '{"receiver_id": "$MAIN_ACCOUNT_2", "token_id": "token-1", "memo": "Go Team :)"}' --accountId $MAIN_ACCOUNT --depositYocto 1 134 | ``` 135 | 136 | In this call you are depositing 1 yoctoNEAR for security and so that the user will be redirected to the NEAR wallet. 137 | 138 | ## Errata 139 | 140 | Large Changes: 141 | * **2022-06-21**: updated the rust SDK to version 4.0.0. PR found [here](https://github.com/near-examples/nft-tutorial/pull/32) 142 | 143 | * **2022-02-12**: updated the enumeration methods `nft_tokens` and `nft_tokens_for_owner` to no longer use any `to_vector` operations to save GAS. In addition, the default limit was changed from 0 to 50. PR found [here](https://github.com/near-examples/nft-tutorial/pull/17). 144 | 145 | Small Changes: 146 | * **2022-02-22**: changed `token_id` parameter type in nft_payout from `String` to `TokenId` for consistency as per pythonicode's suggestion 147 | -------------------------------------------------------------------------------- /integration-tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nonfungible-token-integration-tests" 3 | version = "1.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [dev-dependencies] 8 | near-sdk = { version = "5.11.0", features = ["unit-testing"] } 9 | near-workspaces = { version = "0.18.0", features = ["unstable"] } 10 | tokio = { version = "1.12.0", features = ["full"] } 11 | serde_json = "1" 12 | 13 | [[example]] 14 | name = "integration-tests" 15 | path = "src/tests.rs" -------------------------------------------------------------------------------- /integration-tests/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = ["rustfmt"] 4 | targets = ["wasm32-unknown-unknown"] -------------------------------------------------------------------------------- /integration-tests/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::Gas; 2 | use near_workspaces::{ 3 | types::{AccountDetails, NearToken}, 4 | Account, Contract, 5 | }; 6 | use serde_json::json; 7 | 8 | pub const DEFAULT_DEPOSIT: u128 = 10000000000000000000000; 9 | pub const ONE_YOCTO_NEAR: NearToken = NearToken::from_yoctonear(1); 10 | 11 | pub async fn mint_nft( 12 | user: &Account, 13 | nft_contract: &Contract, 14 | token_id: &str, 15 | ) -> Result<(), Box> { 16 | let request_payload = json!({ 17 | "token_id": token_id, 18 | "token_owner_id": user.id(), 19 | "token_metadata": { 20 | "title": "Grumpy Cat", 21 | "description": "Not amused.", 22 | "media": "https://www.adamsdrafting.com/wp-content/uploads/2018/06/More-Grumpy-Cat.jpg" 23 | }, 24 | }); 25 | 26 | let _ = user 27 | .call(nft_contract.id(), "nft_mint") 28 | .args_json(request_payload) 29 | .deposit(NearToken::from_yoctonear(DEFAULT_DEPOSIT)) 30 | .transact() 31 | .await; 32 | 33 | Ok(()) 34 | } 35 | 36 | pub async fn approve_nft( 37 | market_contract: &Contract, 38 | user: &Account, 39 | nft_contract: &Contract, 40 | token_id: &str, 41 | ) -> Result<(), Box> { 42 | let request_payload = json!({ 43 | "token_id": token_id, 44 | "account_id": market_contract.id(), 45 | "msg": serde_json::Value::Null, 46 | }); 47 | 48 | let _ = user 49 | .call(nft_contract.id(), "nft_approve") 50 | .args_json(request_payload) 51 | .deposit(NearToken::from_yoctonear(DEFAULT_DEPOSIT)) 52 | .transact() 53 | .await; 54 | 55 | Ok(()) 56 | } 57 | 58 | pub async fn pay_for_storage( 59 | user: &Account, 60 | market_contract: &Contract, 61 | amount: NearToken, 62 | ) -> Result<(), Box> { 63 | let request_payload = json!({}); 64 | 65 | let _ = user 66 | .call(market_contract.id(), "storage_deposit") 67 | .args_json(request_payload) 68 | .deposit(amount) 69 | .transact() 70 | .await; 71 | 72 | Ok(()) 73 | } 74 | 75 | pub async fn place_nft_for_sale( 76 | user: &Account, 77 | market_contract: &Contract, 78 | nft_contract: &Contract, 79 | token_id: &str, 80 | approval_id: u64, 81 | price: &NearToken, 82 | ) -> Result<(), Box> { 83 | let request_payload = json!({ 84 | "nft_contract_id": nft_contract.id(), 85 | "token_id": token_id, 86 | "approval_id": approval_id, 87 | "sale_conditions": NearToken::as_yoctonear(price).to_string(), 88 | }); 89 | let _ = user 90 | .call(market_contract.id(), "list_nft_for_sale") 91 | .args_json(request_payload) 92 | .gas(Gas::from_tgas(100)) 93 | .transact() 94 | .await; 95 | 96 | Ok(()) 97 | } 98 | 99 | pub async fn get_user_balance(user: &Account) -> NearToken { 100 | let details: AccountDetails = user 101 | .view_account() 102 | .await 103 | .expect("Account has to have some balance"); 104 | details.balance 105 | } 106 | 107 | pub async fn purchase_listed_nft( 108 | bidder: &Account, 109 | market_contract: &Contract, 110 | nft_contract: &Contract, 111 | token_id: &str, 112 | offer_price: NearToken, 113 | ) -> Result<(), Box> { 114 | let request_payload = json!({ 115 | "token_id": token_id, 116 | "nft_contract_id": nft_contract.id(), 117 | }); 118 | 119 | let _ = bidder 120 | .call(market_contract.id(), "offer") 121 | .args_json(request_payload) 122 | .max_gas() 123 | .deposit(offer_price) 124 | .transact() 125 | .await; 126 | 127 | Ok(()) 128 | } 129 | 130 | pub async fn transfer_nft( 131 | sender: &Account, 132 | receiver: &Account, 133 | nft_contract: &Contract, 134 | token_id: &str, 135 | ) -> Result<(), Box> { 136 | let request_payload = json!({ 137 | "token_id": token_id, 138 | "receiver_id": receiver.id(), 139 | "approval_id": 1 as u64, 140 | }); 141 | 142 | let _ = sender 143 | .call(nft_contract.id(), "nft_transfer") 144 | .args_json(request_payload) 145 | .max_gas() 146 | .deposit(ONE_YOCTO_NEAR) 147 | .transact() 148 | .await; 149 | 150 | Ok(()) 151 | } 152 | 153 | pub async fn get_nft_token_info( 154 | nft_contract: &Contract, 155 | token_id: &str, 156 | ) -> Result> { 157 | let token_info: serde_json::Value = nft_contract 158 | .call("nft_token") 159 | .args_json(json!({"token_id": token_id})) 160 | .transact() 161 | .await? 162 | .json() 163 | .unwrap(); 164 | 165 | Ok(token_info) 166 | } 167 | 168 | pub fn round_to_near_dp(amount: u128, sf: u128) -> String { 169 | let near_amount = amount as f64 / 1_000_000_000_000_000_000_000_000.0; // yocto in 1 NEAR 170 | return format!("{:.1$}", near_amount, sf as usize); 171 | } 172 | -------------------------------------------------------------------------------- /market-contract/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nft_market_contract" 3 | version = "0.1.0" 4 | authors = ["Near Inc "] 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | near-sdk = { version = "5.11.0", features = ["legacy"] } 12 | 13 | [dev-dependencies] 14 | near-sdk = { version = "5.11.0", features = ["unit-testing"] } 15 | near-workspaces = { version = "0.18.0", features = ["unstable"] } 16 | tokio = { version = "1.12.0", features = ["full"] } 17 | serde_json = "1" 18 | 19 | [profile.release] 20 | codegen-units = 1 21 | # Tell `rustc` to optimize for small code size. 22 | opt-level = "z" 23 | lto = true 24 | debug = false 25 | panic = "abort" 26 | # Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801 27 | overflow-checks = true 28 | -------------------------------------------------------------------------------- /market-contract/README.md: -------------------------------------------------------------------------------- 1 | # TBD 2 | -------------------------------------------------------------------------------- /market-contract/src/external.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// external contract calls 4 | 5 | //initiate a cross contract call to the nft contract 6 | #[ext_contract(ext_contract)] 7 | trait ExtContract { 8 | //This will transfer the token to the buyer and return a payout object used for the market to distribute funds to the appropriate accounts 9 | fn nft_transfer_payout( 10 | &mut self, 11 | receiver_id: AccountId, //purchaser (person to transfer the NFT to) 12 | token_id: TokenId, //token ID to transfer 13 | approval_id: u64, //market contract's approval ID in order to transfer the token on behalf of the owner 14 | memo: String, //memo (to include some context) 15 | /* 16 | the price that the token was purchased for. This will be used in conjunction with the royalty percentages 17 | for the token in order to determine how much money should go to which account. 18 | */ 19 | balance: NearToken, 20 | //the maximum amount of accounts the market can payout at once (this is limited by GAS) 21 | max_len_payout: u32, 22 | ); 23 | fn nft_token(&self, token_id: TokenId); 24 | fn nft_is_approved(&self, token_id: TokenId, approved_account_id: AccountId, approval_id: u64); 25 | } 26 | -------------------------------------------------------------------------------- /market-contract/src/internal.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | //used to generate a unique prefix in our storage collections (this is to avoid data collisions) 4 | pub(crate) fn hash_account_id(account_id: &AccountId) -> CryptoHash { 5 | //get the default hash 6 | let mut hash = CryptoHash::default(); 7 | //we hash the account ID and return it 8 | hash.copy_from_slice(&env::sha256(account_id.as_bytes())); 9 | hash 10 | } 11 | 12 | pub(crate) fn storage_per_sale() -> NearToken { 13 | env::storage_byte_cost().saturating_mul(1000) 14 | } 15 | 16 | impl Contract { 17 | //internal method for removing a sale from the market. This returns the previously removed sale object 18 | pub(crate) fn internal_remove_sale( 19 | &mut self, 20 | nft_contract_id: AccountId, 21 | token_id: TokenId, 22 | ) -> Sale { 23 | //get the unique sale ID (contract + DELIMITER + token ID) 24 | let contract_and_token_id = format!("{}{}{}", &nft_contract_id, DELIMETER, token_id); 25 | //get the sale object by removing the unique sale ID. If there was no sale, panic 26 | let sale = self.sales.remove(&contract_and_token_id).expect("No sale"); 27 | 28 | //get the set of sales for the sale's owner. If there's no sale, panic. 29 | let mut by_owner_id = self.by_owner_id.get(&sale.owner_id).expect("No sale by_owner_id"); 30 | //remove the unique sale ID from the set of sales 31 | by_owner_id.remove(&contract_and_token_id); 32 | 33 | //if the set of sales is now empty after removing the unique sale ID, we simply remove that owner from the map 34 | if by_owner_id.is_empty() { 35 | self.by_owner_id.remove(&sale.owner_id); 36 | //if the set of sales is not empty after removing, we insert the set back into the map for the owner 37 | } else { 38 | self.by_owner_id.insert(&sale.owner_id, &by_owner_id); 39 | } 40 | 41 | //get the set of token IDs for sale for the nft contract ID. If there's no sale, panic. 42 | let mut by_nft_contract_id = self 43 | .by_nft_contract_id 44 | .get(&nft_contract_id) 45 | .expect("No sale by nft_contract_id"); 46 | 47 | //remove the token ID from the set 48 | by_nft_contract_id.remove(&token_id); 49 | 50 | //if the set is now empty after removing the token ID, we remove that nft contract ID from the map 51 | if by_nft_contract_id.is_empty() { 52 | self.by_nft_contract_id.remove(&nft_contract_id); 53 | //if the set is not empty after removing, we insert the set back into the map for the nft contract ID 54 | } else { 55 | self.by_nft_contract_id 56 | .insert(&nft_contract_id, &by_nft_contract_id); 57 | } 58 | 59 | //return the sale object 60 | sale 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /market-contract/src/nft_callbacks.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// approval callbacks from NFT Contracts 4 | 5 | /* 6 | trait that will be used as the callback from the NFT contract. When nft_approve is 7 | called, it will fire a cross contract call to this marketplace and this is the function 8 | that is invoked. 9 | */ 10 | trait NonFungibleTokenApprovalsReceiver { 11 | fn nft_on_approve( 12 | &mut self, 13 | token_id: TokenId, 14 | owner_id: AccountId, 15 | approval_id: u64, 16 | msg: String, 17 | ); 18 | } 19 | 20 | //implementation of the trait 21 | #[near_bindgen] 22 | impl NonFungibleTokenApprovalsReceiver for Contract { 23 | fn nft_on_approve( 24 | &mut self, 25 | token_id: TokenId, 26 | owner_id: AccountId, 27 | approval_id: u64, 28 | msg: String, 29 | ) { 30 | /* 31 | YOU CAN PUT SOME INTERNAL MARKETPLACE LOGIC HERE 32 | */ 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /market-contract/src/sale_views.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | /// views 6 | 7 | //returns the number of sales the marketplace has up (as a string) 8 | pub fn get_supply_sales( 9 | &self, 10 | ) -> U64 { 11 | //returns the sales object length 12 | U64(self.sales.len()) 13 | } 14 | 15 | //returns the number of sales for a given account (result is a string) 16 | pub fn get_supply_by_owner_id( 17 | &self, 18 | account_id: AccountId, 19 | ) -> U64 { 20 | //get the set of sales for the given owner Id 21 | let by_owner_id = self.by_owner_id.get(&account_id); 22 | 23 | //if there as some set, we return the length but if there wasn't a set, we return 0 24 | if let Some(by_owner_id) = by_owner_id { 25 | U64(by_owner_id.len()) 26 | } else { 27 | U64(0) 28 | } 29 | } 30 | 31 | //returns paginated sale objects for a given account. (result is a vector of sales) 32 | pub fn get_sales_by_owner_id( 33 | &self, 34 | account_id: AccountId, 35 | from_index: Option, 36 | limit: Option, 37 | ) -> Vec { 38 | //get the set of token IDs for sale for the given account ID 39 | let by_owner_id = self.by_owner_id.get(&account_id); 40 | //if there was some set, we set the sales variable equal to that set. If there wasn't, sales is set to an empty vector 41 | let sales = if let Some(by_owner_id) = by_owner_id { 42 | by_owner_id 43 | } else { 44 | return vec![]; 45 | }; 46 | 47 | //we'll convert the UnorderedSet into a vector of strings 48 | let keys = sales.as_vector(); 49 | 50 | //where to start pagination - if we have a from_index, we'll use that - otherwise start from 0 index 51 | let start = u128::from(from_index.unwrap_or(U128(0))); 52 | 53 | //iterate through the keys vector 54 | keys.iter() 55 | //skip to the index we specified in the start variable 56 | .skip(start as usize) 57 | //take the first "limit" elements in the vector. If we didn't specify a limit, use 0 58 | .take(limit.unwrap_or(0) as usize) 59 | //we'll map the token IDs which are strings into Sale objects 60 | .map(|token_id| self.sales.get(&token_id).unwrap()) 61 | //since we turned the keys into an iterator, we need to turn it back into a vector to return 62 | .collect() 63 | } 64 | 65 | //get the number of sales for an nft contract. (returns a string) 66 | pub fn get_supply_by_nft_contract_id( 67 | &self, 68 | nft_contract_id: AccountId, 69 | ) -> U64 { 70 | //get the set of tokens for associated with the given nft contract 71 | let by_nft_contract_id = self.by_nft_contract_id.get(&nft_contract_id); 72 | 73 | //if there was some set, return it's length. Otherwise return 0 74 | if let Some(by_nft_contract_id) = by_nft_contract_id { 75 | U64(by_nft_contract_id.len()) 76 | } else { 77 | U64(0) 78 | } 79 | } 80 | 81 | //returns paginated sale objects associated with a given nft contract. (result is a vector of sales) 82 | pub fn get_sales_by_nft_contract_id( 83 | &self, 84 | nft_contract_id: AccountId, 85 | from_index: Option, 86 | limit: Option, 87 | ) -> Vec { 88 | //get the set of token IDs for sale for the given contract ID 89 | let by_nft_contract_id = self.by_nft_contract_id.get(&nft_contract_id); 90 | 91 | //if there was some set, we set the sales variable equal to that set. If there wasn't, sales is set to an empty vector 92 | let sales = if let Some(by_nft_contract_id) = by_nft_contract_id { 93 | by_nft_contract_id 94 | } else { 95 | return vec![]; 96 | }; 97 | 98 | //we'll convert the UnorderedSet into a vector of strings 99 | let keys = sales.as_vector(); 100 | 101 | //where to start pagination - if we have a from_index, we'll use that - otherwise start from 0 index 102 | let start = u128::from(from_index.unwrap_or(U128(0))); 103 | 104 | //iterate through the keys vector 105 | keys.iter() 106 | //skip to the index we specified in the start variable 107 | .skip(start as usize) 108 | //take the first "limit" elements in the vector. If we didn't specify a limit, use 0 109 | .take(limit.unwrap_or(0) as usize) 110 | //we'll map the token IDs which are strings into Sale objects by passing in the unique sale ID (contract + DELIMITER + token ID) 111 | .map(|token_id| self.sales.get(&format!("{}{}{}", nft_contract_id, DELIMETER, token_id)).unwrap()) 112 | //since we turned the keys into an iterator, we need to turn it back into a vector to return 113 | .collect() 114 | } 115 | 116 | //get a sale information for a given unique sale ID (contract + DELIMITER + token ID) 117 | pub fn get_sale(&self, nft_contract_token: ContractAndTokenId) -> Option { 118 | //try and get the sale object for the given unique sale ID. Will return an option since 119 | //we're not guaranteed that the unique sale ID passed in will be valid. 120 | self.sales.get(&nft_contract_token) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /nft-contract-approval/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nft_contract_skeleton" 3 | version = "0.1.0" 4 | authors = ["Near Inc "] 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | near-sdk = { version = "5.11.0", features = ["legacy"] } 12 | serde_json = "1.0.113" 13 | 14 | [profile.release] 15 | codegen-units = 1 16 | # Tell `rustc` to optimize for small code size. 17 | opt-level = "z" 18 | lto = true 19 | debug = false 20 | panic = "abort" 21 | # Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801 22 | overflow-checks = true 23 | -------------------------------------------------------------------------------- /nft-contract-approval/README.md: -------------------------------------------------------------------------------- 1 | # TBD 2 | -------------------------------------------------------------------------------- /nft-contract-approval/src/approval.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use near_sdk::ext_contract; 3 | 4 | pub trait NonFungibleTokenCore { 5 | //approve an account ID to transfer a token on your behalf 6 | fn nft_approve(&mut self, token_id: TokenId, account_id: AccountId, msg: Option); 7 | 8 | //check if the passed in account has access to approve the token ID 9 | fn nft_is_approved( 10 | &self, 11 | token_id: TokenId, 12 | approved_account_id: AccountId, 13 | approval_id: Option, 14 | ) -> bool; 15 | 16 | //revoke a specific account from transferring the token on your behalf 17 | fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId); 18 | 19 | //revoke all accounts from transferring the token on your behalf 20 | fn nft_revoke_all(&mut self, token_id: TokenId); 21 | } 22 | 23 | #[ext_contract(ext_non_fungible_approval_receiver)] 24 | trait NonFungibleTokenApprovalsReceiver { 25 | //cross contract call to an external contract that is initiated during nft_approve 26 | fn nft_on_approve( 27 | &mut self, 28 | token_id: TokenId, 29 | owner_id: AccountId, 30 | approval_id: u64, 31 | msg: String, 32 | ); 33 | } 34 | 35 | #[near_bindgen] 36 | impl NonFungibleTokenCore for Contract { 37 | //allow a specific account ID to approve a token on your behalf 38 | #[payable] 39 | fn nft_approve(&mut self, token_id: TokenId, account_id: AccountId, msg: Option) { 40 | /* 41 | assert at least one yocto for security reasons - this will cause a redirect to the NEAR wallet. 42 | The user needs to attach enough to pay for storage on the contract 43 | */ 44 | assert_at_least_one_yocto(); 45 | 46 | //get the token object from the token ID 47 | let mut token = self.tokens_by_id.get(&token_id).expect("No token"); 48 | 49 | //make sure that the person calling the function is the owner of the token 50 | assert_eq!( 51 | &env::predecessor_account_id(), 52 | &token.owner_id, 53 | "Predecessor must be the token owner." 54 | ); 55 | 56 | //get the next approval ID if we need a new approval 57 | let approval_id: u64 = token.next_approval_id; 58 | 59 | //check if the account has been approved already for this token 60 | let is_new_approval = token 61 | .approved_account_ids 62 | //insert returns none if the key was not present. 63 | .insert(account_id.clone(), approval_id) 64 | //if the key was not present, .is_none() will return true so it is a new approval. 65 | .is_none(); 66 | 67 | //if it was a new approval, we need to calculate how much storage is being used to add the account. 68 | let storage_used = if is_new_approval { 69 | bytes_for_approved_account_id(&account_id) 70 | //if it was not a new approval, we used no storage. 71 | } else { 72 | 0 73 | }; 74 | 75 | //increment the token's next approval ID by 1 76 | token.next_approval_id += 1; 77 | //insert the token back into the tokens_by_id collection 78 | self.tokens_by_id.insert(&token_id, &token); 79 | 80 | //refund any excess storage attached by the user. If the user didn't attach enough, panic. 81 | refund_deposit(storage_used); 82 | 83 | //if some message was passed into the function, we initiate a cross contract call on the 84 | //account we're giving access to. 85 | if let Some(msg) = msg { 86 | // Defaulting GAS weight to 1, no attached deposit, and no static GAS to attach. 87 | ext_non_fungible_approval_receiver::ext(account_id) 88 | .nft_on_approve(token_id, token.owner_id, approval_id, msg) 89 | .as_return(); 90 | } 91 | } 92 | 93 | //check if the passed in account has access to approve the token ID 94 | fn nft_is_approved( 95 | &self, 96 | token_id: TokenId, 97 | approved_account_id: AccountId, 98 | approval_id: Option, 99 | ) -> bool { 100 | //get the token object from the token_id 101 | let token = self.tokens_by_id.get(&token_id).expect("No token"); 102 | 103 | //get the approval number for the passed in account ID 104 | let approval = token.approved_account_ids.get(&approved_account_id); 105 | 106 | //if there was some approval ID found for the account ID 107 | if let Some(approval) = approval { 108 | //if a specific approval_id was passed into the function 109 | if let Some(approval_id) = approval_id { 110 | //return if the approval ID passed in matches the actual approval ID for the account 111 | approval_id == *approval 112 | //if there was no approval_id passed into the function, we simply return true 113 | } else { 114 | true 115 | } 116 | //if there was no approval ID found for the account ID, we simply return false 117 | } else { 118 | false 119 | } 120 | } 121 | 122 | //revoke a specific account from transferring the token on your behalf 123 | #[payable] 124 | fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId) { 125 | //assert that the user attached exactly 1 yoctoNEAR for security reasons 126 | assert_one_yocto(); 127 | //get the token object using the passed in token_id 128 | let mut token = self.tokens_by_id.get(&token_id).expect("No token"); 129 | 130 | //get the caller of the function and assert that they are the owner of the token 131 | let predecessor_account_id = env::predecessor_account_id(); 132 | assert_eq!(&predecessor_account_id, &token.owner_id); 133 | 134 | //if the account ID was in the token's approval, we remove it and the if statement logic executes 135 | if token.approved_account_ids.remove(&account_id).is_some() { 136 | //refund the funds released by removing the approved_account_id to the caller of the function 137 | refund_approved_account_ids_iter(predecessor_account_id, [account_id].iter()); 138 | 139 | //insert the token back into the tokens_by_id collection with the account_id removed from the approval list 140 | self.tokens_by_id.insert(&token_id, &token); 141 | } 142 | } 143 | 144 | //revoke all accounts from transferring the token on your behalf 145 | #[payable] 146 | fn nft_revoke_all(&mut self, token_id: TokenId) { 147 | //assert that the caller attached exactly 1 yoctoNEAR for security 148 | assert_one_yocto(); 149 | 150 | //get the token object from the passed in token ID 151 | let mut token = self.tokens_by_id.get(&token_id).expect("No token"); 152 | //get the caller and make sure they are the owner of the tokens 153 | let predecessor_account_id = env::predecessor_account_id(); 154 | assert_eq!(&predecessor_account_id, &token.owner_id); 155 | 156 | //only revoke if the approved account IDs for the token is not empty 157 | if !token.approved_account_ids.is_empty() { 158 | //refund the approved account IDs to the caller of the function 159 | refund_approved_account_ids(predecessor_account_id, &token.approved_account_ids); 160 | //clear the approved account IDs 161 | token.approved_account_ids.clear(); 162 | //insert the token back into the tokens_by_id collection with the approved account IDs cleared 163 | self.tokens_by_id.insert(&token_id, &token); 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /nft-contract-approval/src/enumeration.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | //Query for the total supply of NFTs on the contract 6 | pub fn nft_total_supply(&self) -> U128 { 7 | //return the length of the token metadata by ID 8 | U128(self.token_metadata_by_id.len().into()) 9 | } 10 | 11 | //Query for nft tokens on the contract regardless of the owner using pagination 12 | pub fn nft_tokens(&self, from_index: Option, limit: Option) -> Vec { 13 | //where to start pagination - if we have a from_index, we'll use that - otherwise start from 0 index 14 | let start = u128::from(from_index.unwrap_or(U128(0))); 15 | 16 | //iterate through each token using an iterator 17 | self.token_metadata_by_id 18 | .keys() 19 | //skip to the index we specified in the start variable 20 | .skip(start as usize) 21 | //take the first "limit" elements in the vector. If we didn't specify a limit, use 50 22 | .take(limit.unwrap_or(50) as usize) 23 | //we'll map the token IDs which are strings into Json Tokens 24 | .map(|token_id| self.nft_token(token_id.clone()).unwrap()) 25 | //since we turned the keys into an iterator, we need to turn it back into a vector to return 26 | .collect() 27 | } 28 | 29 | //get the total supply of NFTs for a given owner 30 | pub fn nft_supply_for_owner(&self, account_id: AccountId) -> U128 { 31 | //get the set of tokens for the passed in owner 32 | let tokens_for_owner_set = self.tokens_per_owner.get(&account_id); 33 | 34 | //if there is some set of tokens, we'll return the length 35 | if let Some(tokens_for_owner_set) = tokens_for_owner_set { 36 | U128(tokens_for_owner_set.len().into()) 37 | } else { 38 | //if there isn't a set of tokens for the passed in account ID, we'll return 0 39 | U128(0) 40 | } 41 | } 42 | 43 | //Query for all the tokens for an owner 44 | pub fn nft_tokens_for_owner( 45 | &self, 46 | account_id: AccountId, 47 | from_index: Option, 48 | limit: Option, 49 | ) -> Vec { 50 | //get the set of tokens for the passed in owner 51 | let tokens_for_owner_set = self.tokens_per_owner.get(&account_id); 52 | //if there is some set of tokens, we'll set the tokens variable equal to that set 53 | let tokens = if let Some(tokens_for_owner_set) = tokens_for_owner_set { 54 | tokens_for_owner_set 55 | } else { 56 | //if there is no set of tokens, we'll simply return an empty vector. 57 | return vec![]; 58 | }; 59 | 60 | //where to start pagination - if we have a from_index, we'll use that - otherwise start from 0 index 61 | let start = u128::from(from_index.unwrap_or(U128(0))); 62 | 63 | //iterate through the keys vector 64 | tokens 65 | .iter() 66 | //skip to the index we specified in the start variable 67 | .skip(start as usize) 68 | //take the first "limit" elements in the vector. If we didn't specify a limit, use 50 69 | .take(limit.unwrap_or(50) as usize) 70 | //we'll map the token IDs which are strings into Json Tokens 71 | .map(|token_id| self.nft_token(token_id.clone()).unwrap()) 72 | //since we turned the keys into an iterator, we need to turn it back into a vector to return 73 | .collect() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /nft-contract-approval/src/events.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use near_sdk::serde::{Deserialize, Serialize}; 4 | 5 | /// Enum that represents the data type of the EventLog. 6 | /// The enum can either be an NftMint or an NftTransfer. 7 | #[derive(Serialize, Deserialize, Debug)] 8 | #[serde(tag = "event", content = "data")] 9 | #[serde(rename_all = "snake_case")] 10 | #[serde(crate = "near_sdk::serde")] 11 | #[non_exhaustive] 12 | pub enum EventLogVariant { 13 | NftMint(Vec), 14 | NftTransfer(Vec), 15 | } 16 | 17 | /// Interface to capture data about an event 18 | /// 19 | /// Arguments: 20 | /// * `standard`: name of standard e.g. nep171 21 | /// * `version`: e.g. 1.0.0 22 | /// * `event`: associate event data 23 | #[derive(Serialize, Deserialize, Debug)] 24 | #[serde(crate = "near_sdk::serde")] 25 | pub struct EventLog { 26 | pub standard: String, 27 | pub version: String, 28 | 29 | // `flatten` to not have "event": {} in the JSON, just have the contents of {}. 30 | #[serde(flatten)] 31 | pub event: EventLogVariant, 32 | } 33 | 34 | impl fmt::Display for EventLog { 35 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 36 | f.write_fmt(format_args!( 37 | "EVENT_JSON:{}", 38 | &serde_json::to_string(self).map_err(|_| fmt::Error)? 39 | )) 40 | } 41 | } 42 | 43 | /// An event log to capture token minting 44 | /// 45 | /// Arguments 46 | /// * `owner_id`: "account.near" 47 | /// * `token_ids`: ["1", "abc"] 48 | /// * `memo`: optional message 49 | #[derive(Serialize, Deserialize, Debug)] 50 | #[serde(crate = "near_sdk::serde")] 51 | pub struct NftMintLog { 52 | pub owner_id: String, 53 | pub token_ids: Vec, 54 | 55 | #[serde(skip_serializing_if = "Option::is_none")] 56 | pub memo: Option, 57 | } 58 | 59 | /// An event log to capture token transfer 60 | /// 61 | /// Arguments 62 | /// * `authorized_id`: approved account to transfer 63 | /// * `old_owner_id`: "owner.near" 64 | /// * `new_owner_id`: "receiver.near" 65 | /// * `token_ids`: ["1", "12345abc"] 66 | /// * `memo`: optional message 67 | #[derive(Serialize, Deserialize, Debug)] 68 | #[serde(crate = "near_sdk::serde")] 69 | pub struct NftTransferLog { 70 | #[serde(skip_serializing_if = "Option::is_none")] 71 | pub authorized_id: Option, 72 | 73 | pub old_owner_id: String, 74 | pub new_owner_id: String, 75 | pub token_ids: Vec, 76 | 77 | #[serde(skip_serializing_if = "Option::is_none")] 78 | pub memo: Option, 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::*; 84 | 85 | #[test] 86 | fn nep_format_vector() { 87 | let expected = r#"EVENT_JSON:{"standard":"nep171","version":"1.0.0","event":"nft_mint","data":[{"owner_id":"foundation.near","token_ids":["aurora","proximitylabs"]},{"owner_id":"user1.near","token_ids":["meme"]}]}"#; 88 | let log = EventLog { 89 | standard: "nep171".to_string(), 90 | version: "1.0.0".to_string(), 91 | event: EventLogVariant::NftMint(vec![ 92 | NftMintLog { 93 | owner_id: "foundation.near".to_owned(), 94 | token_ids: vec!["aurora".to_string(), "proximitylabs".to_string()], 95 | memo: None, 96 | }, 97 | NftMintLog { 98 | owner_id: "user1.near".to_owned(), 99 | token_ids: vec!["meme".to_string()], 100 | memo: None, 101 | }, 102 | ]), 103 | }; 104 | assert_eq!(expected, log.to_string()); 105 | } 106 | 107 | #[test] 108 | fn nep_format_mint() { 109 | let expected = r#"EVENT_JSON:{"standard":"nep171","version":"1.0.0","event":"nft_mint","data":[{"owner_id":"foundation.near","token_ids":["aurora","proximitylabs"]}]}"#; 110 | let log = EventLog { 111 | standard: "nep171".to_string(), 112 | version: "1.0.0".to_string(), 113 | event: EventLogVariant::NftMint(vec![NftMintLog { 114 | owner_id: "foundation.near".to_owned(), 115 | token_ids: vec!["aurora".to_string(), "proximitylabs".to_string()], 116 | memo: None, 117 | }]), 118 | }; 119 | assert_eq!(expected, log.to_string()); 120 | } 121 | 122 | #[test] 123 | fn nep_format_transfer_all_fields() { 124 | let expected = r#"EVENT_JSON:{"standard":"nep171","version":"1.0.0","event":"nft_transfer","data":[{"authorized_id":"market.near","old_owner_id":"user1.near","new_owner_id":"user2.near","token_ids":["token"],"memo":"Go Team!"}]}"#; 125 | let log = EventLog { 126 | standard: "nep171".to_string(), 127 | version: "1.0.0".to_string(), 128 | event: EventLogVariant::NftTransfer(vec![NftTransferLog { 129 | authorized_id: Some("market.near".to_string()), 130 | old_owner_id: "user1.near".to_string(), 131 | new_owner_id: "user2.near".to_string(), 132 | token_ids: vec!["token".to_string()], 133 | memo: Some("Go Team!".to_owned()), 134 | }]), 135 | }; 136 | assert_eq!(expected, log.to_string()); 137 | } 138 | } -------------------------------------------------------------------------------- /nft-contract-approval/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use near_sdk::borsh::{BorshSerialize, BorshDeserialize}; 3 | use near_sdk::collections::{LazyOption, LookupMap, UnorderedMap, UnorderedSet}; 4 | use near_sdk::json_types::{Base64VecU8, U64, U128}; 5 | use near_sdk::serde::{Deserialize, Serialize}; 6 | use near_sdk::{ 7 | near_bindgen, env, AccountId, NearToken, CryptoHash, PanicOnDefault, Promise, PromiseOrValue, BorshStorageKey, NearSchema 8 | }; 9 | 10 | use crate::internal::*; 11 | pub use crate::metadata::*; 12 | pub use crate::mint::*; 13 | pub use crate::nft_core::*; 14 | pub use crate::approval::*; 15 | pub use crate::royalty::*; 16 | pub use crate::events::*; 17 | 18 | mod internal; 19 | mod enumeration; 20 | mod metadata; 21 | mod mint; 22 | mod nft_core; 23 | mod approval; 24 | mod royalty; 25 | mod events; 26 | 27 | /// This spec can be treated like a version of the standard. 28 | pub const NFT_METADATA_SPEC: &str = "1.0.0"; 29 | /// This is the name of the NFT standard we're using 30 | pub const NFT_STANDARD_NAME: &str = "nep171"; 31 | 32 | //Basic NEAR amounts as constants 33 | const ONE_YOCTONEAR: NearToken = NearToken::from_yoctonear(1); 34 | 35 | #[near_bindgen] 36 | #[derive(BorshSerialize, BorshDeserialize, PanicOnDefault)] 37 | #[borsh(crate = "near_sdk::borsh")] 38 | pub struct Contract { 39 | //contract owner 40 | pub owner_id: AccountId, 41 | 42 | //keeps track of all the token IDs for a given account 43 | pub tokens_per_owner: LookupMap>, 44 | 45 | //keeps track of the token struct for a given token ID 46 | pub tokens_by_id: LookupMap, 47 | 48 | //keeps track of the token metadata for a given token ID 49 | pub token_metadata_by_id: UnorderedMap, 50 | 51 | //keeps track of the metadata for the contract 52 | pub metadata: LazyOption, 53 | } 54 | 55 | /// Helper structure for keys of the persistent collections. 56 | #[derive(BorshSerialize, BorshStorageKey)] 57 | #[borsh(crate = "near_sdk::borsh")] 58 | pub enum StorageKey { 59 | TokensPerOwner, 60 | TokenPerOwnerInner { account_id_hash: CryptoHash }, 61 | TokensById, 62 | TokenMetadataById, 63 | NFTContractMetadata, 64 | TokensPerType, 65 | TokensPerTypeInner { token_type_hash: CryptoHash }, 66 | TokenTypesLocked, 67 | } 68 | 69 | #[near_bindgen] 70 | impl Contract { 71 | /* 72 | initialization function (can only be called once). 73 | this initializes the contract with default metadata so the 74 | user doesn't have to manually type metadata. 75 | */ 76 | #[init] 77 | pub fn new_default_meta(owner_id: AccountId) -> Self { 78 | //calls the other function "new: with some default metadata and the owner_id passed in 79 | Self::new( 80 | owner_id, 81 | NFTContractMetadata { 82 | spec: "nft-1.0.0".to_string(), 83 | name: "NFT Tutorial Contract".to_string(), 84 | symbol: "GOTEAM".to_string(), 85 | icon: None, 86 | base_uri: None, 87 | reference: None, 88 | reference_hash: None, 89 | }, 90 | ) 91 | } 92 | 93 | /* 94 | initialization function (can only be called once). 95 | this initializes the contract with metadata that was passed in and 96 | the owner_id. 97 | */ 98 | #[init] 99 | pub fn new(owner_id: AccountId, metadata: NFTContractMetadata) -> Self { 100 | //create a variable of type Self with all the fields initialized. 101 | let this = Self { 102 | //Storage keys are simply the prefixes used for the collections. This helps avoid data collision 103 | tokens_per_owner: LookupMap::new(StorageKey::TokensPerOwner), 104 | tokens_by_id: LookupMap::new(StorageKey::TokensById), 105 | token_metadata_by_id: UnorderedMap::new(StorageKey::TokenMetadataById), 106 | //set the owner_id field equal to the passed in owner_id. 107 | owner_id, 108 | metadata: LazyOption::new( 109 | StorageKey::NFTContractMetadata, 110 | Some(&metadata), 111 | ), 112 | }; 113 | 114 | //return the Contract object 115 | this 116 | } 117 | } -------------------------------------------------------------------------------- /nft-contract-approval/src/metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | pub type TokenId = String; 3 | //defines the payout type we'll be returning as a part of the royalty standards. 4 | #[derive(Serialize, Deserialize, NearSchema)] 5 | #[serde(crate = "near_sdk::serde")] 6 | pub struct Payout { 7 | pub payout: HashMap, 8 | } 9 | 10 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Clone, NearSchema)] 11 | #[borsh(crate = "near_sdk::borsh")] 12 | #[serde(crate = "near_sdk::serde")] 13 | pub struct NFTContractMetadata { 14 | pub spec: String, // required, essentially a version like "nft-1.0.0" 15 | pub name: String, // required, ex. "Mosaics" 16 | pub symbol: String, // required, ex. "MOSAIC" 17 | pub icon: Option, // Data URL 18 | pub base_uri: Option, // Centralized gateway known to have reliable access to decentralized storage assets referenced by `reference` or `media` URLs 19 | pub reference: Option, // URL to a JSON file with more info 20 | pub reference_hash: Option, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. 21 | } 22 | 23 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, NearSchema)] 24 | #[borsh(crate = "near_sdk::borsh")] 25 | #[serde(crate = "near_sdk::serde")] 26 | pub struct TokenMetadata { 27 | pub title: Option, // ex. "Arch Nemesis: Mail Carrier" or "Parcel #5055" 28 | pub description: Option, // free-form description 29 | pub media: Option, // URL to associated media, preferably to decentralized, content-addressed storage 30 | pub media_hash: Option, // Base64-encoded sha256 hash of content referenced by the `media` field. Required if `media` is included. 31 | pub copies: Option, // number of copies of this set of metadata in existence when token was minted. 32 | pub issued_at: Option, // When token was issued or minted, Unix epoch in milliseconds 33 | pub expires_at: Option, // When token expires, Unix epoch in milliseconds 34 | pub starts_at: Option, // When token starts being valid, Unix epoch in milliseconds 35 | pub updated_at: Option, // When token was last updated, Unix epoch in milliseconds 36 | pub extra: Option, // anything extra the NFT wants to store on-chain. Can be stringified JSON. 37 | pub reference: Option, // URL to an off-chain JSON file with more info. 38 | pub reference_hash: Option, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. 39 | } 40 | 41 | #[derive(BorshDeserialize, BorshSerialize)] 42 | #[borsh(crate = "near_sdk::borsh")] 43 | pub struct Token { 44 | //owner of the token 45 | pub owner_id: AccountId, 46 | //list of approved account IDs that have access to transfer the token. This maps an account ID to an approval ID 47 | pub approved_account_ids: HashMap, 48 | //the next approval ID to give out. 49 | pub next_approval_id: u64, 50 | } 51 | 52 | //The Json token is what will be returned from view calls. 53 | #[derive(Serialize, Deserialize, NearSchema)] 54 | #[serde(crate = "near_sdk::serde")] 55 | pub struct JsonToken { 56 | //token ID 57 | pub token_id: TokenId, 58 | //owner of the token 59 | pub owner_id: AccountId, 60 | //token metadata 61 | pub metadata: TokenMetadata, 62 | //list of approved account IDs that have access to transfer the token. This maps an account ID to an approval ID 63 | pub approved_account_ids: HashMap, 64 | } 65 | 66 | pub trait NonFungibleTokenMetadata { 67 | //view call for returning the contract metadata 68 | fn nft_metadata(&self) -> NFTContractMetadata; 69 | } 70 | 71 | #[near_bindgen] 72 | impl NonFungibleTokenMetadata for Contract { 73 | fn nft_metadata(&self) -> NFTContractMetadata { 74 | self.metadata.get().unwrap() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /nft-contract-approval/src/mint.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | #[payable] 6 | pub fn nft_mint( 7 | &mut self, 8 | token_id: TokenId, 9 | token_owner_id: AccountId, 10 | token_metadata: TokenMetadata, 11 | //we add an optional parameter for perpetual royalties 12 | perpetual_royalties: Option>, 13 | ) { 14 | //measure the initial storage being used on the contract 15 | let initial_storage_usage = env::storage_usage(); 16 | 17 | // create a royalty map to store in the token 18 | let mut royalty = HashMap::new(); 19 | 20 | // if perpetual royalties were passed into the function: 21 | if let Some(perpetual_royalties) = perpetual_royalties { 22 | //make sure that the length of the perpetual royalties is below 7 since we won't have enough GAS to pay out that many people 23 | assert!( 24 | perpetual_royalties.len() < 7, 25 | "Cannot add more than 6 perpetual royalty amounts" 26 | ); 27 | 28 | //iterate through the perpetual royalties and insert the account and amount in the royalty map 29 | for (account, amount) in perpetual_royalties { 30 | royalty.insert(account, amount); 31 | } 32 | } 33 | 34 | //specify the token struct that contains the owner ID 35 | let token = Token { 36 | owner_id: token_owner_id, 37 | //we set the approved account IDs to the default value (an empty map) 38 | approved_account_ids: Default::default(), 39 | //the next approval ID is set to 0 40 | next_approval_id: 0, 41 | }; 42 | 43 | //insert the token ID and token struct and make sure that the token doesn't exist 44 | assert!( 45 | self.tokens_by_id.insert(&token_id, &token).is_none(), 46 | "Token already exists" 47 | ); 48 | 49 | //insert the token ID and metadata 50 | self.token_metadata_by_id.insert(&token_id, &token_metadata); 51 | 52 | //call the internal method for adding the token to the owner 53 | self.internal_add_token_to_owner(&token.owner_id, &token_id); 54 | 55 | // Construct the mint log as per the events standard. 56 | let nft_mint_log: EventLog = EventLog { 57 | // Standard name ("nep171"). 58 | standard: NFT_STANDARD_NAME.to_string(), 59 | // Version of the standard ("nft-1.0.0"). 60 | version: NFT_METADATA_SPEC.to_string(), 61 | // The data related with the event stored in a vector. 62 | event: EventLogVariant::NftMint(vec![NftMintLog { 63 | // Owner of the token. 64 | owner_id: token.owner_id.to_string(), 65 | // Vector of token IDs that were minted. 66 | token_ids: vec![token_id.to_string()], 67 | // An optional memo to include. 68 | memo: None, 69 | }]), 70 | }; 71 | 72 | // Log the serialized json. 73 | env::log_str(&nft_mint_log.to_string()); 74 | 75 | //calculate the required storage which was the used - initial 76 | let required_storage_in_bytes = env::storage_usage() - initial_storage_usage; 77 | 78 | //refund any excess storage if the user attached too much. Panic if they didn't attach enough to cover the required. 79 | refund_deposit(required_storage_in_bytes.into()); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /nft-contract-approval/src/royalty.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub trait NonFungibleTokenCore { 4 | //calculates the payout for a token given the passed in balance. This is a view method 5 | fn nft_payout(&self, token_id: TokenId, balance: NearToken, max_len_payout: u32) -> Payout; 6 | 7 | //transfers the token to the receiver ID and returns the payout object that should be payed given the passed in balance. 8 | fn nft_transfer_payout( 9 | &mut self, 10 | receiver_id: AccountId, 11 | token_id: TokenId, 12 | approval_id: u64, 13 | memo: Option, 14 | balance: NearToken, 15 | max_len_payout: u32, 16 | ) -> Payout; 17 | } 18 | 19 | #[near_bindgen] 20 | impl NonFungibleTokenCore for Contract { 21 | //calculates the payout for a token given the passed in balance. This is a view method 22 | fn nft_payout(&self, token_id: TokenId, balance: NearToken, max_len_payout: u32) -> Payout { 23 | /* 24 | FILL THIS IN 25 | */ 26 | todo!(); //remove once code is filled in. 27 | } 28 | 29 | //transfers the token to the receiver ID and returns the payout object that should be payed given the passed in balance. 30 | #[payable] 31 | fn nft_transfer_payout( 32 | &mut self, 33 | receiver_id: AccountId, 34 | token_id: TokenId, 35 | approval_id: u64, 36 | memo: Option, 37 | balance: NearToken, 38 | max_len_payout: u32, 39 | ) -> Payout { 40 | /* 41 | FILL THIS IN 42 | */ 43 | todo!(); //remove once code is filled in. 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /nft-contract-basic/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nft_contract_skeleton" 3 | version = "0.1.0" 4 | authors = ["Near Inc "] 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | near-sdk = { version = "5.11.0", features = ["legacy"] } 12 | 13 | [profile.release] 14 | codegen-units = 1 15 | # Tell `rustc` to optimize for small code size. 16 | opt-level = "z" 17 | lto = true 18 | debug = false 19 | panic = "abort" 20 | # Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801 21 | overflow-checks = true 22 | -------------------------------------------------------------------------------- /nft-contract-basic/README.md: -------------------------------------------------------------------------------- 1 | # TBD 2 | -------------------------------------------------------------------------------- /nft-contract-basic/src/approval.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use near_sdk::ext_contract; 3 | 4 | pub trait NonFungibleTokenCore { 5 | //approve an account ID to transfer a token on your behalf 6 | fn nft_approve(&mut self, token_id: TokenId, account_id: AccountId, msg: Option); 7 | 8 | //check if the passed in account has access to approve the token ID 9 | fn nft_is_approved( 10 | &self, 11 | token_id: TokenId, 12 | approved_account_id: AccountId, 13 | approval_id: Option, 14 | ) -> bool; 15 | 16 | //revoke a specific account from transferring the token on your behalf 17 | fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId); 18 | 19 | //revoke all accounts from transferring the token on your behalf 20 | fn nft_revoke_all(&mut self, token_id: TokenId); 21 | } 22 | 23 | #[ext_contract(ext_non_fungible_approval_receiver)] 24 | trait NonFungibleTokenApprovalsReceiver { 25 | //cross contract call to an external contract that is initiated during nft_approve 26 | fn nft_on_approve( 27 | &mut self, 28 | token_id: TokenId, 29 | owner_id: AccountId, 30 | approval_id: u64, 31 | msg: String, 32 | ); 33 | } 34 | 35 | #[near_bindgen] 36 | impl NonFungibleTokenCore for Contract { 37 | //allow a specific account ID to approve a token on your behalf 38 | #[payable] 39 | fn nft_approve(&mut self, token_id: TokenId, account_id: AccountId, msg: Option) { 40 | /* 41 | FILL THIS IN 42 | */ 43 | } 44 | 45 | //check if the passed in account has access to approve the token ID 46 | fn nft_is_approved( 47 | &self, 48 | token_id: TokenId, 49 | approved_account_id: AccountId, 50 | approval_id: Option, 51 | ) -> bool { 52 | /* 53 | FILL THIS IN 54 | */ 55 | todo!(); //remove once code is filled in. 56 | } 57 | 58 | //revoke a specific account from transferring the token on your behalf 59 | #[payable] 60 | fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId) { 61 | /* 62 | FILL THIS IN 63 | */ 64 | } 65 | 66 | //revoke all accounts from transferring the token on your behalf 67 | #[payable] 68 | fn nft_revoke_all(&mut self, token_id: TokenId) { 69 | /* 70 | FILL THIS IN 71 | */ 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /nft-contract-basic/src/enumeration.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | //Query for the total supply of NFTs on the contract 6 | pub fn nft_total_supply(&self) -> U128 { 7 | //return the length of the token metadata by ID 8 | U128(self.token_metadata_by_id.len().into()) 9 | } 10 | 11 | //Query for nft tokens on the contract regardless of the owner using pagination 12 | pub fn nft_tokens(&self, from_index: Option, limit: Option) -> Vec { 13 | //where to start pagination - if we have a from_index, we'll use that - otherwise start from 0 index 14 | let start = u128::from(from_index.unwrap_or(U128(0))); 15 | 16 | //iterate through each token using an iterator 17 | self.token_metadata_by_id 18 | .keys() 19 | //skip to the index we specified in the start variable 20 | .skip(start as usize) 21 | //take the first "limit" elements in the vector. If we didn't specify a limit, use 50 22 | .take(limit.unwrap_or(50) as usize) 23 | //we'll map the token IDs which are strings into Json Tokens 24 | .map(|token_id| self.nft_token(token_id.clone()).unwrap()) 25 | //since we turned the keys into an iterator, we need to turn it back into a vector to return 26 | .collect() 27 | } 28 | 29 | //get the total supply of NFTs for a given owner 30 | pub fn nft_supply_for_owner(&self, account_id: AccountId) -> U128 { 31 | //get the set of tokens for the passed in owner 32 | let tokens_for_owner_set = self.tokens_per_owner.get(&account_id); 33 | 34 | //if there is some set of tokens, we'll return the length 35 | if let Some(tokens_for_owner_set) = tokens_for_owner_set { 36 | U128(tokens_for_owner_set.len().into()) 37 | } else { 38 | //if there isn't a set of tokens for the passed in account ID, we'll return 0 39 | U128(0) 40 | } 41 | } 42 | 43 | //Query for all the tokens for an owner 44 | pub fn nft_tokens_for_owner( 45 | &self, 46 | account_id: AccountId, 47 | from_index: Option, 48 | limit: Option, 49 | ) -> Vec { 50 | //get the set of tokens for the passed in owner 51 | let tokens_for_owner_set = self.tokens_per_owner.get(&account_id); 52 | //if there is some set of tokens, we'll set the tokens variable equal to that set 53 | let tokens = if let Some(tokens_for_owner_set) = tokens_for_owner_set { 54 | tokens_for_owner_set 55 | } else { 56 | //if there is no set of tokens, we'll simply return an empty vector. 57 | return vec![]; 58 | }; 59 | 60 | //where to start pagination - if we have a from_index, we'll use that - otherwise start from 0 index 61 | let start = u128::from(from_index.unwrap_or(U128(0))); 62 | 63 | //iterate through the keys vector 64 | tokens 65 | .iter() 66 | //skip to the index we specified in the start variable 67 | .skip(start as usize) 68 | //take the first "limit" elements in the vector. If we didn't specify a limit, use 50 69 | .take(limit.unwrap_or(50) as usize) 70 | //we'll map the token IDs which are strings into Json Tokens 71 | .map(|token_id| self.nft_token(token_id.clone()).unwrap()) 72 | //since we turned the keys into an iterator, we need to turn it back into a vector to return 73 | .collect() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /nft-contract-basic/src/internal.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use near_sdk::{CryptoHash}; 3 | use std::mem::size_of; 4 | 5 | //used to generate a unique prefix in our storage collections (this is to avoid data collisions) 6 | pub(crate) fn hash_account_id(account_id: &AccountId) -> CryptoHash { 7 | //get the default hash 8 | let mut hash = CryptoHash::default(); 9 | //we hash the account ID and return it 10 | hash.copy_from_slice(&env::sha256(account_id.as_bytes())); 11 | hash 12 | } 13 | 14 | //used to make sure the user attached exactly 1 yoctoNEAR 15 | pub(crate) fn assert_one_yocto() { 16 | assert_eq!( 17 | env::attached_deposit(), 18 | NearToken::from_yoctonear(1), 19 | "Requires attached deposit of exactly 1 yoctoNEAR", 20 | ) 21 | } 22 | 23 | //refund the initial deposit based on the amount of storage that was used up 24 | pub(crate) fn refund_deposit(storage_used: u128) { 25 | //get how much it would cost to store the information 26 | let required_cost = env::storage_byte_cost().saturating_mul(storage_used); 27 | //get the attached deposit 28 | let attached_deposit = env::attached_deposit(); 29 | 30 | //make sure that the attached deposit is greater than or equal to the required cost 31 | assert!( 32 | required_cost <= attached_deposit, 33 | "Must attach {} yoctoNEAR to cover storage", 34 | required_cost, 35 | ); 36 | 37 | //get the refund amount from the attached deposit - required cost 38 | let refund = attached_deposit.saturating_sub(required_cost); 39 | 40 | //if the refund is greater than 1 yocto NEAR, we refund the predecessor that amount 41 | if refund.gt(&ONE_YOCTONEAR) { 42 | Promise::new(env::predecessor_account_id()).transfer(refund); 43 | } 44 | } 45 | 46 | impl Contract { 47 | //add a token to the set of tokens an owner has 48 | pub(crate) fn internal_add_token_to_owner( 49 | &mut self, 50 | account_id: &AccountId, 51 | token_id: &TokenId, 52 | ) { 53 | //get the set of tokens for the given account 54 | let mut tokens_set = self.tokens_per_owner.get(account_id).unwrap_or_else(|| { 55 | //if the account doesn't have any tokens, we create a new unordered set 56 | UnorderedSet::new( 57 | StorageKey::TokenPerOwnerInner { 58 | //we get a new unique prefix for the collection 59 | account_id_hash: hash_account_id(&account_id), 60 | }, 61 | ) 62 | }); 63 | 64 | //we insert the token ID into the set 65 | tokens_set.insert(token_id); 66 | 67 | //we insert that set for the given account ID. 68 | self.tokens_per_owner.insert(account_id, &tokens_set); 69 | } 70 | 71 | //remove a token from an owner (internal method and can't be called directly via CLI). 72 | pub(crate) fn internal_remove_token_from_owner( 73 | &mut self, 74 | account_id: &AccountId, 75 | token_id: &TokenId, 76 | ) { 77 | //we get the set of tokens that the owner has 78 | let mut tokens_set = self 79 | .tokens_per_owner 80 | .get(account_id) 81 | //if there is no set of tokens for the owner, we panic with the following message: 82 | .expect("Token should be owned by the sender"); 83 | 84 | //we remove the the token_id from the set of tokens 85 | tokens_set.remove(token_id); 86 | 87 | //if the token set is now empty, we remove the owner from the tokens_per_owner collection 88 | if tokens_set.is_empty() { 89 | self.tokens_per_owner.remove(account_id); 90 | } else { 91 | //if the token set is not empty, we simply insert it back for the account ID. 92 | self.tokens_per_owner.insert(account_id, &tokens_set); 93 | } 94 | } 95 | 96 | //transfers the NFT to the receiver_id (internal method and can't be called directly via CLI). 97 | pub(crate) fn internal_transfer( 98 | &mut self, 99 | sender_id: &AccountId, 100 | receiver_id: &AccountId, 101 | token_id: &TokenId, 102 | memo: Option, 103 | ) -> Token { 104 | //get the token object by passing in the token_id 105 | let token = self.tokens_by_id.get(token_id).expect("No token"); 106 | 107 | //we make sure that the sender isn't sending the token to themselves 108 | assert_ne!( 109 | &token.owner_id, receiver_id, 110 | "The token owner and the receiver should be different" 111 | ); 112 | 113 | //we remove the token from it's current owner's set 114 | self.internal_remove_token_from_owner(&token.owner_id, token_id); 115 | //we then add the token to the receiver_id's set 116 | self.internal_add_token_to_owner(receiver_id, token_id); 117 | 118 | //we create a new token struct 119 | let new_token = Token { 120 | owner_id: receiver_id.clone(), 121 | }; 122 | //insert that new token into the tokens_by_id, replacing the old entry 123 | self.tokens_by_id.insert(token_id, &new_token); 124 | 125 | //if there was some memo attached, we log it. 126 | if let Some(memo) = memo.as_ref() { 127 | env::log_str(&format!("Memo: {}", memo).to_string()); 128 | } 129 | 130 | //return the previous token object that was transferred. 131 | token 132 | } 133 | } -------------------------------------------------------------------------------- /nft-contract-basic/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use near_sdk::borsh::{BorshSerialize, BorshDeserialize}; 3 | use near_sdk::collections::{LazyOption, LookupMap, UnorderedMap, UnorderedSet}; 4 | use near_sdk::json_types::{Base64VecU8, U64, U128}; 5 | use near_sdk::serde::{Deserialize, Serialize}; 6 | use near_sdk::{ 7 | near_bindgen, env, AccountId, NearToken, CryptoHash, PanicOnDefault, Promise, PromiseOrValue, BorshStorageKey, NearSchema 8 | }; 9 | 10 | use crate::internal::*; 11 | pub use crate::metadata::*; 12 | pub use crate::mint::*; 13 | pub use crate::nft_core::*; 14 | pub use crate::approval::*; 15 | pub use crate::royalty::*; 16 | 17 | mod internal; 18 | mod enumeration; 19 | mod metadata; 20 | mod mint; 21 | mod nft_core; 22 | mod approval; 23 | mod royalty; 24 | 25 | /// This spec can be treated like a version of the standard. 26 | pub const NFT_METADATA_SPEC: &str = "1.0.0"; 27 | /// This is the name of the NFT standard we're using 28 | pub const NFT_STANDARD_NAME: &str = "nep171"; 29 | 30 | //Basic NEAR amounts as constants 31 | const ONE_YOCTONEAR: NearToken = NearToken::from_yoctonear(1); 32 | 33 | #[near_bindgen] 34 | #[derive(BorshSerialize, BorshDeserialize, PanicOnDefault)] 35 | #[borsh(crate = "near_sdk::borsh")] 36 | pub struct Contract { 37 | //contract owner 38 | pub owner_id: AccountId, 39 | 40 | //keeps track of all the token IDs for a given account 41 | pub tokens_per_owner: LookupMap>, 42 | 43 | //keeps track of the token struct for a given token ID 44 | pub tokens_by_id: LookupMap, 45 | 46 | //keeps track of the token metadata for a given token ID 47 | pub token_metadata_by_id: UnorderedMap, 48 | 49 | //keeps track of the metadata for the contract 50 | pub metadata: LazyOption, 51 | } 52 | 53 | /// Helper structure for keys of the persistent collections. 54 | #[derive(BorshSerialize, BorshStorageKey)] 55 | #[borsh(crate = "near_sdk::borsh")] 56 | pub enum StorageKey { 57 | TokensPerOwner, 58 | TokenPerOwnerInner { account_id_hash: CryptoHash }, 59 | TokensById, 60 | TokenMetadataById, 61 | NFTContractMetadata, 62 | TokensPerType, 63 | TokensPerTypeInner { token_type_hash: CryptoHash }, 64 | TokenTypesLocked, 65 | } 66 | 67 | #[near_bindgen] 68 | impl Contract { 69 | /* 70 | initialization function (can only be called once). 71 | this initializes the contract with default metadata so the 72 | user doesn't have to manually type metadata. 73 | */ 74 | #[init] 75 | pub fn new_default_meta(owner_id: AccountId) -> Self { 76 | //calls the other function "new: with some default metadata and the owner_id passed in 77 | Self::new( 78 | owner_id, 79 | NFTContractMetadata { 80 | spec: "nft-1.0.0".to_string(), 81 | name: "NFT Tutorial Contract".to_string(), 82 | symbol: "GOTEAM".to_string(), 83 | icon: None, 84 | base_uri: None, 85 | reference: None, 86 | reference_hash: None, 87 | }, 88 | ) 89 | } 90 | 91 | /* 92 | initialization function (can only be called once). 93 | this initializes the contract with metadata that was passed in and 94 | the owner_id. 95 | */ 96 | #[init] 97 | pub fn new(owner_id: AccountId, metadata: NFTContractMetadata) -> Self { 98 | //create a variable of type Self with all the fields initialized. 99 | let this = Self { 100 | //Storage keys are simply the prefixes used for the collections. This helps avoid data collision 101 | tokens_per_owner: LookupMap::new(StorageKey::TokensPerOwner), 102 | tokens_by_id: LookupMap::new(StorageKey::TokensById), 103 | token_metadata_by_id: UnorderedMap::new(StorageKey::TokenMetadataById), 104 | //set the owner_id field equal to the passed in owner_id. 105 | owner_id, 106 | metadata: LazyOption::new( 107 | StorageKey::NFTContractMetadata, 108 | Some(&metadata), 109 | ), 110 | }; 111 | 112 | //return the Contract object 113 | this 114 | } 115 | } -------------------------------------------------------------------------------- /nft-contract-basic/src/metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | pub type TokenId = String; 3 | //defines the payout type we'll be returning as a part of the royalty standards. 4 | #[derive(Serialize, Deserialize, NearSchema)] 5 | #[serde(crate = "near_sdk::serde")] 6 | pub struct Payout { 7 | pub payout: HashMap, 8 | } 9 | 10 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Clone, NearSchema)] 11 | #[borsh(crate = "near_sdk::borsh")] 12 | #[serde(crate = "near_sdk::serde")] 13 | pub struct NFTContractMetadata { 14 | pub spec: String, // required, essentially a version like "nft-1.0.0" 15 | pub name: String, // required, ex. "Mosaics" 16 | pub symbol: String, // required, ex. "MOSAIC" 17 | pub icon: Option, // Data URL 18 | pub base_uri: Option, // Centralized gateway known to have reliable access to decentralized storage assets referenced by `reference` or `media` URLs 19 | pub reference: Option, // URL to a JSON file with more info 20 | pub reference_hash: Option, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. 21 | } 22 | 23 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, NearSchema)] 24 | #[borsh(crate = "near_sdk::borsh")] 25 | #[serde(crate = "near_sdk::serde")] 26 | pub struct TokenMetadata { 27 | pub title: Option, // ex. "Arch Nemesis: Mail Carrier" or "Parcel #5055" 28 | pub description: Option, // free-form description 29 | pub media: Option, // URL to associated media, preferably to decentralized, content-addressed storage 30 | pub media_hash: Option, // Base64-encoded sha256 hash of content referenced by the `media` field. Required if `media` is included. 31 | pub copies: Option, // number of copies of this set of metadata in existence when token was minted. 32 | pub issued_at: Option, // When token was issued or minted, Unix epoch in milliseconds 33 | pub expires_at: Option, // When token expires, Unix epoch in milliseconds 34 | pub starts_at: Option, // When token starts being valid, Unix epoch in milliseconds 35 | pub updated_at: Option, // When token was last updated, Unix epoch in milliseconds 36 | pub extra: Option, // anything extra the NFT wants to store on-chain. Can be stringified JSON. 37 | pub reference: Option, // URL to an off-chain JSON file with more info. 38 | pub reference_hash: Option, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. 39 | } 40 | 41 | #[derive(BorshDeserialize, BorshSerialize)] 42 | #[borsh(crate = "near_sdk::borsh")] 43 | pub struct Token { 44 | //owner of the token 45 | pub owner_id: AccountId, 46 | } 47 | 48 | //The Json token is what will be returned from view calls. 49 | #[derive(Serialize, Deserialize, NearSchema)] 50 | #[serde(crate = "near_sdk::serde")] 51 | pub struct JsonToken { 52 | //token ID 53 | pub token_id: TokenId, 54 | //owner of the token 55 | pub owner_id: AccountId, 56 | //token metadata 57 | pub metadata: TokenMetadata, 58 | } 59 | 60 | pub trait NonFungibleTokenMetadata { 61 | //view call for returning the contract metadata 62 | fn nft_metadata(&self) -> NFTContractMetadata; 63 | } 64 | 65 | #[near_bindgen] 66 | impl NonFungibleTokenMetadata for Contract { 67 | fn nft_metadata(&self) -> NFTContractMetadata { 68 | self.metadata.get().unwrap() 69 | } 70 | } -------------------------------------------------------------------------------- /nft-contract-basic/src/mint.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | #[payable] 6 | pub fn nft_mint( 7 | &mut self, 8 | token_id: TokenId, 9 | token_owner_id: AccountId, 10 | token_metadata: TokenMetadata, 11 | ) { 12 | //measure the initial storage being used on the contract 13 | let initial_storage_usage = env::storage_usage(); 14 | 15 | //specify the token struct that contains the owner ID 16 | let token = Token { 17 | //set the owner ID equal to the receiver ID passed into the function 18 | owner_id: token_owner_id, 19 | }; 20 | 21 | //insert the token ID and token struct and make sure that the token doesn't exist 22 | assert!( 23 | self.tokens_by_id.insert(&token_id, &token).is_none(), 24 | "Token already exists" 25 | ); 26 | 27 | //insert the token ID and metadata 28 | self.token_metadata_by_id.insert(&token_id, &token_metadata); 29 | 30 | //call the internal method for adding the token to the owner 31 | self.internal_add_token_to_owner(&token.owner_id, &token_id); 32 | 33 | //calculate the required storage which was the used - initial 34 | let required_storage_in_bytes = env::storage_usage() - initial_storage_usage; 35 | 36 | //refund any excess storage if the user attached too much. Panic if they didn't attach enough to cover the required. 37 | refund_deposit(required_storage_in_bytes.into()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /nft-contract-basic/src/nft_core.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use near_sdk::{ext_contract, log, Gas, PromiseResult}; 3 | 4 | const GAS_FOR_RESOLVE_TRANSFER: Gas = Gas::from_tgas(10); 5 | const GAS_FOR_NFT_ON_TRANSFER: Gas = Gas::from_tgas(25); 6 | 7 | pub trait NonFungibleTokenCore { 8 | //transfers an NFT to a receiver ID 9 | fn nft_transfer(&mut self, receiver_id: AccountId, token_id: TokenId, memo: Option); 10 | 11 | //transfers an NFT to a receiver and calls a function on the receiver ID's contract 12 | /// Returns `true` if the token was transferred from the sender's account. 13 | fn nft_transfer_call( 14 | &mut self, 15 | receiver_id: AccountId, 16 | token_id: TokenId, 17 | memo: Option, 18 | msg: String, 19 | ) -> PromiseOrValue; 20 | 21 | //get information about the NFT token passed in 22 | fn nft_token(&self, token_id: TokenId) -> Option; 23 | } 24 | 25 | #[ext_contract(ext_non_fungible_token_receiver)] 26 | trait NonFungibleTokenReceiver { 27 | //Method stored on the receiver contract that is called via cross contract call when nft_transfer_call is called 28 | /// Returns `true` if the token should be returned back to the sender. 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 | ) -> Promise; 36 | } 37 | 38 | #[ext_contract(ext_self)] 39 | trait NonFungibleTokenResolver { 40 | /* 41 | resolves the promise of the cross contract call to the receiver contract 42 | this is stored on THIS contract and is meant to analyze what happened in the cross contract call when nft_on_transfer was called 43 | as part of the nft_transfer_call method 44 | */ 45 | fn nft_resolve_transfer( 46 | &mut self, 47 | previous_owner_id: AccountId, 48 | receiver_id: AccountId, 49 | token_id: TokenId, 50 | ) -> bool; 51 | } 52 | 53 | #[near_bindgen] 54 | impl NonFungibleTokenCore for Contract { 55 | //implementation of the nft_transfer method. This transfers the NFT from the current owner to the receiver. 56 | #[payable] 57 | fn nft_transfer(&mut self, receiver_id: AccountId, token_id: TokenId, memo: Option) { 58 | //assert that the user attached exactly 1 yoctoNEAR. This is for security and so that the user will be redirected to the NEAR wallet. 59 | assert_one_yocto(); 60 | //get the sender to transfer the token from the sender to the receiver 61 | let sender_id = env::predecessor_account_id(); 62 | 63 | //call the internal transfer method 64 | self.internal_transfer(&sender_id, &receiver_id, &token_id, memo); 65 | } 66 | 67 | //implementation of the transfer call method. This will transfer the NFT and call a method on the receiver_id contract 68 | #[payable] 69 | fn nft_transfer_call( 70 | &mut self, 71 | receiver_id: AccountId, 72 | token_id: TokenId, 73 | memo: Option, 74 | msg: String, 75 | ) -> PromiseOrValue { 76 | //assert that the user attached exactly 1 yocto for security reasons. 77 | assert_one_yocto(); 78 | 79 | //get the sender ID 80 | let sender_id = env::predecessor_account_id(); 81 | 82 | //transfer the token and get the previous token object 83 | let previous_token = 84 | self.internal_transfer(&sender_id, &receiver_id, &token_id, memo.clone()); 85 | 86 | // Initiating receiver's call and the callback 87 | // Defaulting GAS weight to 1, no attached deposit, and static GAS equal to the GAS for nft on transfer. 88 | ext_non_fungible_token_receiver::ext(receiver_id.clone()) 89 | .with_static_gas(GAS_FOR_NFT_ON_TRANSFER) 90 | .nft_on_transfer( 91 | sender_id, 92 | previous_token.owner_id.clone(), 93 | token_id.clone(), 94 | msg, 95 | ) 96 | // We then resolve the promise and call nft_resolve_transfer on our own contract 97 | .then( 98 | // Defaulting GAS weight to 1, no attached deposit, and static GAS equal to the GAS for resolve transfer 99 | Self::ext(env::current_account_id()) 100 | .with_static_gas(GAS_FOR_RESOLVE_TRANSFER) 101 | .nft_resolve_transfer(previous_token.owner_id, receiver_id, token_id), 102 | ) 103 | .into() 104 | } 105 | 106 | //get the information for a specific token ID 107 | fn nft_token(&self, token_id: TokenId) -> Option { 108 | //if there is some token ID in the tokens_by_id collection 109 | if let Some(token) = self.tokens_by_id.get(&token_id) { 110 | //we'll get the metadata for that token 111 | let metadata = self.token_metadata_by_id.get(&token_id).unwrap(); 112 | //we return the JsonToken (wrapped by Some since we return an option) 113 | Some(JsonToken { 114 | token_id, 115 | owner_id: token.owner_id, 116 | metadata, 117 | }) 118 | } else { 119 | //if there wasn't a token ID in the tokens_by_id collection, we return None 120 | None 121 | } 122 | } 123 | } 124 | 125 | #[near_bindgen] 126 | impl NonFungibleTokenResolver for Contract { 127 | //resolves the cross contract call when calling nft_on_transfer in the nft_transfer_call method 128 | //returns true if the token was successfully transferred to the receiver_id 129 | #[private] 130 | fn nft_resolve_transfer( 131 | &mut self, 132 | previous_owner_id: AccountId, 133 | receiver_id: AccountId, 134 | token_id: TokenId, 135 | ) -> bool { 136 | // Whether receiver wants to return token back to the sender, based on `nft_on_transfer` 137 | // call result. 138 | if let PromiseResult::Successful(value) = env::promise_result(0) { 139 | //As per the standard, the nft_on_transfer should return whether we should return the token to it's owner or not 140 | if let Ok(return_token) = near_sdk::serde_json::from_slice::(&value) { 141 | //if we need don't need to return the token, we simply return true meaning everything went fine 142 | if !return_token { 143 | /* 144 | since we've already transferred the token and nft_on_transfer returned false, we don't have to 145 | revert the original transfer and thus we can just return true since nothing went wrong. 146 | */ 147 | return true; 148 | } 149 | } 150 | } 151 | 152 | //get the token object if there is some token object 153 | let mut token = if let Some(token) = self.tokens_by_id.get(&token_id) { 154 | if token.owner_id != receiver_id { 155 | // The token is not owner by the receiver anymore. Can't return it. 156 | return true; 157 | } 158 | token 159 | //if there isn't a token object, it was burned and so we return true 160 | } else { 161 | return true; 162 | }; 163 | 164 | //if at the end, we haven't returned true, that means that we should return the token to it's original owner 165 | log!( 166 | "Return {} from @{} to @{}", 167 | token_id, 168 | receiver_id, 169 | previous_owner_id 170 | ); 171 | 172 | //we remove the token from the receiver 173 | self.internal_remove_token_from_owner(&receiver_id, &token_id); 174 | //we add the token to the original owner 175 | self.internal_add_token_to_owner(&previous_owner_id, &token_id); 176 | 177 | //we change the token struct's owner to be the original owner 178 | token.owner_id = previous_owner_id.clone(); 179 | //we inset the token back into the tokens_by_id collection 180 | self.tokens_by_id.insert(&token_id, &token); 181 | 182 | //return false 183 | false 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /nft-contract-basic/src/royalty.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub trait NonFungibleTokenCore { 4 | //calculates the payout for a token given the passed in balance. This is a view method 5 | fn nft_payout(&self, token_id: TokenId, balance: NearToken, max_len_payout: u32) -> Payout; 6 | 7 | //transfers the token to the receiver ID and returns the payout object that should be payed given the passed in balance. 8 | fn nft_transfer_payout( 9 | &mut self, 10 | receiver_id: AccountId, 11 | token_id: TokenId, 12 | approval_id: u64, 13 | memo: Option, 14 | balance: NearToken, 15 | max_len_payout: u32, 16 | ) -> Payout; 17 | } 18 | 19 | #[near_bindgen] 20 | impl NonFungibleTokenCore for Contract { 21 | //calculates the payout for a token given the passed in balance. This is a view method 22 | fn nft_payout(&self, token_id: TokenId, balance: NearToken, max_len_payout: u32) -> Payout { 23 | /* 24 | FILL THIS IN 25 | */ 26 | todo!(); //remove once code is filled in. 27 | } 28 | 29 | //transfers the token to the receiver ID and returns the payout object that should be payed given the passed in balance. 30 | #[payable] 31 | fn nft_transfer_payout( 32 | &mut self, 33 | receiver_id: AccountId, 34 | token_id: TokenId, 35 | approval_id: u64, 36 | memo: Option, 37 | balance: NearToken, 38 | max_len_payout: u32, 39 | ) -> Payout { 40 | /* 41 | FILL THIS IN 42 | */ 43 | todo!(); //remove once code is filled in. 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /nft-contract-events/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nft_contract_skeleton" 3 | version = "0.1.0" 4 | authors = ["Near Inc "] 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | near-sdk = { version = "5.11.0", features = ["legacy"] } 12 | serde_json = "1.0.113" 13 | 14 | [profile.release] 15 | codegen-units = 1 16 | # Tell `rustc` to optimize for small code size. 17 | opt-level = "z" 18 | lto = true 19 | debug = false 20 | panic = "abort" 21 | # Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801 22 | overflow-checks = true 23 | -------------------------------------------------------------------------------- /nft-contract-events/README.md: -------------------------------------------------------------------------------- 1 | # TBD 2 | -------------------------------------------------------------------------------- /nft-contract-events/src/approval.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use near_sdk::ext_contract; 3 | 4 | pub trait NonFungibleTokenCore { 5 | //approve an account ID to transfer a token on your behalf 6 | fn nft_approve(&mut self, token_id: TokenId, account_id: AccountId, msg: Option); 7 | 8 | //check if the passed in account has access to approve the token ID 9 | fn nft_is_approved( 10 | &self, 11 | token_id: TokenId, 12 | approved_account_id: AccountId, 13 | approval_id: Option, 14 | ) -> bool; 15 | 16 | //revoke a specific account from transferring the token on your behalf 17 | fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId); 18 | 19 | //revoke all accounts from transferring the token on your behalf 20 | fn nft_revoke_all(&mut self, token_id: TokenId); 21 | } 22 | 23 | #[ext_contract(ext_non_fungible_approval_receiver)] 24 | trait NonFungibleTokenApprovalsReceiver { 25 | //cross contract call to an external contract that is initiated during nft_approve 26 | fn nft_on_approve( 27 | &mut self, 28 | token_id: TokenId, 29 | owner_id: AccountId, 30 | approval_id: u64, 31 | msg: String, 32 | ); 33 | } 34 | 35 | #[near_bindgen] 36 | impl NonFungibleTokenCore for Contract { 37 | //allow a specific account ID to approve a token on your behalf 38 | #[payable] 39 | fn nft_approve(&mut self, token_id: TokenId, account_id: AccountId, msg: Option) { 40 | /* 41 | FILL THIS IN 42 | */ 43 | } 44 | 45 | //check if the passed in account has access to approve the token ID 46 | fn nft_is_approved( 47 | &self, 48 | token_id: TokenId, 49 | approved_account_id: AccountId, 50 | approval_id: Option, 51 | ) -> bool { 52 | /* 53 | FILL THIS IN 54 | */ 55 | todo!(); //remove once code is filled in. 56 | } 57 | 58 | //revoke a specific account from transferring the token on your behalf 59 | #[payable] 60 | fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId) { 61 | /* 62 | FILL THIS IN 63 | */ 64 | } 65 | 66 | //revoke all accounts from transferring the token on your behalf 67 | #[payable] 68 | fn nft_revoke_all(&mut self, token_id: TokenId) { 69 | /* 70 | FILL THIS IN 71 | */ 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /nft-contract-events/src/enumeration.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | //Query for the total supply of NFTs on the contract 6 | pub fn nft_total_supply(&self) -> U128 { 7 | //return the length of the token metadata by ID 8 | U128(self.token_metadata_by_id.len().into()) 9 | } 10 | 11 | //Query for nft tokens on the contract regardless of the owner using pagination 12 | pub fn nft_tokens(&self, from_index: Option, limit: Option) -> Vec { 13 | //where to start pagination - if we have a from_index, we'll use that - otherwise start from 0 index 14 | let start = u128::from(from_index.unwrap_or(U128(0))); 15 | 16 | //iterate through each token using an iterator 17 | self.token_metadata_by_id 18 | .keys() 19 | //skip to the index we specified in the start variable 20 | .skip(start as usize) 21 | //take the first "limit" elements in the vector. If we didn't specify a limit, use 50 22 | .take(limit.unwrap_or(50) as usize) 23 | //we'll map the token IDs which are strings into Json Tokens 24 | .map(|token_id| self.nft_token(token_id.clone()).unwrap()) 25 | //since we turned the keys into an iterator, we need to turn it back into a vector to return 26 | .collect() 27 | } 28 | 29 | //get the total supply of NFTs for a given owner 30 | pub fn nft_supply_for_owner(&self, account_id: AccountId) -> U128 { 31 | //get the set of tokens for the passed in owner 32 | let tokens_for_owner_set = self.tokens_per_owner.get(&account_id); 33 | 34 | //if there is some set of tokens, we'll return the length 35 | if let Some(tokens_for_owner_set) = tokens_for_owner_set { 36 | U128(tokens_for_owner_set.len().into()) 37 | } else { 38 | //if there isn't a set of tokens for the passed in account ID, we'll return 0 39 | U128(0) 40 | } 41 | } 42 | 43 | //Query for all the tokens for an owner 44 | pub fn nft_tokens_for_owner( 45 | &self, 46 | account_id: AccountId, 47 | from_index: Option, 48 | limit: Option, 49 | ) -> Vec { 50 | //get the set of tokens for the passed in owner 51 | let tokens_for_owner_set = self.tokens_per_owner.get(&account_id); 52 | //if there is some set of tokens, we'll set the tokens variable equal to that set 53 | let tokens = if let Some(tokens_for_owner_set) = tokens_for_owner_set { 54 | tokens_for_owner_set 55 | } else { 56 | //if there is no set of tokens, we'll simply return an empty vector. 57 | return vec![]; 58 | }; 59 | 60 | //where to start pagination - if we have a from_index, we'll use that - otherwise start from 0 index 61 | let start = u128::from(from_index.unwrap_or(U128(0))); 62 | 63 | //iterate through the keys vector 64 | tokens 65 | .iter() 66 | //skip to the index we specified in the start variable 67 | .skip(start as usize) 68 | //take the first "limit" elements in the vector. If we didn't specify a limit, use 50 69 | .take(limit.unwrap_or(50) as usize) 70 | //we'll map the token IDs which are strings into Json Tokens 71 | .map(|token_id| self.nft_token(token_id.clone()).unwrap()) 72 | //since we turned the keys into an iterator, we need to turn it back into a vector to return 73 | .collect() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /nft-contract-events/src/events.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use near_sdk::serde::{Deserialize, Serialize}; 4 | 5 | /// Enum that represents the data type of the EventLog. 6 | /// The enum can either be an NftMint or an NftTransfer. 7 | #[derive(Serialize, Deserialize, Debug)] 8 | #[serde(tag = "event", content = "data")] 9 | #[serde(rename_all = "snake_case")] 10 | #[serde(crate = "near_sdk::serde")] 11 | #[non_exhaustive] 12 | pub enum EventLogVariant { 13 | NftMint(Vec), 14 | NftTransfer(Vec), 15 | } 16 | 17 | /// Interface to capture data about an event 18 | /// 19 | /// Arguments: 20 | /// * `standard`: name of standard e.g. nep171 21 | /// * `version`: e.g. 1.0.0 22 | /// * `event`: associate event data 23 | #[derive(Serialize, Deserialize, Debug)] 24 | #[serde(crate = "near_sdk::serde")] 25 | pub struct EventLog { 26 | pub standard: String, 27 | pub version: String, 28 | 29 | // `flatten` to not have "event": {} in the JSON, just have the contents of {}. 30 | #[serde(flatten)] 31 | pub event: EventLogVariant, 32 | } 33 | 34 | impl fmt::Display for EventLog { 35 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 36 | f.write_fmt(format_args!( 37 | "EVENT_JSON:{}", 38 | &serde_json::to_string(self).map_err(|_| fmt::Error)? 39 | )) 40 | } 41 | } 42 | 43 | /// An event log to capture token minting 44 | /// 45 | /// Arguments 46 | /// * `owner_id`: "account.near" 47 | /// * `token_ids`: ["1", "abc"] 48 | /// * `memo`: optional message 49 | #[derive(Serialize, Deserialize, Debug)] 50 | #[serde(crate = "near_sdk::serde")] 51 | pub struct NftMintLog { 52 | pub owner_id: String, 53 | pub token_ids: Vec, 54 | 55 | #[serde(skip_serializing_if = "Option::is_none")] 56 | pub memo: Option, 57 | } 58 | 59 | /// An event log to capture token transfer 60 | /// 61 | /// Arguments 62 | /// * `authorized_id`: approved account to transfer 63 | /// * `old_owner_id`: "owner.near" 64 | /// * `new_owner_id`: "receiver.near" 65 | /// * `token_ids`: ["1", "12345abc"] 66 | /// * `memo`: optional message 67 | #[derive(Serialize, Deserialize, Debug)] 68 | #[serde(crate = "near_sdk::serde")] 69 | pub struct NftTransferLog { 70 | #[serde(skip_serializing_if = "Option::is_none")] 71 | pub authorized_id: Option, 72 | 73 | pub old_owner_id: String, 74 | pub new_owner_id: String, 75 | pub token_ids: Vec, 76 | 77 | #[serde(skip_serializing_if = "Option::is_none")] 78 | pub memo: Option, 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::*; 84 | 85 | #[test] 86 | fn nep_format_vector() { 87 | let expected = r#"EVENT_JSON:{"standard":"nep171","version":"1.0.0","event":"nft_mint","data":[{"owner_id":"foundation.near","token_ids":["aurora","proximitylabs"]},{"owner_id":"user1.near","token_ids":["meme"]}]}"#; 88 | let log = EventLog { 89 | standard: "nep171".to_string(), 90 | version: "1.0.0".to_string(), 91 | event: EventLogVariant::NftMint(vec![ 92 | NftMintLog { 93 | owner_id: "foundation.near".to_owned(), 94 | token_ids: vec!["aurora".to_string(), "proximitylabs".to_string()], 95 | memo: None, 96 | }, 97 | NftMintLog { 98 | owner_id: "user1.near".to_owned(), 99 | token_ids: vec!["meme".to_string()], 100 | memo: None, 101 | }, 102 | ]), 103 | }; 104 | assert_eq!(expected, log.to_string()); 105 | } 106 | 107 | #[test] 108 | fn nep_format_mint() { 109 | let expected = r#"EVENT_JSON:{"standard":"nep171","version":"1.0.0","event":"nft_mint","data":[{"owner_id":"foundation.near","token_ids":["aurora","proximitylabs"]}]}"#; 110 | let log = EventLog { 111 | standard: "nep171".to_string(), 112 | version: "1.0.0".to_string(), 113 | event: EventLogVariant::NftMint(vec![NftMintLog { 114 | owner_id: "foundation.near".to_owned(), 115 | token_ids: vec!["aurora".to_string(), "proximitylabs".to_string()], 116 | memo: None, 117 | }]), 118 | }; 119 | assert_eq!(expected, log.to_string()); 120 | } 121 | 122 | #[test] 123 | fn nep_format_transfer_all_fields() { 124 | let expected = r#"EVENT_JSON:{"standard":"nep171","version":"1.0.0","event":"nft_transfer","data":[{"authorized_id":"market.near","old_owner_id":"user1.near","new_owner_id":"user2.near","token_ids":["token"],"memo":"Go Team!"}]}"#; 125 | let log = EventLog { 126 | standard: "nep171".to_string(), 127 | version: "1.0.0".to_string(), 128 | event: EventLogVariant::NftTransfer(vec![NftTransferLog { 129 | authorized_id: Some("market.near".to_string()), 130 | old_owner_id: "user1.near".to_string(), 131 | new_owner_id: "user2.near".to_string(), 132 | token_ids: vec!["token".to_string()], 133 | memo: Some("Go Team!".to_owned()), 134 | }]), 135 | }; 136 | assert_eq!(expected, log.to_string()); 137 | } 138 | } -------------------------------------------------------------------------------- /nft-contract-events/src/internal.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use near_sdk::{CryptoHash}; 3 | use std::mem::size_of; 4 | 5 | //used to generate a unique prefix in our storage collections (this is to avoid data collisions) 6 | pub(crate) fn hash_account_id(account_id: &AccountId) -> CryptoHash { 7 | //get the default hash 8 | let mut hash = CryptoHash::default(); 9 | //we hash the account ID and return it 10 | hash.copy_from_slice(&env::sha256(account_id.as_bytes())); 11 | hash 12 | } 13 | 14 | //used to make sure the user attached exactly 1 yoctoNEAR 15 | pub(crate) fn assert_one_yocto() { 16 | assert_eq!( 17 | env::attached_deposit(), 18 | NearToken::from_yoctonear(1), 19 | "Requires attached deposit of exactly 1 yoctoNEAR", 20 | ) 21 | } 22 | 23 | //refund the initial deposit based on the amount of storage that was used up 24 | pub(crate) fn refund_deposit(storage_used: u128) { 25 | //get how much it would cost to store the information 26 | let required_cost = env::storage_byte_cost().saturating_mul(storage_used); 27 | //get the attached deposit 28 | let attached_deposit = env::attached_deposit(); 29 | 30 | //make sure that the attached deposit is greater than or equal to the required cost 31 | assert!( 32 | required_cost <= attached_deposit, 33 | "Must attach {} yoctoNEAR to cover storage", 34 | required_cost, 35 | ); 36 | 37 | //get the refund amount from the attached deposit - required cost 38 | let refund = attached_deposit.saturating_sub(required_cost); 39 | 40 | //if the refund is greater than 1 yocto NEAR, we refund the predecessor that amount 41 | if refund.gt(&ONE_YOCTONEAR) { 42 | Promise::new(env::predecessor_account_id()).transfer(refund); 43 | } 44 | } 45 | 46 | impl Contract { 47 | //add a token to the set of tokens an owner has 48 | pub(crate) fn internal_add_token_to_owner( 49 | &mut self, 50 | account_id: &AccountId, 51 | token_id: &TokenId, 52 | ) { 53 | //get the set of tokens for the given account 54 | let mut tokens_set = self.tokens_per_owner.get(account_id).unwrap_or_else(|| { 55 | //if the account doesn't have any tokens, we create a new unordered set 56 | UnorderedSet::new( 57 | StorageKey::TokenPerOwnerInner { 58 | //we get a new unique prefix for the collection 59 | account_id_hash: hash_account_id(&account_id), 60 | }, 61 | ) 62 | }); 63 | 64 | //we insert the token ID into the set 65 | tokens_set.insert(token_id); 66 | 67 | //we insert that set for the given account ID. 68 | self.tokens_per_owner.insert(account_id, &tokens_set); 69 | } 70 | 71 | //remove a token from an owner (internal method and can't be called directly via CLI). 72 | pub(crate) fn internal_remove_token_from_owner( 73 | &mut self, 74 | account_id: &AccountId, 75 | token_id: &TokenId, 76 | ) { 77 | //we get the set of tokens that the owner has 78 | let mut tokens_set = self 79 | .tokens_per_owner 80 | .get(account_id) 81 | //if there is no set of tokens for the owner, we panic with the following message: 82 | .expect("Token should be owned by the sender"); 83 | 84 | //we remove the the token_id from the set of tokens 85 | tokens_set.remove(token_id); 86 | 87 | //if the token set is now empty, we remove the owner from the tokens_per_owner collection 88 | if tokens_set.is_empty() { 89 | self.tokens_per_owner.remove(account_id); 90 | } else { 91 | //if the token set is not empty, we simply insert it back for the account ID. 92 | self.tokens_per_owner.insert(account_id, &tokens_set); 93 | } 94 | } 95 | 96 | //transfers the NFT to the receiver_id (internal method and can't be called directly via CLI). 97 | pub(crate) fn internal_transfer( 98 | &mut self, 99 | sender_id: &AccountId, 100 | receiver_id: &AccountId, 101 | token_id: &TokenId, 102 | memo: Option, 103 | ) -> Token { 104 | //get the token object by passing in the token_id 105 | let token = self.tokens_by_id.get(token_id).expect("No token"); 106 | 107 | //we make sure that the sender isn't sending the token to themselves 108 | assert_ne!( 109 | &token.owner_id, receiver_id, 110 | "The token owner and the receiver should be different" 111 | ); 112 | 113 | //we remove the token from it's current owner's set 114 | self.internal_remove_token_from_owner(&token.owner_id, token_id); 115 | //we then add the token to the receiver_id's set 116 | self.internal_add_token_to_owner(receiver_id, token_id); 117 | 118 | //we create a new token struct 119 | let new_token = Token { 120 | owner_id: receiver_id.clone(), 121 | }; 122 | //insert that new token into the tokens_by_id, replacing the old entry 123 | self.tokens_by_id.insert(token_id, &new_token); 124 | 125 | //if there was some memo attached, we log it. 126 | if let Some(memo) = memo.as_ref() { 127 | env::log_str(&format!("Memo: {}", memo).to_string()); 128 | } 129 | 130 | // Default the authorized ID to be None for the logs. // We will return here in the future when we study the approval functionality 131 | let mut authorized_id = None; 132 | 133 | // Construct the transfer log as per the events standard. 134 | let nft_transfer_log: EventLog = EventLog { 135 | // Standard name ("nep171"). 136 | standard: NFT_STANDARD_NAME.to_string(), 137 | // Version of the standard ("nft-1.0.0"). 138 | version: NFT_METADATA_SPEC.to_string(), 139 | // The data related with the event stored in a vector. 140 | event: EventLogVariant::NftTransfer(vec![NftTransferLog { 141 | // The optional authorized account ID to transfer the token on behalf of the old owner. 142 | authorized_id, 143 | // The old owner's account ID. 144 | old_owner_id: token.owner_id.to_string(), 145 | // The account ID of the new owner of the token. 146 | new_owner_id: receiver_id.to_string(), 147 | // A vector containing the token IDs as strings. 148 | token_ids: vec![token_id.to_string()], 149 | // An optional memo to include. 150 | memo, 151 | }]), 152 | }; 153 | 154 | // Log the serialized json. 155 | env::log_str(&nft_transfer_log.to_string()); 156 | 157 | //return the previous token object that was transferred. 158 | token 159 | } 160 | } -------------------------------------------------------------------------------- /nft-contract-events/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use near_sdk::borsh::{BorshSerialize, BorshDeserialize}; 3 | use near_sdk::collections::{LazyOption, LookupMap, UnorderedMap, UnorderedSet}; 4 | use near_sdk::json_types::{Base64VecU8, U64, U128}; 5 | use near_sdk::serde::{Deserialize, Serialize}; 6 | use near_sdk::{ 7 | near_bindgen, env, AccountId, NearToken, CryptoHash, PanicOnDefault, Promise, PromiseOrValue, BorshStorageKey, NearSchema 8 | }; 9 | 10 | use crate::internal::*; 11 | pub use crate::metadata::*; 12 | pub use crate::mint::*; 13 | pub use crate::nft_core::*; 14 | pub use crate::approval::*; 15 | pub use crate::royalty::*; 16 | pub use crate::events::*; 17 | 18 | mod internal; 19 | mod enumeration; 20 | mod metadata; 21 | mod mint; 22 | mod nft_core; 23 | mod approval; 24 | mod royalty; 25 | mod events; 26 | 27 | /// This spec can be treated like a version of the standard. 28 | pub const NFT_METADATA_SPEC: &str = "1.0.0"; 29 | /// This is the name of the NFT standard we're using 30 | pub const NFT_STANDARD_NAME: &str = "nep171"; 31 | 32 | //Basic NEAR amounts as constants 33 | const ONE_YOCTONEAR: NearToken = NearToken::from_yoctonear(1); 34 | 35 | #[near_bindgen] 36 | #[derive(BorshSerialize, BorshDeserialize, PanicOnDefault)] 37 | #[borsh(crate = "near_sdk::borsh")] 38 | pub struct Contract { 39 | //contract owner 40 | pub owner_id: AccountId, 41 | 42 | //keeps track of all the token IDs for a given account 43 | pub tokens_per_owner: LookupMap>, 44 | 45 | //keeps track of the token struct for a given token ID 46 | pub tokens_by_id: LookupMap, 47 | 48 | //keeps track of the token metadata for a given token ID 49 | pub token_metadata_by_id: UnorderedMap, 50 | 51 | //keeps track of the metadata for the contract 52 | pub metadata: LazyOption, 53 | } 54 | 55 | /// Helper structure for keys of the persistent collections. 56 | #[derive(BorshSerialize, BorshStorageKey)] 57 | #[borsh(crate = "near_sdk::borsh")] 58 | pub enum StorageKey { 59 | TokensPerOwner, 60 | TokenPerOwnerInner { account_id_hash: CryptoHash }, 61 | TokensById, 62 | TokenMetadataById, 63 | NFTContractMetadata, 64 | TokensPerType, 65 | TokensPerTypeInner { token_type_hash: CryptoHash }, 66 | TokenTypesLocked, 67 | } 68 | 69 | #[near_bindgen] 70 | impl Contract { 71 | /* 72 | initialization function (can only be called once). 73 | this initializes the contract with default metadata so the 74 | user doesn't have to manually type metadata. 75 | */ 76 | #[init] 77 | pub fn new_default_meta(owner_id: AccountId) -> Self { 78 | //calls the other function "new: with some default metadata and the owner_id passed in 79 | Self::new( 80 | owner_id, 81 | NFTContractMetadata { 82 | spec: "nft-1.0.0".to_string(), 83 | name: "NFT Tutorial Contract".to_string(), 84 | symbol: "GOTEAM".to_string(), 85 | icon: None, 86 | base_uri: None, 87 | reference: None, 88 | reference_hash: None, 89 | }, 90 | ) 91 | } 92 | 93 | /* 94 | initialization function (can only be called once). 95 | this initializes the contract with metadata that was passed in and 96 | the owner_id. 97 | */ 98 | #[init] 99 | pub fn new(owner_id: AccountId, metadata: NFTContractMetadata) -> Self { 100 | //create a variable of type Self with all the fields initialized. 101 | let this = Self { 102 | //Storage keys are simply the prefixes used for the collections. This helps avoid data collision 103 | tokens_per_owner: LookupMap::new(StorageKey::TokensPerOwner), 104 | tokens_by_id: LookupMap::new(StorageKey::TokensById), 105 | token_metadata_by_id: UnorderedMap::new(StorageKey::TokenMetadataById), 106 | //set the owner_id field equal to the passed in owner_id. 107 | owner_id, 108 | metadata: LazyOption::new( 109 | StorageKey::NFTContractMetadata, 110 | Some(&metadata), 111 | ), 112 | }; 113 | 114 | //return the Contract object 115 | this 116 | } 117 | } -------------------------------------------------------------------------------- /nft-contract-events/src/metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | pub type TokenId = String; 3 | //defines the payout type we'll be returning as a part of the royalty standards. 4 | #[derive(Serialize, Deserialize, NearSchema)] 5 | #[serde(crate = "near_sdk::serde")] 6 | pub struct Payout { 7 | pub payout: HashMap, 8 | } 9 | 10 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Clone, NearSchema)] 11 | #[borsh(crate = "near_sdk::borsh")] 12 | #[serde(crate = "near_sdk::serde")] 13 | pub struct NFTContractMetadata { 14 | pub spec: String, // required, essentially a version like "nft-1.0.0" 15 | pub name: String, // required, ex. "Mosaics" 16 | pub symbol: String, // required, ex. "MOSAIC" 17 | pub icon: Option, // Data URL 18 | pub base_uri: Option, // Centralized gateway known to have reliable access to decentralized storage assets referenced by `reference` or `media` URLs 19 | pub reference: Option, // URL to a JSON file with more info 20 | pub reference_hash: Option, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. 21 | } 22 | 23 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, NearSchema)] 24 | #[borsh(crate = "near_sdk::borsh")] 25 | #[serde(crate = "near_sdk::serde")] 26 | pub struct TokenMetadata { 27 | pub title: Option, // ex. "Arch Nemesis: Mail Carrier" or "Parcel #5055" 28 | pub description: Option, // free-form description 29 | pub media: Option, // URL to associated media, preferably to decentralized, content-addressed storage 30 | pub media_hash: Option, // Base64-encoded sha256 hash of content referenced by the `media` field. Required if `media` is included. 31 | pub copies: Option, // number of copies of this set of metadata in existence when token was minted. 32 | pub issued_at: Option, // When token was issued or minted, Unix epoch in milliseconds 33 | pub expires_at: Option, // When token expires, Unix epoch in milliseconds 34 | pub starts_at: Option, // When token starts being valid, Unix epoch in milliseconds 35 | pub updated_at: Option, // When token was last updated, Unix epoch in milliseconds 36 | pub extra: Option, // anything extra the NFT wants to store on-chain. Can be stringified JSON. 37 | pub reference: Option, // URL to an off-chain JSON file with more info. 38 | pub reference_hash: Option, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. 39 | } 40 | 41 | #[derive(BorshDeserialize, BorshSerialize)] 42 | #[borsh(crate = "near_sdk::borsh")] 43 | pub struct Token { 44 | //owner of the token 45 | pub owner_id: AccountId, 46 | } 47 | 48 | //The Json token is what will be returned from view calls. 49 | #[derive(Serialize, Deserialize, NearSchema)] 50 | #[serde(crate = "near_sdk::serde")] 51 | pub struct JsonToken { 52 | //token ID 53 | pub token_id: TokenId, 54 | //owner of the token 55 | pub owner_id: AccountId, 56 | //token metadata 57 | pub metadata: TokenMetadata, 58 | } 59 | 60 | pub trait NonFungibleTokenMetadata { 61 | //view call for returning the contract metadata 62 | fn nft_metadata(&self) -> NFTContractMetadata; 63 | } 64 | 65 | #[near_bindgen] 66 | impl NonFungibleTokenMetadata for Contract { 67 | fn nft_metadata(&self) -> NFTContractMetadata { 68 | self.metadata.get().unwrap() 69 | } 70 | } -------------------------------------------------------------------------------- /nft-contract-events/src/mint.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | #[payable] 6 | pub fn nft_mint( 7 | &mut self, 8 | token_id: TokenId, 9 | token_owner_id: AccountId, 10 | token_metadata: TokenMetadata, 11 | ) { 12 | //measure the initial storage being used on the contract 13 | let initial_storage_usage = env::storage_usage(); 14 | 15 | //specify the token struct that contains the owner ID 16 | let token = Token { 17 | //set the owner ID equal to the receiver ID passed into the function 18 | owner_id: token_owner_id, 19 | }; 20 | 21 | //insert the token ID and token struct and make sure that the token doesn't exist 22 | assert!( 23 | self.tokens_by_id.insert(&token_id, &token).is_none(), 24 | "Token already exists" 25 | ); 26 | 27 | //insert the token ID and metadata 28 | self.token_metadata_by_id.insert(&token_id, &token_metadata); 29 | 30 | //call the internal method for adding the token to the owner 31 | self.internal_add_token_to_owner(&token.owner_id, &token_id); 32 | 33 | // Construct the mint log as per the events standard. 34 | let nft_mint_log: EventLog = EventLog { 35 | // Standard name ("nep171"). 36 | standard: NFT_STANDARD_NAME.to_string(), 37 | // Version of the standard ("nft-1.0.0"). 38 | version: NFT_METADATA_SPEC.to_string(), 39 | // The data related with the event stored in a vector. 40 | event: EventLogVariant::NftMint(vec![NftMintLog { 41 | // Owner of the token. 42 | owner_id: token.owner_id.to_string(), 43 | // Vector of token IDs that were minted. 44 | token_ids: vec![token_id.to_string()], 45 | // An optional memo to include. 46 | memo: None, 47 | }]), 48 | }; 49 | 50 | // Log the serialized json. 51 | env::log_str(&nft_mint_log.to_string()); 52 | 53 | //calculate the required storage which was the used - initial 54 | let required_storage_in_bytes = env::storage_usage() - initial_storage_usage; 55 | 56 | //refund any excess storage if the user attached too much. Panic if they didn't attach enough to cover the required. 57 | refund_deposit(required_storage_in_bytes.into()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /nft-contract-events/src/royalty.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub trait NonFungibleTokenCore { 4 | //calculates the payout for a token given the passed in balance. This is a view method 5 | fn nft_payout(&self, token_id: TokenId, balance: NearToken, max_len_payout: u32) -> Payout; 6 | 7 | //transfers the token to the receiver ID and returns the payout object that should be payed given the passed in balance. 8 | fn nft_transfer_payout( 9 | &mut self, 10 | receiver_id: AccountId, 11 | token_id: TokenId, 12 | approval_id: u64, 13 | memo: Option, 14 | balance: NearToken, 15 | max_len_payout: u32, 16 | ) -> Payout; 17 | } 18 | 19 | #[near_bindgen] 20 | impl NonFungibleTokenCore for Contract { 21 | //calculates the payout for a token given the passed in balance. This is a view method 22 | fn nft_payout(&self, token_id: TokenId, balance: NearToken, max_len_payout: u32) -> Payout { 23 | /* 24 | FILL THIS IN 25 | */ 26 | todo!(); //remove once code is filled in. 27 | } 28 | 29 | //transfers the token to the receiver ID and returns the payout object that should be payed given the passed in balance. 30 | #[payable] 31 | fn nft_transfer_payout( 32 | &mut self, 33 | receiver_id: AccountId, 34 | token_id: TokenId, 35 | approval_id: u64, 36 | memo: Option, 37 | balance: NearToken, 38 | max_len_payout: u32, 39 | ) -> Payout { 40 | /* 41 | FILL THIS IN 42 | */ 43 | todo!(); //remove once code is filled in. 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /nft-contract-royalty/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nft_contract_skeleton" 3 | version = "0.1.0" 4 | authors = ["Near Inc "] 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | near-sdk = { version = "5.11.0", features = ["legacy"] } 12 | serde_json = "1.0.113" 13 | 14 | [profile.release] 15 | codegen-units = 1 16 | # Tell `rustc` to optimize for small code size. 17 | opt-level = "z" 18 | lto = true 19 | debug = false 20 | panic = "abort" 21 | # Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801 22 | overflow-checks = true 23 | -------------------------------------------------------------------------------- /nft-contract-royalty/README.md: -------------------------------------------------------------------------------- 1 | # TBD 2 | -------------------------------------------------------------------------------- /nft-contract-royalty/src/approval.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use near_sdk::ext_contract; 3 | 4 | pub trait NonFungibleTokenCore { 5 | //approve an account ID to transfer a token on your behalf 6 | fn nft_approve(&mut self, token_id: TokenId, account_id: AccountId, msg: Option); 7 | 8 | //check if the passed in account has access to approve the token ID 9 | fn nft_is_approved( 10 | &self, 11 | token_id: TokenId, 12 | approved_account_id: AccountId, 13 | approval_id: Option, 14 | ) -> bool; 15 | 16 | //revoke a specific account from transferring the token on your behalf 17 | fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId); 18 | 19 | //revoke all accounts from transferring the token on your behalf 20 | fn nft_revoke_all(&mut self, token_id: TokenId); 21 | } 22 | 23 | #[ext_contract(ext_non_fungible_approval_receiver)] 24 | trait NonFungibleTokenApprovalsReceiver { 25 | //cross contract call to an external contract that is initiated during nft_approve 26 | fn nft_on_approve( 27 | &mut self, 28 | token_id: TokenId, 29 | owner_id: AccountId, 30 | approval_id: u64, 31 | msg: String, 32 | ); 33 | } 34 | 35 | #[near_bindgen] 36 | impl NonFungibleTokenCore for Contract { 37 | //allow a specific account ID to approve a token on your behalf 38 | #[payable] 39 | fn nft_approve(&mut self, token_id: TokenId, account_id: AccountId, msg: Option) { 40 | /* 41 | assert at least one yocto for security reasons - this will cause a redirect to the NEAR wallet. 42 | The user needs to attach enough to pay for storage on the contract 43 | */ 44 | assert_at_least_one_yocto(); 45 | 46 | //get the token object from the token ID 47 | let mut token = self.tokens_by_id.get(&token_id).expect("No token"); 48 | 49 | //make sure that the person calling the function is the owner of the token 50 | assert_eq!( 51 | &env::predecessor_account_id(), 52 | &token.owner_id, 53 | "Predecessor must be the token owner." 54 | ); 55 | 56 | //get the next approval ID if we need a new approval 57 | let approval_id: u64 = token.next_approval_id; 58 | 59 | //check if the account has been approved already for this token 60 | let is_new_approval = token 61 | .approved_account_ids 62 | //insert returns none if the key was not present. 63 | .insert(account_id.clone(), approval_id) 64 | //if the key was not present, .is_none() will return true so it is a new approval. 65 | .is_none(); 66 | 67 | //if it was a new approval, we need to calculate how much storage is being used to add the account. 68 | let storage_used = if is_new_approval { 69 | bytes_for_approved_account_id(&account_id) 70 | //if it was not a new approval, we used no storage. 71 | } else { 72 | 0 73 | }; 74 | 75 | //increment the token's next approval ID by 1 76 | token.next_approval_id += 1; 77 | //insert the token back into the tokens_by_id collection 78 | self.tokens_by_id.insert(&token_id, &token); 79 | 80 | //refund any excess storage attached by the user. If the user didn't attach enough, panic. 81 | refund_deposit(storage_used); 82 | 83 | //if some message was passed into the function, we initiate a cross contract call on the 84 | //account we're giving access to. 85 | if let Some(msg) = msg { 86 | // Defaulting GAS weight to 1, no attached deposit, and no static GAS to attach. 87 | ext_non_fungible_approval_receiver::ext(account_id) 88 | .nft_on_approve(token_id, token.owner_id, approval_id, msg) 89 | .as_return(); 90 | } 91 | } 92 | 93 | //check if the passed in account has access to approve the token ID 94 | fn nft_is_approved( 95 | &self, 96 | token_id: TokenId, 97 | approved_account_id: AccountId, 98 | approval_id: Option, 99 | ) -> bool { 100 | //get the token object from the token_id 101 | let token = self.tokens_by_id.get(&token_id).expect("No token"); 102 | 103 | //get the approval number for the passed in account ID 104 | let approval = token.approved_account_ids.get(&approved_account_id); 105 | 106 | //if there was some approval ID found for the account ID 107 | if let Some(approval) = approval { 108 | //if a specific approval_id was passed into the function 109 | if let Some(approval_id) = approval_id { 110 | //return if the approval ID passed in matches the actual approval ID for the account 111 | approval_id == *approval 112 | //if there was no approval_id passed into the function, we simply return true 113 | } else { 114 | true 115 | } 116 | //if there was no approval ID found for the account ID, we simply return false 117 | } else { 118 | false 119 | } 120 | } 121 | 122 | //revoke a specific account from transferring the token on your behalf 123 | #[payable] 124 | fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId) { 125 | //assert that the user attached exactly 1 yoctoNEAR for security reasons 126 | assert_one_yocto(); 127 | //get the token object using the passed in token_id 128 | let mut token = self.tokens_by_id.get(&token_id).expect("No token"); 129 | 130 | //get the caller of the function and assert that they are the owner of the token 131 | let predecessor_account_id = env::predecessor_account_id(); 132 | assert_eq!(&predecessor_account_id, &token.owner_id); 133 | 134 | //if the account ID was in the token's approval, we remove it and the if statement logic executes 135 | if token.approved_account_ids.remove(&account_id).is_some() { 136 | //refund the funds released by removing the approved_account_id to the caller of the function 137 | refund_approved_account_ids_iter(predecessor_account_id, [account_id].iter()); 138 | 139 | //insert the token back into the tokens_by_id collection with the account_id removed from the approval list 140 | self.tokens_by_id.insert(&token_id, &token); 141 | } 142 | } 143 | 144 | //revoke all accounts from transferring the token on your behalf 145 | #[payable] 146 | fn nft_revoke_all(&mut self, token_id: TokenId) { 147 | //assert that the caller attached exactly 1 yoctoNEAR for security 148 | assert_one_yocto(); 149 | 150 | //get the token object from the passed in token ID 151 | let mut token = self.tokens_by_id.get(&token_id).expect("No token"); 152 | //get the caller and make sure they are the owner of the tokens 153 | let predecessor_account_id = env::predecessor_account_id(); 154 | assert_eq!(&predecessor_account_id, &token.owner_id); 155 | 156 | //only revoke if the approved account IDs for the token is not empty 157 | if !token.approved_account_ids.is_empty() { 158 | //refund the approved account IDs to the caller of the function 159 | refund_approved_account_ids(predecessor_account_id, &token.approved_account_ids); 160 | //clear the approved account IDs 161 | token.approved_account_ids.clear(); 162 | //insert the token back into the tokens_by_id collection with the approved account IDs cleared 163 | self.tokens_by_id.insert(&token_id, &token); 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /nft-contract-royalty/src/enumeration.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | //Query for the total supply of NFTs on the contract 6 | pub fn nft_total_supply(&self) -> U128 { 7 | //return the length of the token metadata by ID 8 | U128(self.token_metadata_by_id.len().into()) 9 | } 10 | 11 | //Query for nft tokens on the contract regardless of the owner using pagination 12 | pub fn nft_tokens(&self, from_index: Option, limit: Option) -> Vec { 13 | //where to start pagination - if we have a from_index, we'll use that - otherwise start from 0 index 14 | let start = u128::from(from_index.unwrap_or(U128(0))); 15 | 16 | //iterate through each token using an iterator 17 | self.token_metadata_by_id 18 | .keys() 19 | //skip to the index we specified in the start variable 20 | .skip(start as usize) 21 | //take the first "limit" elements in the vector. If we didn't specify a limit, use 50 22 | .take(limit.unwrap_or(50) as usize) 23 | //we'll map the token IDs which are strings into Json Tokens 24 | .map(|token_id| self.nft_token(token_id.clone()).unwrap()) 25 | //since we turned the keys into an iterator, we need to turn it back into a vector to return 26 | .collect() 27 | } 28 | 29 | //get the total supply of NFTs for a given owner 30 | pub fn nft_supply_for_owner(&self, account_id: AccountId) -> U128 { 31 | //get the set of tokens for the passed in owner 32 | let tokens_for_owner_set = self.tokens_per_owner.get(&account_id); 33 | 34 | //if there is some set of tokens, we'll return the length 35 | if let Some(tokens_for_owner_set) = tokens_for_owner_set { 36 | U128(tokens_for_owner_set.len().into()) 37 | } else { 38 | //if there isn't a set of tokens for the passed in account ID, we'll return 0 39 | U128(0) 40 | } 41 | } 42 | 43 | //Query for all the tokens for an owner 44 | pub fn nft_tokens_for_owner( 45 | &self, 46 | account_id: AccountId, 47 | from_index: Option, 48 | limit: Option, 49 | ) -> Vec { 50 | //get the set of tokens for the passed in owner 51 | let tokens_for_owner_set = self.tokens_per_owner.get(&account_id); 52 | //if there is some set of tokens, we'll set the tokens variable equal to that set 53 | let tokens = if let Some(tokens_for_owner_set) = tokens_for_owner_set { 54 | tokens_for_owner_set 55 | } else { 56 | //if there is no set of tokens, we'll simply return an empty vector. 57 | return vec![]; 58 | }; 59 | 60 | //where to start pagination - if we have a from_index, we'll use that - otherwise start from 0 index 61 | let start = u128::from(from_index.unwrap_or(U128(0))); 62 | 63 | //iterate through the keys vector 64 | tokens 65 | .iter() 66 | //skip to the index we specified in the start variable 67 | .skip(start as usize) 68 | //take the first "limit" elements in the vector. If we didn't specify a limit, use 50 69 | .take(limit.unwrap_or(50) as usize) 70 | //we'll map the token IDs which are strings into Json Tokens 71 | .map(|token_id| self.nft_token(token_id.clone()).unwrap()) 72 | //since we turned the keys into an iterator, we need to turn it back into a vector to return 73 | .collect() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /nft-contract-royalty/src/events.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use near_sdk::serde::{Deserialize, Serialize}; 4 | 5 | /// Enum that represents the data type of the EventLog. 6 | /// The enum can either be an NftMint or an NftTransfer. 7 | #[derive(Serialize, Deserialize, Debug)] 8 | #[serde(tag = "event", content = "data")] 9 | #[serde(rename_all = "snake_case")] 10 | #[serde(crate = "near_sdk::serde")] 11 | #[non_exhaustive] 12 | pub enum EventLogVariant { 13 | NftMint(Vec), 14 | NftTransfer(Vec), 15 | } 16 | 17 | /// Interface to capture data about an event 18 | /// 19 | /// Arguments: 20 | /// * `standard`: name of standard e.g. nep171 21 | /// * `version`: e.g. 1.0.0 22 | /// * `event`: associate event data 23 | #[derive(Serialize, Deserialize, Debug)] 24 | #[serde(crate = "near_sdk::serde")] 25 | pub struct EventLog { 26 | pub standard: String, 27 | pub version: String, 28 | 29 | // `flatten` to not have "event": {} in the JSON, just have the contents of {}. 30 | #[serde(flatten)] 31 | pub event: EventLogVariant, 32 | } 33 | 34 | impl fmt::Display for EventLog { 35 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 36 | f.write_fmt(format_args!( 37 | "EVENT_JSON:{}", 38 | &serde_json::to_string(self).map_err(|_| fmt::Error)? 39 | )) 40 | } 41 | } 42 | 43 | /// An event log to capture token minting 44 | /// 45 | /// Arguments 46 | /// * `owner_id`: "account.near" 47 | /// * `token_ids`: ["1", "abc"] 48 | /// * `memo`: optional message 49 | #[derive(Serialize, Deserialize, Debug)] 50 | #[serde(crate = "near_sdk::serde")] 51 | pub struct NftMintLog { 52 | pub owner_id: String, 53 | pub token_ids: Vec, 54 | 55 | #[serde(skip_serializing_if = "Option::is_none")] 56 | pub memo: Option, 57 | } 58 | 59 | /// An event log to capture token transfer 60 | /// 61 | /// Arguments 62 | /// * `authorized_id`: approved account to transfer 63 | /// * `old_owner_id`: "owner.near" 64 | /// * `new_owner_id`: "receiver.near" 65 | /// * `token_ids`: ["1", "12345abc"] 66 | /// * `memo`: optional message 67 | #[derive(Serialize, Deserialize, Debug)] 68 | #[serde(crate = "near_sdk::serde")] 69 | pub struct NftTransferLog { 70 | #[serde(skip_serializing_if = "Option::is_none")] 71 | pub authorized_id: Option, 72 | 73 | pub old_owner_id: String, 74 | pub new_owner_id: String, 75 | pub token_ids: Vec, 76 | 77 | #[serde(skip_serializing_if = "Option::is_none")] 78 | pub memo: Option, 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::*; 84 | 85 | #[test] 86 | fn nep_format_vector() { 87 | let expected = r#"EVENT_JSON:{"standard":"nep171","version":"1.0.0","event":"nft_mint","data":[{"owner_id":"foundation.near","token_ids":["aurora","proximitylabs"]},{"owner_id":"user1.near","token_ids":["meme"]}]}"#; 88 | let log = EventLog { 89 | standard: "nep171".to_string(), 90 | version: "1.0.0".to_string(), 91 | event: EventLogVariant::NftMint(vec![ 92 | NftMintLog { 93 | owner_id: "foundation.near".to_owned(), 94 | token_ids: vec!["aurora".to_string(), "proximitylabs".to_string()], 95 | memo: None, 96 | }, 97 | NftMintLog { 98 | owner_id: "user1.near".to_owned(), 99 | token_ids: vec!["meme".to_string()], 100 | memo: None, 101 | }, 102 | ]), 103 | }; 104 | assert_eq!(expected, log.to_string()); 105 | } 106 | 107 | #[test] 108 | fn nep_format_mint() { 109 | let expected = r#"EVENT_JSON:{"standard":"nep171","version":"1.0.0","event":"nft_mint","data":[{"owner_id":"foundation.near","token_ids":["aurora","proximitylabs"]}]}"#; 110 | let log = EventLog { 111 | standard: "nep171".to_string(), 112 | version: "1.0.0".to_string(), 113 | event: EventLogVariant::NftMint(vec![NftMintLog { 114 | owner_id: "foundation.near".to_owned(), 115 | token_ids: vec!["aurora".to_string(), "proximitylabs".to_string()], 116 | memo: None, 117 | }]), 118 | }; 119 | assert_eq!(expected, log.to_string()); 120 | } 121 | 122 | #[test] 123 | fn nep_format_transfer_all_fields() { 124 | let expected = r#"EVENT_JSON:{"standard":"nep171","version":"1.0.0","event":"nft_transfer","data":[{"authorized_id":"market.near","old_owner_id":"user1.near","new_owner_id":"user2.near","token_ids":["token"],"memo":"Go Team!"}]}"#; 125 | let log = EventLog { 126 | standard: "nep171".to_string(), 127 | version: "1.0.0".to_string(), 128 | event: EventLogVariant::NftTransfer(vec![NftTransferLog { 129 | authorized_id: Some("market.near".to_string()), 130 | old_owner_id: "user1.near".to_string(), 131 | new_owner_id: "user2.near".to_string(), 132 | token_ids: vec!["token".to_string()], 133 | memo: Some("Go Team!".to_owned()), 134 | }]), 135 | }; 136 | assert_eq!(expected, log.to_string()); 137 | } 138 | } -------------------------------------------------------------------------------- /nft-contract-royalty/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use near_sdk::borsh::{BorshSerialize, BorshDeserialize}; 3 | use near_sdk::collections::{LazyOption, LookupMap, UnorderedMap, UnorderedSet}; 4 | use near_sdk::json_types::{Base64VecU8, U64, U128}; 5 | use near_sdk::serde::{Deserialize, Serialize}; 6 | use near_sdk::{ 7 | near_bindgen, env, AccountId, NearToken, CryptoHash, PanicOnDefault, Promise, PromiseOrValue, BorshStorageKey, NearSchema 8 | }; 9 | 10 | use crate::internal::*; 11 | pub use crate::metadata::*; 12 | pub use crate::mint::*; 13 | pub use crate::nft_core::*; 14 | pub use crate::approval::*; 15 | pub use crate::royalty::*; 16 | pub use crate::events::*; 17 | 18 | mod internal; 19 | mod enumeration; 20 | mod metadata; 21 | mod mint; 22 | mod nft_core; 23 | mod approval; 24 | mod royalty; 25 | mod events; 26 | 27 | /// This spec can be treated like a version of the standard. 28 | pub const NFT_METADATA_SPEC: &str = "1.0.0"; 29 | /// This is the name of the NFT standard we're using 30 | pub const NFT_STANDARD_NAME: &str = "nep171"; 31 | 32 | //Basic NEAR amounts as constants 33 | const ONE_YOCTONEAR: NearToken = NearToken::from_yoctonear(1); 34 | 35 | #[near_bindgen] 36 | #[derive(BorshSerialize, BorshDeserialize, PanicOnDefault)] 37 | #[borsh(crate = "near_sdk::borsh")] 38 | pub struct Contract { 39 | //contract owner 40 | pub owner_id: AccountId, 41 | 42 | //keeps track of all the token IDs for a given account 43 | pub tokens_per_owner: LookupMap>, 44 | 45 | //keeps track of the token struct for a given token ID 46 | pub tokens_by_id: LookupMap, 47 | 48 | //keeps track of the token metadata for a given token ID 49 | pub token_metadata_by_id: UnorderedMap, 50 | 51 | //keeps track of the metadata for the contract 52 | pub metadata: LazyOption, 53 | } 54 | 55 | /// Helper structure for keys of the persistent collections. 56 | #[derive(BorshSerialize, BorshStorageKey)] 57 | #[borsh(crate = "near_sdk::borsh")] 58 | pub enum StorageKey { 59 | TokensPerOwner, 60 | TokenPerOwnerInner { account_id_hash: CryptoHash }, 61 | TokensById, 62 | TokenMetadataById, 63 | NFTContractMetadata, 64 | TokensPerType, 65 | TokensPerTypeInner { token_type_hash: CryptoHash }, 66 | TokenTypesLocked, 67 | } 68 | 69 | #[near_bindgen] 70 | impl Contract { 71 | /* 72 | initialization function (can only be called once). 73 | this initializes the contract with default metadata so the 74 | user doesn't have to manually type metadata. 75 | */ 76 | #[init] 77 | pub fn new_default_meta(owner_id: AccountId) -> Self { 78 | //calls the other function "new: with some default metadata and the owner_id passed in 79 | Self::new( 80 | owner_id, 81 | NFTContractMetadata { 82 | spec: "nft-1.0.0".to_string(), 83 | name: "NFT Tutorial Contract".to_string(), 84 | symbol: "GOTEAM".to_string(), 85 | icon: None, 86 | base_uri: None, 87 | reference: None, 88 | reference_hash: None, 89 | }, 90 | ) 91 | } 92 | 93 | /* 94 | initialization function (can only be called once). 95 | this initializes the contract with metadata that was passed in and 96 | the owner_id. 97 | */ 98 | #[init] 99 | pub fn new(owner_id: AccountId, metadata: NFTContractMetadata) -> Self { 100 | //create a variable of type Self with all the fields initialized. 101 | let this = Self { 102 | //Storage keys are simply the prefixes used for the collections. This helps avoid data collision 103 | tokens_per_owner: LookupMap::new(StorageKey::TokensPerOwner), 104 | tokens_by_id: LookupMap::new(StorageKey::TokensById), 105 | token_metadata_by_id: UnorderedMap::new(StorageKey::TokenMetadataById), 106 | //set the owner_id field equal to the passed in owner_id. 107 | owner_id, 108 | metadata: LazyOption::new( 109 | StorageKey::NFTContractMetadata, 110 | Some(&metadata), 111 | ), 112 | }; 113 | 114 | //return the Contract object 115 | this 116 | } 117 | } -------------------------------------------------------------------------------- /nft-contract-royalty/src/metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | pub type TokenId = String; 3 | //defines the payout type we'll be returning as a part of the royalty standards. 4 | #[derive(Serialize, Deserialize, NearSchema)] 5 | #[serde(crate = "near_sdk::serde")] 6 | pub struct Payout { 7 | pub payout: HashMap, 8 | } 9 | 10 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Clone, NearSchema)] 11 | #[borsh(crate = "near_sdk::borsh")] 12 | #[serde(crate = "near_sdk::serde")] 13 | pub struct NFTContractMetadata { 14 | pub spec: String, // required, essentially a version like "nft-1.0.0" 15 | pub name: String, // required, ex. "Mosaics" 16 | pub symbol: String, // required, ex. "MOSAIC" 17 | pub icon: Option, // Data URL 18 | pub base_uri: Option, // Centralized gateway known to have reliable access to decentralized storage assets referenced by `reference` or `media` URLs 19 | pub reference: Option, // URL to a JSON file with more info 20 | pub reference_hash: Option, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. 21 | } 22 | 23 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, NearSchema)] 24 | #[borsh(crate = "near_sdk::borsh")] 25 | #[serde(crate = "near_sdk::serde")] 26 | pub struct TokenMetadata { 27 | pub title: Option, // ex. "Arch Nemesis: Mail Carrier" or "Parcel #5055" 28 | pub description: Option, // free-form description 29 | pub media: Option, // URL to associated media, preferably to decentralized, content-addressed storage 30 | pub media_hash: Option, // Base64-encoded sha256 hash of content referenced by the `media` field. Required if `media` is included. 31 | pub copies: Option, // number of copies of this set of metadata in existence when token was minted. 32 | pub issued_at: Option, // When token was issued or minted, Unix epoch in milliseconds 33 | pub expires_at: Option, // When token expires, Unix epoch in milliseconds 34 | pub starts_at: Option, // When token starts being valid, Unix epoch in milliseconds 35 | pub updated_at: Option, // When token was last updated, Unix epoch in milliseconds 36 | pub extra: Option, // anything extra the NFT wants to store on-chain. Can be stringified JSON. 37 | pub reference: Option, // URL to an off-chain JSON file with more info. 38 | pub reference_hash: Option, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. 39 | } 40 | 41 | #[derive(BorshDeserialize, BorshSerialize)] 42 | #[borsh(crate = "near_sdk::borsh")] 43 | pub struct Token { 44 | //owner of the token 45 | pub owner_id: AccountId, 46 | //list of approved account IDs that have access to transfer the token. This maps an account ID to an approval ID 47 | pub approved_account_ids: HashMap, 48 | //the next approval ID to give out. 49 | pub next_approval_id: u64, 50 | //keep track of the royalty percentages for the token in a hash map 51 | pub royalty: HashMap, 52 | } 53 | 54 | //The Json token is what will be returned from view calls. 55 | #[derive(Serialize, Deserialize, NearSchema)] 56 | #[serde(crate = "near_sdk::serde")] 57 | pub struct JsonToken { 58 | //token ID 59 | pub token_id: TokenId, 60 | //owner of the token 61 | pub owner_id: AccountId, 62 | //token metadata 63 | pub metadata: TokenMetadata, 64 | //list of approved account IDs that have access to transfer the token. This maps an account ID to an approval ID 65 | pub approved_account_ids: HashMap, 66 | //keep track of the royalty percentages for the token in a hash map 67 | pub royalty: HashMap, 68 | } 69 | 70 | pub trait NonFungibleTokenMetadata { 71 | //view call for returning the contract metadata 72 | fn nft_metadata(&self) -> NFTContractMetadata; 73 | } 74 | 75 | #[near_bindgen] 76 | impl NonFungibleTokenMetadata for Contract { 77 | fn nft_metadata(&self) -> NFTContractMetadata { 78 | self.metadata.get().unwrap() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /nft-contract-royalty/src/mint.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | #[payable] 6 | pub fn nft_mint( 7 | &mut self, 8 | token_id: TokenId, 9 | token_owner_id: AccountId, 10 | token_metadata: TokenMetadata, 11 | //we add an optional parameter for perpetual royalties 12 | perpetual_royalties: Option>, 13 | ) { 14 | //measure the initial storage being used on the contract 15 | let initial_storage_usage = env::storage_usage(); 16 | 17 | // create a royalty map to store in the token 18 | let mut royalty = HashMap::new(); 19 | 20 | // if perpetual royalties were passed into the function: 21 | if let Some(perpetual_royalties) = perpetual_royalties { 22 | //make sure that the length of the perpetual royalties is below 7 since we won't have enough GAS to pay out that many people 23 | assert!( 24 | perpetual_royalties.len() < 7, 25 | "Cannot add more than 6 perpetual royalty amounts" 26 | ); 27 | 28 | //iterate through the perpetual royalties and insert the account and amount in the royalty map 29 | for (account, amount) in perpetual_royalties { 30 | royalty.insert(account, amount); 31 | } 32 | } 33 | 34 | //specify the token struct that contains the owner ID 35 | let token = Token { 36 | //set the owner ID equal to the receiver ID passed into the function 37 | owner_id: token_owner_id, 38 | //we set the approved account IDs to the default value (an empty map) 39 | approved_account_ids: Default::default(), 40 | //the next approval ID is set to 0 41 | next_approval_id: 0, 42 | //the map of perpetual royalties for the token (The owner will get 100% - total perpetual royalties) 43 | royalty, 44 | }; 45 | 46 | //insert the token ID and token struct and make sure that the token doesn't exist 47 | assert!( 48 | self.tokens_by_id.insert(&token_id, &token).is_none(), 49 | "Token already exists" 50 | ); 51 | 52 | //insert the token ID and metadata 53 | self.token_metadata_by_id.insert(&token_id, &token_metadata); 54 | 55 | //call the internal method for adding the token to the owner 56 | self.internal_add_token_to_owner(&token.owner_id, &token_id); 57 | 58 | // Construct the mint log as per the events standard. 59 | let nft_mint_log: EventLog = EventLog { 60 | // Standard name ("nep171"). 61 | standard: NFT_STANDARD_NAME.to_string(), 62 | // Version of the standard ("nft-1.0.0"). 63 | version: NFT_METADATA_SPEC.to_string(), 64 | // The data related with the event stored in a vector. 65 | event: EventLogVariant::NftMint(vec![NftMintLog { 66 | // Owner of the token. 67 | owner_id: token.owner_id.to_string(), 68 | // Vector of token IDs that were minted. 69 | token_ids: vec![token_id.to_string()], 70 | // An optional memo to include. 71 | memo: None, 72 | }]), 73 | }; 74 | 75 | // Log the serialized json. 76 | env::log_str(&nft_mint_log.to_string()); 77 | 78 | //calculate the required storage which was the used - initial 79 | let required_storage_in_bytes = env::storage_usage() - initial_storage_usage; 80 | 81 | //refund any excess storage if the user attached too much. Panic if they didn't attach enough to cover the required. 82 | refund_deposit(required_storage_in_bytes.into()); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /nft-contract-royalty/src/royalty.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub trait NonFungibleTokenCore { 4 | //calculates the payout for a token given the passed in balance. This is a view method 5 | fn nft_payout(&self, token_id: TokenId, balance: NearToken, max_len_payout: u32) -> Payout; 6 | 7 | //transfers the token to the receiver ID and returns the payout object that should be payed given the passed in balance. 8 | fn nft_transfer_payout( 9 | &mut self, 10 | receiver_id: AccountId, 11 | token_id: TokenId, 12 | approval_id: u64, 13 | memo: Option, 14 | balance: NearToken, 15 | max_len_payout: u32, 16 | ) -> Payout; 17 | } 18 | 19 | #[near_bindgen] 20 | impl NonFungibleTokenCore for Contract { 21 | //calculates the payout for a token given the passed in balance. This is a view method 22 | fn nft_payout(&self, token_id: TokenId, balance: NearToken, max_len_payout: u32) -> Payout { 23 | //get the token object 24 | let token = self.tokens_by_id.get(&token_id).expect("No token"); 25 | 26 | //get the owner of the token 27 | let owner_id = token.owner_id; 28 | //keep track of the total perpetual royalties 29 | let mut total_perpetual = 0; 30 | //keep track of the payout object to send back 31 | let mut payout_object = Payout { 32 | payout: HashMap::new(), 33 | }; 34 | //get the royalty object from token 35 | let royalty = token.royalty; 36 | 37 | //make sure we're not paying out to too many people (GAS limits this) 38 | assert!( 39 | royalty.len() as u32 <= max_len_payout, 40 | "Market cannot payout to that many receivers" 41 | ); 42 | 43 | //go through each key and value in the royalty object 44 | for (k, v) in royalty.iter() { 45 | //get the key 46 | let key = k.clone(); 47 | 48 | //only insert into the payout if the key isn't the token owner (we add their payout at the end) 49 | if key != owner_id { 50 | payout_object 51 | .payout 52 | .insert(key, royalty_to_payout(*v as u128, balance)); 53 | total_perpetual += *v; 54 | } 55 | } 56 | 57 | // payout to previous owner who gets 100% - total perpetual royalties 58 | payout_object.payout.insert( 59 | owner_id, 60 | royalty_to_payout((10000 - total_perpetual).into(), balance), 61 | ); 62 | 63 | //return the payout object 64 | payout_object 65 | } 66 | 67 | //transfers the token to the receiver ID and returns the payout object that should be payed given the passed in balance. 68 | #[payable] 69 | fn nft_transfer_payout( 70 | &mut self, 71 | receiver_id: AccountId, 72 | token_id: TokenId, 73 | approval_id: u64, 74 | memo: Option, 75 | balance: NearToken, 76 | max_len_payout: u32, 77 | ) -> Payout { 78 | //assert that the user attached 1 yocto NEAR for security reasons 79 | assert_one_yocto(); 80 | //get the sender ID 81 | let sender_id = env::predecessor_account_id(); 82 | //transfer the token to the passed in receiver and get the previous token object back 83 | let previous_token = 84 | self.internal_transfer(&sender_id, &receiver_id, &token_id, Some(approval_id), memo); 85 | 86 | //refund the previous token owner for the storage used up by the previous approved account IDs 87 | refund_approved_account_ids( 88 | previous_token.owner_id.clone(), 89 | &previous_token.approved_account_ids, 90 | ); 91 | 92 | //get the owner of the token 93 | let owner_id = previous_token.owner_id; 94 | //keep track of the total perpetual royalties 95 | let mut total_perpetual = 0; 96 | //keep track of the payout object to send back 97 | let mut payout_object = Payout { 98 | payout: HashMap::new(), 99 | }; 100 | //get the royalty object from token 101 | let royalty = previous_token.royalty; 102 | 103 | //make sure we're not paying out to too many people (GAS limits this) 104 | assert!( 105 | royalty.len() as u32 <= max_len_payout, 106 | "Market cannot payout to that many receivers" 107 | ); 108 | 109 | //go through each key and value in the royalty object 110 | for (k, v) in royalty.iter() { 111 | //get the key 112 | let key = k.clone(); 113 | 114 | //only insert into the payout if the key isn't the token owner (we add their payout at the end) 115 | if key != owner_id { 116 | payout_object 117 | .payout 118 | .insert(key, royalty_to_payout(*v as u128, balance)); 119 | total_perpetual += *v; 120 | } 121 | } 122 | 123 | // payout to previous owner who gets 100% - total perpetual royalties 124 | payout_object.payout.insert( 125 | owner_id, 126 | royalty_to_payout((10000 - total_perpetual).into(), balance), 127 | ); 128 | 129 | //return the payout object 130 | payout_object 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /nft-contract-skeleton/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nft_contract_skeleton" 3 | version = "0.1.0" 4 | authors = ["Near Inc "] 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | near-sdk = { version = "5.11.0", features = ["legacy"] } 12 | 13 | [profile.release] 14 | codegen-units = 1 15 | # Tell `rustc` to optimize for small code size. 16 | opt-level = "z" 17 | lto = true 18 | debug = false 19 | panic = "abort" 20 | # Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801 21 | overflow-checks = true 22 | -------------------------------------------------------------------------------- /nft-contract-skeleton/README.md: -------------------------------------------------------------------------------- 1 | # TBD 2 | -------------------------------------------------------------------------------- /nft-contract-skeleton/src/approval.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use near_sdk::ext_contract; 3 | 4 | pub trait NonFungibleTokenCore { 5 | //approve an account ID to transfer a token on your behalf 6 | fn nft_approve(&mut self, token_id: TokenId, account_id: AccountId, msg: Option); 7 | 8 | //check if the passed in account has access to approve the token ID 9 | fn nft_is_approved( 10 | &self, 11 | token_id: TokenId, 12 | approved_account_id: AccountId, 13 | approval_id: Option, 14 | ) -> bool; 15 | 16 | //revoke a specific account from transferring the token on your behalf 17 | fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId); 18 | 19 | //revoke all accounts from transferring the token on your behalf 20 | fn nft_revoke_all(&mut self, token_id: TokenId); 21 | } 22 | 23 | #[ext_contract(ext_non_fungible_approval_receiver)] 24 | trait NonFungibleTokenApprovalsReceiver { 25 | //cross contract call to an external contract that is initiated during nft_approve 26 | fn nft_on_approve( 27 | &mut self, 28 | token_id: TokenId, 29 | owner_id: AccountId, 30 | approval_id: u64, 31 | msg: String, 32 | ); 33 | } 34 | 35 | #[near_bindgen] 36 | impl NonFungibleTokenCore for Contract { 37 | //allow a specific account ID to approve a token on your behalf 38 | #[payable] 39 | fn nft_approve(&mut self, token_id: TokenId, account_id: AccountId, msg: Option) { 40 | /* 41 | FILL THIS IN 42 | */ 43 | } 44 | 45 | //check if the passed in account has access to approve the token ID 46 | fn nft_is_approved( 47 | &self, 48 | token_id: TokenId, 49 | approved_account_id: AccountId, 50 | approval_id: Option, 51 | ) -> bool { 52 | /* 53 | FILL THIS IN 54 | */ 55 | todo!(); //remove once code is filled in. 56 | } 57 | 58 | //revoke a specific account from transferring the token on your behalf 59 | #[payable] 60 | fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId) { 61 | /* 62 | FILL THIS IN 63 | */ 64 | } 65 | 66 | //revoke all accounts from transferring the token on your behalf 67 | #[payable] 68 | fn nft_revoke_all(&mut self, token_id: TokenId) { 69 | /* 70 | FILL THIS IN 71 | */ 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /nft-contract-skeleton/src/enumeration.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | //Query for the total supply of NFTs on the contract 6 | pub fn nft_total_supply(&self) -> U128 { 7 | /* 8 | FILL THIS IN 9 | */ 10 | todo!(); //remove once code is filled in. 11 | } 12 | 13 | //Query for nft tokens on the contract regardless of the owner using pagination 14 | pub fn nft_tokens(&self, from_index: Option, limit: Option) -> Vec { 15 | /* 16 | FILL THIS IN 17 | */ 18 | todo!(); //remove once code is filled in. 19 | } 20 | 21 | //get the total supply of NFTs for a given owner 22 | pub fn nft_supply_for_owner(&self, account_id: AccountId) -> U128 { 23 | /* 24 | FILL THIS IN 25 | */ 26 | todo!(); //remove once code is filled in. 27 | } 28 | 29 | //Query for all the tokens for an owner 30 | pub fn nft_tokens_for_owner( 31 | &self, 32 | account_id: AccountId, 33 | from_index: Option, 34 | limit: Option, 35 | ) -> Vec { 36 | /* 37 | FILL THIS IN 38 | */ 39 | todo!(); //remove once code is filled in. 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /nft-contract-skeleton/src/events.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use near_sdk::serde::{Deserialize, Serialize}; 4 | 5 | /// Enum that represents the data type of the EventLog. 6 | /// The enum can either be an NftMint or an NftTransfer. 7 | #[derive(Serialize, Deserialize, Debug)] 8 | #[serde(tag = "event", content = "data")] 9 | #[serde(rename_all = "snake_case")] 10 | #[serde(crate = "near_sdk::serde")] 11 | #[non_exhaustive] 12 | pub enum EventLogVariant { 13 | NftMint(Vec), 14 | NftTransfer(Vec), 15 | } 16 | 17 | /// Interface to capture data about an event 18 | /// 19 | /// Arguments: 20 | /// * `standard`: name of standard e.g. nep171 21 | /// * `version`: e.g. 1.0.0 22 | /// * `event`: associate event data 23 | #[derive(Serialize, Deserialize, Debug)] 24 | #[serde(crate = "near_sdk::serde")] 25 | pub struct EventLog { 26 | pub standard: String, 27 | pub version: String, 28 | 29 | // `flatten` to not have "event": {} in the JSON, just have the contents of {}. 30 | #[serde(flatten)] 31 | pub event: EventLogVariant, 32 | } 33 | 34 | impl fmt::Display for EventLog { 35 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 36 | f.write_fmt(format_args!( 37 | "EVENT_JSON:{}", 38 | &serde_json::to_string(self).map_err(|_| fmt::Error)? 39 | )) 40 | } 41 | } 42 | 43 | /// An event log to capture token minting 44 | /// 45 | /// Arguments 46 | /// * `owner_id`: "account.near" 47 | /// * `token_ids`: ["1", "abc"] 48 | /// * `memo`: optional message 49 | #[derive(Serialize, Deserialize, Debug)] 50 | #[serde(crate = "near_sdk::serde")] 51 | pub struct NftMintLog { 52 | pub owner_id: String, 53 | pub token_ids: Vec, 54 | 55 | #[serde(skip_serializing_if = "Option::is_none")] 56 | pub memo: Option, 57 | } 58 | 59 | /// An event log to capture token transfer 60 | /// 61 | /// Arguments 62 | /// * `authorized_id`: approved account to transfer 63 | /// * `old_owner_id`: "owner.near" 64 | /// * `new_owner_id`: "receiver.near" 65 | /// * `token_ids`: ["1", "12345abc"] 66 | /// * `memo`: optional message 67 | #[derive(Serialize, Deserialize, Debug)] 68 | #[serde(crate = "near_sdk::serde")] 69 | pub struct NftTransferLog { 70 | #[serde(skip_serializing_if = "Option::is_none")] 71 | pub authorized_id: Option, 72 | 73 | pub old_owner_id: String, 74 | pub new_owner_id: String, 75 | pub token_ids: Vec, 76 | 77 | #[serde(skip_serializing_if = "Option::is_none")] 78 | pub memo: Option, 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::*; 84 | 85 | #[test] 86 | fn nep_format_vector() { 87 | let expected = r#"EVENT_JSON:{"standard":"nep171","version":"1.0.0","event":"nft_mint","data":[{"owner_id":"foundation.near","token_ids":["aurora","proximitylabs"]},{"owner_id":"user1.near","token_ids":["meme"]}]}"#; 88 | let log = EventLog { 89 | standard: "nep171".to_string(), 90 | version: "1.0.0".to_string(), 91 | event: EventLogVariant::NftMint(vec![ 92 | NftMintLog { 93 | owner_id: "foundation.near".to_owned(), 94 | token_ids: vec!["aurora".to_string(), "proximitylabs".to_string()], 95 | memo: None, 96 | }, 97 | NftMintLog { 98 | owner_id: "user1.near".to_owned(), 99 | token_ids: vec!["meme".to_string()], 100 | memo: None, 101 | }, 102 | ]), 103 | }; 104 | assert_eq!(expected, log.to_string()); 105 | } 106 | 107 | #[test] 108 | fn nep_format_mint() { 109 | let expected = r#"EVENT_JSON:{"standard":"nep171","version":"1.0.0","event":"nft_mint","data":[{"owner_id":"foundation.near","token_ids":["aurora","proximitylabs"]}]}"#; 110 | let log = EventLog { 111 | standard: "nep171".to_string(), 112 | version: "1.0.0".to_string(), 113 | event: EventLogVariant::NftMint(vec![NftMintLog { 114 | owner_id: "foundation.near".to_owned(), 115 | token_ids: vec!["aurora".to_string(), "proximitylabs".to_string()], 116 | memo: None, 117 | }]), 118 | }; 119 | assert_eq!(expected, log.to_string()); 120 | } 121 | 122 | #[test] 123 | fn nep_format_transfer_all_fields() { 124 | let expected = r#"EVENT_JSON:{"standard":"nep171","version":"1.0.0","event":"nft_transfer","data":[{"authorized_id":"market.near","old_owner_id":"user1.near","new_owner_id":"user2.near","token_ids":["token"],"memo":"Go Team!"}]}"#; 125 | let log = EventLog { 126 | standard: "nep171".to_string(), 127 | version: "1.0.0".to_string(), 128 | event: EventLogVariant::NftTransfer(vec![NftTransferLog { 129 | authorized_id: Some("market.near".to_string()), 130 | old_owner_id: "user1.near".to_string(), 131 | new_owner_id: "user2.near".to_string(), 132 | token_ids: vec!["token".to_string()], 133 | memo: Some("Go Team!".to_owned()), 134 | }]), 135 | }; 136 | assert_eq!(expected, log.to_string()); 137 | } 138 | } -------------------------------------------------------------------------------- /nft-contract-skeleton/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; 3 | use near_sdk::collections::{LazyOption, LookupMap, UnorderedMap, UnorderedSet}; 4 | use near_sdk::json_types::{Base64VecU8, U64, U128}; 5 | use near_sdk::serde::{Deserialize, Serialize}; 6 | use near_sdk::{ 7 | near_bindgen, env, NearToken, AccountId, CryptoHash, PanicOnDefault, Promise, PromiseOrValue, BorshStorageKey, NearSchema 8 | }; 9 | 10 | pub use crate::metadata::*; 11 | pub use crate::mint::*; 12 | pub use crate::nft_core::*; 13 | pub use crate::approval::*; 14 | pub use crate::royalty::*; 15 | 16 | mod enumeration; 17 | mod metadata; 18 | mod mint; 19 | mod nft_core; 20 | mod approval; 21 | mod royalty; 22 | 23 | #[near_bindgen] 24 | #[derive(BorshSerialize, BorshDeserialize, BorshStorageKey, PanicOnDefault)] 25 | #[borsh(crate = "near_sdk::borsh")] 26 | pub struct Contract { 27 | /* 28 | FILL THIS IN 29 | */ 30 | } 31 | 32 | /// Helper structure for keys of the persistent collections. 33 | #[derive(BorshSerialize)] 34 | #[borsh(crate = "near_sdk::borsh")] 35 | pub enum StorageKey { 36 | TokensPerOwner, 37 | TokenPerOwnerInner { account_id_hash: CryptoHash }, 38 | TokensById, 39 | TokenMetadataById, 40 | NFTContractMetadata, 41 | TokensPerType, 42 | TokensPerTypeInner { token_type_hash: CryptoHash }, 43 | TokenTypesLocked, 44 | } 45 | 46 | #[near_bindgen] 47 | impl Contract { 48 | /* 49 | initialization function (can only be called once). 50 | this initializes the contract with default metadata so the 51 | user doesn't have to manually type metadata. 52 | */ 53 | #[init] 54 | pub fn new_default_meta(owner_id: AccountId) -> Self { 55 | /* 56 | FILL THIS IN 57 | */ 58 | todo!(); //remove once code is filled in. 59 | } 60 | 61 | /* 62 | initialization function (can only be called once). 63 | this initializes the contract with metadata that was passed in and 64 | the owner_id. 65 | */ 66 | #[init] 67 | pub fn new(owner_id: AccountId, metadata: NFTContractMetadata) -> Self { 68 | /* 69 | FILL THIS IN 70 | */ 71 | todo!(); //remove once code is filled in. 72 | } 73 | } -------------------------------------------------------------------------------- /nft-contract-skeleton/src/metadata.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::NearToken; 2 | 3 | use crate::*; 4 | pub type TokenId = String; 5 | //defines the payout type we'll be returning as a part of the royalty standards. 6 | #[derive(Serialize, Deserialize, NearSchema)] 7 | #[serde(crate = "near_sdk::serde")] 8 | pub struct Payout { 9 | pub payout: HashMap, 10 | } 11 | 12 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Clone, NearSchema)] 13 | #[borsh(crate = "near_sdk::borsh")] 14 | #[serde(crate = "near_sdk::serde")] 15 | pub struct NFTContractMetadata { 16 | /* 17 | FILL THIS IN 18 | */ 19 | } 20 | 21 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, NearSchema)] 22 | #[borsh(crate = "near_sdk::borsh")] 23 | #[serde(crate = "near_sdk::serde")] 24 | pub struct TokenMetadata { 25 | /* 26 | FILL THIS IN 27 | */ 28 | } 29 | 30 | #[derive(BorshDeserialize, BorshSerialize)] 31 | #[borsh(crate = "near_sdk::borsh")] 32 | pub struct Token { 33 | /* 34 | FILL THIS IN 35 | */ 36 | } 37 | 38 | //The Json token is what will be returned from view calls. 39 | #[derive(Serialize, Deserialize, NearSchema)] 40 | #[serde(crate = "near_sdk::serde")] 41 | pub struct JsonToken { 42 | /* 43 | FILL THIS IN 44 | */ 45 | } 46 | 47 | pub trait NonFungibleTokenMetadata { 48 | //view call for returning the contract metadata 49 | fn nft_metadata(&self) -> NFTContractMetadata; 50 | } 51 | 52 | #[near_bindgen] 53 | impl NonFungibleTokenMetadata for Contract { 54 | fn nft_metadata(&self) -> NFTContractMetadata { 55 | /* 56 | FILL THIS IN 57 | */ 58 | todo!(); //remove once code is filled in. 59 | } 60 | } -------------------------------------------------------------------------------- /nft-contract-skeleton/src/mint.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | #[payable] 6 | pub fn nft_mint( 7 | &mut self, 8 | token_id: Option, 9 | token_owner_id: AccountId, 10 | token_metadata: TokenMetadata, 11 | ) { 12 | /* 13 | FILL THIS IN 14 | */ 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /nft-contract-skeleton/src/nft_core.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use near_sdk::{ext_contract, log, Gas, PromiseResult}; 3 | 4 | const GAS_FOR_RESOLVE_TRANSFER: Gas = Gas::from_tgas(10); 5 | const GAS_FOR_NFT_ON_TRANSFER: Gas = Gas::from_tgas(25); 6 | 7 | pub trait NonFungibleTokenCore { 8 | //transfers an NFT to a receiver ID 9 | fn nft_transfer(&mut self, receiver_id: AccountId, token_id: TokenId, memo: Option); 10 | 11 | //transfers an NFT to a receiver and calls a function on the receiver ID's contract 12 | /// Returns `true` if the token was transferred from the sender's account. 13 | fn nft_transfer_call( 14 | &mut self, 15 | receiver_id: AccountId, 16 | token_id: TokenId, 17 | memo: Option, 18 | msg: String, 19 | ) -> PromiseOrValue; 20 | 21 | //get information about the NFT token passed in 22 | fn nft_token(&self, token_id: TokenId) -> Option; 23 | } 24 | 25 | #[ext_contract(ext_non_fungible_token_receiver)] 26 | trait NonFungibleTokenReceiver { 27 | //Method stored on the receiver contract that is called via cross contract call when nft_transfer_call is called 28 | /// Returns `true` if the token should be returned back to the sender. 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 | ) -> Promise; 36 | } 37 | 38 | #[ext_contract(ext_self)] 39 | trait NonFungibleTokenResolver { 40 | /* 41 | resolves the promise of the cross contract call to the receiver contract 42 | this is stored on THIS contract and is meant to analyze what happened in the cross contract call when nft_on_transfer was called 43 | as part of the nft_transfer_call method 44 | */ 45 | fn nft_resolve_transfer( 46 | &mut self, 47 | previous_owner_id: AccountId, 48 | receiver_id: AccountId, 49 | token_id: TokenId, 50 | ) -> bool; 51 | } 52 | 53 | #[near_bindgen] 54 | impl NonFungibleTokenCore for Contract { 55 | //implementation of the nft_transfer method. This transfers the NFT from the current owner to the receiver. 56 | #[payable] 57 | fn nft_transfer(&mut self, receiver_id: AccountId, token_id: TokenId, memo: Option) { 58 | /* 59 | FILL THIS IN 60 | */ 61 | } 62 | 63 | //implementation of the transfer call method. This will transfer the NFT and call a method on the receiver_id contract 64 | #[payable] 65 | fn nft_transfer_call( 66 | &mut self, 67 | receiver_id: AccountId, 68 | token_id: TokenId, 69 | memo: Option, 70 | msg: String, 71 | ) -> PromiseOrValue { 72 | /* 73 | FILL THIS IN 74 | */ 75 | todo!(); //remove once code is filled in. 76 | } 77 | 78 | //get the information for a specific token ID 79 | fn nft_token(&self, token_id: TokenId) -> Option { 80 | /* 81 | FILL THIS IN 82 | */ 83 | todo!(); //remove once code is filled in. 84 | } 85 | } 86 | 87 | #[near_bindgen] 88 | impl NonFungibleTokenResolver for Contract { 89 | //resolves the cross contract call when calling nft_on_transfer in the nft_transfer_call method 90 | //returns true if the token was successfully transferred to the receiver_id 91 | #[private] 92 | fn nft_resolve_transfer( 93 | &mut self, 94 | previous_owner_id: AccountId, 95 | receiver_id: AccountId, 96 | token_id: TokenId, 97 | ) -> bool { 98 | /* 99 | FILL THIS IN 100 | */ 101 | todo!(); //remove once code is filled in. 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /nft-contract-skeleton/src/royalty.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub trait NonFungibleTokenCore { 4 | //calculates the payout for a token given the passed in balance. This is a view method 5 | fn nft_payout(&self, token_id: TokenId, balance: NearToken, max_len_payout: u32) -> Payout; 6 | 7 | //transfers the token to the receiver ID and returns the payout object that should be payed given the passed in balance. 8 | fn nft_transfer_payout( 9 | &mut self, 10 | receiver_id: AccountId, 11 | token_id: TokenId, 12 | approval_id: u64, 13 | memo: Option, 14 | balance: NearToken, 15 | max_len_payout: u32, 16 | ) -> Payout; 17 | } 18 | 19 | #[near_bindgen] 20 | impl NonFungibleTokenCore for Contract { 21 | //calculates the payout for a token given the passed in balance. This is a view method 22 | fn nft_payout(&self, token_id: TokenId, balance: NearToken, max_len_payout: u32) -> Payout { 23 | /* 24 | FILL THIS IN 25 | */ 26 | todo!(); //remove once code is filled in. 27 | } 28 | 29 | //transfers the token to the receiver ID and returns the payout object that should be payed given the passed in balance. 30 | #[payable] 31 | fn nft_transfer_payout( 32 | &mut self, 33 | receiver_id: AccountId, 34 | token_id: TokenId, 35 | approval_id: u64, 36 | memo: Option, 37 | balance: NearToken, 38 | max_len_payout: u32, 39 | ) -> Payout { 40 | /* 41 | FILL THIS IN 42 | */ 43 | todo!(); //remove once code is filled in. 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /nft-series/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nft_series_contract" 3 | version = "0.1.0" 4 | authors = ["Near Inc "] 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | near-sdk = { version = "5.11.0", features = ["legacy"] } 12 | serde_json = "1.0.113" 13 | 14 | [dev-dependencies] 15 | near-sdk = { version = "5.11.0", features = ["unit-testing"] } 16 | near-workspaces = { version = "0.18.0", features = ["unstable"] } 17 | tokio = { version = "1.12.0", features = ["full"] } 18 | serde_json = "1" 19 | 20 | [profile.release] 21 | codegen-units = 1 22 | # Tell `rustc` to optimize for small code size. 23 | opt-level = "z" 24 | lto = true 25 | debug = false 26 | panic = "abort" 27 | # Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801 28 | overflow-checks = true -------------------------------------------------------------------------------- /nft-series/README.md: -------------------------------------------------------------------------------- 1 | # TBD 2 | -------------------------------------------------------------------------------- /nft-series/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = ["rustfmt"] 4 | targets = ["wasm32-unknown-unknown"] -------------------------------------------------------------------------------- /nft-series/src/approval.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use near_sdk::ext_contract; 3 | 4 | pub trait NonFungibleTokenCore { 5 | //approve an account ID to transfer a token on your behalf 6 | fn nft_approve(&mut self, token_id: TokenId, account_id: AccountId, msg: Option); 7 | 8 | //check if the passed in account has access to approve the token ID 9 | fn nft_is_approved( 10 | &self, 11 | token_id: TokenId, 12 | approved_account_id: AccountId, 13 | approval_id: Option, 14 | ) -> bool; 15 | 16 | //revoke a specific account from transferring the token on your behalf 17 | fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId); 18 | 19 | //revoke all accounts from transferring the token on your behalf 20 | fn nft_revoke_all(&mut self, token_id: TokenId); 21 | } 22 | 23 | #[ext_contract(ext_non_fungible_approval_receiver)] 24 | trait NonFungibleTokenApprovalsReceiver { 25 | //cross contract call to an external contract that is initiated during nft_approve 26 | fn nft_on_approve( 27 | &mut self, 28 | token_id: TokenId, 29 | owner_id: AccountId, 30 | approval_id: u64, 31 | msg: String, 32 | ); 33 | } 34 | 35 | #[near_bindgen] 36 | impl NonFungibleTokenCore for Contract { 37 | //allow a specific account ID to approve a token on your behalf 38 | #[payable] 39 | fn nft_approve(&mut self, token_id: TokenId, account_id: AccountId, msg: Option) { 40 | /* 41 | assert at least one yocto for security reasons - this will cause a redirect to the NEAR wallet. 42 | The user needs to attach enough to pay for storage on the contract 43 | */ 44 | assert_at_least_one_yocto(); 45 | 46 | //get the token object from the token ID 47 | let mut token = self.tokens_by_id.get(&token_id).expect("No token"); 48 | 49 | //make sure that the person calling the function is the owner of the token 50 | assert_eq!( 51 | &env::predecessor_account_id(), 52 | &token.owner_id, 53 | "Predecessor must be the token owner." 54 | ); 55 | 56 | //get the next approval ID if we need a new approval 57 | let approval_id: u64 = token.next_approval_id; 58 | 59 | //check if the account has been approved already for this token 60 | let is_new_approval = token 61 | .approved_account_ids 62 | //insert returns none if the key was not present. 63 | .insert(account_id.clone(), approval_id) 64 | //if the key was not present, .is_none() will return true so it is a new approval. 65 | .is_none(); 66 | 67 | //if it was a new approval, we need to calculate how much storage is being used to add the account. 68 | let storage_used = if is_new_approval { 69 | bytes_for_approved_account_id(&account_id) 70 | //if it was not a new approval, we used no storage. 71 | } else { 72 | 0 73 | }; 74 | 75 | //increment the token's next approval ID by 1 76 | token.next_approval_id += 1; 77 | //insert the token back into the tokens_by_id collection 78 | self.tokens_by_id.insert(&token_id, &token); 79 | 80 | //refund any excess storage attached by the user. If the user didn't attach enough, panic. 81 | refund_deposit(storage_used); 82 | 83 | //if some message was passed into the function, we initiate a cross contract call on the 84 | //account we're giving access to. 85 | if let Some(msg) = msg { 86 | // Defaulting GAS weight to 1, no attached deposit, and no static GAS to attach. 87 | ext_non_fungible_approval_receiver::ext(account_id) 88 | .nft_on_approve(token_id, token.owner_id, approval_id, msg) 89 | .as_return(); 90 | } 91 | } 92 | 93 | //check if the passed in account has access to approve the token ID 94 | fn nft_is_approved( 95 | &self, 96 | token_id: TokenId, 97 | approved_account_id: AccountId, 98 | approval_id: Option, 99 | ) -> bool { 100 | //get the token object from the token_id 101 | let token = self.tokens_by_id.get(&token_id).expect("No token"); 102 | 103 | //get the approval number for the passed in account ID 104 | let approval = token.approved_account_ids.get(&approved_account_id); 105 | 106 | //if there was some approval ID found for the account ID 107 | if let Some(approval) = approval { 108 | //if a specific approval_id was passed into the function 109 | if let Some(approval_id) = approval_id { 110 | //return if the approval ID passed in matches the actual approval ID for the account 111 | approval_id == *approval 112 | //if there was no approval_id passed into the function, we simply return true 113 | } else { 114 | true 115 | } 116 | //if there was no approval ID found for the account ID, we simply return false 117 | } else { 118 | false 119 | } 120 | } 121 | 122 | //revoke a specific account from transferring the token on your behalf 123 | #[payable] 124 | fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId) { 125 | //assert that the user attached exactly 1 yoctoNEAR for security reasons 126 | assert_one_yocto(); 127 | //get the token object using the passed in token_id 128 | let mut token = self.tokens_by_id.get(&token_id).expect("No token"); 129 | 130 | //get the caller of the function and assert that they are the owner of the token 131 | let predecessor_account_id = env::predecessor_account_id(); 132 | assert_eq!(&predecessor_account_id, &token.owner_id); 133 | 134 | //if the account ID was in the token's approval, we remove it and the if statement logic executes 135 | if token.approved_account_ids.remove(&account_id).is_some() { 136 | //refund the funds released by removing the approved_account_id to the caller of the function 137 | refund_approved_account_ids_iter(predecessor_account_id, [account_id].iter()); 138 | 139 | //insert the token back into the tokens_by_id collection with the account_id removed from the approval list 140 | self.tokens_by_id.insert(&token_id, &token); 141 | } 142 | } 143 | 144 | //revoke all accounts from transferring the token on your behalf 145 | #[payable] 146 | fn nft_revoke_all(&mut self, token_id: TokenId) { 147 | //assert that the caller attached exactly 1 yoctoNEAR for security 148 | assert_one_yocto(); 149 | 150 | //get the token object from the passed in token ID 151 | let mut token = self.tokens_by_id.get(&token_id).expect("No token"); 152 | //get the caller and make sure they are the owner of the tokens 153 | let predecessor_account_id = env::predecessor_account_id(); 154 | assert_eq!(&predecessor_account_id, &token.owner_id); 155 | 156 | //only revoke if the approved account IDs for the token is not empty 157 | if !token.approved_account_ids.is_empty() { 158 | //refund the approved account IDs to the caller of the function 159 | refund_approved_account_ids(predecessor_account_id, &token.approved_account_ids); 160 | //clear the approved account IDs 161 | token.approved_account_ids.clear(); 162 | //insert the token back into the tokens_by_id collection with the approved account IDs cleared 163 | self.tokens_by_id.insert(&token_id, &token); 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /nft-series/src/enumeration.rs: -------------------------------------------------------------------------------- 1 | use crate::nft_core::NonFungibleTokenCore; 2 | use crate::*; 3 | 4 | /// Struct to return in views to query for specific data related to a series 5 | #[derive(BorshDeserialize, BorshSerialize, Serialize, NearSchema)] 6 | #[borsh(crate = "near_sdk::borsh")] 7 | #[serde(crate = "near_sdk::serde")] 8 | pub struct JsonSeries { 9 | series_id: u64, 10 | // Metadata including title, num copies etc.. that all tokens will derive from 11 | metadata: TokenMetadata, 12 | // Royalty used for all tokens in the collection 13 | royalty: Option>, 14 | // Owner of the collection 15 | owner_id: AccountId, 16 | } 17 | 18 | #[near_bindgen] 19 | impl Contract { 20 | //Query for the total supply of NFTs on the contract 21 | pub fn nft_total_supply(&self) -> U128 { 22 | //return the length of the tokens by id 23 | U128(self.tokens_by_id.len().into()) 24 | } 25 | 26 | //Query for nft tokens on the contract regardless of the owner using pagination 27 | pub fn nft_tokens(&self, from_index: Option, limit: Option) -> Vec { 28 | //where to start pagination - if we have a from_index, we'll use that - otherwise start from 0 index 29 | let start = u128::from(from_index.unwrap_or(U128(0))); 30 | 31 | //iterate through each token using an iterator 32 | self.tokens_by_id 33 | .keys() 34 | //skip to the index we specified in the start variable 35 | .skip(start as usize) 36 | //take the first "limit" elements in the vector. If we didn't specify a limit, use 50 37 | .take(limit.unwrap_or(50) as usize) 38 | //we'll map the token IDs which are strings into Json Tokens 39 | .map(|token_id| self.nft_token(token_id.clone()).unwrap()) 40 | //since we turned the keys into an iterator, we need to turn it back into a vector to return 41 | .collect() 42 | } 43 | 44 | //get the total supply of NFTs for a given owner 45 | pub fn nft_supply_for_owner(&self, account_id: AccountId) -> U128 { 46 | //get the set of tokens for the passed in owner 47 | let tokens_for_owner_set = self.tokens_per_owner.get(&account_id); 48 | 49 | //if there is some set of tokens, we'll return the length 50 | if let Some(tokens_for_owner_set) = tokens_for_owner_set { 51 | U128(tokens_for_owner_set.len().into()) 52 | } else { 53 | //if there isn't a set of tokens for the passed in account ID, we'll return 0 54 | U128(0) 55 | } 56 | } 57 | 58 | //Query for all the tokens for an owner 59 | pub fn nft_tokens_for_owner( 60 | &self, 61 | account_id: AccountId, 62 | from_index: Option, 63 | limit: Option, 64 | ) -> Vec { 65 | //get the set of tokens for the passed in owner 66 | let tokens_for_owner_set = self.tokens_per_owner.get(&account_id); 67 | //if there is some set of tokens, we'll set the tokens variable equal to that set 68 | let tokens = if let Some(tokens_for_owner_set) = tokens_for_owner_set { 69 | tokens_for_owner_set 70 | } else { 71 | //if there is no set of tokens, we'll simply return an empty vector. 72 | return vec![]; 73 | }; 74 | 75 | //where to start pagination - if we have a from_index, we'll use that - otherwise start from 0 index 76 | let start = u128::from(from_index.unwrap_or(U128(0))); 77 | 78 | //iterate through the keys vector 79 | tokens 80 | .iter() 81 | //skip to the index we specified in the start variable 82 | .skip(start as usize) 83 | //take the first "limit" elements in the vector. If we didn't specify a limit, use 50 84 | .take(limit.unwrap_or(50) as usize) 85 | //we'll map the token IDs which are strings into Json Tokens 86 | .map(|token_id| self.nft_token(token_id.clone()).unwrap()) 87 | //since we turned the keys into an iterator, we need to turn it back into a vector to return 88 | .collect() 89 | } 90 | 91 | // Get the total supply of series on the contract 92 | pub fn get_series_total_supply(&self) -> U64 { 93 | U64(self.series_by_id.len()) 94 | } 95 | 96 | // Paginate through all the series on the contract and return the a vector of JsonSeries 97 | pub fn get_series(&self, from_index: Option, limit: Option) -> Vec { 98 | //where to start pagination - if we have a from_index, we'll use that - otherwise start from 0 index 99 | let start = u128::from(from_index.unwrap_or(U128(0))); 100 | 101 | //iterate through each series using an iterator 102 | self.series_by_id 103 | .keys() 104 | //skip to the index we specified in the start variable 105 | .skip(start as usize) 106 | //take the first "limit" elements in the vector. If we didn't specify a limit, use 50 107 | .take(limit.unwrap_or(50) as usize) 108 | //we'll map the series IDs which are strings into Json Series 109 | .map(|series_id| self.get_series_details(U64(series_id.clone())).unwrap()) 110 | //since we turned the keys into an iterator, we need to turn it back into a vector to return 111 | .collect() 112 | } 113 | 114 | // get info for a specific series 115 | pub fn get_series_details(&self, id: U64) -> Option { 116 | //get the series from the map 117 | let series = self.series_by_id.get(&id.0); 118 | //if there is some series, we'll return the series 119 | if let Some(series) = series { 120 | Some(JsonSeries { 121 | series_id: id.0, 122 | metadata: series.metadata, 123 | royalty: series.royalty, 124 | owner_id: series.owner_id, 125 | }) 126 | } else { 127 | //if there isn't a series, we'll return None 128 | None 129 | } 130 | } 131 | 132 | //get the total supply of NFTs on a current series 133 | pub fn nft_supply_for_series(&self, id: U64) -> U64 { 134 | //get the series 135 | let series = self.series_by_id.get(&id.0); 136 | 137 | //if there is some series, get the length of the tokens. Otherwise return - 138 | if let Some(series) = series { 139 | U64(series.tokens.len()) 140 | } else { 141 | U64(0) 142 | } 143 | } 144 | 145 | /// Paginate through NFTs within a given series 146 | pub fn nft_tokens_for_series( 147 | &self, 148 | id: U64, 149 | from_index: Option, 150 | limit: Option, 151 | ) -> Vec { 152 | // Get the series and its tokens 153 | let series = self.series_by_id.get(&id.0); 154 | let tokens = if let Some(series) = series { 155 | series.tokens 156 | } else { 157 | return vec![]; 158 | }; 159 | 160 | //where to start pagination - if we have a from_index, we'll use that - otherwise start from 0 index 161 | let start = u128::from(from_index.unwrap_or(U128(0))); 162 | 163 | //iterate through the tokens 164 | tokens 165 | .iter() 166 | //skip to the index we specified in the start variable 167 | .skip(start as usize) 168 | //take the first "limit" elements in the vector. If we didn't specify a limit, use 50 169 | .take(limit.unwrap_or(50) as usize) 170 | //we'll map the token IDs which are strings into Json Tokens 171 | .map(|token_id| self.nft_token(token_id.clone()).unwrap()) 172 | //since we turned the keys into an iterator, we need to turn it back into a vector to return 173 | .collect() 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /nft-series/src/events.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use near_sdk::serde::{Deserialize, Serialize}; 4 | 5 | /// Enum that represents the data type of the EventLog. 6 | /// The enum can either be an NftMint or an NftTransfer. 7 | #[derive(Serialize, Deserialize, Debug)] 8 | #[serde(tag = "event", content = "data")] 9 | #[serde(rename_all = "snake_case")] 10 | #[serde(crate = "near_sdk::serde")] 11 | #[non_exhaustive] 12 | pub enum EventLogVariant { 13 | NftMint(Vec), 14 | NftTransfer(Vec), 15 | } 16 | 17 | /// Interface to capture data about an event 18 | /// 19 | /// Arguments: 20 | /// * `standard`: name of standard e.g. nep171 21 | /// * `version`: e.g. 1.0.0 22 | /// * `event`: associate event data 23 | #[derive(Serialize, Deserialize, Debug)] 24 | #[serde(crate = "near_sdk::serde")] 25 | pub struct EventLog { 26 | pub standard: String, 27 | pub version: String, 28 | 29 | // `flatten` to not have "event": {} in the JSON, just have the contents of {}. 30 | #[serde(flatten)] 31 | pub event: EventLogVariant, 32 | } 33 | 34 | impl fmt::Display for EventLog { 35 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 36 | f.write_fmt(format_args!( 37 | "EVENT_JSON:{}", 38 | &serde_json::to_string(self).map_err(|_| fmt::Error)? 39 | )) 40 | } 41 | } 42 | 43 | /// An event log to capture token minting 44 | /// 45 | /// Arguments 46 | /// * `owner_id`: "account.near" 47 | /// * `token_ids`: ["1", "abc"] 48 | /// * `memo`: optional message 49 | #[derive(Serialize, Deserialize, Debug)] 50 | #[serde(crate = "near_sdk::serde")] 51 | pub struct NftMintLog { 52 | pub owner_id: String, 53 | pub token_ids: Vec, 54 | 55 | #[serde(skip_serializing_if = "Option::is_none")] 56 | pub memo: Option, 57 | } 58 | 59 | /// An event log to capture token transfer 60 | /// 61 | /// Arguments 62 | /// * `authorized_id`: approved account to transfer 63 | /// * `old_owner_id`: "owner.near" 64 | /// * `new_owner_id`: "receiver.near" 65 | /// * `token_ids`: ["1", "12345abc"] 66 | /// * `memo`: optional message 67 | #[derive(Serialize, Deserialize, Debug)] 68 | #[serde(crate = "near_sdk::serde")] 69 | pub struct NftTransferLog { 70 | #[serde(skip_serializing_if = "Option::is_none")] 71 | pub authorized_id: Option, 72 | 73 | pub old_owner_id: String, 74 | pub new_owner_id: String, 75 | pub token_ids: Vec, 76 | 77 | #[serde(skip_serializing_if = "Option::is_none")] 78 | pub memo: Option, 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::*; 84 | 85 | #[test] 86 | fn nep_format_vector() { 87 | let expected = r#"EVENT_JSON:{"standard":"nep171","version":"1.0.0","event":"nft_mint","data":[{"owner_id":"foundation.near","token_ids":["aurora","proximitylabs"]},{"owner_id":"user1.near","token_ids":["meme"]}]}"#; 88 | let log = EventLog { 89 | standard: "nep171".to_string(), 90 | version: "1.0.0".to_string(), 91 | event: EventLogVariant::NftMint(vec![ 92 | NftMintLog { 93 | owner_id: "foundation.near".to_owned(), 94 | token_ids: vec!["aurora".to_string(), "proximitylabs".to_string()], 95 | memo: None, 96 | }, 97 | NftMintLog { 98 | owner_id: "user1.near".to_owned(), 99 | token_ids: vec!["meme".to_string()], 100 | memo: None, 101 | }, 102 | ]), 103 | }; 104 | assert_eq!(expected, log.to_string()); 105 | } 106 | 107 | #[test] 108 | fn nep_format_mint() { 109 | let expected = r#"EVENT_JSON:{"standard":"nep171","version":"1.0.0","event":"nft_mint","data":[{"owner_id":"foundation.near","token_ids":["aurora","proximitylabs"]}]}"#; 110 | let log = EventLog { 111 | standard: "nep171".to_string(), 112 | version: "1.0.0".to_string(), 113 | event: EventLogVariant::NftMint(vec![NftMintLog { 114 | owner_id: "foundation.near".to_owned(), 115 | token_ids: vec!["aurora".to_string(), "proximitylabs".to_string()], 116 | memo: None, 117 | }]), 118 | }; 119 | assert_eq!(expected, log.to_string()); 120 | } 121 | 122 | #[test] 123 | fn nep_format_transfer_all_fields() { 124 | let expected = r#"EVENT_JSON:{"standard":"nep171","version":"1.0.0","event":"nft_transfer","data":[{"authorized_id":"market.near","old_owner_id":"user1.near","new_owner_id":"user2.near","token_ids":["token"],"memo":"Go Team!"}]}"#; 125 | let log = EventLog { 126 | standard: "nep171".to_string(), 127 | version: "1.0.0".to_string(), 128 | event: EventLogVariant::NftTransfer(vec![NftTransferLog { 129 | authorized_id: Some("market.near".to_string()), 130 | old_owner_id: "user1.near".to_string(), 131 | new_owner_id: "user2.near".to_string(), 132 | token_ids: vec!["token".to_string()], 133 | memo: Some("Go Team!".to_owned()), 134 | }]), 135 | }; 136 | assert_eq!(expected, log.to_string()); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /nft-series/src/lib.rs: -------------------------------------------------------------------------------- 1 | use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; 2 | use near_sdk::collections::{LazyOption, LookupMap, LookupSet, UnorderedMap, UnorderedSet}; 3 | use near_sdk::json_types::{Base64VecU8, U64, U128}; 4 | use near_sdk::serde::{Deserialize, Serialize}; 5 | use near_sdk::{ 6 | env, near_bindgen, require, AccountId, NearToken, BorshStorageKey, CryptoHash, PanicOnDefault, 7 | Promise, PromiseOrValue, NearSchema 8 | }; 9 | use std::collections::HashMap; 10 | 11 | pub use crate::approval::*; 12 | pub use crate::events::*; 13 | use crate::internal::*; 14 | pub use crate::metadata::*; 15 | pub use crate::nft_core::*; 16 | pub use crate::owner::*; 17 | pub use crate::royalty::*; 18 | pub use crate::series::*; 19 | 20 | mod approval; 21 | mod enumeration; 22 | mod events; 23 | mod internal; 24 | mod metadata; 25 | mod nft_core; 26 | mod owner; 27 | mod royalty; 28 | mod series; 29 | 30 | /// This spec can be treated like a version of the standard. 31 | pub const NFT_METADATA_SPEC: &str = "1.0.0"; 32 | /// This is the name of the NFT standard we're using 33 | pub const NFT_STANDARD_NAME: &str = "nep171"; 34 | 35 | // Represents the series type. All tokens will derive this data. 36 | #[derive(BorshDeserialize, BorshSerialize)] 37 | #[borsh(crate = "near_sdk::borsh")] 38 | pub struct Series { 39 | // Metadata including title, num copies etc.. that all tokens will derive from 40 | metadata: TokenMetadata, 41 | // Royalty used for all tokens in the collection 42 | royalty: Option>, 43 | // Set of tokens in the collection 44 | tokens: UnorderedSet, 45 | // What is the price of each token in this series? If this is specified, when minting, 46 | // Users will need to attach enough $NEAR to cover the price. 47 | price: Option, 48 | // Owner of the collection 49 | owner_id: AccountId, 50 | } 51 | 52 | pub type SeriesId = u64; 53 | 54 | #[near_bindgen] 55 | #[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] 56 | #[borsh(crate = "near_sdk::borsh")] 57 | pub struct Contract { 58 | //contract owner 59 | pub owner_id: AccountId, 60 | 61 | //approved minters 62 | pub approved_minters: LookupSet, 63 | 64 | //approved users that can create series 65 | pub approved_creators: LookupSet, 66 | 67 | //Map the collection ID (stored in Token obj) to the collection data 68 | pub series_by_id: UnorderedMap, 69 | 70 | //keeps track of the token struct for a given token ID 71 | pub tokens_by_id: UnorderedMap, 72 | 73 | //keeps track of all the token IDs for a given account 74 | pub tokens_per_owner: LookupMap>, 75 | 76 | //keeps track of the metadata for the contract 77 | pub metadata: LazyOption, 78 | } 79 | 80 | /// Helper structure for keys of the persistent collections. 81 | #[derive(BorshSerialize, BorshStorageKey)] 82 | #[borsh(crate = "near_sdk::borsh")] 83 | pub enum StorageKey { 84 | ApprovedMinters, 85 | ApprovedCreators, 86 | SeriesById, 87 | SeriesByIdInner { account_id_hash: CryptoHash }, 88 | TokensPerOwner, 89 | TokenPerOwnerInner { account_id_hash: CryptoHash }, 90 | TokensById, 91 | NFTContractMetadata, 92 | } 93 | 94 | #[near_bindgen] 95 | impl Contract { 96 | /* 97 | initialization function (can only be called once). 98 | this initializes the contract with default metadata so the 99 | user doesn't have to manually type metadata. 100 | */ 101 | #[init] 102 | pub fn new_default_meta(owner_id: AccountId) -> Self { 103 | //calls the other function "new: with some default metadata and the owner_id passed in 104 | Self::new( 105 | owner_id, 106 | NFTContractMetadata { 107 | spec: "nft-1.0.0".to_string(), 108 | name: "NFT Series Contract".to_string(), 109 | symbol: "GOTEAM".to_string(), 110 | icon: None, 111 | base_uri: None, 112 | reference: None, 113 | reference_hash: None, 114 | }, 115 | ) 116 | } 117 | 118 | /* 119 | initialization function (can only be called once). 120 | this initializes the contract with metadata that was passed in and 121 | the owner_id. 122 | */ 123 | #[init] 124 | pub fn new(owner_id: AccountId, metadata: NFTContractMetadata) -> Self { 125 | // Create the approved minters set and insert the owner 126 | let mut approved_minters = 127 | LookupSet::new(StorageKey::ApprovedMinters); 128 | approved_minters.insert(&owner_id); 129 | 130 | // Create the approved creators set and insert the owner 131 | let mut approved_creators = 132 | LookupSet::new(StorageKey::ApprovedCreators); 133 | approved_creators.insert(&owner_id); 134 | 135 | // Create a variable of type Self with all the fields initialized. 136 | let this = Self { 137 | approved_minters, 138 | approved_creators, 139 | series_by_id: UnorderedMap::new(StorageKey::SeriesById), 140 | //Storage keys are simply the prefixes used for the collections. This helps avoid data collision 141 | tokens_per_owner: LookupMap::new(StorageKey::TokensPerOwner), 142 | tokens_by_id: UnorderedMap::new(StorageKey::TokensById), 143 | //set the &owner_id field equal to the passed in owner_id. 144 | owner_id, 145 | metadata: LazyOption::new( 146 | StorageKey::NFTContractMetadata, 147 | Some(&metadata), 148 | ), 149 | }; 150 | 151 | //return the Contract object 152 | this 153 | } 154 | } -------------------------------------------------------------------------------- /nft-series/src/metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | pub type TokenId = String; 3 | //defines the payout type we'll be returning as a part of the royalty standards. 4 | #[derive(Serialize, Deserialize, NearSchema)] 5 | #[serde(crate = "near_sdk::serde")] 6 | pub struct Payout { 7 | pub payout: HashMap, 8 | } 9 | 10 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Clone, NearSchema)] 11 | #[borsh(crate = "near_sdk::borsh")] 12 | #[serde(crate = "near_sdk::serde")] 13 | pub struct NFTContractMetadata { 14 | pub spec: String, // required, essentially a version like "nft-1.0.0" 15 | pub name: String, // required, ex. "Mosaics" 16 | pub symbol: String, // required, ex. "MOSAIC" 17 | pub icon: Option, // Data URL 18 | pub base_uri: Option, // Centralized gateway known to have reliable access to decentralized storage assets referenced by `reference` or `media` URLs 19 | pub reference: Option, // URL to a JSON file with more info 20 | pub reference_hash: Option, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. 21 | } 22 | 23 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, NearSchema)] 24 | #[borsh(crate = "near_sdk::borsh")] 25 | #[serde(crate = "near_sdk::serde")] 26 | pub struct TokenMetadata { 27 | pub title: Option, // ex. "Arch Nemesis: Mail Carrier" or "Parcel #5055" 28 | pub description: Option, // free-form description 29 | pub media: Option, // URL to associated media, preferably to decentralized, content-addressed storage 30 | pub media_hash: Option, // Base64-encoded sha256 hash of content referenced by the `media` field. Required if `media` is included. 31 | pub copies: Option, // number of copies of this set of metadata in existence when token was minted. 32 | pub issued_at: Option, // When token was issued or minted, Unix epoch in milliseconds 33 | pub expires_at: Option, // When token expires, Unix epoch in milliseconds 34 | pub starts_at: Option, // When token starts being valid, Unix epoch in milliseconds 35 | pub updated_at: Option, // When token was last updated, Unix epoch in milliseconds 36 | pub extra: Option, // anything extra the NFT wants to store on-chain. Can be stringified JSON. 37 | pub reference: Option, // URL to an off-chain JSON file with more info. 38 | pub reference_hash: Option, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. 39 | } 40 | 41 | #[derive(BorshDeserialize, BorshSerialize)] 42 | #[borsh(crate = "near_sdk::borsh")] 43 | pub struct Token { 44 | // Series that the token belongs to 45 | pub series_id: u64, 46 | //owner of the token 47 | pub owner_id: AccountId, 48 | //list of approved account IDs that have access to transfer the token. This maps an account ID to an approval ID 49 | pub approved_account_ids: HashMap, 50 | //the next approval ID to give out. 51 | pub next_approval_id: u64, 52 | } 53 | 54 | //The Json token is what will be returned from view calls. 55 | #[derive(Serialize, Deserialize, NearSchema)] 56 | #[serde(crate = "near_sdk::serde")] 57 | pub struct JsonToken { 58 | // Series that the token belongs to 59 | pub series_id: u64, 60 | //token ID 61 | pub token_id: TokenId, 62 | //owner of the token 63 | pub owner_id: AccountId, 64 | //token metadata 65 | pub metadata: TokenMetadata, 66 | //list of approved account IDs that have access to transfer the token. This maps an account ID to an approval ID 67 | pub approved_account_ids: HashMap, 68 | //keep track of the royalty percentages for the token in a hash map 69 | pub royalty: Option>, 70 | } 71 | 72 | pub trait NonFungibleTokenMetadata { 73 | //view call for returning the contract metadata 74 | fn nft_metadata(&self) -> NFTContractMetadata; 75 | } 76 | 77 | #[near_bindgen] 78 | impl NonFungibleTokenMetadata for Contract { 79 | fn nft_metadata(&self) -> NFTContractMetadata { 80 | self.metadata.get().unwrap() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /nft-series/src/owner.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | /// Add a specified account as an approved minter 6 | pub fn add_approved_minter(&mut self, account_id: AccountId) { 7 | self.assert_contract_owner(); 8 | self.approved_minters.insert(&account_id); 9 | } 10 | 11 | /// Remove a specified account as an approved minter 12 | pub fn remove_approved_minter(&mut self, account_id: AccountId) { 13 | self.assert_contract_owner(); 14 | self.approved_minters.remove(&account_id); 15 | } 16 | 17 | /// Check if a specified account is an approved minter 18 | pub fn is_approved_minter(&self, account_id: AccountId) -> bool { 19 | self.approved_minters.contains(&account_id) 20 | } 21 | 22 | /// Add a specified account as an approved creator 23 | pub fn add_approved_creator(&mut self, account_id: AccountId) { 24 | self.assert_contract_owner(); 25 | self.approved_creators.insert(&account_id); 26 | } 27 | 28 | /// Remove a specified account as an approved creator 29 | pub fn remove_approved_creator(&mut self, account_id: AccountId) { 30 | self.assert_contract_owner(); 31 | self.approved_creators.remove(&account_id); 32 | } 33 | 34 | /// Check if a specified account is an approved creator 35 | pub fn is_approved_creator(&self, account_id: AccountId) -> bool { 36 | self.approved_creators.contains(&account_id) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /nft-series/src/royalty.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub trait NonFungibleTokenCore { 4 | //calculates the payout for a token given the passed in balance. This is a view method 5 | fn nft_payout(&self, token_id: TokenId, balance: NearToken, max_len_payout: u32) -> Payout; 6 | 7 | //transfers the token to the receiver ID and returns the payout object that should be payed given the passed in balance. 8 | fn nft_transfer_payout( 9 | &mut self, 10 | receiver_id: AccountId, 11 | token_id: TokenId, 12 | approval_id: u64, 13 | memo: Option, 14 | balance: NearToken, 15 | max_len_payout: u32, 16 | ) -> Payout; 17 | } 18 | 19 | #[near_bindgen] 20 | impl NonFungibleTokenCore for Contract { 21 | //calculates the payout for a token given the passed in balance. This is a view method 22 | fn nft_payout(&self, token_id: TokenId, balance: NearToken, max_len_payout: u32) -> Payout { 23 | //get the token object 24 | let token = self.tokens_by_id.get(&token_id).expect("No token"); 25 | 26 | //get the owner of the token 27 | let owner_id = token.owner_id; 28 | //keep track of the total perpetual royalties 29 | let mut total_perpetual = 0; 30 | //keep track of the payout object to send back 31 | let mut payout_object = Payout { 32 | payout: HashMap::new(), 33 | }; 34 | //get the royalty object from series 35 | let cur_series = self 36 | .series_by_id 37 | .get(&token.series_id) 38 | .expect("Not a series"); 39 | 40 | // If the series doesn't have a royalty, we'll return an a payout object that just includes the owner 41 | let royalty_option = cur_series.royalty; 42 | if royalty_option.is_none() { 43 | let mut payout = HashMap::new(); 44 | payout.insert(owner_id, balance); 45 | return Payout { payout: payout }; 46 | } 47 | // Otherwise, we will get the royalty object from the series 48 | let royalty = royalty_option.unwrap(); 49 | 50 | //make sure we're not paying out to too many people (GAS limits this) 51 | assert!( 52 | royalty.len() as u32 <= max_len_payout, 53 | "Market can¬ payout to that many receivers" 54 | ); 55 | 56 | //go through each key and value in the royalty object 57 | for (k, v) in royalty.iter() { 58 | //get the key 59 | let key = k.clone(); 60 | //only insert into the payout if the key isn't the token owner (we add their payout at the end) 61 | if key != owner_id { 62 | // 63 | payout_object 64 | .payout 65 | .insert(key, royalty_to_payout(*v as u128, balance)); 66 | total_perpetual += *v; 67 | } 68 | } 69 | 70 | // payout to previous owner who gets 100% - total perpetual royalties 71 | payout_object.payout.insert( 72 | owner_id, 73 | royalty_to_payout((10000 - total_perpetual).into(), balance), 74 | ); 75 | 76 | //return the payout object 77 | payout_object 78 | } 79 | 80 | //transfers the token to the receiver ID and returns the payout object that should be payed given the passed in balance. 81 | #[payable] 82 | fn nft_transfer_payout( 83 | &mut self, 84 | receiver_id: AccountId, 85 | token_id: TokenId, 86 | approval_id: u64, 87 | memo: Option, 88 | balance: NearToken, 89 | max_len_payout: u32, 90 | ) -> Payout { 91 | //assert that the user attached 1 yocto NEAR for security reasons 92 | assert_one_yocto(); 93 | //get the sender ID 94 | let sender_id = env::predecessor_account_id(); 95 | //transfer the token to the passed in receiver and get the previous token object back 96 | let previous_token = 97 | self.internal_transfer(&sender_id, &receiver_id, &token_id, Some(approval_id), memo); 98 | 99 | //refund the previous token owner for the storage used up by the previous approved account IDs 100 | refund_approved_account_ids( 101 | previous_token.owner_id.clone(), 102 | &previous_token.approved_account_ids, 103 | ); 104 | 105 | //get the owner of the token 106 | let owner_id = previous_token.owner_id; 107 | //keep track of the total perpetual royalties 108 | let mut total_perpetual = 0; 109 | //keep track of the payout object to send back 110 | let mut payout_object = Payout { 111 | payout: HashMap::new(), 112 | }; 113 | 114 | //get the royalty object from series 115 | let cur_series = self 116 | .series_by_id 117 | .get(&previous_token.series_id) 118 | .expect("Not a series"); 119 | 120 | // If the series doesn't have a royalty, we'll return an a payout object that just includes the owner 121 | let royalty_option = cur_series.royalty; 122 | if royalty_option.is_none() { 123 | let mut payout = HashMap::new(); 124 | payout.insert(owner_id, balance); 125 | return Payout { payout: payout }; 126 | } 127 | // Otherwise, we will get the royalty object from the series 128 | let royalty = royalty_option.unwrap(); 129 | 130 | //make sure we're not paying out to too many people (GAS limits this) 131 | assert!( 132 | royalty.len() as u32 <= max_len_payout, 133 | "Market cannot payout to that many receivers" 134 | ); 135 | 136 | //go through each key and value in the royalty object 137 | for (k, v) in royalty.iter() { 138 | //get the key 139 | let key = k.clone(); 140 | //only insert into the payout if the key isn't the token owner (we add their payout at the end) 141 | if key != owner_id { 142 | // 143 | payout_object 144 | .payout 145 | .insert(key, royalty_to_payout(*v as u128, balance)); 146 | total_perpetual += *v; 147 | } 148 | } 149 | 150 | // payout to previous owner who gets 100% - total perpetual royalties 151 | payout_object.payout.insert( 152 | owner_id, 153 | royalty_to_payout((10000 - total_perpetual).into(), balance), 154 | ); 155 | 156 | //return the payout object 157 | payout_object 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /nft-series/src/series.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[near_bindgen] 4 | impl Contract { 5 | /// Create a new series. The caller must be an approved creator. All tokens in the series will inherit the same metadata 6 | /// If copies are set in the metadata, it will enforce that only that number of NFTs can be minted. If not, unlimited NFTs can be minted. 7 | /// If a title is set in the metadata, enumeration methods will return the `${title} - ${edition}` else, `${series_id} - ${edition}` 8 | /// All token IDs internally are stored as `${series_id}:${edition}` 9 | /// Caller must attach enough $NEAR to cover storage. 10 | #[payable] 11 | pub fn create_series( 12 | &mut self, 13 | id: U64, 14 | metadata: TokenMetadata, 15 | royalty: Option>, 16 | price: Option, 17 | ) { 18 | // Measure the initial storage being used on the contract 19 | let initial_storage_usage = env::storage_usage(); 20 | 21 | // Ensure the caller is an approved creator 22 | let caller = env::predecessor_account_id(); 23 | require!( 24 | self.approved_creators.contains(&caller) == true, 25 | "only approved creators can add a type" 26 | ); 27 | 28 | // Check that the total royalty amount does not exceed 100% 29 | if !royalty.is_none() { 30 | let mut total_royalty = 0; 31 | 32 | for (_, v) in royalty.clone().unwrap().iter() { 33 | total_royalty += *v; 34 | } 35 | require!(total_royalty <= 100, "total royalty can't exceed 100%"); 36 | } 37 | 38 | // Insert the series and ensure it doesn't already exist 39 | require!( 40 | self.series_by_id 41 | .insert( 42 | &id.0, 43 | &Series { 44 | metadata, 45 | royalty, 46 | tokens: UnorderedSet::new(StorageKey::SeriesByIdInner { 47 | // We get a new unique prefix for the collection 48 | account_id_hash: hash_account_id(&format!("{}.{}", id.0, caller)), 49 | }), 50 | owner_id: caller, 51 | price: price.map(|p| p), 52 | } 53 | ) 54 | .is_none(), 55 | "collection ID already exists" 56 | ); 57 | 58 | //calculate the required storage which was the used - initial 59 | let required_storage_in_bytes = env::storage_usage() - initial_storage_usage; 60 | 61 | //refund any excess storage if the user attached too much. Panic if they didn't attach enough to cover the required. 62 | refund_deposit(required_storage_in_bytes.into()); 63 | } 64 | 65 | /// Mint a new NFT that is part of a series. The caller must be an approved minter. 66 | /// The series ID must exist and if the metadata specifies a copy limit, you cannot exceed it. 67 | #[payable] 68 | pub fn nft_mint(&mut self, id: U64, token_owner_id: AccountId) { 69 | // Measure the initial storage being used on the contract 70 | let initial_storage_usage = env::storage_usage(); 71 | 72 | // Get the series and how many tokens currently exist (edition number = cur_len + 1) 73 | let mut series = self.series_by_id.get(&id.0).expect("Not a series"); 74 | 75 | // Check if the series has a price per token. If it does, ensure the caller has attached at least that amount 76 | let mut price_per_token = NearToken::from_yoctonear(0); 77 | if let Some(price) = series.price { 78 | price_per_token = price; 79 | require!( 80 | env::attached_deposit().ge(&price_per_token), 81 | "Need to attach at least enough to cover price" 82 | ); 83 | // If the series doesn't have a price, ensure the caller is an approved minter. 84 | } else { 85 | // Ensure the caller is an approved minter 86 | let predecessor = env::predecessor_account_id(); 87 | assert!( 88 | self.approved_minters.contains(&predecessor), 89 | "Not approved minter" 90 | ); 91 | } 92 | 93 | let cur_len = series.tokens.len(); 94 | // Ensure we haven't overflowed on the number of copies minted 95 | if let Some(copies) = series.metadata.copies { 96 | require!( 97 | cur_len < copies, 98 | "cannot mint anymore NFTs for the given series. Limit reached" 99 | ); 100 | } 101 | 102 | // The token ID is stored internally as `${series_id}:${edition}` 103 | let token_id = format!("{}:{}", id.0, cur_len + 1); 104 | series.tokens.insert(&token_id); 105 | self.series_by_id.insert(&id.0, &series); 106 | 107 | //specify the token struct that contains the owner ID 108 | let token = Token { 109 | // Series ID that the token belongs to 110 | series_id: id.0, 111 | //set the owner ID equal to the receiver ID passed into the function 112 | owner_id: token_owner_id, 113 | //we set the approved account IDs to the default value (an empty map) 114 | approved_account_ids: Default::default(), 115 | //the next approval ID is set to 0 116 | next_approval_id: 0, 117 | }; 118 | 119 | //insert the token ID and token struct and make sure that the token doesn't exist 120 | require!( 121 | self.tokens_by_id.insert(&token_id, &token).is_none(), 122 | "Token already exists" 123 | ); 124 | 125 | //call the internal method for adding the token to the owner 126 | self.internal_add_token_to_owner(&token.owner_id, &token_id); 127 | 128 | // Construct the mint log as per the events standard. 129 | let nft_mint_log: EventLog = EventLog { 130 | // Standard name ("nep171"). 131 | standard: NFT_STANDARD_NAME.to_string(), 132 | // Version of the standard ("nft-1.0.0"). 133 | version: NFT_METADATA_SPEC.to_string(), 134 | // The data related with the event stored in a vector. 135 | event: EventLogVariant::NftMint(vec![NftMintLog { 136 | // Owner of the token. 137 | owner_id: token.owner_id.to_string(), 138 | // Vector of token IDs that were minted. 139 | token_ids: vec![token_id.to_string()], 140 | // An optional memo to include. 141 | memo: None, 142 | }]), 143 | }; 144 | 145 | // Log the serialized json. 146 | env::log_str(&nft_mint_log.to_string()); 147 | 148 | //calculate the required storage which was the used - initial 149 | let required_storage_in_bytes = env::storage_usage() - initial_storage_usage; 150 | 151 | // If there's some price for the token, we'll payout the series owner. Otherwise, refund the excess deposit for storage to the caller 152 | if price_per_token.gt(&NearToken::from_yoctonear(0)) { 153 | payout_series_owner( 154 | required_storage_in_bytes.into(), 155 | price_per_token, 156 | series.owner_id, 157 | ); 158 | } else { 159 | refund_deposit(required_storage_in_bytes.into()); 160 | } 161 | } 162 | } 163 | --------------------------------------------------------------------------------