├── .gitignore ├── README.md ├── contract ├── .cargo │ └── config ├── .gitignore ├── Cargo.toml ├── README.md ├── build.sh ├── res │ └── .gitkeep └── src │ ├── approval.rs │ ├── enumeration.rs │ ├── events.rs │ ├── internal.rs │ ├── lib.rs │ ├── metadata.rs │ ├── mint.rs │ ├── nft_core.rs │ └── royalty.rs ├── package.json └── src ├── App.js ├── Components ├── InfoBubble.js └── MintingTool.js ├── __mocks__ └── fileMock.js ├── assets ├── favicon.ico ├── logo-black.svg ├── logo-white.svg ├── near_icon.svg └── near_logo_wht.svg ├── config.js ├── global.css ├── index.html ├── index.js ├── jest.init.js ├── main.test.js ├── utils.js └── wallet └── login └── index.html /.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 | /.pnp 6 | .pnp.js 7 | package-lock.json 8 | 9 | # build 10 | /out 11 | /dist 12 | 13 | # keys 14 | /neardev 15 | 16 | # testing 17 | /coverage 18 | 19 | # production 20 | /build 21 | 22 | # misc 23 | .DS_Store 24 | .env.local 25 | .env.development.local 26 | .env.test.local 27 | .env.production.local 28 | /.cache 29 | 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | nft-mint-frontend 2 | ================== 3 | 4 | This [React] app was initialized with [create-near-app] 5 | 6 | 7 | Quick Start 8 | =========== 9 | 10 | To run this project locally: 11 | 12 | 1. Prerequisites: Make sure you've installed [Node.js] ≥ 12 13 | 2. Install dependencies: `yarn install` 14 | 3. Run the local development server: `yarn dev` (see `package.json` for a 15 | full list of `scripts` you can run with `yarn`) 16 | 17 | Now you'll have a local development environment backed by the NEAR TestNet! 18 | 19 | Go ahead and play with the app and the code. As you make code changes, the app will automatically reload. 20 | 21 | 22 | Exploring The Code 23 | ================== 24 | 25 | 1. The "backend" code lives in the `/contract` folder. See the README there for 26 | more info. 27 | 2. The frontend code lives in the `/src` folder. `/src/index.html` is a great 28 | place to start exploring. Note that it loads in `/src/index.js`, where you 29 | can learn how the frontend connects to the NEAR blockchain. 30 | 3. Tests: there are different kinds of tests for the frontend and the smart 31 | contract. See `contract/README` for info about how it's tested. The frontend 32 | code gets tested with [jest]. You can run both of these at once with `yarn 33 | run test`. 34 | 35 | 36 | Deploy 37 | ====== 38 | 39 | Every smart contract in NEAR has its [own associated account][NEAR accounts]. When you run `yarn dev`, your smart contract gets deployed to the live NEAR TestNet with a throwaway account. When you're ready to make it permanent, here's how. 40 | 41 | 42 | Step 0: Install near-cli (optional) 43 | ------------------------------------- 44 | 45 | [near-cli] is a command line interface (CLI) for interacting with the NEAR blockchain. It was installed to the local `node_modules` folder when you ran `yarn install`, but for best ergonomics you may want to install it globally: 46 | 47 | ```sh 48 | yarn install --global near-cli 49 | ``` 50 | 51 | Or, if you'd rather use the locally-installed version, you can prefix all `near` commands with `npx` 52 | 53 | Ensure that it's installed with `near --version` (or `npx near --version`) 54 | 55 | 56 | Step 1: Create an account for the contract 57 | ------------------------------------------ 58 | 59 | Each account on NEAR can have at most one contract deployed to it. If you've already created an account such as `your-name.testnet`, you can deploy your contract to `nft-mint-frontend.your-name.testnet`. Assuming you've already created an account on [NEAR Wallet], here's how to create `nft-mint-frontend.your-name.testnet`: 60 | 61 | 1. Authorize NEAR CLI, following the commands it gives you: 62 | 63 | ```sh 64 | near login 65 | ``` 66 | 67 | 2. Create a subaccount (replace `YOUR-NAME` below with your actual account name): 68 | 69 | ```sh 70 | near create-account nft-mint-frontend.YOUR-NAME.testnet --masterAccount YOUR-NAME.testnet 71 | ``` 72 | 73 | Step 2: set contract name in code 74 | --------------------------------- 75 | 76 | Modify the line in `src/config.js` that sets the account name of the contract. Set it to the account id you used above. 77 | 78 | ```js 79 | const CONTRACT_NAME = process.env.CONTRACT_NAME || 'nft-mint-frontend.YOUR-NAME.testnet' 80 | ``` 81 | 82 | Step 3: deploy! 83 | --------------- 84 | 85 | One command: 86 | 87 | ```sh 88 | yarn deploy 89 | ``` 90 | 91 | As you can see in `package.json`, this does two things: 92 | 93 | 1. builds & deploys smart contract to NEAR TestNet 94 | 2. builds & deploys frontend code to GitHub using [gh-pages]. This will only work if the project already has a repository set up on GitHub. Feel free to modify the `deploy` script in `package.json` to deploy elsewhere. 95 | 96 | 97 | Troubleshooting 98 | =============== 99 | 100 | On Windows, if you're seeing an error containing `EPERM` it may be related to spaces in your path. Please see [this issue](https://github.com/zkat/npx/issues/209) for more details. 101 | 102 | 103 | [React]: https://reactjs.org/ 104 | [create-near-app]: https://github.com/near/create-near-app 105 | [Node.js]: https://nodejs.org/en/download/package-manager/ 106 | [jest]: https://jestjs.io/ 107 | [NEAR accounts]: https://docs.near.org/docs/concepts/account 108 | [NEAR Wallet]: https://wallet.testnet.near.org/ 109 | [near-cli]: https://github.com/near/near-cli 110 | [gh-pages]: https://github.com/tschaub/gh-pages 111 | -------------------------------------------------------------------------------- /contract/.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-C", "link-args=-s"] 3 | -------------------------------------------------------------------------------- /contract/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /contract/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "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 = "4.0.0" 12 | serde_json = "1.0" 13 | 14 | [profile.release] 15 | codegen-units=1 16 | opt-level = "z" 17 | lto = true 18 | debug = false 19 | panic = "abort" 20 | overflow-checks = true 21 | -------------------------------------------------------------------------------- /contract/README.md: -------------------------------------------------------------------------------- 1 | nft-mint-frontend Smart Contract 2 | ================== 3 | 4 | A [smart contract] written in [Rust] for an app initialized with [create-near-app] 5 | 6 | 7 | Quick Start 8 | =========== 9 | 10 | Before you compile this code, you will need to install Rust with [correct target] 11 | 12 | 13 | Exploring The Code 14 | ================== 15 | 16 | 1. The main smart contract code lives in `src/lib.rs`. You can compile it with 17 | the `./compile` script. 18 | 2. Tests: You can run smart contract tests with the `./test` script. This runs 19 | standard Rust tests using [cargo] with a `--nocapture` flag so that you 20 | can see any debug info you print to the console. 21 | 22 | 23 | [smart contract]: https://docs.near.org/develop/welcome 24 | [Rust]: https://www.rust-lang.org/ 25 | [create-near-app]: https://github.com/near/create-near-app 26 | [correct target]: https://github.com/near/near-sdk-rs#pre-requisites 27 | [cargo]: https://doc.rust-lang.org/book/ch01-03-hello-cargo.html 28 | -------------------------------------------------------------------------------- /contract/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release 5 | mkdir -p ../out 6 | cp target/wasm32-unknown-unknown/release/*.wasm ../out/main.wasm -------------------------------------------------------------------------------- /contract/res/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/near-examples/nft-tutorial-frontend/c178c19f6cceb411718abb7dbbc8dd85251b5dee/contract/res/.gitkeep -------------------------------------------------------------------------------- /contract/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 | 38 | //allow a specific account ID to approve a token on your behalf 39 | #[payable] 40 | fn nft_approve(&mut self, token_id: TokenId, account_id: AccountId, msg: Option) { 41 | /* 42 | assert at least one yocto for security reasons - this will cause a redirect to the NEAR wallet. 43 | The user needs to attach enough to pay for storage on the contract 44 | */ 45 | assert_at_least_one_yocto(); 46 | 47 | //get the token object from the token ID 48 | let mut token = self.tokens_by_id.get(&token_id).expect("No token"); 49 | 50 | //make sure that the person calling the function is the owner of the token 51 | assert_eq!( 52 | &env::predecessor_account_id(), 53 | &token.owner_id, 54 | "Predecessor must be the token owner." 55 | ); 56 | 57 | //get the next approval ID if we need a new approval 58 | let approval_id: u64 = token.next_approval_id; 59 | 60 | //check if the account has been approved already for this token 61 | let is_new_approval = token 62 | .approved_account_ids 63 | //insert returns none if the key was not present. 64 | .insert(account_id.clone(), approval_id) 65 | //if the key was not present, .is_none() will return true so it is a new approval. 66 | .is_none(); 67 | 68 | //if it was a new approval, we need to calculate how much storage is being used to add the account. 69 | let storage_used = if is_new_approval { 70 | bytes_for_approved_account_id(&account_id) 71 | //if it was not a new approval, we used no storage. 72 | } else { 73 | 0 74 | }; 75 | 76 | //increment the token's next approval ID by 1 77 | token.next_approval_id += 1; 78 | //insert the token back into the tokens_by_id collection 79 | self.tokens_by_id.insert(&token_id, &token); 80 | 81 | //refund any excess storage attached by the user. If the user didn't attach enough, panic. 82 | refund_deposit(storage_used); 83 | 84 | //if some message was passed into the function, we initiate a cross contract call on the 85 | //account we're giving access to. 86 | if let Some(msg) = msg { 87 | // Defaulting GAS weight to 1, no attached deposit, and no static GAS to attach. 88 | ext_non_fungible_approval_receiver::ext(account_id) 89 | .nft_on_approve( 90 | token_id, 91 | token.owner_id, 92 | approval_id, 93 | msg 94 | ).as_return(); 95 | } 96 | } 97 | 98 | //check if the passed in account has access to approve the token ID 99 | fn nft_is_approved( 100 | &self, 101 | token_id: TokenId, 102 | approved_account_id: AccountId, 103 | approval_id: Option, 104 | ) -> bool { 105 | //get the token object from the token_id 106 | let token = self.tokens_by_id.get(&token_id).expect("No token"); 107 | 108 | //get the approval number for the passed in account ID 109 | let approval = token.approved_account_ids.get(&approved_account_id); 110 | 111 | //if there was some approval ID found for the account ID 112 | if let Some(approval) = approval { 113 | //if a specific approval_id was passed into the function 114 | if let Some(approval_id) = approval_id { 115 | //return if the approval ID passed in matches the actual approval ID for the account 116 | approval_id == *approval 117 | //if there was no approval_id passed into the function, we simply return true 118 | } else { 119 | true 120 | } 121 | //if there was no approval ID found for the account ID, we simply return false 122 | } else { 123 | false 124 | } 125 | } 126 | 127 | //revoke a specific account from transferring the token on your behalf 128 | #[payable] 129 | fn nft_revoke(&mut self, token_id: TokenId, account_id: AccountId) { 130 | //assert that the user attached exactly 1 yoctoNEAR for security reasons 131 | assert_one_yocto(); 132 | //get the token object using the passed in token_id 133 | let mut token = self.tokens_by_id.get(&token_id).expect("No token"); 134 | 135 | //get the caller of the function and assert that they are the owner of the token 136 | let predecessor_account_id = env::predecessor_account_id(); 137 | assert_eq!(&predecessor_account_id, &token.owner_id); 138 | 139 | //if the account ID was in the token's approval, we remove it and the if statement logic executes 140 | if token 141 | .approved_account_ids 142 | .remove(&account_id) 143 | .is_some() 144 | { 145 | //refund the funds released by removing the approved_account_id to the caller of the function 146 | refund_approved_account_ids_iter(predecessor_account_id, [account_id].iter()); 147 | 148 | //insert the token back into the tokens_by_id collection with the account_id removed from the approval list 149 | self.tokens_by_id.insert(&token_id, &token); 150 | } 151 | } 152 | 153 | //revoke all accounts from transferring the token on your behalf 154 | #[payable] 155 | fn nft_revoke_all(&mut self, token_id: TokenId) { 156 | //assert that the caller attached exactly 1 yoctoNEAR for security 157 | assert_one_yocto(); 158 | 159 | //get the token object from the passed in token ID 160 | let mut token = self.tokens_by_id.get(&token_id).expect("No token"); 161 | //get the caller and make sure they are the owner of the tokens 162 | let predecessor_account_id = env::predecessor_account_id(); 163 | assert_eq!(&predecessor_account_id, &token.owner_id); 164 | 165 | //only revoke if the approved account IDs for the token is not empty 166 | if !token.approved_account_ids.is_empty() { 167 | //refund the approved account IDs to the caller of the function 168 | refund_approved_account_ids(predecessor_account_id, &token.approved_account_ids); 169 | //clear the approved account IDs 170 | token.approved_account_ids.clear(); 171 | //insert the token back into the tokens_by_id collection with the approved account IDs cleared 172 | self.tokens_by_id.insert(&token_id, &token); 173 | } 174 | } 175 | } -------------------------------------------------------------------------------- /contract/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() as u128) 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.keys() 18 | //skip to the index we specified in the start variable 19 | .skip(start as usize) 20 | //take the first "limit" elements in the vector. If we didn't specify a limit, use 50 21 | .take(limit.unwrap_or(50) as usize) 22 | //we'll map the token IDs which are strings into Json Tokens 23 | .map(|token_id| self.nft_token(token_id.clone()).unwrap()) 24 | //since we turned the keys into an iterator, we need to turn it back into a vector to return 25 | .collect() 26 | } 27 | 28 | //get the total supply of NFTs for a given owner 29 | pub fn nft_supply_for_owner( 30 | &self, 31 | account_id: AccountId, 32 | ) -> U128 { 33 | //get the set of tokens for the passed in owner 34 | let tokens_for_owner_set = self.tokens_per_owner.get(&account_id); 35 | 36 | //if there is some set of tokens, we'll return the length as a U128 37 | if let Some(tokens_for_owner_set) = tokens_for_owner_set { 38 | U128(tokens_for_owner_set.len() as u128) 39 | } else { 40 | //if there isn't a set of tokens for the passed in account ID, we'll return 0 41 | U128(0) 42 | } 43 | } 44 | 45 | //Query for all the tokens for an owner 46 | pub fn nft_tokens_for_owner( 47 | &self, 48 | account_id: AccountId, 49 | from_index: Option, 50 | limit: Option, 51 | ) -> Vec { 52 | //get the set of tokens for the passed in owner 53 | let tokens_for_owner_set = self.tokens_per_owner.get(&account_id); 54 | //if there is some set of tokens, we'll set the tokens variable equal to that set 55 | let tokens = if let Some(tokens_for_owner_set) = tokens_for_owner_set { 56 | tokens_for_owner_set 57 | } else { 58 | //if there is no set of tokens, we'll simply return an empty vector. 59 | return vec![]; 60 | }; 61 | 62 | //where to start pagination - if we have a from_index, we'll use that - otherwise start from 0 index 63 | let start = u128::from(from_index.unwrap_or(U128(0))); 64 | 65 | //iterate through the keys vector 66 | tokens.iter() 67 | //skip to the index we specified in the start variable 68 | .skip(start as usize) 69 | //take the first "limit" elements in the vector. If we didn't specify a limit, use 50 70 | .take(limit.unwrap_or(50) as usize) 71 | //we'll map the token IDs which are strings into Json Tokens 72 | .map(|token_id| self.nft_token(token_id.clone()).unwrap()) 73 | //since we turned the keys into an iterator, we need to turn it back into a vector to return 74 | .collect() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /contract/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 | } -------------------------------------------------------------------------------- /contract/src/internal.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use near_sdk::{CryptoHash}; 3 | use std::mem::size_of; 4 | 5 | //convert the royalty percentage and amount to pay into a payout (U128) 6 | pub(crate) fn royalty_to_payout(royalty_percentage: u32, amount_to_pay: Balance) -> U128 { 7 | U128(royalty_percentage as u128 * amount_to_pay / 10_000u128) 8 | } 9 | 10 | //calculate how many bytes the account ID is taking up 11 | pub(crate) fn bytes_for_approved_account_id(account_id: &AccountId) -> u64 { 12 | // The extra 4 bytes are coming from Borsh serialization to store the length of the string. 13 | account_id.as_str().len() as u64 + 4 + size_of::() as u64 14 | } 15 | 16 | //refund the storage taken up by passed in approved account IDs and send the funds to the passed in account ID. 17 | pub(crate) fn refund_approved_account_ids_iter<'a, I>( 18 | account_id: AccountId, 19 | approved_account_ids: I, //the approved account IDs must be passed in as an iterator 20 | ) -> Promise 21 | where 22 | I: Iterator, 23 | { 24 | //get the storage total by going through and summing all the bytes for each approved account IDs 25 | let storage_released: u64 = approved_account_ids.map(bytes_for_approved_account_id).sum(); 26 | //transfer the account the storage that is released 27 | Promise::new(account_id).transfer(Balance::from(storage_released) * env::storage_byte_cost()) 28 | } 29 | 30 | //refund a map of approved account IDs and send the funds to the passed in account ID 31 | pub(crate) fn refund_approved_account_ids( 32 | account_id: AccountId, 33 | approved_account_ids: &HashMap, 34 | ) -> Promise { 35 | //call the refund_approved_account_ids_iter with the approved account IDs as keys 36 | refund_approved_account_ids_iter(account_id, approved_account_ids.keys()) 37 | } 38 | 39 | //used to generate a unique prefix in our storage collections (this is to avoid data collisions) 40 | pub(crate) fn hash_account_id(account_id: &AccountId) -> CryptoHash { 41 | //get the default hash 42 | let mut hash = CryptoHash::default(); 43 | //we hash the account ID and return it 44 | hash.copy_from_slice(&env::sha256(account_id.as_bytes())); 45 | hash 46 | } 47 | 48 | //used to make sure the user attached exactly 1 yoctoNEAR 49 | pub(crate) fn assert_one_yocto() { 50 | assert_eq!( 51 | env::attached_deposit(), 52 | 1, 53 | "Requires attached deposit of exactly 1 yoctoNEAR", 54 | ) 55 | } 56 | 57 | //Assert that the user has attached at least 1 yoctoNEAR (for security reasons and to pay for storage) 58 | pub(crate) fn assert_at_least_one_yocto() { 59 | assert!( 60 | env::attached_deposit() >= 1, 61 | "Requires attached deposit of at least 1 yoctoNEAR", 62 | ) 63 | } 64 | 65 | //refund the initial deposit based on the amount of storage that was used up 66 | pub(crate) fn refund_deposit(storage_used: u64) { 67 | //get how much it would cost to store the information 68 | let required_cost = env::storage_byte_cost() * Balance::from(storage_used); 69 | //get the attached deposit 70 | let attached_deposit = env::attached_deposit(); 71 | 72 | //make sure that the attached deposit is greater than or equal to the required cost 73 | assert!( 74 | required_cost <= attached_deposit, 75 | "Must attach {} yoctoNEAR to cover storage", 76 | required_cost, 77 | ); 78 | 79 | //get the refund amount from the attached deposit - required cost 80 | let refund = attached_deposit - required_cost; 81 | 82 | //if the refund is greater than 1 yocto NEAR, we refund the predecessor that amount 83 | if refund > 1 { 84 | Promise::new(env::predecessor_account_id()).transfer(refund); 85 | } 86 | } 87 | 88 | impl Contract { 89 | //add a token to the set of tokens an owner has 90 | pub(crate) fn internal_add_token_to_owner( 91 | &mut self, 92 | account_id: &AccountId, 93 | token_id: &TokenId, 94 | ) { 95 | //get the set of tokens for the given account 96 | let mut tokens_set = self.tokens_per_owner.get(account_id).unwrap_or_else(|| { 97 | //if the account doesn't have any tokens, we create a new unordered set 98 | UnorderedSet::new( 99 | StorageKey::TokenPerOwnerInner { 100 | //we get a new unique prefix for the collection 101 | account_id_hash: hash_account_id(&account_id), 102 | } 103 | .try_to_vec() 104 | .unwrap(), 105 | ) 106 | }); 107 | 108 | //we insert the token ID into the set 109 | tokens_set.insert(token_id); 110 | 111 | //we insert that set for the given account ID. 112 | self.tokens_per_owner.insert(account_id, &tokens_set); 113 | } 114 | 115 | //remove a token from an owner (internal method and can't be called directly via CLI). 116 | pub(crate) fn internal_remove_token_from_owner( 117 | &mut self, 118 | account_id: &AccountId, 119 | token_id: &TokenId, 120 | ) { 121 | //we get the set of tokens that the owner has 122 | let mut tokens_set = self 123 | .tokens_per_owner 124 | .get(account_id) 125 | //if there is no set of tokens for the owner, we panic with the following message: 126 | .expect("Token should be owned by the sender"); 127 | 128 | //we remove the the token_id from the set of tokens 129 | tokens_set.remove(token_id); 130 | 131 | //if the token set is now empty, we remove the owner from the tokens_per_owner collection 132 | if tokens_set.is_empty() { 133 | self.tokens_per_owner.remove(account_id); 134 | } else { 135 | //if the token set is not empty, we simply insert it back for the account ID. 136 | self.tokens_per_owner.insert(account_id, &tokens_set); 137 | } 138 | } 139 | 140 | //transfers the NFT to the receiver_id (internal method and can't be called directly via CLI). 141 | pub(crate) fn internal_transfer( 142 | &mut self, 143 | sender_id: &AccountId, 144 | receiver_id: &AccountId, 145 | token_id: &TokenId, 146 | //we introduce an approval ID so that people with that approval ID can transfer the token 147 | approval_id: Option, 148 | memo: Option, 149 | ) -> Token { 150 | //get the token object by passing in the token_id 151 | let token = self.tokens_by_id.get(token_id).expect("No token"); 152 | 153 | //if the sender doesn't equal the owner, we check if the sender is in the approval list 154 | if sender_id != &token.owner_id { 155 | //if the token's approved account IDs doesn't contain the sender, we panic 156 | if !token.approved_account_ids.contains_key(sender_id) { 157 | env::panic_str("Unauthorized"); 158 | } 159 | 160 | // If they included an approval_id, check if the sender's actual approval_id is the same as the one included 161 | if let Some(enforced_approval_id) = approval_id { 162 | //get the actual approval ID 163 | let actual_approval_id = token 164 | .approved_account_ids 165 | .get(sender_id) 166 | //if the sender isn't in the map, we panic 167 | .expect("Sender is not approved account"); 168 | 169 | //make sure that the actual approval ID is the same as the one provided 170 | assert_eq!( 171 | actual_approval_id, &enforced_approval_id, 172 | "The actual approval_id {} is different from the given approval_id {}", 173 | actual_approval_id, enforced_approval_id, 174 | ); 175 | } 176 | } 177 | 178 | //we make sure that the sender isn't sending the token to themselves 179 | assert_ne!( 180 | &token.owner_id, receiver_id, 181 | "The token owner and the receiver should be different" 182 | ); 183 | 184 | //we remove the token from it's current owner's set 185 | self.internal_remove_token_from_owner(&token.owner_id, token_id); 186 | //we then add the token to the receiver_id's set 187 | self.internal_add_token_to_owner(receiver_id, token_id); 188 | 189 | //we create a new token struct 190 | let new_token = Token { 191 | owner_id: receiver_id.clone(), 192 | //reset the approval account IDs 193 | approved_account_ids: Default::default(), 194 | next_approval_id: token.next_approval_id, 195 | //we copy over the royalties from the previous token 196 | royalty: token.royalty.clone(), 197 | }; 198 | //insert that new token into the tokens_by_id, replacing the old entry 199 | self.tokens_by_id.insert(token_id, &new_token); 200 | 201 | //if there was some memo attached, we log it. 202 | if let Some(memo) = memo.as_ref() { 203 | env::log_str(&format!("Memo: {}", memo).to_string()); 204 | } 205 | 206 | // Default the authorized ID to be None for the logs. 207 | let mut authorized_id = None; 208 | //if the approval ID was provided, set the authorized ID equal to the sender 209 | if approval_id.is_some() { 210 | authorized_id = Some(sender_id.to_string()); 211 | } 212 | 213 | // Construct the transfer log as per the events standard. 214 | let nft_transfer_log: EventLog = EventLog { 215 | // Standard name ("nep171"). 216 | standard: NFT_STANDARD_NAME.to_string(), 217 | // Version of the standard ("nft-1.0.0"). 218 | version: NFT_METADATA_SPEC.to_string(), 219 | // The data related with the event stored in a vector. 220 | event: EventLogVariant::NftTransfer(vec![NftTransferLog { 221 | // The optional authorized account ID to transfer the token on behalf of the old owner. 222 | authorized_id, 223 | // The old owner's account ID. 224 | old_owner_id: token.owner_id.to_string(), 225 | // The account ID of the new owner of the token. 226 | new_owner_id: receiver_id.to_string(), 227 | // A vector containing the token IDs as strings. 228 | token_ids: vec![token_id.to_string()], 229 | // An optional memo to include. 230 | memo, 231 | }]), 232 | }; 233 | 234 | // Log the serialized json. 235 | env::log_str(&nft_transfer_log.to_string()); 236 | 237 | //return the previous token object that was transferred. 238 | token 239 | } 240 | } -------------------------------------------------------------------------------- /contract/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, U128}; 5 | use near_sdk::serde::{Deserialize, Serialize}; 6 | use near_sdk::{ 7 | env, near_bindgen, AccountId, Balance, CryptoHash, PanicOnDefault, Promise, PromiseOrValue, 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 approval; 20 | mod enumeration; 21 | mod metadata; 22 | mod mint; 23 | mod nft_core; 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 = "nft-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 | #[near_bindgen] 33 | #[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] 34 | pub struct Contract { 35 | //contract owner 36 | pub owner_id: AccountId, 37 | 38 | //keeps track of all the token IDs for a given account 39 | pub tokens_per_owner: LookupMap>, 40 | 41 | //keeps track of the token struct for a given token ID 42 | pub tokens_by_id: LookupMap, 43 | 44 | //keeps track of the token metadata for a given token ID 45 | pub token_metadata_by_id: UnorderedMap, 46 | 47 | //keeps track of the metadata for the contract 48 | pub metadata: LazyOption, 49 | } 50 | 51 | /// Helper structure for keys of the persistent collections. 52 | #[derive(BorshSerialize)] 53 | pub enum StorageKey { 54 | TokensPerOwner, 55 | TokenPerOwnerInner { account_id_hash: CryptoHash }, 56 | TokensById, 57 | TokenMetadataById, 58 | NFTContractMetadata, 59 | TokensPerType, 60 | TokensPerTypeInner { token_type_hash: CryptoHash }, 61 | TokenTypesLocked, 62 | } 63 | 64 | #[near_bindgen] 65 | impl Contract { 66 | /* 67 | initialization function (can only be called once). 68 | this initializes the contract with default metadata so the 69 | user doesn't have to manually type metadata. 70 | */ 71 | #[init] 72 | pub fn new_default_meta(owner_id: AccountId) -> Self { 73 | //calls the other function "new: with some default metadata and the owner_id passed in 74 | Self::new( 75 | owner_id, 76 | NFTContractMetadata { 77 | spec: "nft-1.0.0".to_string(), 78 | name: "NFT Tutorial Contract".to_string(), 79 | symbol: "GOTEAM".to_string(), 80 | icon: None, 81 | base_uri: None, 82 | reference: None, 83 | reference_hash: None, 84 | }, 85 | ) 86 | } 87 | 88 | /* 89 | initialization function (can only be called once). 90 | this initializes the contract with metadata that was passed in and 91 | the owner_id. 92 | */ 93 | #[init] 94 | pub fn new(owner_id: AccountId, metadata: NFTContractMetadata) -> Self { 95 | //create a variable of type Self with all the fields initialized. 96 | let this = Self { 97 | //Storage keys are simply the prefixes used for the collections. This helps avoid data collision 98 | tokens_per_owner: LookupMap::new(StorageKey::TokensPerOwner.try_to_vec().unwrap()), 99 | tokens_by_id: LookupMap::new(StorageKey::TokensById.try_to_vec().unwrap()), 100 | token_metadata_by_id: UnorderedMap::new( 101 | StorageKey::TokenMetadataById.try_to_vec().unwrap(), 102 | ), 103 | //set the owner_id field equal to the passed in owner_id. 104 | owner_id, 105 | metadata: LazyOption::new( 106 | StorageKey::NFTContractMetadata.try_to_vec().unwrap(), 107 | Some(&metadata), 108 | ), 109 | }; 110 | 111 | //return the Contract object 112 | this 113 | } 114 | } -------------------------------------------------------------------------------- /contract/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)] 5 | #[serde(crate = "near_sdk::serde")] 6 | pub struct Payout { 7 | pub payout: HashMap, 8 | } 9 | 10 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, Clone)] 11 | #[serde(crate = "near_sdk::serde")] 12 | pub struct NFTContractMetadata { 13 | pub spec: String, // required, essentially a version like "nft-1.0.0" 14 | pub name: String, // required, ex. "Mosaics" 15 | pub symbol: String, // required, ex. "MOSAIC" 16 | pub icon: Option, // Data URL 17 | pub base_uri: Option, // Centralized gateway known to have reliable access to decentralized storage assets referenced by `reference` or `media` URLs 18 | pub reference: Option, // URL to a JSON file with more info 19 | pub reference_hash: Option, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. 20 | } 21 | 22 | #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] 23 | #[serde(crate = "near_sdk::serde")] 24 | pub struct TokenMetadata { 25 | pub title: Option, // ex. "Arch Nemesis: Mail Carrier" or "Parcel #5055" 26 | pub description: Option, // free-form description 27 | pub media: Option, // URL to associated media, preferably to decentralized, content-addressed storage 28 | pub media_hash: Option, // Base64-encoded sha256 hash of content referenced by the `media` field. Required if `media` is included. 29 | pub copies: Option, // number of copies of this set of metadata in existence when token was minted. 30 | pub issued_at: Option, // When token was issued or minted, Unix epoch in milliseconds 31 | pub expires_at: Option, // When token expires, Unix epoch in milliseconds 32 | pub starts_at: Option, // When token starts being valid, Unix epoch in milliseconds 33 | pub updated_at: Option, // When token was last updated, Unix epoch in milliseconds 34 | pub extra: Option, // anything extra the NFT wants to store on-chain. Can be stringified JSON. 35 | pub reference: Option, // URL to an off-chain JSON file with more info. 36 | pub reference_hash: Option, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. 37 | } 38 | 39 | #[derive(BorshDeserialize, BorshSerialize)] 40 | pub struct Token { 41 | //owner of the token 42 | pub owner_id: AccountId, 43 | //list of approved account IDs that have access to transfer the token. This maps an account ID to an approval ID 44 | pub approved_account_ids: HashMap, 45 | //the next approval ID to give out. 46 | pub next_approval_id: u64, 47 | //keep track of the royalty percentages for the token in a hash map 48 | pub royalty: HashMap, 49 | } 50 | 51 | //The Json token is what will be returned from view calls. 52 | #[derive(Serialize, Deserialize)] 53 | #[serde(crate = "near_sdk::serde")] 54 | pub struct JsonToken { 55 | //token ID 56 | pub token_id: TokenId, 57 | //owner of the token 58 | pub owner_id: AccountId, 59 | //token metadata 60 | pub metadata: TokenMetadata, 61 | //list of approved account IDs that have access to transfer the token. This maps an account ID to an approval ID 62 | pub approved_account_ids: HashMap, 63 | //keep track of the royalty percentages for the token in a hash map 64 | pub royalty: HashMap, 65 | } 66 | 67 | pub trait NonFungibleTokenMetadata { 68 | //view call for returning the contract metadata 69 | fn nft_metadata(&self) -> NFTContractMetadata; 70 | } 71 | 72 | #[near_bindgen] 73 | impl NonFungibleTokenMetadata for Contract { 74 | fn nft_metadata(&self) -> NFTContractMetadata { 75 | self.metadata.get().unwrap() 76 | } 77 | } -------------------------------------------------------------------------------- /contract/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 | metadata: TokenMetadata, 10 | receiver_id: AccountId, 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!(perpetual_royalties.len() < 7, "Cannot add more than 6 perpetual royalty amounts"); 24 | 25 | //iterate through the perpetual royalties and insert the account and amount in the royalty map 26 | for (account, amount) in perpetual_royalties { 27 | royalty.insert(account, amount); 28 | } 29 | } 30 | 31 | //specify the token struct that contains the owner ID 32 | let token = Token { 33 | //set the owner ID equal to the receiver ID passed into the function 34 | owner_id: receiver_id, 35 | //we set the approved account IDs to the default value (an empty map) 36 | approved_account_ids: Default::default(), 37 | //the next approval ID is set to 0 38 | next_approval_id: 0, 39 | //the map of perpetual royalties for the token (The owner will get 100% - total perpetual royalties) 40 | royalty, 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, &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); 80 | } 81 | } -------------------------------------------------------------------------------- /contract/src/nft_core.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use near_sdk::{ext_contract, Gas, PromiseResult}; 3 | 4 | const GAS_FOR_RESOLVE_TRANSFER: Gas = Gas(10_000_000_000_000); 5 | const GAS_FOR_NFT_ON_TRANSFER: Gas = Gas(25_000_000_000_000); 6 | 7 | pub trait NonFungibleTokenCore { 8 | //transfers an NFT to a receiver ID 9 | fn nft_transfer( 10 | &mut self, 11 | receiver_id: AccountId, 12 | token_id: TokenId, 13 | //we introduce an approval ID so that people with that approval ID can transfer the token 14 | approval_id: Option, 15 | memo: Option, 16 | ); 17 | 18 | //transfers an NFT to a receiver and calls a function on the receiver ID's contract 19 | /// Returns `true` if the token was transferred from the sender's account. 20 | fn nft_transfer_call( 21 | &mut self, 22 | receiver_id: AccountId, 23 | token_id: TokenId, 24 | //we introduce an approval ID so that people with that approval ID can transfer the token 25 | approval_id: Option, 26 | memo: Option, 27 | msg: String, 28 | ) -> PromiseOrValue; 29 | 30 | //get information about the NFT token passed in 31 | fn nft_token(&self, token_id: TokenId) -> Option; 32 | } 33 | 34 | #[ext_contract(ext_non_fungible_token_receiver)] 35 | trait NonFungibleTokenReceiver { 36 | //Method stored on the receiver contract that is called via cross contract call when nft_transfer_call is called 37 | /// Returns `true` if the token should be returned back to the sender. 38 | fn nft_on_transfer( 39 | &mut self, 40 | sender_id: AccountId, 41 | previous_owner_id: AccountId, 42 | token_id: TokenId, 43 | msg: String, 44 | ) -> Promise; 45 | } 46 | 47 | #[ext_contract(ext_self)] 48 | /* 49 | resolves the promise of the cross contract call to the receiver contract 50 | this is stored on THIS contract and is meant to analyze what happened in the cross contract call when nft_on_transfer was called 51 | as part of the nft_transfer_call method 52 | */ 53 | trait NonFungibleTokenResolver { 54 | fn nft_resolve_transfer( 55 | &mut self, 56 | //we introduce an authorized ID for logging the transfer event 57 | authorized_id: Option, 58 | owner_id: AccountId, 59 | receiver_id: AccountId, 60 | token_id: TokenId, 61 | //we introduce the approval map so we can keep track of what the approvals were before the transfer 62 | approved_account_ids: HashMap, 63 | //we introduce a memo for logging the transfer event 64 | memo: Option, 65 | ) -> bool; 66 | } 67 | 68 | #[near_bindgen] 69 | impl NonFungibleTokenCore for Contract { 70 | 71 | //implementation of the nft_transfer method. This transfers the NFT from the current owner to the receiver. 72 | #[payable] 73 | fn nft_transfer( 74 | &mut self, 75 | receiver_id: AccountId, 76 | token_id: TokenId, 77 | //we introduce an approval ID so that people with that approval ID can transfer the token 78 | approval_id: Option, 79 | memo: Option, 80 | ) { 81 | //assert that the user attached exactly 1 yoctoNEAR. This is for security and so that the user will be redirected to the NEAR wallet. 82 | assert_one_yocto(); 83 | //get the sender to transfer the token from the sender to the receiver 84 | let sender_id = env::predecessor_account_id(); 85 | 86 | //call the internal transfer method and get back the previous token so we can refund the approved account IDs 87 | let previous_token = self.internal_transfer( 88 | &sender_id, 89 | &receiver_id, 90 | &token_id, 91 | approval_id, 92 | memo, 93 | ); 94 | 95 | //we refund the owner for releasing the storage used up by the approved account IDs 96 | refund_approved_account_ids( 97 | previous_token.owner_id.clone(), 98 | &previous_token.approved_account_ids, 99 | ); 100 | } 101 | 102 | //implementation of the transfer call method. This will transfer the NFT and call a method on the receiver_id contract 103 | #[payable] 104 | fn nft_transfer_call( 105 | &mut self, 106 | receiver_id: AccountId, 107 | token_id: TokenId, 108 | //we introduce an approval ID so that people with that approval ID can transfer the token 109 | approval_id: Option, 110 | memo: Option, 111 | msg: String, 112 | ) -> PromiseOrValue { 113 | //assert that the user attached exactly 1 yocto for security reasons. 114 | assert_one_yocto(); 115 | 116 | //get the sender ID 117 | let sender_id = env::predecessor_account_id(); 118 | 119 | //transfer the token and get the previous token object 120 | let previous_token = self.internal_transfer( 121 | &sender_id, 122 | &receiver_id, 123 | &token_id, 124 | approval_id, 125 | memo.clone(), 126 | ); 127 | 128 | //default the authorized_id to none 129 | let mut authorized_id = None; 130 | //if the sender isn't the owner of the token, we set the authorized ID equal to the sender. 131 | if sender_id != previous_token.owner_id { 132 | authorized_id = Some(sender_id.to_string()); 133 | } 134 | 135 | // Initiating receiver's call and the callback 136 | // Defaulting GAS weight to 1, no attached deposit, and static GAS equal to the GAS for nft on transfer. 137 | ext_non_fungible_token_receiver::ext(receiver_id.clone()) 138 | .with_static_gas(GAS_FOR_NFT_ON_TRANSFER) 139 | .nft_on_transfer( 140 | sender_id, 141 | previous_token.owner_id.clone(), 142 | token_id.clone(), 143 | msg 144 | ) 145 | // We then resolve the promise and call nft_resolve_transfer on our own contract 146 | .then( 147 | // Defaulting GAS weight to 1, no attached deposit, and static GAS equal to the GAS for resolve transfer 148 | Self::ext(env::current_account_id()) 149 | .with_static_gas(GAS_FOR_RESOLVE_TRANSFER) 150 | .nft_resolve_transfer( 151 | authorized_id, // we introduce an authorized ID so that we can log the transfer 152 | previous_token.owner_id, 153 | receiver_id, 154 | token_id, 155 | previous_token.approved_account_ids, 156 | memo, // we introduce a memo for logging in the events standard 157 | ) 158 | ).into() 159 | } 160 | 161 | //get the information for a specific token ID 162 | fn nft_token(&self, token_id: TokenId) -> Option { 163 | //if there is some token ID in the tokens_by_id collection 164 | if let Some(token) = self.tokens_by_id.get(&token_id) { 165 | //we'll get the metadata for that token 166 | let metadata = self.token_metadata_by_id.get(&token_id).unwrap(); 167 | //we return the JsonToken (wrapped by Some since we return an option) 168 | Some(JsonToken { 169 | token_id, 170 | owner_id: token.owner_id, 171 | metadata, 172 | approved_account_ids: token.approved_account_ids, 173 | royalty: token.royalty, 174 | }) 175 | } else { //if there wasn't a token ID in the tokens_by_id collection, we return None 176 | None 177 | } 178 | } 179 | } 180 | 181 | #[near_bindgen] 182 | impl NonFungibleTokenResolver for Contract { 183 | //resolves the cross contract call when calling nft_on_transfer in the nft_transfer_call method 184 | //returns true if the token was successfully transferred to the receiver_id 185 | #[private] 186 | fn nft_resolve_transfer( 187 | &mut self, 188 | //we introduce an authorized ID for logging the transfer event 189 | authorized_id: Option, 190 | owner_id: AccountId, 191 | receiver_id: AccountId, 192 | token_id: TokenId, 193 | //we introduce the approval map so we can keep track of what the approvals were before the transfer 194 | approved_account_ids: HashMap, 195 | //we introduce a memo for logging the transfer event 196 | memo: Option, 197 | ) -> bool { 198 | // Whether receiver wants to return token back to the sender, based on `nft_on_transfer` 199 | // call result. 200 | if let PromiseResult::Successful(value) = env::promise_result(0) { 201 | //As per the standard, the nft_on_transfer should return whether we should return the token to it's owner or not 202 | if let Ok(return_token) = near_sdk::serde_json::from_slice::(&value) { 203 | //if we need don't need to return the token, we simply return true meaning everything went fine 204 | if !return_token { 205 | /* 206 | since we've already transferred the token and nft_on_transfer returned false, we don't have to 207 | revert the original transfer and thus we can just return true since nothing went wrong. 208 | */ 209 | //we refund the owner for releasing the storage used up by the approved account IDs 210 | refund_approved_account_ids(owner_id, &approved_account_ids); 211 | return true; 212 | } 213 | } 214 | } 215 | 216 | //get the token object if there is some token object 217 | let mut token = if let Some(token) = self.tokens_by_id.get(&token_id) { 218 | if token.owner_id != receiver_id { 219 | //we refund the owner for releasing the storage used up by the approved account IDs 220 | refund_approved_account_ids(owner_id, &approved_account_ids); 221 | // The token is not owner by the receiver anymore. Can't return it. 222 | return true; 223 | } 224 | token 225 | //if there isn't a token object, it was burned and so we return true 226 | } else { 227 | //we refund the owner for releasing the storage used up by the approved account IDs 228 | refund_approved_account_ids(owner_id, &approved_account_ids); 229 | return true; 230 | }; 231 | 232 | //we remove the token from the receiver 233 | self.internal_remove_token_from_owner(&receiver_id.clone(), &token_id); 234 | //we add the token to the original owner 235 | self.internal_add_token_to_owner(&owner_id, &token_id); 236 | 237 | //we change the token struct's owner to be the original owner 238 | token.owner_id = owner_id.clone(); 239 | 240 | //we refund the receiver any approved account IDs that they may have set on the token 241 | refund_approved_account_ids(receiver_id.clone(), &token.approved_account_ids); 242 | //reset the approved account IDs to what they were before the transfer 243 | token.approved_account_ids = approved_account_ids; 244 | 245 | //we inset the token back into the tokens_by_id collection 246 | self.tokens_by_id.insert(&token_id, &token); 247 | 248 | /* 249 | We need to log that the NFT was reverted back to the original owner. 250 | The old_owner_id will be the receiver and the new_owner_id will be the 251 | original owner of the token since we're reverting the transfer. 252 | */ 253 | let nft_transfer_log: EventLog = EventLog { 254 | // Standard name ("nep171"). 255 | standard: NFT_STANDARD_NAME.to_string(), 256 | // Version of the standard ("nft-1.0.0"). 257 | version: NFT_METADATA_SPEC.to_string(), 258 | // The data related with the event stored in a vector. 259 | event: EventLogVariant::NftTransfer(vec![NftTransferLog { 260 | // The optional authorized account ID to transfer the token on behalf of the old owner. 261 | authorized_id, 262 | // The old owner's account ID. 263 | old_owner_id: receiver_id.to_string(), 264 | // The account ID of the new owner of the token. 265 | new_owner_id: owner_id.to_string(), 266 | // A vector containing the token IDs as strings. 267 | token_ids: vec![token_id.to_string()], 268 | // An optional memo to include. 269 | memo, 270 | }]), 271 | }; 272 | 273 | //we perform the actual logging 274 | env::log_str(&nft_transfer_log.to_string()); 275 | 276 | //return false 277 | false 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /contract/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: U128, 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: U128, 15 | max_len_payout: u32, 16 | ) -> Payout; 17 | } 18 | 19 | #[near_bindgen] 20 | impl NonFungibleTokenCore for Contract { 21 | 22 | //calculates the payout for a token given the passed in balance. This is a view method 23 | fn nft_payout(&self, token_id: TokenId, balance: U128, max_len_payout: u32) -> Payout { 24 | //get the token object 25 | let token = self.tokens_by_id.get(&token_id).expect("No token"); 26 | 27 | //get the owner of the token 28 | let owner_id = token.owner_id; 29 | //keep track of the total perpetual royalties 30 | let mut total_perpetual = 0; 31 | //get the u128 version of the passed in balance (which was U128 before) 32 | let balance_u128 = u128::from(balance); 33 | //keep track of the payout object to send back 34 | let mut payout_object = Payout { 35 | payout: HashMap::new() 36 | }; 37 | //get the royalty object from token 38 | let royalty = token.royalty; 39 | 40 | //make sure we're not paying out to too many people (GAS limits this) 41 | assert!(royalty.len() as u32 <= max_len_payout, "Market cannot payout to that many receivers"); 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 | //only insert into the payout if the key isn't the token owner (we add their payout at the end) 48 | if key != owner_id { 49 | // 50 | payout_object.payout.insert(key, royalty_to_payout(*v, balance_u128)); 51 | total_perpetual += *v; 52 | } 53 | } 54 | 55 | // payout to previous owner who gets 100% - total perpetual royalties 56 | payout_object.payout.insert(owner_id, royalty_to_payout(10000 - total_perpetual, balance_u128)); 57 | 58 | //return the payout object 59 | payout_object 60 | } 61 | 62 | //transfers the token to the receiver ID and returns the payout object that should be payed given the passed in balance. 63 | #[payable] 64 | fn nft_transfer_payout( 65 | &mut self, 66 | receiver_id: AccountId, 67 | token_id: TokenId, 68 | approval_id: u64, 69 | memo: Option, 70 | balance: U128, 71 | max_len_payout: u32, 72 | ) -> Payout { 73 | //assert that the user attached 1 yocto NEAR for security reasons 74 | assert_one_yocto(); 75 | //get the sender ID 76 | let sender_id = env::predecessor_account_id(); 77 | //transfer the token to the passed in receiver and get the previous token object back 78 | let previous_token = self.internal_transfer( 79 | &sender_id, 80 | &receiver_id, 81 | &token_id, 82 | Some(approval_id), 83 | memo, 84 | ); 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 | //get the u128 version of the passed in balance (which was U128 before) 97 | let balance_u128 = u128::from(balance); 98 | //keep track of the payout object to send back 99 | let mut payout_object = Payout { 100 | payout: HashMap::new() 101 | }; 102 | //get the royalty object from token 103 | let royalty = previous_token.royalty; 104 | 105 | //make sure we're not paying out to too many people (GAS limits this) 106 | assert!(royalty.len() as u32 <= max_len_payout, "Market cannot payout to that many receivers"); 107 | 108 | //go through each key and value in the royalty object 109 | for (k, v) in royalty.iter() { 110 | //get the key 111 | let key = k.clone(); 112 | //only insert into the payout if the key isn't the token owner (we add their payout at the end) 113 | if key != owner_id { 114 | // 115 | payout_object.payout.insert(key, royalty_to_payout(*v, balance_u128)); 116 | total_perpetual += *v; 117 | } 118 | } 119 | 120 | // payout to previous owner who gets 100% - total perpetual royalties 121 | payout_object.payout.insert(owner_id, royalty_to_payout(10000 - total_perpetual, balance_u128)); 122 | 123 | //return the payout object 124 | payout_object 125 | } 126 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nft-mint-frontend", 3 | "version": "0.1.0", 4 | "license": "UNLICENSED", 5 | "scripts": { 6 | "build": "npm run build:contract && npm run build:web", 7 | "build:contract": "cd contract && ./build.sh ", 8 | "build:contract:debug": "cd contract && ./build.sh ", 9 | "build:web": "parcel build src/index.html --public-url ./", 10 | "dev:deploy:contract": "near dev-deploy", 11 | "deploy:contract": "near deploy", 12 | "deploy:pages": "gh-pages -d dist/", 13 | "deploy": "npm run build && npm run deploy:contract && npm run deploy:pages", 14 | "prestart": "npm run build:contract:debug && npm run dev:deploy:contract", 15 | "start": "echo The app is starting! It will automatically open in your browser when ready && env-cmd -f ./neardev/dev-account.env parcel src/index.html --open", 16 | "dev": "nodemon --watch contract/src -e rs --exec \"npm run start\"", 17 | "test": "npm run build:contract:debug && cd contract && cargo test -- --nocapture && cd .. && jest test --runInBand", 18 | "makecontract": "cd contract && ./build.sh && cd .. && ls && cd market-contract && ./build.sh && cd .." 19 | }, 20 | "devDependencies": { 21 | "@babel/core": "~7.14.0", 22 | "@babel/preset-env": "~7.14.0", 23 | "@babel/preset-react": "~7.13.13", 24 | "babel-jest": "~26.6.2", 25 | "env-cmd": "~10.1.0", 26 | "gh-pages": "~3.1.0", 27 | "jest": "~26.6.2", 28 | "jest-environment-node": "~26.6.2", 29 | "near-cli": "~2.1.1", 30 | "nodemon": "~2.0.3", 31 | "parcel-bundler": "~1.12.4", 32 | "react-test-renderer": "~17.0.1", 33 | "shelljs": "~0.8.4" 34 | }, 35 | "dependencies": { 36 | "bn.js": "^5.2.0", 37 | "bootstrap": "^5.1.3", 38 | "near-api-js": "~0.43.1", 39 | "react": "~17.0.1", 40 | "react-bootstrap": "^2.1.0", 41 | "react-dom": "~17.0.1", 42 | "regenerator-runtime": "~0.13.5" 43 | }, 44 | "resolutions": { 45 | "@babel/preset-env": "7.13.8" 46 | }, 47 | "jest": { 48 | "moduleNameMapper": { 49 | "\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/src/__mocks__/fileMock.js", 50 | "\\.(css|less)$": "/src/__mocks__/fileMock.js" 51 | }, 52 | "setupFiles": [ 53 | "/src/jest.init.js" 54 | ], 55 | "testEnvironment": "near-cli/test_environment", 56 | "testPathIgnorePatterns": [ 57 | "/contract/", 58 | "/node_modules/" 59 | ] 60 | }, 61 | "browserslist": { 62 | "production": [ 63 | ">0.2%", 64 | "not dead", 65 | "not op_mini all" 66 | ], 67 | "development": [ 68 | "last 1 chrome version", 69 | "last 1 firefox version", 70 | "last 1 safari version" 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import "regenerator-runtime/runtime"; 2 | import React, { useEffect, useState } from "react"; 3 | import { login, logout } from "./utils"; 4 | 5 | // React Bootstrap css 6 | import "bootstrap/dist/css/bootstrap.min.css"; 7 | 8 | // React Bootstraps imports 9 | import { Nav, Navbar, Container, Row, Card, Alert } from "react-bootstrap"; 10 | 11 | // Custom Components 12 | import MintingTool from "./Components/MintingTool"; 13 | import InfoBubble from "./Components/InfoBubble"; 14 | 15 | // assets 16 | import Logo from "./assets/logo-white.svg"; 17 | 18 | import getConfig from "./config"; 19 | const { networkId } = getConfig(process.env.NODE_ENV || "development"); 20 | 21 | export default function App() { 22 | const [userHasNFT, setuserHasNFT] = useState(false); 23 | 24 | useEffect(() => { 25 | const receivedNFT = async () => { 26 | console.log( 27 | await window.contract.check_token({ 28 | id: `${window.accountId}-go-team-token`, 29 | }) 30 | ); 31 | if (window.accountId !== "") { 32 | console.log( 33 | await window.contract.check_token({ 34 | id: `${window.accountId}-go-team-token`, 35 | }) 36 | ); 37 | 38 | setuserHasNFT( 39 | await window.contract.check_token({ 40 | id: `${window.accountId}-go-team-token`, 41 | }) 42 | ); 43 | } 44 | }; 45 | receivedNFT(); 46 | }, []); 47 | 48 | return ( 49 | 50 | {" "} 51 | 52 | 53 | 54 | {" "} 61 | NEAR Protocol 62 | 63 | 64 | 65 | 66 | 75 | 76 | 77 | 78 | 79 | {" "} 80 | 81 | 82 | Hello! We are going to mint an NFT and have it appear in your 83 | wallet! Sign in, mint your nft and head over to{" "} 84 | 85 | wallet.testnet.near.org 86 | {" "} 87 | to see your new "Go Team" NFT! 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /src/Components/InfoBubble.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Alert, Card, Button, Row } from "react-bootstrap"; 4 | import { login, logout } from "../utils"; 5 | 6 | const InfoBubble = (props) => { 7 | return ( 8 | 9 | Step 1: Hit this button to login! 10 | 11 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | InfoBubble.propTypes = {}; 23 | 24 | export default InfoBubble; 25 | -------------------------------------------------------------------------------- /src/Components/MintingTool.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Form, Button, Card, Container, Row, Alert } from "react-bootstrap"; 4 | import { keys } from "regenerator-runtime"; 5 | const BN = require("bn.js"); 6 | 7 | const MintingTool = (props) => { 8 | const mintNFT = async () => { 9 | await window.contract.nft_mint( 10 | { 11 | token_id: `${window.accountId}-go-team-token`, 12 | metadata: { 13 | title: "My Non Fungible Team Token", 14 | description: "The Team Most Certainly Goes :)", 15 | media: 16 | "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif", 17 | }, 18 | receiver_id: window.accountId, 19 | }, 20 | 300000000000000, // attached GAS (optional) 21 | new BN("1000000000000000000000000") 22 | ); 23 | }; 24 | 25 | return ( 26 | 27 | 28 | 29 |

30 | Step 2: After you have logged in, hit this button to mint your "Go 31 | Team" Token and go your{" "} 32 | wallet and see your 33 | NFT 34 |

35 |
36 | 37 | 44 | 45 | 46 | {console.log(props.userNFTStatus)} 47 | {props.userNFTStatus ? ( 48 | 49 |

50 | bruh/sis.... You have an NFT already. You can see it{" "} 51 | 52 | here! 53 | 54 | :) 55 |

56 |
57 | ) : null} 58 |
59 |
60 |
61 | ); 62 | }; 63 | 64 | MintingTool.propTypes = {}; 65 | 66 | export default MintingTool; 67 | -------------------------------------------------------------------------------- /src/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | // NOTE: This is used to mock resource imports in JSX for tests 2 | module.exports = '' 3 | 4 | -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/near-examples/nft-tutorial-frontend/c178c19f6cceb411718abb7dbbc8dd85251b5dee/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo-black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/logo-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/near_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/near_logo_wht.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const CONTRACT_NAME = "nft-frontend-simple-mint.blockhead.testnet"; 2 | 3 | function getConfig(env) { 4 | switch (env) { 5 | case "production": 6 | case "mainnet": 7 | return { 8 | networkId: "mainnet", 9 | nodeUrl: "https://rpc.mainnet.near.org", 10 | contractName: CONTRACT_NAME, 11 | walletUrl: "https://wallet.near.org", 12 | helperUrl: "https://helper.mainnet.near.org", 13 | explorerUrl: "https://explorer.mainnet.near.org", 14 | }; 15 | case "development": 16 | case "testnet": 17 | return { 18 | networkId: "testnet", 19 | nodeUrl: "https://rpc.testnet.near.org", 20 | contractName: CONTRACT_NAME, 21 | walletUrl: "https://wallet.testnet.near.org", 22 | helperUrl: "https://helper.testnet.near.org", 23 | explorerUrl: "https://explorer.testnet.near.org", 24 | }; 25 | case "betanet": 26 | return { 27 | networkId: "betanet", 28 | nodeUrl: "https://rpc.betanet.near.org", 29 | contractName: CONTRACT_NAME, 30 | walletUrl: "https://wallet.betanet.near.org", 31 | helperUrl: "https://helper.betanet.near.org", 32 | explorerUrl: "https://explorer.betanet.near.org", 33 | }; 34 | case "local": 35 | return { 36 | networkId: "local", 37 | nodeUrl: "http://localhost:3030", 38 | keyPath: `${process.env.HOME}/.near/validator_key.json`, 39 | walletUrl: "http://localhost:4000/wallet", 40 | contractName: CONTRACT_NAME, 41 | }; 42 | case "test": 43 | case "ci": 44 | return { 45 | networkId: "shared-test", 46 | nodeUrl: "https://rpc.ci-testnet.near.org", 47 | contractName: CONTRACT_NAME, 48 | masterAccount: "test.near", 49 | }; 50 | case "ci-betanet": 51 | return { 52 | networkId: "shared-test-staging", 53 | nodeUrl: "https://rpc.ci-betanet.near.org", 54 | contractName: CONTRACT_NAME, 55 | masterAccount: "test.near", 56 | }; 57 | default: 58 | throw Error( 59 | `Unconfigured environment '${env}'. Can be configured in src/config.js.` 60 | ); 61 | } 62 | } 63 | 64 | module.exports = getConfig; 65 | -------------------------------------------------------------------------------- /src/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html { 6 | --bg: #efefef; 7 | --fg: #1e1e1e; 8 | --gray: #555; 9 | --light-gray: #ccc; 10 | --shadow: #e6e6e6; 11 | --success: rgb(90, 206, 132); 12 | --primary: #FF585D; 13 | --secondary: #0072CE; 14 | 15 | background-color: var(--bg); 16 | color: var(--fg); 17 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif; 18 | font-size: calc(0.9em + 0.5vw); 19 | line-height: 1.3; 20 | } 21 | 22 | body { 23 | margin: 0; 24 | padding: 1em; 25 | } 26 | 27 | main { 28 | margin: 0 auto; 29 | max-width: 25em; 30 | } 31 | 32 | h1 { 33 | background-image: url(assets/logo-black.svg); 34 | background-position: center 1em; 35 | background-repeat: no-repeat; 36 | background-size: auto 1.5em; 37 | margin-top: 0; 38 | padding: 3.5em 0 0.5em; 39 | text-align: center; 40 | } 41 | 42 | a, 43 | .link { 44 | color: var(--primary); 45 | text-decoration: none; 46 | } 47 | a:hover, 48 | a:focus, 49 | .link:hover, 50 | .link:focus { 51 | text-decoration: underline; 52 | } 53 | a:active, 54 | .link:active { 55 | color: var(--secondary); 56 | } 57 | 58 | button, input { 59 | font: inherit; 60 | outline: none; 61 | } 62 | 63 | button { 64 | background-color: var(--secondary); 65 | border-radius: 5px; 66 | border: none; 67 | color: #efefef; 68 | cursor: pointer; 69 | padding: 0.3em 0.75em; 70 | transition: transform 30ms; 71 | } 72 | button:hover, button:focus { 73 | box-shadow: 0 0 10em rgba(255, 255, 255, 0.2) inset; 74 | } 75 | button:active { 76 | box-shadow: 0 0 10em rgba(0, 0, 0, 0.1) inset; 77 | } 78 | button.link { 79 | background: none; 80 | border: none; 81 | box-shadow: none; 82 | display: inline; 83 | } 84 | [disabled] button, button[disabled] { 85 | box-shadow: none; 86 | background-color: var(--light-gray); 87 | color: gray; 88 | cursor: not-allowed; 89 | transform: none; 90 | } 91 | [disabled] button { 92 | text-indent: -900em; 93 | width: 2em; 94 | position: relative; 95 | } 96 | [disabled] button:after { 97 | content: " "; 98 | display: block; 99 | width: 0.8em; 100 | height: 0.8em; 101 | border-radius: 50%; 102 | border: 2px solid #fff; 103 | border-color: var(--fg) transparent var(--fg) transparent; 104 | animation: loader 1.2s linear infinite; 105 | position: absolute; 106 | top: 0.45em; 107 | right: 0.5em; 108 | } 109 | @keyframes loader { 110 | 0% { transform: rotate(0deg) } 111 | 100% { transform: rotate(360deg) } 112 | } 113 | 114 | fieldset { 115 | border: none; 116 | padding: 2em 0; 117 | } 118 | 119 | input { 120 | background-color: var(--shadow); 121 | border: none; 122 | border-radius: 5px 0 0 5px; 123 | caret-color: var(--primary); 124 | color: inherit; 125 | padding: 0.25em 1em; 126 | } 127 | input::selection { 128 | background-color: var(--secondary); 129 | color: #efefef; 130 | } 131 | input:focus { 132 | box-shadow: 0 0 10em rgba(0, 0, 0, 0.02) inset; 133 | } 134 | 135 | code { 136 | color: var(--gray); 137 | } 138 | 139 | li { 140 | padding-bottom: 1em; 141 | } 142 | 143 | aside { 144 | animation: notify ease-in-out 10s; 145 | background-color: var(--shadow); 146 | border-radius: 5px; 147 | bottom: 0; 148 | font-size: 0.8em; 149 | margin: 1em; 150 | padding: 1em; 151 | position: fixed; 152 | transform: translateY(10em); 153 | right: 0; 154 | } 155 | aside footer { 156 | display: flex; 157 | font-size: 0.9em; 158 | justify-content: space-between; 159 | margin-top: 0.5em; 160 | } 161 | aside footer *:first-child { 162 | color: var(--success); 163 | } 164 | aside footer *:last-child { 165 | color: var(--gray); 166 | } 167 | @keyframes notify { 168 | 0% { transform: translateY(10em) } 169 | 5% { transform: translateY(0) } 170 | 95% { transform: translateY(0) } 171 | 100% { transform: translateY(10em) } 172 | } 173 | 174 | @media (prefers-color-scheme: dark) { 175 | html { 176 | --bg: #1e1e1e; 177 | --fg: #efefef; 178 | --gray: #aaa; 179 | --shadow: #2a2a2a; 180 | --light-gray: #444; 181 | } 182 | h1 { 183 | background-image: url(assets/logo-white.svg); 184 | } 185 | input:focus { 186 | box-shadow: 0 0 10em rgba(255, 255, 255, 0.02) inset; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Welcome to NEAR with React 9 | 10 | 11 | 12 |
13 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | import { initContract } from './utils' 5 | 6 | window.nearInitPromise = initContract() 7 | .then(() => { 8 | ReactDOM.render( 9 | , 10 | document.querySelector('#root') 11 | ) 12 | }) 13 | .catch(console.error) 14 | -------------------------------------------------------------------------------- /src/jest.init.js: -------------------------------------------------------------------------------- 1 | import 'regenerator-runtime/runtime' 2 | -------------------------------------------------------------------------------- /src/main.test.js: -------------------------------------------------------------------------------- 1 | beforeAll(async function () { 2 | // NOTE: nearlib and nearConfig are made available by near-cli/test_environment 3 | const near = await nearlib.connect(nearConfig) 4 | window.accountId = nearConfig.contractName 5 | window.contract = await near.loadContract(nearConfig.contractName, { 6 | viewMethods: ['get_greeting'], 7 | changeMethods: [], 8 | sender: window.accountId 9 | }) 10 | 11 | window.walletConnection = { 12 | requestSignIn() { 13 | }, 14 | signOut() { 15 | }, 16 | isSignedIn() { 17 | return true 18 | }, 19 | getAccountId() { 20 | return window.accountId 21 | } 22 | } 23 | }) 24 | 25 | test('get_greeting', async () => { 26 | const message = await window.contract.get_greeting({ account_id: window.accountId }) 27 | expect(message).toEqual('Hello') 28 | }) 29 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import { 2 | connect, 3 | Contract, 4 | keyStores, 5 | WalletConnection, 6 | Account, 7 | utils, 8 | } from "near-api-js"; 9 | import getConfig from "./config"; 10 | 11 | const nearConfig = getConfig(process.env.NODE_ENV || "development"); 12 | 13 | // Initialize contract & set global variables 14 | export async function initContract() { 15 | // Initialize connection to the NEAR testnet 16 | const near = await connect( 17 | Object.assign( 18 | { deps: { keyStore: new keyStores.BrowserLocalStorageKeyStore() } }, 19 | nearConfig 20 | ) 21 | ); 22 | 23 | window.near = near; 24 | // Initializing Wallet based Account. It can work with NEAR testnet wallet that 25 | // is hosted at https://wallet.testnet.near.org 26 | window.walletConnection = new WalletConnection(near); 27 | 28 | // Getting the Account ID. If still unauthorized, it's just empty string 29 | window.accountId = window.walletConnection.getAccountId(); 30 | 31 | // Making Config Info Public 32 | window.configInfo = nearConfig; 33 | 34 | //making utils public 35 | window.utils = utils; 36 | 37 | // Creating new account object 38 | window.account = new Account(near, window.accountId); 39 | // Initializing our contract APIs by contract name and configuration 40 | window.contract = await new Contract( 41 | window.walletConnection.account(), 42 | nearConfig.contractName, 43 | { 44 | // View methods are read only. They don't modify the state, but usually return some value. 45 | viewMethods: ["check_token"], 46 | // Change methods can modify the state. But you don't receive the returned value when called. 47 | changeMethods: ["nft_mint"], 48 | } 49 | ); 50 | } 51 | 52 | export function logout() { 53 | window.walletConnection.signOut(); 54 | // reload page 55 | window.location.replace(window.location.origin + window.location.pathname); 56 | } 57 | 58 | export function login() { 59 | // Allow the current app to make calls to the specified contract on the 60 | // user's behalf. 61 | // This works by creating a new access key for the user's account and storing 62 | // the private key in localStorage. 63 | window.walletConnection.requestSignIn(nearConfig.contractName); 64 | } 65 | -------------------------------------------------------------------------------- /src/wallet/login/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
For local account login, Please run the following command in NEAR CLI, then enter account id here. 9 |
10 |
11 | 12 |
13 | 14 | 15 | 27 | 28 | --------------------------------------------------------------------------------