diff --git a/docs/primitives/ft/ft.md b/docs/primitives/ft/ft.md index eae3636b11f..a3acd8337fb 100644 --- a/docs/primitives/ft/ft.md +++ b/docs/primitives/ft/ft.md @@ -537,7 +537,7 @@ Here is an example from our [auctions tutorial](../../tutorials/auction/3.2-ft.m url="https://github.com/near-examples/auctions-tutorial/blob/main/contract-rs/03-bid-with-fts/src/lib.rs#L56-L87" start="56" end="87" /> -_Note: The [`near_contract_standards::fungible_token::receiver`](https://docs.rs/near-contract-standards/latest/near_contract_standards/fungible_token/receiver/trait.FungibleTokenReceiver.html) module exposes a `FungibleTokenReceiver` trait that you could implement on your contract_ +_Note: The [`near_contract_standards::fungible_token::receiver`](https://docs.rs/near-contract-standards/latest/near_contract_standards/fungible_token/receiver/trait.FungibleTokenReceiver.html) module exposes a `FungibleTokenReceiver` trait that you could implement on your contract_ --- @@ -547,4 +547,4 @@ _Note: The [`near_contract_standards::fungible_token::receiver`](https://docs.rs 2. [NEP-148 standard](https://github.com/near/NEPs/tree/master/neps/nep-0148.md) 3. [FT Event Standards](https://github.com/near/NEPs/blob/master/neps/nep-0300.md) 4. [FT reference implementation](https://github.com/near-examples/FT) -5. [Fungible Tokens 101](../../tutorials/fts/0-intro.md) - a set of tutorials that cover how to create a FT contract using Rust. \ No newline at end of file +5. [Fungible Tokens 101](../../tutorials/fts.md) - a set of tutorials that cover how to create a FT contract using Rust. \ No newline at end of file diff --git a/docs/primitives/nft/nft.md b/docs/primitives/nft/nft.md index 8d7a87bbd60..12bbd671af5 100644 --- a/docs/primitives/nft/nft.md +++ b/docs/primitives/nft/nft.md @@ -516,5 +516,5 @@ If the `msg` parameter is included, then a cross-contract call will be made to ` ## Tutorials -- [NFT Tutorial](/tutorials/nfts/js/introduction) _Zero to Hero_ (JavaScript SDK) - a set of tutorials that cover how to create a NFT contract using JavaScript. -- [NFT Tutorial](/tutorials/nfts/introduction) _Zero to Hero_ (Rust SDK) - a set of tutorials that cover how to create a NFT contract using Rust. +- [NFT Tutorial](/tutorials/nfts-js) _Zero to Hero_ (JavaScript SDK) - a set of tutorials that cover how to create a NFT contract using JavaScript. +- [NFT Tutorial](/tutorials/nfts) _Zero to Hero_ (Rust SDK) - a set of tutorials that cover how to create a NFT contract using Rust. diff --git a/docs/tutorials/fts.md b/docs/tutorials/fts.md new file mode 100644 index 00000000000..ca89a5384bb --- /dev/null +++ b/docs/tutorials/fts.md @@ -0,0 +1,47 @@ +--- +id: fts +title: Fungible Tokens Zero to Hero +sidebar_label: Build a FT Contract from Scratch +description: "Master NEAR fungible tokens from pre-deployed contracts to building fully-featured FT smart contracts." +--- + +In this _Zero to Hero_ series, you'll find a set of tutorials covering every aspect of a fungible token (FT) smart contract. You'll start by interacting with a pre-deployed contract and by the end you'll have built a fully-fledged FT smart contract that supports every extension of the standards. + +--- + +## Prerequisites + +To complete these tutorials successfully, you'll need: + +- [Rust](/smart-contracts/quickstart#prerequisites) +- [A NEAR wallet](https://testnet.mynearwallet.com) +- [NEAR-CLI](/tools/near-cli#installation) +- [cargo-near](https://github.com/near/cargo-near) + +:::info New to Rust? +If you are new to Rust and want to dive into smart contract development, our [Quick-start guide](https://docs.near.org/smart-contracts/quickstart) is a great place to start. +::: + +--- + +## Overview + +These are the steps that will bring you from **_Zero_** to **_Hero_** in no time! πŸ’ͺ + +| Step | Name | Description | +| ---- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | [Pre-deployed contract](https://near-examples.github.io/ft-tutorial/predeployed-contract) | Receive FTs without the need to code, create, or deploy a smart contract. | +| 2 | [Contract architecture](https://near-examples.github.io/ft-tutorial/skeleton) | Learn the basic architecture of the FT smart contract and compile the code. | +| 3 | [Defining a Token](https://near-examples.github.io/ft-tutorial/defining-a-token) | Flesh out what it means to have a FT and how you can customize your own. | +| 4 | [Circulating Supply](https://near-examples.github.io/ft-tutorial/circulating-supply) | Learn how to create an initial supply and have the token show up in your wallet. | +| 5 | [Registering Accounts](https://near-examples.github.io/ft-tutorial/registering-accounts) | Explore how you can implement and understand the storage management standard to avoid malicious users from draining your funds. | +| 6 | [Transferring FTs](https://near-examples.github.io/ft-tutorial/transfers) | Learn how to transfer FTs and discover some of the true powers that the core standard brings | +| 7 | [Marketplace](https://near-examples.github.io/ft-tutorial/marketplace) | Learn about how common marketplaces operate on NEAR and dive into some of the code that allows buying and selling NFTs by using Fungible Tokens. | + +--- + +## Next steps + +Ready to start? Jump to the [Pre-deployed Contract](https://near-examples.github.io/ft-tutorial/predeployed-contract) tutorial and begin your learning journey! + +If you already know about fungible tokens and smart contracts, feel free to skip and jump directly to the tutorial of your interest. The tutorials have been designed so you can start at any given point! diff --git a/docs/tutorials/fts/0-intro.md b/docs/tutorials/fts/0-intro.md deleted file mode 100644 index 5cc443f715d..00000000000 --- a/docs/tutorials/fts/0-intro.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -id: introduction -title: Fungible Tokens Zero to Hero -sidebar_label: Introduction -description: "Master NEAR fungible tokens from pre-deployed contracts to building fully-featured FT smart contracts." ---- - -In this _Zero to Hero_ series, you'll find a set of tutorials covering every aspect of a fungible token (FT) smart contract. You'll start by interacting with a pre-deployed contract and by the end you'll have built a fully-fledged FT smart contract that supports every extension of the standards. - ---- - -## Prerequisites - -To complete these tutorials successfully, you'll need: - -- [Rust](/smart-contracts/quickstart#prerequisites) -- [A NEAR wallet](https://testnet.mynearwallet.com) -- [NEAR-CLI](/tools/near-cli#installation) -- [cargo-near](https://github.com/near/cargo-near) - -:::info New to Rust? -If you are new to Rust and want to dive into smart contract development, our [Quick-start guide](../../smart-contracts/quickstart.md) is a great place to start. -::: - ---- - -## Overview - -These are the steps that will bring you from **_Zero_** to **_Hero_** in no time! πŸ’ͺ - -| Step | Name | Description | -| ---- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| 1 | [Pre-deployed contract](/tutorials/fts/predeployed-contract) | Receive FTs without the need to code, create, or deploy a smart contract. | -| 2 | [Contract architecture](/tutorials/fts/skeleton) | Learn the basic architecture of the FT smart contract and compile the code. | -| 3 | [Defining a Token](/tutorials/fts/defining-a-token) | Flesh out what it means to have a FT and how you can customize your own. | -| 4 | [Circulating Supply](/tutorials/fts/circulating-supply) | Learn how to create an initial supply and have the token show up in your wallet. | -| 5 | [Registering Accounts](/tutorials/fts/registering-accounts) | Explore how you can implement and understand the storage management standard to avoid malicious users from draining your funds. | -| 6 | [Transferring FTs](/tutorials/fts/transfers) | Learn how to transfer FTs and discover some of the true powers that the core standard brings | -| 7 | [Marketplace](/tutorials/fts/marketplace) | Learn about how common marketplaces operate on NEAR and dive into some of the code that allows buying and selling NFTs by using Fungible Tokens. | - - - ---- - -## Next steps - -Ready to start? Jump to the [Pre-deployed Contract](/tutorials/fts/predeployed-contract) tutorial and begin your learning journey! - -If you already know about fungible tokens and smart contracts, feel free to skip and jump directly to the tutorial of your interest. The tutorials have been designed so you can start at any given point! diff --git a/docs/tutorials/fts/0-predeployed.md b/docs/tutorials/fts/0-predeployed.md deleted file mode 100644 index 592075514ac..00000000000 --- a/docs/tutorials/fts/0-predeployed.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -id: predeployed-contract -title: Pre-deployed Contract -sidebar_label: Pre-deployed Contract -description: "Learn how to easily receive fungible tokens without coding." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Learn how to easily receive fungible tokens without doing any software development by using a readily-available FT smart contract. - ---- - -## Prerequisites - -To complete this tutorial successfully, you'll need: - -- [A NEAR testnet account](https://testnet.mynearwallet.com) -- [NEAR-CLI](/tools/near-cli/#installation) - ---- - -## Using the FT contract - -Create a new `testnet` account using the [web wallet](https://testnet.mynearwallet.com). - -### Setup - -Log in to your newly created account with `near-cli-rs` by running the following command in your terminal: - -```bash -near account import-account using-web-wallet network-config testnet -``` - -Set an environment variable for your account ID to make it easy to copy and paste commands from this tutorial: - -```bash -export NEARID=YOUR_ACCOUNT_NAME -``` -:::note - -Be sure to replace `YOUR_ACCOUNT_NAME` with the account name you just logged in with including the `.testnet`. - -::: - -Test that the environment variable is set correctly by running: - -```bash -echo $NEARID -``` - -
- -### Receiving Fungible Tokens - -NEAR has deployed a new Fungible Token contract to the account `ft.predeployed.examples.testnet` which allows users to freely receive some `gtNEAR` - a new fungible token aimed to promote the power of teamwork! Each `gtNEAR` is equal to `1e24 yocto-gtNEAR` similar to how 1 $NEAR is equal to 1e24 yoctoNEAR. - -Using this pre-deployed contract, let's get some gtNEAR! - -Start by calling the method `ft_mint` which is a custom function implemented on this contract in order to send your account some `gtNEAR`! The following command will send `0.01 gtNEAR` to your account. - - - - - ```bash - near call ft.predeployed.examples.testnet ft_mint '{"account_id": "'$NEARID'", "amount": "10000000000000000000000"}' --gas 100000000000000 --accountId $NEARID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction ft.predeployed.examples.testnet ft_mint json-args '{"account_id": "'$NEARID'", "amount": "10000000000000000000000"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as $NEARID network-config testnet sign-with-keychain send - ``` - - - -
-Example response: -

- -```json -Log [ft.predeployed.examples.testnet]: EVENT_JSON:{"standard":"nep141","version":"1.0.0","event":"ft_mint","data":[{"owner_id":"benjiman.testnet","amount":"10000000000000000000000","memo":"FTs Minted"}]} -Transaction Id Fhqa8YDLKxnxM9jjHCPN4hn1w1RKESYrav3kwDjhWWUu -To see the transaction in the transaction explorer, please open this url in your browser -https://testnet.nearblocks.io/txns/Fhqa8YDLKxnxM9jjHCPN4hn1w1RKESYrav3kwDjhWWUu -'' -``` - -

-
- -To view tokens owned by an account you can call the FT contract with the following `near-cli` command: - - - - - ```bash - near view ft.predeployed.examples.testnet ft_balance_of '{"account_id": "'$NEARID'"}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only ft.predeployed.examples.testnet ft_balance_of json-args '{"account_id": "'$NEARID'"}' network-config testnet now - ``` - - - -
-Example response: -

- -```json -'10000000000000000000000' -``` - -

-
- -***Congratulations! You just received your first Team Tokens on the NEAR blockchain!*** πŸŽ‰ - -πŸ‘‰ Now try going to your [NEAR Wallet](https://testnet.mynearwallet.com) and view your FTs in the "Balances" tab. πŸ‘ˆ - -:::note Pre-deployed Contract -The contract used in this section has been modified such that you can infinitely get `gtNEAR` by calling `ft_mint`. This function is not part of the FT [standards](https://github.com/near/NEPs/tree/master/neps/nep-0141.md) and has been implemented for the purpose of this tutorial. -::: - ---- - -## Final remarks - -This basic example illustrates all the required steps to call an FT smart contract on NEAR and receive your own fungible tokens. - -Now that you're familiar with the process, you can jump to [Contract Architecture](/tutorials/fts/skeleton) and learn more about the smart contract structure and how you can build your own FT contract from the ground up. - -***Happy minting!*** πŸͺ™ - -:::note Versioning for this article - -At the time of this writing, this example works with the following versions: - -- near-cli-rs: `0.17.0` -::: diff --git a/docs/tutorials/fts/1-skeleton.md b/docs/tutorials/fts/1-skeleton.md deleted file mode 100644 index 3e103f4b30f..00000000000 --- a/docs/tutorials/fts/1-skeleton.md +++ /dev/null @@ -1,186 +0,0 @@ ---- -id: skeleton -title: Skeleton and Rust Architecture -sidebar_label: Contract Architecture -description: "Discover the architecture and layout of the FT smart contract in this Zero to Hero series." ---- -import {Github} from "@site/src/components/UI/Codetabs" - -In this article, you'll learn about the basic architecture behind the FT contract that you'll develop while following this _"Zero to Hero"_ series. You'll discover the contract's layout and you'll see how the Rust files are structured in order to build a feature-complete smart contract. - -:::info New to Rust? -If you are new to Rust and want to dive into smart contract development, our [Quick-start guide](../../smart-contracts/quickstart.md) is a great place to start. -::: - ---- - -## Introduction - -This tutorial presents the code skeleton for the FT smart contract and its file structure. -You'll find how all the functions are laid out as well as the missing Rust code that needs to be filled in. -Once every file and function has been covered, you'll go through the process of building the mock-up contract to confirm that your Rust toolchain works as expected. - ---- - -## Files structure - -The repository comes with many different folders. Each folder represents a different milestone of this tutorial starting with the skeleton folder and ending with the finished contract folder. If you step into any of these folders, you'll find that they each follow a regular [Rust](https://www.rust-lang.org/) project. The file structure for these smart contracts have: - -- `Cargo.toml` file to define the code dependencies (similar to `package.json` in JavaScript and node projects) -- `src` folder where all the Rust source files are stored -- `target` folder where the compiled `wasm` will output to. - -
- -### Source files - -| File | Description | -| -------------------------------- | -------------------------------------------------------------------------------- | -| [ft_core.rs](#ft_corers) | Contains the logic for transferring and controlling FTs. This file represents the implementation of the [core](https://github.com/near/NEPs/tree/master/neps/nep-0141.md) standard. | | -| [lib.rs](#librs) | Holds the smart contract initialization functions and dictates what information is kept on-chain. | -| [metadata.rs](#metadatars) | Defines the metadata structure. This file represents the implementation of the [metadata](https://github.com/near/NEPs/blob/master/neps/nep-0245/Metadata.md) extension of the standard. | -| [storage.rs](#storagers) | Contains the logic for registration and storage. This file represents the implementation of the [storage management](https://github.com/near/NEPs/tree/master/neps/nep-0145.md) standard. | - -``` -skeleton -β”œβ”€β”€ Cargo.lock -β”œβ”€β”€ Cargo.toml -└── src - β”œβ”€β”€ ft_core.rs - β”œβ”€β”€ lib.rs - β”œβ”€β”€ metadata.rs - └── storage.rs -``` - -:::tip -Explore the code in our [GitHub repository](https://github.com/near-examples/ft-tutorial/tree/main/1.skeleton). -::: - ---- - -## `ft_core.rs` - -Core logic that allows you to transfer FTs between users and query for important information. - -| Method | Description | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **ft_transfer** | Transfers a specified amount of FTs to a receiver ID.| -| **ft_transfer_call** | Transfers a specified amount of FTs to a receiver and attempts to perform a cross-contract call on the receiver’s contract to execute the `ft_on_transfer` method. The implementation of this `ft_on_transfer` method is up to the contract writer. You’ll see an example implementation in the marketplace section of this tutorial. Once `ft_on_transfer` finishes executing, `ft_resolve_transfer` is called to check if things were successful or not.| -| **ft_total_supply** | Returns the total amount of fungible tokens in circulation on the contract. | -| **ft_balance_of** | Returns how many fungible tokens a specific user owns.| -| **ft_on_transfer** | Method that lives on a receiver's contract. It is called when FTs are transferred to the receiver's contract account via the `ft_transfer_call` method. It returns how many FTs should be refunded back to the sender. | -| **ft_resolve_transfer** | Invoked after the `ft_on_transfer` is finished executing. This function will refund any FTs not used by the receiver contract and will return the net number of FTs sent to the receiver after the refund (if any). | - - - -You'll learn more about these functions in the [circulating supply](/tutorials/fts/circulating-supply) and [transfers](/tutorials/fts/transfers) sections of the tutorial series. - ---- - -## `lib.rs` - -This file outlines what information the contract stores and keeps track of. - -| Method | Description | -| -------------------- | ----------------------------------------------------------------------------------------------- | -| **new_default_meta** | Initializes the contract with default `metadata` so the user doesn't have to provide any input. In addition, a total supply is passed in which is sent to the owner. | -| **new** | Initializes the contract with the user-provided `metadata` and total supply. | - -:::info Keep in mind -The initialization functions (`new`, `new_default_meta`) can only be called once. -::: - - - -You'll learn more about these functions in the [define a token](2-define-a-token.md) section of the tutorial series. - ---- - -## `metadata.rs` - -This file is used to outline the metadata for the Fungible Token itself. -In addition, you can define a function to view the contract's metadata which is part of the standard's [metadata](https://github.com/near/NEPs/blob/master/neps/nep-0245/Metadata.md) extension. - -| Name | Description | -| ----------------- | ------------------------------------------------------------------------------------------------------------- | -| **FungibleTokenMetadata** | This structure defines the metadata for the fungible token. | -| **ft_metadata** | This function allows users to query for the token's metadata. | - - - -You'll learn more about these functions in the [define a token](2-define-a-token.md) section of the tutorial series. - ---- - -## `storage.rs` - -Contains the registration logic as per the [storage management](https://github.com/near/NEPs/tree/master/neps/nep-0145.md) standard. - -| Method | Description | -| ----------------------- | ------------------------------------------------------------------------------------------------------------- | -| **storage_deposit** | Payable method that receives an attached deposit of Ⓝ for a given account. This will register the user on the contract. | -| **storage_balance_bounds** | Returns the minimum and maximum allowed storage deposit required to interact with the contract. In the FT contract's case, min = max.| -| **storage_balance_of** | Returns the total and available storage paid by a given user. In the FT contract's case, available is always 0 since it's used by the contract for registration and you can't overpay for storage. | - - - -:::tip -You'll learn more about these functions in the [storage](4.storage.md) section of the tutorial series. -::: - -## Building the skeleton - -If you haven't cloned the main repository yet, open a terminal and run: - -```sh -git clone https://github.com/near-examples/ft-tutorial/ -``` - -Next, build the skeleton contract with the build script found in the `1.skeleton/build.sh` file. - -```sh -cd ft-tutorial/1.skeleton -cargo near build -``` - -Since this source is just a skeleton you'll get many warnings about unused code, such as: - -``` - = note: `#[warn(dead_code)]` on by default - -warning: constant is never used: `GAS_FOR_RESOLVE_TRANSFER` - --> src/ft_core.rs:5:1 - | -5 | const GAS_FOR_RESOLVE_TRANSFER: Gas = Gas(5_000_000_000_000); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -warning: constant is never used: `GAS_FOR_FT_TRANSFER_CALL` - --> src/ft_core.rs:6:1 - | -6 | const GAS_FOR_FT_TRANSFER_CALL: Gas = Gas(25_000_000_000_000 + GAS_FOR_RESOLVE_TRANSFER.0); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -warning: `fungible-token` (lib) generated 25 warnings - Finished release [optimized] target(s) in 1.93s -✨ Done in 2.03s. -``` - -Don't worry about these warnings, you're not going to deploy this contract yet. -Building the skeleton is useful to validate that your Rust toolchain works properly and that you'll be able to compile improved versions of this FT contract in the upcoming tutorials. - ---- - -## Conclusion - -You've seen the layout of this FT smart contract, and how all the functions are laid out across the different source files. -Using `yarn`, you've been able to compile the contract, and you'll start fleshing out this skeleton in the next [section](/tutorials/fts/circulating-supply) of the tutorial. - ---- - -:::note Versioning for this article -At the time of this writing, this example works with the following versions: - -- rustc: `1.77.1` -- near-sdk-rs: `5.1.0` (with enabled `legacy` feature) -- cargo-near: `0.13.2` -::: diff --git a/docs/tutorials/fts/2-define-a-token.md b/docs/tutorials/fts/2-define-a-token.md deleted file mode 100644 index 904b9701457..00000000000 --- a/docs/tutorials/fts/2-define-a-token.md +++ /dev/null @@ -1,192 +0,0 @@ ---- -id: defining-a-token -title: Defining a Fungible Token -sidebar_label: Defining Your Token -description: "Learn to define a NEAR fungible token, set metadata, initialize, and query it on-chain." ---- -import {Github} from "@site/src/components/UI/Codetabs"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -This is the first of many tutorials in a series where you'll be creating a complete FT smart contract from scratch that conforms with all the NEAR [FT standards](https://github.com/near/NEPs/tree/master/neps/nep-0141.md). Today you'll learn what a Fungible Token is and how you can define one on the NEAR blockchain. You will be modifying a bare-bones [skeleton smart contract](/tutorials/fts/skeleton) by filling in the necessary code snippets needed to add this functionality. - ---- - -## Introduction - -To get started, switch to the `1.skeleton` folder in our repo. If you haven't cloned the repository, refer to the [Contract Architecture](/tutorials/fts/skeleton) to get started. - -If you wish to see the finished code for this portion of the tutorial, that can be found on the `2.defining-a-token` folder. - ---- - -## Modifications to the skeleton contract {#modifications} - -At its very core, a fungible token is an exchangeable asset that **is divisible** but is **not unique**. For example, if Benji had 1 Canadian dollar, it would be worth the exact same as Matt's Canadian dollar. Both their dollars are fungible and exchangeable. In this case, the fungible token is the canadian dollar. All fiat currencies are fungible and exchangeable. - -Non-fungible tokens, on the other hand, are **unique** and **indivisible** such as a house or a car. You **cannot** have another asset that is exactly the same. Even if you had a specific car model, such as a Corvette 1963 C2 Stingray, each car would have a separate serial number with a different number of kilometers driven etc... - -Now that you understand what a fungible token is, let's look at how you can define one in the contract itself. - -
- -### Defining a fungible token {#defining-a-fungible-token} - -Start by navigating to the `1.skeleton/src/metadata.rs` file. This is where you'll define the metadata for the fungible token itself. There are several ways NEAR allows you to customize your token, all of which are found in the [metadata](https://github.com/near/NEPs/tree/master/neps/nep-0141.md#metadata) standard. Let's break them up into the optional and non-optional portions. - -Required: -- **spec**: Indicates the version of the standard the contract is using. This should be set to `ft-1.0.0`. -- **name**: The human readable name of the token such as "Wrapped NEAR" or "TEAM Tokens". -- **symbol**: The abbreviation of the token such as `wNEAR` or `gtNEAR`. -- **decimals**: used in frontends to show the proper significant digits of a token. This concept is explained well in this [OpenZeppelin post](https://docs.openzeppelin.com/contracts/3.x/erc20#a-note-on-decimals). - -Optional: -- **icon**: The image for the token (must be a [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs)). -- **reference**: A link to any supplementary JSON details for the token stored off-chain. -- **reference_hash**: A hash of the referenced JSON. - -With this finished, you can now add these fields to the metadata in the contract. - - - -Now that you've defined what the metadata will look like, you need someway to store it on the contract. Switch to the `1.skeleton/src/lib.rs` file and add the following to the `Contract` struct. You'll want to store the metadata on the contract under the `metadata` field. - - - -You've now defined *where* the metadata will live but you'll also need someway to pass in the metadata itself. This is where the initialization function comes into play. - -
- -#### Initialization Functions - -You'll now create what's called an initialization function; you can name it `new`. This function needs to be invoked when you first deploy the contract. It will initialize all the contract's fields that you've defined with default values. It's important to note that you **cannot** call these methods more than once. - - - -More often than not when doing development, you'll need to deploy contracts several times. You can imagine that it might get tedious to have to pass in metadata every single time you want to initialize the contract. For this reason, let's create a function that can initialize the contract with a set of default `metadata`. You can call it `new_default_meta`. - - - -This function is simply calling the previous `new` function and passing in some default metadata behind the scenes. - -At this point, you've defined the metadata for your fungible tokens and you've created a way to store this information on the contract. The last step is to introduce a getter that will query for and return the metadata. Switch to the `1.skeleton/src/metadata.rs` file and add the following code to the `ft_metadata` function. - - - -This function will get the `metadata` object from the contract which is of type `FungibleTokenMetadata` and will return it. - ---- - -## Interacting with the contract on-chain - -Now that the logic for defining a custom fungible token is complete and you've added a way to query for the metadata, it's time to build and deploy your contract to the blockchain. - -### Deploying and initializing the contract {#deploy-the-contract} - -You can build a contract using the following command: - -```bash -cd 2.define-a-token -cargo near build -``` - -There will be a list of warnings on your console, but as the tutorial progresses, these warnings will go away. - -For deployment, you will need a NEAR account with the keys stored on your local machine. Navigate to the [NEAR wallet](https://testnet.mynearwallet.com/) site and create an account. - -:::info -Please ensure that you deploy the contract to an account with no pre-existing contracts. It's easiest to simply create a new account or create a sub-account for this tutorial. -::: - -Log in to your newly created account with `near-cli-rs` by running the following command in your terminal. - -```bash -near account import-account using-web-wallet network-config testnet -``` - -To make this tutorial easier to copy/paste, we're going to set an environment variable for your account ID. In the command below, replace `YOUR_ACCOUNT_NAME` with the account name you just logged in with including the `.testnet` portion: - -```bash -export FT_CONTRACT_ID="YOUR_ACCOUNT_NAME.testnet" -``` - -Test that the environment variable is set correctly by running: - -```bash -echo $FT_CONTRACT_ID -``` - -Verify that the correct account ID is printed in the terminal. If everything looks correct you can now deploy your contract. For simplicity, let's call the default metadata initialization function you wrote earlier so that you don't have to type the metadata manually in the CLI. In the root of your FT project run the following command to deploy your smart contract. - -```bash -cargo near deploy build-non-reproducible-wasm $FT_CONTRACT_ID with-init-call new_default_meta json-args '{"owner_id": "'$FT_CONTRACT_ID'", "total_supply": "0"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send -``` - -At this point, the contract should have been deployed to your account and initialized. - -
- -### Viewing the contract's metadata - -Now that the contract has been initialized, you can query for the metadata by calling the function you wrote earlier. - - - - - ```bash - near view $FT_CONTRACT_ID ft_metadata '{}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $FT_CONTRACT_ID ft_metadata json-args {} network-config testnet now - ``` - - - -This should return an output similar to the following: - -```js -{ - spec: 'ft-1.0.0', - name: 'Team Token FT Tutorial', - symbol: 'gtNEAR', - icon: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/ - /* - ...lots of base64 data... - */ - j4Mvhy9H9NlnieJ4iwoo9ZlyLGx4pnrPWeB4CVGRZZcJ7Vohwhi0z5MJY4cVL4MdP/Z', - reference: null, - reference_hash: null, - decimals: 24 -} -``` - -**Go team!** You've now verified that everything works correctly and you've defined your own fungible token! - -In the next tutorial, you'll learn about how to create a total supply and view the tokens in the wallet. - ---- - -## Conclusion - -In this tutorial, you went through the basics of setting up and understanding the logic behind creating a fungible token on the blockchain using a skeleton contract. - -You first looked at [what a fungible token is](#modifications) and how it differs from a non-fungible token. You then learned how to customize and create your own fungible tokens and how you could modify the skeleton contract to achieve this. Finally you built and deployed the contract and interacted with it using the NEAR CLI. - -## Next Steps - -In the [next tutorial](/tutorials/fts/circulating-supply), you'll find out how to create an initial supply of tokens and have them show up in the NEAR wallet. - ---- - -:::note Versioning for this article -At the time of this writing, this example works with the following versions: - -- rustc: `1.77.1` -- near-sdk-rs: `5.1.0` (with enabled `legacy` feature) -- cargo-near: `0.13.2` -- near-cli-rs: `0.17.0` -::: diff --git a/docs/tutorials/fts/3-circulating-supply.md b/docs/tutorials/fts/3-circulating-supply.md deleted file mode 100644 index 8c477e76a98..00000000000 --- a/docs/tutorials/fts/3-circulating-supply.md +++ /dev/null @@ -1,392 +0,0 @@ ---- -id: circulating-supply -title: Creating a Circulating Supply -sidebar_label: Circulating Supply -description: "Learn to create a NEAR fungible token's initial supply, log events, and view it in wallet." ---- -import {Github} from "@site/src/components/UI/Codetabs"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -In the previous tutorial, you looked at what a fungible token was and how you could define one in your smart contract. In this tutorial, you'll learn how to create a circulating supply belonging to the contract owner and view all the tokens, with their metadata, in the NEAR wallet. - -To get started, either work off the code you wrote in the previous tutorial or switch to the `2.define-a-token` folder in our repo. If you haven't cloned the repository, refer to the [Contract Architecture](1-skeleton.md) to start. - -If you wish to see the finished code for this tutorial, you can find it in the `3.initial-supply` folder. - ---- - -## Introduction - -Every fungible token contract on NEAR has what's known as a circulating supply. This is the number of tokens that exist on the contract and are actively available to trade. - -When creating your contract, there are many different ways you could implement this to start. A few examples could be: -- Specify a starting total supply and distribute it based on a set of parameters (Benji gets 20%, Josh gets 2.5%, and the rest goes to Mike). -- Have a first come first serve pool where everybody claims up to X amount of tokens. -- Create tokens on demand resulting in a steady increase of the circulating supply overtime up to a specified cap. - -The simplest approach, however, is to specify a total supply when initializing the contract. The entire circulating supply is then created and sent to the owner of the contract. The owner would then be able to transfer or sell the tokens as they wish. Once the initial supply is created, no more FTs could be minted. This means that the circulating supply will always be equal to the total supply. - ---- - -## Modifications to contract - -In order to implement this logic, you'll need to keep track of two things in your smart contract: -- A mapping of an account to the number of tokens they own. -- The total supply of tokens. - -The mapping is so that you can easily check or modify the tokens owned by any given account at anytime within your contract. You'll also need to keep track of the total supply since it's required by the standard that you have a function to query for the supply of tokens on the contract. - -
- -### Setting the supply - -Head over to the `src/lib.rs` file and add the following code to the `Contract` struct. - - - -You'll now want to add the functionality for depositing the tokens into the owner's account. Do this by creating a helper function that takes an amount and an account ID and performs the deposit logic for you. First create a new file `src/internal.rs` such that your file structure now looks as follows. - -``` -src - β”œβ”€β”€ ft_core.rs - β”œβ”€β”€ internal.rs - β”œβ”€β”€ lib.rs - β”œβ”€β”€ metadata.rs - └── storage.rs -``` - -In the `internal.rs` file, add the following code to create a function called `internal_deposit` which takes an `AccountId` and a `NearToken` as a balance and adds the amount to the account's current supply of FTs. - - - -Now that the functionality for depositing FTs is in place, switch back to the `src/lib.rs` file and add the `internal` module: - - - -In addition, add the following code to the `new` initialization function. - -```rust -#[init] -pub fn new( - owner_id: AccountId, - total_supply: U128, - metadata: FungibleTokenMetadata, -) -> Self { - let casted_total_supply = NearToken::from_yoctonear(total_supply.0); - // Create a variable of type Self with all the fields initialized. - let mut this = Self { - // Set the total supply - total_supply: casted_total_supply, - // Storage keys are simply the prefixes used for the collections. This helps avoid data collision - accounts: LookupMap::new(StorageKey::Accounts), - metadata: LazyOption::new( - StorageKey::Metadata, - Some(&metadata), - ), - }; - - // Set the owner's balance to the total supply. - this.internal_deposit(&owner_id, casted_total_supply); - - // Return the Contract object - this -} -``` - -This will initialize the total supply to what you passed in and will call the `internal_deposit` function to add the total supply to the owner's account. - -
- -### Getting the supply - -Now that you've created a way to set the total supply, you'll also want a way to query for it as well as the balance for a specific user. The [standard](https://nomicon.io/Standards/Tokens/FungibleToken/Core) dictates that you should have two methods on your smart contract for doing these operations: -- **`ft_total_supply`** -- **`ft_balance_of`** - -Head on over to the `src/ft_core.rs` file and add the following code to these functions. - - - -At this point, you have everything you need to create an initial supply of tokens and query for the balance of a given account. There is, however, a problem that we need to solve. How will the wallet know that the total supply was created and is owned by the contract owner? How would it even know that our contract is a fungible token contract? If you were to deploy the contract and run through the setup process, you would be able to query for the information from the contract but you wouldn't see any FTs in the owner's NEAR wallet. - ---- - -## Events - -Have you ever wondered how the wallet knows which FTs you own and how it can display them in the [balances tab](https://testnet.mynearwallet.com/)? Originally, an indexer used to listen for any function calls starting with `ft_` on your account. These contracts were then flagged on your account as likely FT contracts. - -When you navigated to your balances tab, the wallet would then query all those contracts for the number of FTs you owned using the `ft_balance_of` function you just wrote. - -
- -### The problem {#the-problem} - -This method of flagging contracts was not reliable as each FT-driven application might have its own way of minting or transferring FTs. In addition, it's common for apps to transfer or mint many tokens at a time using batch functions. - -
- -### The solution {#the-solution} - -A standard was introduced so that smart contracts could emit an event anytime FTs were transferred, minted, or burnt. This event was in the form of a log. No matter how a contract implemented the functionality, an indexer could now listen for those standardized logs. - -As per the standard, you need to implement a logging functionality that gets fired when FTs are transferred or minted. In this case, the contract doesn't support burning so you don't need to worry about that for now. - -It's important to note the standard dictates that the log should begin with `"EVENT_JSON:"`. The structure of your log should, however, always contain the 3 following things: - -- **standard**: the current name of the standard (e.g. `nep141`) -- **version**: the version of the standard you're using (e.g. `1.0.0`) -- **event**: a list of events you're emitting. - -The event interface differs based on whether you're recording transfers or mints. The interface for both events is outlined below. - -**Transfer events**: -- **old_owner_id**: the old owner of the FTs. -- **new_owner_id**: the new owner that the FTs are being transferred to. -- **amount**: the number of tokens transferred. -- *Optional* - **memo**: an optional message to include with the event. - -**Minting events**: -- **owner_id**: the owner that the FTs are being minted to. -- **amount**: the amount of FTs being minted. -- *Optional* - **memo**: an optional message to include with the event. - -
- -### Examples {#examples} - -In order to solidify your understanding of the standard, let's walk through two scenarios and see what the logs should look like. - -#### Scenario A - simple mint - -In this scenario, the Benji mints 50 FTs to himself and doesn't include a message. The log should look as follows. - -```js -EVENT_JSON:{ - "standard": "nep141", - "version": "1.0.0", - "event": "ft_mint", - "data": [ - {"owner_id": "benji.testnet", "amount": "50"} - ] -} -``` - -
- -#### Scenario B - batch transfer - -In this scenario, Benji wants to perform a batch transfer. He will send FTs to Jada, Mike, Josh, and Maria. The log is as follows. - -```js -EVENT_JSON:{ - "standard": "nep141", - "version": "1.0.0", - "event": "ft_transfer", - "data": [ - {"old_owner_id": "benji.near", "new_owner_id": "josh.near", "amount": "1", "memo": "go team"}, - {"old_owner_id": "benji.near", "new_owner_id": "mike.near", "amount": "9000"}, - {"old_owner_id": "benji.near", "new_owner_id": "jada.near", "amount": "500"}, - {"old_owner_id": "benji.near", "new_owner_id": "maria.near", "amount": "500"} - ] -} -``` - ---- - -## Modifications to the contract {#modifications-to-the-contract} - -At this point, you should have a good understanding of what the end goal should be so let's get to work! Open the `src` directory and create a new file called `events.rs`. This is where your log structs will live. - -### Creating the events file {#events-rs} - -Copy the following into your file. This will outline the structs for your `EventLog`, `FtMintLog`, and `FtTransferLog`. In addition, we've added a way for `EVENT_JSON:` to be prefixed whenever you log the `EventLog`. - - - -
- -### Adding modules and constants {#lib-rs} - -Now that you've created a new file, you need to add the module to the `lib.rs` file. - - - -
- -### Logging the total supply minted - -Now that all the tools are set in place, you can implement the actual logging functionality. Since the contract will only be minting tokens at the very start when it's initialized, it's trivial where you should place the log. Open the `src/lib.rs` file and navigate to the bottom of the `new` initialization function. This is where you'll construct the log for minting. - - - -With that finished, you've successfully implemented the backbone of the events standard and it's time to start testing. - ---- - -## Deploying the contract {#redeploying-contract} - -Since the current contract you have is already initialized, let's create a sub-account and deploy to that instead. - -### Creating a sub-account - -Run the following command to create a sub-account `events` of your main account with an initial balance of 3 NEAR which will be transferred from the original to your new account. - - - - - ```bash - near create-account events.$FT_CONTRACT_ID --use-account $FT_CONTRACT_ID --initial-balance 3 --network-id testnet - ``` - - - - - ```bash - near account create-account fund-myself events.$FT_CONTRACT_ID '3 NEAR' autogenerate-new-keypair save-to-keychain sign-as $FT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -Next, you'll want to export an environment variable for ease of development: - -```bash -export EVENTS_FT_CONTRACT_ID=events.$FT_CONTRACT_ID -``` - -Build the contract as you did in the previous tutorials: - -```bash -cd 2.define-a-token -cargo near build -``` - -
- -### Deploying and Initialization {#deploying-initialization} - -It's time to deploy the contract, initialize it and mint the total supply. Let's create an initial supply of 1000 `gtNEAR`. Since it has 24 decimal places, you should put `1000` followed by 24 zeros in the total supply field. - -```bash -cargo near deploy build-non-reproducible-wasm $EVENTS_FT_CONTRACT_ID with-init-call new_default_meta json-args '{"owner_id": "'$EVENTS_FT_CONTRACT_ID'", "total_supply": "1000000000000000000000000000"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send -``` - -You can check to see if everything went through properly by looking at the output in your CLI: - -```bash -... -Transaction sent ... ---- Logs --------------------------- -Logs [events.aha_3.testnet]: - EVENT_JSON:{"standard":"nep141","version":"1.0.0","event":"ft_mint","data":[{"owner_id":"events.goteam.testnet","amount":"1000000000000000000000000000","memo":"Initial token supply is minted"}]} ---- Result ------------------------- -Empty result ------------------------------------- - -Contract code has been successfully deployed. -The "new_default_meta" call to on behalf of succeeded. -... -``` - -You can see that the event was properly logged! - -
- -### Querying Supply Information {#testing} - -You can now test if your view functions work properly. First, try to query for the total supply. - - - - - ```bash - near view $EVENTS_FT_CONTRACT_ID ft_total_supply '{}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $EVENTS_FT_CONTRACT_ID ft_total_supply json-args {} network-config testnet now - ``` - - - -This should return an output similar to the following: - -```bash -'1000000000000000000000000000' -``` - -Hurray! Now you can check if the balance of the owner account works properly. If you call the following function, it should return the same number as the total supply. - - - - - ```bash - near view $EVENTS_FT_CONTRACT_ID ft_balance_of '{"account_id": "'$EVENTS_FT_CONTRACT_ID'"}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $EVENTS_FT_CONTRACT_ID ft_balance_of json-args '{"account_id": "'$EVENTS_FT_CONTRACT_ID'"}' network-config testnet now - ``` - - - - -Returns: - -```bash -'1000000000000000000000000000' -``` - -If you query for the balance of some other account, it should return `0`. - - - - - ```bash - near view $EVENTS_FT_CONTRACT_ID ft_balance_of '{"account_id": "benjiman.testnet"}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $EVENTS_FT_CONTRACT_ID ft_balance_of json-args '{"account_id": "benjiman.testnet"}' network-config testnet now - ``` - - - ---- - -## Viewing FTs in the wallet {#viewing-fts-in-wallet} - -Now that your contract implements the necessary functions that the wallet uses to pickup your contract and display the FTs, you should be able to see your tokens on display in the [balances tab](https://testnet.mynearwallet.com/). - - - -πŸŽ‰πŸŽ‰πŸŽ‰ **This is awesome! Go team!** πŸŽ‰πŸŽ‰πŸŽ‰ You can now see your very first fungible tokens in the wallet! - ---- - -## Conclusion - -Today you went through and created the logic for minting a total supply. You then implemented some of the core standard logic and the [events standard](https://nomicon.io/Standards/Tokens/FungibleToken/Event). You created events for [minting](#modifications-to-the-contract) FTs on initialization. You then deployed and [tested](#testing) your changes and saw your very first FTs in the wallet! - -In the next tutorial, you'll look at the basics of registering accounts so that they can transfer and receive FTs. - ---- - -:::note Versioning for this article -At the time of this writing, this example works with the following versions: - -- rustc: `1.77.1` -- near-sdk-rs: `5.1.0` (with enabled `legacy` feature) -- cargo-near: `0.13.2` -- cargo-near: `0.6.1` -- near-cli-rs: `0.17.0` -::: diff --git a/docs/tutorials/fts/4.storage.md b/docs/tutorials/fts/4.storage.md deleted file mode 100644 index 09c6c940479..00000000000 --- a/docs/tutorials/fts/4.storage.md +++ /dev/null @@ -1,241 +0,0 @@ ---- -id: registering-accounts -title: Registering Accounts -sidebar_label: Registering Accounts -description: "Learn to register accounts in a NEAR FT contract using the storage management standard safely." ---- -import {Github} from "@site/src/components/UI/Codetabs"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -In the previous tutorial, you looked at how to mint an initial circulating supply of tokens and how you could log events as per the [events standard](https://nomicon.io/Standards/Tokens/FungibleToken/Event). You then deployed the contract and saw the FTs in your wallet balance. In this tutorial, you'll learn about the [storage management](https://nomicon.io/Standards/StorageManagement) standard and how you can register accounts in your FT contract in order to prevent a malicious party from draining your contract of all its funds. - ---- - -## Introduction - -Whenever a new person receives any fungible tokens, they're added to the `accounts` lookup map on the contract. By doing this, you're adding bytes to the contract. If you made it so any user could receive FTs for free, that system could easily be abused. Users could essentially "drain" the contract of all its funds by sending small amounts of FTs to many accounts. For this reason, you'll want to charge users for the information they're storing and the bytes they're using on the contract. This way of charging users, however, should be standardized so it works across all contracts. - -*Enter the [storage management](https://nomicon.io/Standards/StorageManagement) standard* - - - -
- -### Storage Management Standard - -The storage management standard is a set of rules that govern how a contract should charge users for storage. It outlines functions and behaviors such that all contracts implementing the standard are interoperable with each other. The 3 functions you'll need to implement are: -- **`storage_deposit`** - Allows a user to deposit some amount of storage to the contract. If the user over deposits, they're refunded for the excess $NEAR. -- **`storage_balance_of`** - Query for the storage paid by a given user -- **`storage_balance_bounds`** - Query for the minimum and maximum amount of storage needed to interact with a given contract. - -With these functions outlined, you could make a reasonable assumption that the flow would be: -1. If a user doesn't exist on the contract, they need to deposit some storage to cover the bytes they use. -2. Once the user deposits $NEAR via the `storage_deposit` function, they're free to interact with the contract. - -You might be asking yourself what the deposit amount should be. There are two ways you can go about getting this information: -- Dynamically calculate the bytes every individual user would take up in the `storage_deposit` function by inserting them into `accounts` map, measuring the bytes, and then removing them from the map after. -- Calculate the bytes for inserting a largest possible account ID once upon initializing the contract and simply charge every user the same amount. - -For the purpose of simplicity, we'll assume the second method. - ---- - -## Modifications to the Contract - -This "bytes for longest account ID" should be stored in the contract's state such that we can pull the value during the `storage_deposit` function and ensure the user attaches enough $NEAR. Open the `src/lib.rs` file and add the following code to the `Contract` struct. If you're just joining us now, you can find the skeleton code for this tutorial in the `3.initial-supply` folder. - - - -You'll now need a way to calculate this amount which will be done in the initialization function. Move to the `src/internal.rs` file and add the following private function `measure_bytes_for_longest_account_id` which will add the longest possible account ID and remove it while measuring how many bytes the operation took. It will then set the `bytes_for_longest_account_id` field to the result. - - - -You'll also want to create a function for _registering_ an account after they've paid for storage. To do this, you can simply insert them into the `accounts` map with a balance of 0. This way, you know that any account currently in the map is considered "registered" and have paid for storage. Any account that attempts to receive FTs must be in the map with a balance of 0 or greater. If they aren't, the contract should throw. - - - -Let's also create a function to panic with a custom message if the user doesn't exist yet. - - - -Now when you call the `internal_deposit` function, rather than defaulting the user's balance to `0` if they don't exist yet via: - -```rust -let balance = self.accounts.get(&account_id).unwrap_or(0); -``` -You can replace it with the following: - - - -With this finished, your `internal.rs` should look as follows: - -```rust -use near_sdk::{require}; - -use crate::*; - -impl Contract { - pub(crate) fn internal_unwrap_balance_of(&self, account_id: &AccountId) -> NearToken { - ... - } - - pub(crate) fn internal_deposit(&mut self, account_id: &AccountId, amount: NearToken) { - ... - } - - pub(crate) fn internal_register_account(&mut self, account_id: &AccountId) { - ... - } - - pub(crate) fn measure_bytes_for_longest_account_id(&mut self) { - ... - } -} -``` - -There's only one problem you need to solve with this. When initializing the contract, the implementation will throw. This is because you call `internal_deposit` and the owner doesn't have a balance yet. To fix this, let's modify the initialization function to register the owner before depositing the FTs in their account. In addition, you should measure the bytes for the registration in this function. - - - -
- -### Implementing Storage Standard - -With this finished, you're now ready to implement the storage management standard. If you remember, the three functions you'll be implementing, we can break each down into their core functionality and decide how to proceed. - -- **`storage_balance_bounds`** - Query for the minimum and maximum amount of storage needed to interact with a given contract. - -Since you're creating a fungible token contract and the storage price won't change (unless the `$NEAR` cost per byte changes), the minimum and maximum storage costs should be the same. These values should be equal to the amount of bytes for the longest account ID you calculated in the `measure_bytes_for_longest_account_id` function multiplied by the current `$NEAR` price per byte. Switch to the `src/storage.rs` file to get started. - - - -- **`storage_balance_of`** - Query for the storage paid by a given user. - -As we mentioned earlier, you can tell if somebody has paid for storage by checking if they're in the `accounts` map. If they are, you know that they've paid the amount returned by `storage_balance_bounds`. - - - -- **`storage_deposit`** - Allows a user to deposit some amount of storage to the contract. If the user over deposits, they're refunded for the excess $NEAR. - -In order to implement this logic, you first need to get the attached deposit. You'll also need to ensure that the user hasn't already paid for storage (i.e. they're in the `accounts` map). If they are, simply refund the caller for the $NEAR they attached to the call. - -If the user isn't registered yet, you should get the storage cost by calling `storage_balance_bounds` and make sure they've attached enough to cover that amount. Once this if finished, you can register them and refund any excess $NEAR. - - - -With this finished, you're ready to build and deploy the contract! - ---- - -## Deploying the contract {#redeploying-contract} - -Since the current contract you have is already initialized, let's create a sub-account and deploy to again. - -### Creating a sub-account - -Run the following command to create a sub-account `storage` of your main account with an initial balance of 3 NEAR which will be transferred from the original to your new account. - - - - - ```bash - near create-account storage.$FT_CONTRACT_ID --use-account $FT_CONTRACT_ID --initial-balance 3 --network-id testnet - ``` - - - - - ```bash - near account create-account fund-myself storage.$FT_CONTRACT_ID '3 NEAR' autogenerate-new-keypair save-to-keychain sign-as $FT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -Next, you'll want to export an environment variable for ease of development: - -```bash -export STORAGE_FT_CONTRACT_ID=storage.$FT_CONTRACT_ID -``` - -Build the contract as you did in the previous tutorials: - -```bash -cd 3.initial-supply -cargo near build -``` - -
- -### Deploying and Initialization {#deploying-initialization} - -It's time to deploy the contract, initialize it and mint the total supply. Let's once again create an initial supply of 1000 `gtNEAR`. - -```bash -cargo near deploy build-non-reproducible-wasm $STORAGE_FT_CONTRACT_ID with-init-call new_default_meta json-args '{"owner_id": "'$STORAGE_FT_CONTRACT_ID'", "total_supply": "1000000000000000000000000000"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send -``` - -If you now query for the storage paid by the owner, you should see that they're registered! - - - - - ```bash - near view $STORAGE_FT_CONTRACT_ID storage_balance_of '{"account_id": "'$STORAGE_FT_CONTRACT_ID'"}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $STORAGE_FT_CONTRACT_ID storage_balance_of json-args '{"account_id": "'$STORAGE_FT_CONTRACT_ID'"}' network-config testnet now - ``` - - - -This should return a struct. The Total amount is roughly `0.00125 $NEAR` to register and the user has 0 available $NEAR since it's all being used up to pay for registration. - -```js -{ total: '1250000000000000000000', available: '0' } -``` - -You can also query for the storage balance required to interact with the contract: - - - - - ```bash - near view $STORAGE_FT_CONTRACT_ID storage_balance_bounds '{}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $STORAGE_FT_CONTRACT_ID storage_balance_bounds json-args {} network-config testnet now - ``` - - - -You'll see that it returns the same amount as the `storage_balance_of` query above with the min equal to the max: - -```js -{ min: '1250000000000000000000', max: '1250000000000000000000' } -``` - ---- - -## Conclusion - -Today you went through and created the logic for registering users on the contract. This logic adheres to the [storage management standard](https://nomicon.io/Standards/StorageManagement) and is customized to meet our needs when writing a FT contract. You then built, deployed, and tested those changes. In the [next tutorial](5.transfers.md), you'll look at the basics of how to transfer FTs to other users. - ---- - -:::note Versioning for this article -At the time of this writing, this example works with the following versions: - -- rustc: `1.77.1` -- near-sdk-rs: `5.1.0` (with enabled `legacy` feature) -- cargo-near: `0.13.2` -- near-cli-rs: `0.17.0` -::: diff --git a/docs/tutorials/fts/5.transfers.md b/docs/tutorials/fts/5.transfers.md deleted file mode 100644 index 8f1852d686f..00000000000 --- a/docs/tutorials/fts/5.transfers.md +++ /dev/null @@ -1,387 +0,0 @@ ---- -id: transfers -title: Transferring Fungible Tokens -sidebar_label: Transferring FTs -description: "Learn how to transfer and receive fungible tokens between registered accounts securely." ---- -import {Github} from "@site/src/components/UI/Codetabs"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -In this tutorial, you'll learn how to implement the [core standards](https://github.com/near/NEPs/tree/master/neps/nep-0141.md) into your smart contract. You'll implement the logic that allows you to transfer and receive tokens. -If you're joining us for the first time, feel free to clone [this repository](https://github.com/near-examples/ft-tutorial) and follow along in the `4.storage` folder. - -:::tip - -If you wish to see the finished code for this _Core_ tutorial, you can find it in the [`5.transfers`](https://github.com/near-examples/ft-tutorial/blob/main/5.transfers) folder. - -::: - ---- - -## Introduction {#introduction} - -Up until this point, you've created a simple FT smart contract that allows the owner to mint a total supply of tokens and view information about the Fungible Token itself. In addition, you've added the functionality to register accounts and emit events. Today, you'll expand your smart contract to allow for users to transfer and receive fungible tokens. - -The logic for doing a simple transfer is quite easy to understand. Let's say Benji wants to transfer Mike 100 of his fungible tokens. The contract should do a few things: -- Check if Benji owns at least 100 tokens. -- Make sure Benji is calling the function. -- Ensure Mike is registered on the contract. -- Take 100 tokens out of Benji's account. -- Put 100 tokens into Mike's account. - -At this point, you're ready to move on and make the necessary modifications to your smart contract. - ---- - -## Modifications to the contract - -Let's start our journey in the `src/ft_core.rs` file. - -### Transfer function {#transfer-function} - -You'll start by implementing the `ft_transfer` logic which is found in the `src/ft_core.rs` file. This function will transfer the specified `amount` to the `receiver_id` with an optional `memo` such as `"Happy Birthday Mike!"`. - - - -There are a couple things to notice here. - -1. We've introduced a new function called `assert_one_yocto()`. This method will ensure that the user is signing the transaction with a full access key by requiring a deposit of exactly 1 yoctoNEAR, the smallest possible amount of $NEAR that can be transferred. Since the transfer function is potentially transferring very valuable assets, you'll want to make sure that whoever is calling the function has a full access key. - -2. We've introduced an `internal_transfer` method. This will perform all the logic necessary to transfer the tokens internally. - -
- -### Internal helper functions - -Let's quickly move over to the `ft-contract/src/internal.rs` file so that you can implement the `internal_transfer` method which is the core of this tutorial. This function will take the following parameters: - -- **sender_id**: the account that's attempting to transfer the tokens. -- **receiver_id**: the account that's receiving the tokens. -- **amount**: the amount of FTs being transferred. -- **memo**: an optional memo to include. - -The first thing you'll want to do is make sure the sender isn't sending tokens to themselves and that they're sending a positive number. After that, you'll want to withdraw the tokens from the sender's balance and deposit them into the receiver's balance. You've already written the logic to deposit FTs by using the `internal_deposit` function. - -Let's use similar logic to implement `internal_withdraw`: - - - -In this case, the contract will get the account's balance and ensure they are registered by calling the `internal_unwrap_balance_of` function. It will then subtract the amount from the user's balance and re-insert them into the map. - -Using the `internal_deposit` and `internal_withdraw` functions together, the core of the `internal_transfer` function is complete. - -There's only one more thing you need to do. When transferring the tokens, you need to remember to emit a log as per the [events](https://github.com/near/NEPs/blob/master/neps/nep-0300.md) standard: - - - -Now that this is finished, the simple transfer case is done! You can now transfer FTs between registered users! - -
- -### Transfer call function {#transfer-call-function} - -In this section, you'll learn about a new function `ft_transfer_call`. This will transfer FTs to a receiver and also call a method on the receiver's contract all in the same transaction. - -Let's consider the following scenario. An account wants to transfer FTs to a smart contract for performing a service. The traditional approach would be to perform the service and then ask for the tokens in two separate transactions. If we introduce a β€œtransfer and call” workflow baked into a single transaction, the process can be greatly improved: - - - -This function will do several things: - -1. Ensures the caller attached exactly 1 yocto for security purposes. -2. Transfer the tokens using the `internal_transfer` you just wrote. -3. Creates a promise to call the method `ft_on_transfer` on the `receiver_id`'s contract. -4. After the promise finishes executing, the function `ft_resolve_transfer` is called. - -:::info - -This is a very common workflow when dealing with cross contract calls. You first initiate the call and wait for it to finish executing. Then, you invoke a function that resolves the result of the promise and act accordingly. -[Learn more here](../../smart-contracts/anatomy/crosscontract.md). - -::: - -When calling `ft_on_transfer`, it will return how many tokens the contract should refund the original sender. - -This is important for a couple of reasons: -1. If you send the receiver too many FTs and their contract wants to refund the excess. -2. If any of the logic panics, all of the tokens should be refunded back to the sender. - -This logic will all happen in the `ft_resolve_transfer` function: - - - -The first thing you'll do is check the status of the promise. If anything failed, you'll refund the sender for the full amount of tokens. If the promise succeeded, you'll extract the amount of tokens to refund the sender based on the value returned from `ft_on_transfer`. Once you have the amount needed to be refunded, you'll perform the actual refund logic by using the `internal_transfer` function you wrote previously. - -You'll then return the net amount of tokens that were transferred to the receiver. If the sender transferred 100 tokens but 20 were refunded, this function should return 80. - -With that finished, you've now successfully added the necessary logic to allow users to transfer FTs. It's now time to deploy and do some testing. - ---- - -## Deploying the contract {#redeploying-contract} - -Let's create a new sub-account to deploy the contract to. Since these changes are only additive and non state breaking, you could have deployed a patch fix to the contract you deployed in the storage section as well. To learn more about upgrading contracts, see the [upgrading a contract](/tutorials/nfts/upgrade-contract) section in the NFT Zero to Hero tutorial. - -
- -### Creating a sub-account - -Run the following command to create a sub-account `transfer` of your main account with an initial balance of 3 NEAR which will be transferred from the original to your new account. - - - - - ```bash - near create-account transfer.$FT_CONTRACT_ID --use-account $FT_CONTRACT_ID --initial-balance 3 --network-id testnet - ``` - - - - - ```bash - near account create-account fund-myself transfer.$FT_CONTRACT_ID '3 NEAR' autogenerate-new-keypair save-to-keychain sign-as $FT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -Next, you'll want to export an environment variable for ease of development: - -```bash -export TRANSFER_FT_CONTRACT_ID=transfer.$FT_CONTRACT_ID -``` - -Build the contract as you did in the previous tutorials: - -```bash -cd 4.storage -cargo near build -``` - -
- -### Deployment and Initialization - -It's time to deploy the contract, initialize it and mint the total supply. Let's once again create an initial supply of 1000 `gtNEAR`. - -```bash -cargo near deploy build-non-reproducible-wasm $TRANSFER_FT_CONTRACT_ID with-init-call new_default_meta json-args '{"owner_id": "'$TRANSFER_FT_CONTRACT_ID'", "total_supply": "1000000000000000000000000000"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send -``` - -You can check if you own the FTs by running the following command: - - - - - ```bash - near view $TRANSFER_FT_CONTRACT_ID ft_balance_of '{"account_id": "'$TRANSFER_FT_CONTRACT_ID'"}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $TRANSFER_FT_CONTRACT_ID ft_balance_of json-args '{"account_id": "'$TRANSFER_FT_CONTRACT_ID'"}' network-config testnet now - ``` - - - -
- -### Testing the transfer function - -Let's test the transfer function by transferring 1 `gtNEAR` from the owner account to the account `benjiman.testnet` - -:::warning Keep in mind -The Fungible tokens won't be recoverable unless the account `benjiman.testnet` transfers them back to you. If you don't want your FTs lost, make a new account and transfer the token to that account instead. -::: - -You'll first need to register the account `benjiman.testnet` by running the following command. - - - - - ```bash - near call $TRANSFER_FT_CONTRACT_ID storage_deposit '{"account_id": "benjiman.testnet"}' --gas 100000000000000 --deposit 0.01 --accountId $TRANSFER_FT_CONTRACT_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $TRANSFER_FT_CONTRACT_ID storage_deposit json-args '{"account_id": "benjiman.testnet"}' prepaid-gas '100.0 Tgas' attached-deposit '0.01 NEAR' sign-as $TRANSFER_FT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -Once the account is registered, you can transfer the FTs by running the following command. Take note that you're also attaching exactly 1 yoctoNEAR by using the `--depositYocto` flag. - - - - - ```bash - near call $TRANSFER_FT_CONTRACT_ID ft_transfer '{"receiver_id": "benjiman.testnet", "amount": "1000000000000000000000000", "memo": "Go Team :)"}' --gas 100000000000000 --depositYocto 1 --accountId $TRANSFER_FT_CONTRACT_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $TRANSFER_FT_CONTRACT_ID ft_transfer json-args '{"receiver_id": "benjiman.testnet", "amount": "1000000000000000000000000", "memo": "Go Team :)"}' prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' sign-as $TRANSFER_FT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -You should see the `FtTransferEvent` being emitted in the console. At this point, if you check for the total supply, it should still be 1000 `gtNEAR` but if you check both the balance of Benji and the balance of the owner, they should reflect the transfer. - -- Check owner balance: - - - - - ```bash - near view $TRANSFER_FT_CONTRACT_ID ft_balance_of '{"account_id": "'$TRANSFER_FT_CONTRACT_ID'"}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $TRANSFER_FT_CONTRACT_ID ft_balance_of json-args '{"account_id": "'$TRANSFER_FT_CONTRACT_ID'"}' network-config testnet now - ``` - - - -- Check `benjiman.testnet` balance: - - - - - ```bash - near view $TRANSFER_FT_CONTRACT_ID ft_balance_of '{"account_id": "benjiman.testnet"}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $TRANSFER_FT_CONTRACT_ID ft_balance_of json-args '{"account_id": "benjiman.testnet"}' network-config testnet now - ``` - - - -- Check total supply: - - - - - ```bash - near view $TRANSFER_FT_CONTRACT_ID ft_total_supply '{}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $TRANSFER_FT_CONTRACT_ID ft_total_supply json-args {} network-config testnet now - ``` - - - -
- -### Testing the transfer call function - -Now that you've tested the `ft_transfer` function, it's time to test the `ft_transfer_call` function. If you try to transfer tokens to a receiver that does **not** implement the `ft_on_transfer` function, the contract will panic and the FTs will be **refunded**. Let's test this functionality below. - -You can try to transfer the FTs to the account `no-contract.testnet` which, as the name suggests, doesn't have a contract. This means that the receiver doesn't implement the `ft_on_transfer` function and the FTs should remain yours after the transaction is complete. You'll first have to register the account, however. - - - - - ```bash - near call $TRANSFER_FT_CONTRACT_ID storage_deposit '{"account_id": "no-contract.testnet"}' --gas 100000000000000 --deposit 0.01 --accountId $TRANSFER_FT_CONTRACT_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $TRANSFER_FT_CONTRACT_ID storage_deposit json-args '{"account_id": "no-contract.testnet"}' prepaid-gas '100.0 Tgas' attached-deposit '0.01 NEAR' sign-as $TRANSFER_FT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - - - - - ```bash - near call $TRANSFER_FT_CONTRACT_ID ft_transfer_call '{"receiver_id": "no-contract.testnet", "amount": "1000000000000000000000000", "msg": "foo"}' --gas 100000000000000 --depositYocto 1 --accountId $TRANSFER_FT_CONTRACT_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $TRANSFER_FT_CONTRACT_ID ft_transfer_call json-args '{"receiver_id": "no-contract.testnet", "amount": "1000000000000000000000000", "msg": "foo"}' prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' sign-as $TRANSFER_FT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -The output response should be as follows. - -```bash -Scheduling a call: transfer.dev-1660680326316-91393402417293.ft_transfer_call({"receiver_id": "no-contract.testnet", "amount": "1000000000000000000000000", "msg": "foo"}) with attached 0.000000000000000000000001 NEAR -Doing account.functionCall() -Receipts: AJ3yWv7tSiZRLtoTkyTgfdzmQP1dpjX9DxJDiD33uwTZ, EKtpDFoJWNnbyxJ7SriAFQYX8XV9ZTzwmCF2qhSaYMAc, 21UzDZ791pWZRKAHv8WaRKN8mqDVrz8zewLWGTNZTckh - Log [transfer.dev-1660680326316-91393402417293]: EVENT_JSON:{"standard":"nep141","version":"1.0.0","event":"ft_transfer","data":[{"old_owner_id":"transfer.dev-1660680326316-91393402417293","new_owner_id":"no-contract.testnet","amount":"1000000000000000000000000"}]} -Receipt: 5N2WV8picxwUNC5TYa3A3v4qGquQAhkU6P81tgRt1UFN - Failure [transfer.dev-1660680326316-91393402417293]: Error: Cannot find contract code for account no-contract.testnet -Receipt: AdT1bSZNCu9ACq7m6ynb12GgSb3zBenfzvvzRwfYPBmg - Log [transfer.dev-1660680326316-91393402417293]: EVENT_JSON:{"standard":"nep141","version":"1.0.0","event":"ft_transfer","data":[{"old_owner_id":"no-contract.testnet","new_owner_id":"transfer.dev-1660680326316-91393402417293","amount":"1000000000000000000000000","memo":"Refund"}]} -Transaction Id 2XVy8MZU8TWreW8C9zK6HSyBsxE5hyTbyUyxNdncxL8g -To see the transaction in the transaction explorer, please open this url in your browser -https://testnet.nearblocks.io/txns/2XVy8MZU8TWreW8C9zK6HSyBsxE5hyTbyUyxNdncxL8g -'0' -``` - -There should be a transfer event emitted for the initial transfer of tokens and then also for the refund. In addition, `0` should have been returned from the function because the sender ended up transferring net 0 tokens to the receiver since all the tokens were refunded. - -If you query for the balance of `no-contract.testnet`, it should still be 0. - - - - - ```bash - near view $TRANSFER_FT_CONTRACT_ID ft_balance_of '{"account_id": "no-contract.testnet"}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $TRANSFER_FT_CONTRACT_ID ft_balance_of json-args '{"account_id": "no-contract.testnet"}' network-config testnet now - ``` - - - -Hurray! At this point, your FT contract is fully complete and all the functionality is working as expected. Go forth and experiment! The world is your oyster and don't forget, go team! - ---- - -## Conclusion - -In this tutorial, you learned how to expand a FT contract by adding ways for users to transfer FTs. You [broke down](#introduction) the problem into smaller, more digestible subtasks and took that information and implemented both the [FT transfer](#transfer-function) and [FT transfer call](#transfer-call-function) functions. In addition, you deployed another [contract](#redeploying-contract) and [tested](#testing-the-transfer-function) the transfer functionality. - -In the [next tutorial](/tutorials/fts/marketplace), you'll learn about how an NFT marketplace can operate to purchase NFTs by using Fungible Tokens. - ---- - -:::note Versioning for this article -At the time of this writing, this example works with the following versions: - -- rustc: `1.77.1` -- near-sdk-rs: `5.1.0` (with enabled `legacy` feature) -- cargo-near: `0.13.2` -- near-cli-rs: `0.17.0` -::: diff --git a/docs/tutorials/fts/6-marketplace.md b/docs/tutorials/fts/6-marketplace.md deleted file mode 100644 index aab79a4fd6a..00000000000 --- a/docs/tutorials/fts/6-marketplace.md +++ /dev/null @@ -1,619 +0,0 @@ ---- -id: marketplace -title: Integrating FT Payments into an NFT Marketplace -sidebar_label: Adding FTs to a Marketplace -description: "Learn how to integrate fungible token payments into an NFT marketplace on NEAR Protocol." ---- -import {Github} from "@site/src/components/UI/Codetabs"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -In this tutorial, you'll learn the basics of how an NFT marketplace contract works and how you can modify it to allow for purchasing NFTs using Fungible Tokens. In the previous tutorials, you went through and created a fully fledged FT contract that incorporates all the standards found in the [FT standard](https://github.com/near/NEPs/tree/master/neps/nep-0141.md). - ---- - -## Introduction - -Throughout this tutorial, you'll learn how a marketplace contract could work on NEAR. This is meant to be an example and there is no canonical implementation. Feel free to branch off and modify this contract to meet your specific needs. - -Using the same repository as the previous tutorials, if you visit the `market-contract` directory, you should have the necessary files to complete the tutorial. - ---- - -## File structure {#file-structure} - -This directory contains the actual contract code and dependencies as outlined below. - -``` -market-contract -β”œβ”€β”€ Cargo.lock -β”œβ”€β”€ Cargo.toml -β”œβ”€β”€ README.md -└── src - β”œβ”€β”€ external.rs - β”œβ”€β”€ ft_balances.rs - β”œβ”€β”€ internal.rs - β”œβ”€β”€ lib.rs - β”œβ”€β”€ nft_callbacks.rs - β”œβ”€β”€ sale.rs - └── sale_views.rs -``` - -Let's start by building both the finished FT contract and the marketplace contract. Make sure you're in the root directory and run the following command in your terminal: - - -```bash -cd market-contract && cargo near build non-reproducible-wasm && cd .. -``` - -This will install the dependencies for the marketplace contract as well as the FT contract. Note that there's also `ft-tutorial/out` directory with pre-build nft contract wasm file which you'll use to place the NFTs for sale. - -``` -ft-tutorial -└── out - └── nft-contract.wasm -``` - ---- - -## Understanding the contract - -The marketplace contract used in this tutorial is a modified version of the contract created at the end of the NFT zero to hero tutorial. If you'd like to learn about the backbone of how the NFTs are put for sale and the process by which they are minted / sold, check out the [NFT zero to hero tutorial](/tutorials/nfts/marketplace). - -The core functionalities are the same in the sense that both this contract and the marketplace contract built in the NFT zero to hero have the following features: -- Users can put NFTs for sale and specify sale conditions -- Users must pay for storage deposit to put NFTs for sale and they can withdraw the storage at any time -- Users can update the price of an NFT or remove the sale entirely -- Buyers can purchase the NFTs by calling `offer`. - -The only difference is that this marketplace has removed the ability to purchase NFTs for `$NEAR` and instead allows users to buy them with Fungible Tokens. The fungible token is specified when the contract is initialized and only **1 type of fungible token** can be used to purchase NFTs. You can't, for example, offer 100 Team Tokens for an NFT and 5000 Mike Tokens for another. - -In addition, the marketplace does **not** support royalties. This is because FT transfers are less Gas efficient than regular $NEAR transfers. In addition, each user would have to be registered and it's much easier to say "hey seller, make sure you're registered before your NFT is sold" rather than enforcing that the seller and **all** accounts in the payout object are registered. When an NFT is sold, the entire price is sent directly to the seller. - -
- -### Purchasing Flow - -In order to purchase an NFT, the contract utilizes the "transfer and call" workflow that the FT contract provides. The marketplace contract implements the `ft_on_transfer` method that is called whenever someone transfers FTs to the marketplace contract. - -The marketplace keeps track of a balance for each user that outlines how many FTs they've sent to the contract. Each time `ft_on_transfer` is called, the marketplace contract will update the balance for the user. When that user wishes to purchase an NFT, they call `offer` and pass in the amount of tokens they're willing to spend. The marketplace will then decrement from their balance and transfer the NFT to the buyer / send the FTs to the seller. - -It's important to note that the seller **must** be registered in the FT contract before a sale is made otherwise the `ft_transfer` method will panic and the seller won't receive any tokens. - ---- - -## Looking at the Code - -Most of the code is the same as what's been outlined in the [NFT zero to hero tutorial](/tutorials/nfts/marketplace) but we'll go through a refresher in case you're new or have forgotten some of the details. - -
- -### Main Library File - -Starting at the `lib.rs` file, this outlines what information is stored on the contract as well as some other crucial functions that you'll learn about below. - -
- -### Initialization function {#initialization-function} - -The first function you'll look at is the initialization function. This takes an `owner_id` as well as the `ft_id` as the parameters and will default all the storage collections to their default values. The `ft_id` outlines the account ID for the fungible token that the contract will allow. - - - -
- -### Storage management model {#storage-management-model} - -Next, let's talk about the storage management model chosen for this contract. Users will need to deposit $NEAR onto the marketplace to cover the storage costs. Whenever someone puts an NFT for sale, the marketplace needs to store that information which costs $NEAR. Users can either deposit a large amount of $NEAR so that they never have to worry about storage again or they can deposit the minimum amount to cover 1 sale on an as-needed basis. - -You might be thinking about the scenario when a sale is purchased. What happens to the storage that is now being released on the contract? This is why we have a storage withdrawal function. This allows users to withdraw any excess storage that is not being used. Let's go through some scenarios to understand the logic. The required storage for 1 sale is 0.01 NEAR on the marketplace contract. - -**Scenario A** - -- Benji wants to list his NFT on the marketplace but has never paid for storage. -- He deposits exactly 0.01 NEAR using the `storage_deposit` method. This will cover 1 sale. -- He lists his NFT on the marketplace and is now using up 1 out of his prepaid 1 sales and has no more storage left. If he were to call `storage_withdraw`, nothing would happen. -- Dorian loves his NFT and quickly purchases it before anybody else can. This means that Benji's sale has now been taken down (since it was purchased) and Benji is using up 0 out of his prepaid 1 sales. In other words, he has an excess of 1 sale or 0.01 NEAR. -- Benji can now call `storage_withdraw` and will be transferred his 0.01 NEAR back. On the contract's side, after withdrawing, he will have 0 sales paid for and will need to deposit storage before trying to list anymore NFTs. - -**Scenario B** - -- Dorian owns one hundred beautiful NFTs and knows that he wants to list all of them. -- To avoid having to call `storage_deposit` everytime he wants to list an NFT, he calls it once. Since Dorian is a baller, he attaches 10 NEAR which is enough to cover 1000 sales. He now has an excess of 9 NEAR or 900 sales. -- Dorian needs the 9 NEAR for something else but doesn't want to take down his 100 listings. Since he has an excess of 9 NEAR, he can easily withdraw and still have his 100 listings. After calling `storage_withdraw` and being transferred 9 NEAR, he will have an excess of 0 sales. - -With this behavior in mind, the following two functions outline the logic. - - - -In this contract, the storage required for each sale is 0.01 NEAR but you can query that information using the `storage_minimum_balance` function. In addition, if you wanted to check how much storage a given account has paid, you can query the `storage_balance_of` function. - ---- - -## FT Deposits - -If you want to learn more about how NFTs are put for sale, check out the [NFT zero to hero tutorial](/tutorials/nfts/marketplace). Once NFTs are put for sale, the owner has three options: -- Update the price of the NFT -- Remove the sale from the marketplace -- Wait for somebody to purchase it - -In order to purchase NFTs, buyers need to deposit FTs in the contract and call the `offer` function. All the logic for FT deposits is outlined in the `src/ft_balances.rs` file. Starting with the `ft_on_transfer` function, this is called when a user transfers FTs to the marketplace contract. The logic can be seen below. - - - -Once FTs are deposited into the contract, users can either withdraw their FTs or they can use them to purchase NFTs. The withdrawing flow is outlined in the `ft_withdraw` function. It's important to note that you should decrement the user's balance **before** calling the `ft_transfer` function to avoid a common exploit scenario where a user spams the `ft_withdraw`. If you were to decrement their balance in the callback function (if the transfer was successful), they could spam the `ft_withdraw` during the time it takes the callback function to execute. A better pattern is to decrement their balance before the transfer and then if the promise was **unsuccessful**, revert the balance back to what it was before. - - - ---- - -## Purchasing NFTs - -Now that you're familiar with the process of both adding storage and depositing FTs on the marketplace, let's go through what you can do once a sale has been listed. The `src/sale.rs` file outlines the functions for updating the price, removing, and purchasing NFTs. In this tutorial, we'll focus **only** on the purchasing flow. If you'd like to learn about the sale objects, updating the price, and removing a sale, check out the [NFT zero to hero tutorial](/tutorials/nfts/marketplace). - -For purchasing NFTs, you must call the `offer` function. It takes an `nft_contract_id`, `token_id`, and the amount you wish to offer as parameters. Behind the scenes, this function will make sure your offer amount is greater than the list price and also that you have enough FTs deposited. It will then call a private method `process_purchase` which will perform a cross-contract call to the NFT contract to invoke the `nft_transfer` function where the NFT will be transferred to the seller. - - - -Once the transfer is complete, the contract will call `resolve_purchase` where it will check the status of the transfer.If the transfer succeeded, it will send the FTs to the seller. If the transfer didn't succeed, it will increment the buyer's FT balance (acting as a refund). - - - - -## View Methods - -There are several view functions that the marketplace contract exposes. All of these functions are the same as the [NFT zero to hero tutorial](/tutorials/nfts/marketplace) except for one extra function we've added. In the `src/ft_balances.rs` file, we've added the `ft_balance_of` function. This function returns the balance of a given account. - ---- - -## Testing - -Now that you *hopefully* have a good understanding of how the marketplace contract works and how you can use the powers of the FT standard to purchase NFTs, let's move onto testing everything. - -### Deploying and Initializing the Contracts - -The first thing you'll want to do is deploy a new FT, NFT, and marketplace contract. - -```bash -cd market-contract && cargo near build non-reproducible-wasm && cd .. -``` - -To deploy the FT contract and export an environment variable, run the following command: - -```bash -export FT_CONTRACT= -near create-account $FT_CONTRACT --useFaucet -cd 5.transfers/ && cargo near deploy build-non-reproducible-wasm $FT_CONTRACT with-init-call new_default_meta json-args '{"owner_id": "'$FT_CONTRACT'", "total_supply": "1000000000000000000000000000"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send && cd ../ -``` - -Next, you'll deploy the NFT and marketplace contracts. - -```bash -export NFT_CONTRACT= -near create-account $NFT_CONTRACT --useFaucet -near deploy $NFT_CONTRACT out/nft-contract.wasm -``` - -```bash -export MARKETPLACE_CONTRACT= -near create-account $MARKETPLACE_CONTRACT --useFaucet -cd market-contract/ && cargo near deploy build-non-reproducible-wasm $MARKETPLACE_CONTRACT with-init-call new json-args '{"owner_id": "'$MARKETPLACE_CONTRACT'", "ft_id": "'$FT_CONTRACT'"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send && cd ../ -``` - -Check and see if your environment variables are all correct by running the following command. Each output should be different. - -```bash -echo $FT_CONTRACT && echo $MARKETPLACE_CONTRACT && echo $NFT_CONTRACT -``` -An example output is: - -```bash -ft-contract.testnet -marketplace-contract.testnet -nft-contract.testnet -``` - -Once that's finished, go ahead and initialize NFT contract by running the following command (FT and marketplace contract were initialized during deploying process above). - - - - - ```bash - near call $NFT_CONTRACT new_default_meta '{"owner_id": "'$NFT_CONTRACT'"}' --accountId $NFT_CONTRACT - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT new_default_meta json-args '{"owner_id": "'$NFT_CONTRACT'"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as $NFT_CONTRACT network-config testnet sign-with-keychain send - ``` - - - -Let's check if each contract was initialized correctly. You can do this by checking the metadata of the FT and NFT contracts: - -```bash -near view $FT_CONTRACT ft_metadata && near view $NFT_CONTRACT nft_metadata -``` -In addition, you can check the sales of the marketplace contract and it should return 0. - -```bash -near view $MARKETPLACE_CONTRACT get_supply_sales -``` - -
- -### Placing a Token For Sale - -Now that the marketplace and NFT contracts are initialized, let's place a token for sale. Start by creating a new buyer and seller account by running the following command. In this case, we'll create a sub-account of the FT contract. - -```bash -near create-account buyer.$FT_CONTRACT --masterAccount $FT_CONTRACT --initialBalance 2 && export BUYER_ID=buyer.$FT_CONTRACT -``` - -```bash -near create-account seller.$FT_CONTRACT --masterAccount $FT_CONTRACT --initialBalance 2 && export SELLER_ID=seller.$FT_CONTRACT -``` - -Check if everything went well by running the following command. - -```bash -echo $BUYER_ID && echo $SELLER_ID -``` -This should return something similar to: -```bash -buyer.ft-contract.testnet -seller.ft-contract.testnet -``` - -The next thing you'll want to do is mint a token to the seller. - - - - - ```bash - near call $NFT_CONTRACT nft_mint '{"token_id": "market-token", "metadata": {"title": "Marketplace Token", "description": "testing out the marketplace", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$SELLER_ID'"}' --accountId $NFT_CONTRACT --amount 0.1 - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT nft_mint json-args '{"token_id": "market-token", "metadata": {"title": "Marketplace Token", "description": "testing out the marketplace", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$SELLER_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $NFT_CONTRACT network-config testnet sign-with-keychain send - ``` - - - -Now you'll need to place the token for sale. This requires paying for storage as well as calling the `nft_approve` function. - - - - - ```bash - near call $MARKETPLACE_CONTRACT storage_deposit --accountId $SELLER_ID --amount 0.1 - ``` - - - - - ```bash - near contract call-function as-transaction $MARKETPLACE_CONTRACT storage_deposit json-args '{}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $SELLER_ID network-config testnet sign-with-keychain send - ``` - - - -In this case, we'll place the token for sale for `10 gtNEAR`. - - - - - ```bash - near call $NFT_CONTRACT nft_approve '{"token_id": "market-token", "account_id": "'$MARKETPLACE_CONTRACT'", "msg": "{\"sale_conditions\":\"10000000000000000000000000\"}"}' --accountId $SELLER_ID --amount 0.1 - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT nft_approve json-args '{"token_id": "market-token", "account_id": "'$MARKETPLACE_CONTRACT'", "msg": "{\"sale_conditions\":\"10000000000000000000000000\"}"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $SELLER_ID network-config testnet sign-with-keychain send - ``` - - - -If you now query for the supply of sales again on the marketplace, it should be 1. - - - - - ```bash - near view $MARKETPLACE_CONTRACT get_supply_sales - ``` - - - - - ```bash - near contract call-function as-read-only $MARKETPLACE_CONTRACT get_supply_sales json-args {} network-config testnet now - ``` - - - -In addition, if you query for the sales by the owner ID, it should reflect the `10 gtNEAR` price. - - - - - ```bash - near view $MARKETPLACE_CONTRACT get_sales_by_owner_id '{"account_id": "'$SELLER_ID'"}' - ``` - - - - - ```bash - near contract call-function as-read-only $MARKETPLACE_CONTRACT get_sales_by_owner_id json-args '{"account_id": "'$SELLER_ID'"}' network-config testnet now - ``` - - - -Expected output: - -```bash -[ - { - owner_id: 'seller.ft-contract.testnet', - approval_id: 0, - nft_contract_id: 'nft-contract.testnet', - token_id: 'market-token', - sale_conditions: '10000000000000000000000000' - } -] -``` - -
- -### Deposit FTs into the Marketplace - -Now that you have an NFT up for sale for `10 gtNEAR` on the marketplace contract, the buyer needs to deposit some FTs. The first thing you need to do is register both the marketplace contract and the buyer on the FT contract otherwise you won't be able to transfer any FTs. - - - - - ```bash - near call $FT_CONTRACT storage_deposit '{"account_id": "'$MARKETPLACE_CONTRACT'"}' --accountId $FT_CONTRACT --amount 0.1 - - near call $FT_CONTRACT storage_deposit '{"account_id": "'$BUYER_ID'"}' --accountId $FT_CONTRACT --amount 0.1 - ``` - - - - - ```bash - near contract call-function as-transaction $FT_CONTRACT storage_deposit json-args '{"account_id": "'$MARKETPLACE_CONTRACT'"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $FT_CONTRACT network-config testnet sign-with-keychain send - - near contract call-function as-transaction $FT_CONTRACT storage_deposit json-args '{"account_id": "'$BUYER_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $FT_CONTRACT network-config testnet sign-with-keychain send - ``` - - - -After this, you should transfer the buyer some FTs so that they can deposit at least `10 gtNEAR`. Lets start with 50 `gtNEAR`. Run the following command to send the buyer FTs on behalf of the FT contract owner. - - - - - ```bash - near call $FT_CONTRACT ft_transfer '{"receiver_id": "'$BUYER_ID'", "amount": "50000000000000000000000000", "memo": "Go Team!"}' --accountId $FT_CONTRACT --depositYocto 1 - ``` - - - - - ```bash - near contract call-function as-transaction $FT_CONTRACT ft_transfer json-args '{"receiver_id": "'$BUYER_ID'", "amount": "50000000000000000000000000", "memo": "Go Team!"}' prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' sign-as $FT_CONTRACT network-config testnet sign-with-keychain send - ``` - - - -You'll now need to deposit those tokens into the marketplace contract. - - - - - ```bash - near call $FT_CONTRACT ft_transfer_call '{"receiver_id": "'$MARKETPLACE_CONTRACT'", "amount": "50000000000000000000000000", "msg": "Wooooooo!"}' --accountId $BUYER_ID --depositYocto 1 --gas 200000000000000 - ``` - - - - - ```bash - near contract call-function as-transaction $FT_CONTRACT ft_transfer_call json-args '{"receiver_id": "'$MARKETPLACE_CONTRACT'", "amount": "50000000000000000000000000", "msg": "Wooooooo!"}' prepaid-gas '200.0 Tgas' attached-deposit '1 yoctoNEAR' sign-as $BUYER_ID network-config testnet sign-with-keychain send - ``` - - - -If you now query for your balance on the marketplace contract, it should be `50 gtNEAR`. - - - - - ```bash - near view $MARKETPLACE_CONTRACT ft_deposits_of '{"account_id": "'$BUYER_ID'"}' - ``` - - - - - ```bash - near contract call-function as-read-only $MARKETPLACE_CONTRACT ft_deposits_of json-args '{"account_id": "'$BUYER_ID'"}' network-config testnet now - ``` - - - -
- -### Purchasing the NFT - -Now that the buyer has deposited FTs into the marketplace and the token is up for sale, let's go ahead and make an offer! If you try to offer more FTs than what you have, the contract will panic. Similarly, if you try to offer lower than the sale price, the contract will also panic. Since the sale price is `10 gtNEAR`, let's try to offer `20 gtNEAR` and see what happens. The expected outcome is: -- The NFT will be transferred to the buyer -- `20 gtNEAR` will be sent to the seller -- The buyer will have `30 gtNEAR` left to withdraw. - -There is one thing we're forgetting, however. We need to make sure that the seller is registered on the FT contract so let's go ahead and do that now. - - - - - ```bash - near call $FT_CONTRACT storage_deposit '{"account_id": "'$SELLER_ID'"}' --accountId $FT_CONTRACT --amount 0.1 - ``` - - - - - ```bash - near contract call-function as-transaction $FT_CONTRACT storage_deposit json-args '{"account_id": "'$SELLER_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $FT_CONTRACT network-config testnet sign-with-keychain send - ``` - - - -Now let's make an offer! - - - - - ```bash - near call $MARKETPLACE_CONTRACT offer '{"nft_contract_id": "'$NFT_CONTRACT'", "token_id": "market-token", "amount": "20000000000000000000000000"}' --accountId $BUYER_ID --depositYocto 1 --gas 300000000000000 - ``` - - - - - ```bash - near contract call-function as-transaction $MARKETPLACE_CONTRACT offer json-args '{"nft_contract_id": "'$NFT_CONTRACT'", "token_id": "market-token", "amount": "20000000000000000000000000"}' prepaid-gas '300.0 Tgas' attached-deposit '1 yoctoNEAR' sign-as $BUYER_ID network-config testnet sign-with-keychain send - ``` - - - -If everything went well, you should see 2 events in your terminal. One event is the NFT transfer coming from the NFT contract when the token was transferred from the seller to the buyer. The other event is the FT transfer for when the seller receives their fungible tokens. - -```bash -Log [dev-1660831638497-73655245450834]: Memo: payout from market -Log [dev-1660831638497-73655245450834]: EVENT_JSON:{"standard":"nep171","version":"nft-1.0.0","event":"nft_transfer","data":[{"authorized_id":"dev-1660831638497-73655245450834","old_owner_id":"seller.dev-1660831615048-16894106456797","new_owner_id":"buyer.dev-1660831615048-16894106456797","token_ids":["market-token"],"memo":"payout from market"}]} -Receipt: BBvHig5zg1n2vmxFPTpxED4FNCAU1ZzZ3H8EBqqaeRw5 -Log [dev-1660831638497-73655245450834]: EVENT_JSON:{"standard":"nep141","version":"1.0.0","event":"ft_transfer","data":[{"old_owner_id":"dev-1660831638497-73655245450834","new_owner_id":"seller.dev-1660831615048-16894106456797","amount":"20000000000000000000000000","memo":"Sale from marketplace"}]} -``` - -Let's call some view methods to double check if everything went well. First let's check if the seller now has `20 gtNEAR`. - - - - - ```bash - near view $FT_CONTRACT ft_balance_of '{"account_id": "'$SELLER_ID'"}' - ``` - - - - - ```bash - near contract call-function as-read-only $FT_CONTRACT ft_balance_of json-args '{"account_id": "'$SELLER_ID'"}' network-config testnet now - ``` - - - -Next, let's check if the buyer has `30 gtNEAR` left to withdraw. - - - - - ```bash - near view $MARKETPLACE_CONTRACT ft_deposits_of '{"account_id": "'$BUYER_ID'"}' - ``` - - - - - ```bash - near contract call-function as-read-only $MARKETPLACE_CONTRACT ft_deposits_of json-args '{"account_id": "'$BUYER_ID'"}' network-config testnet now - ``` - - - -Finally, let's check if the NFT is now owned by the buyer. - - - - - ```bash - near view $NFT_CONTRACT nft_token '{"token_id": "market-token"}' - ``` - - - - - ```bash - near contract call-function as-read-only $NFT_CONTRACT nft_token json-args '{"token_id": "market-token"}' network-config testnet now - ``` - - - -
- -### Withdrawing the Excess Deposits - -Now that the buyer purchased the NFT with `20 gtNEAR`, they should have `30 gtNEAR` left to withdraw. If they withdraw the tokens, they should be left with a balance of `30 gtNEAR` on the FT contract. - - - - - ```bash - near call $MARKETPLACE_CONTRACT ft_withdraw '{"amount": "30000000000000000000000000"}' --accountId $BUYER_ID --depositYocto 1 --gas 300000000000000 - ``` - - - - - ```bash - near contract call-function as-transaction $MARKETPLACE_CONTRACT ft_withdraw json-args '{"amount": "30000000000000000000000000"}' prepaid-gas '300.0 Tgas' attached-deposit '1 yoctoNEAR' sign-as $BUYER_ID network-config testnet sign-with-keychain send - ``` - - - -If you now query for the buyer's balance, it should be `30 gtNEAR`. - - - - - ```bash - near view $FT_CONTRACT ft_balance_of '{"account_id": "'$BUYER_ID'"}' - ``` - - - - - ```bash - near contract call-function as-read-only $FT_CONTRACT ft_balance_of json-args '{"account_id": "'$BUYER_ID'"}' network-config testnet now - ``` - - - -And just like that you're finished! You went through and put an NFT up for sale and purchased it using fungible tokens! **Go team πŸš€** - ---- - -## Conclusion - -In this tutorial, you learned about the basics of a marketplace contract and how it works. You went through the core logic both at a high level and looked at the code. You deployed an NFT, marketplace, and FT contract, initialized them all and then put an NFT for sale and sold it for fungible tokens! What an amazing experience! Go forth and expand these contracts to meet whatever needs you have. The world is your oyster and thank you so much for following along with this tutorial series. Don't hesitate to ask for help or clarification on anything in our discord or social media channels. **Go Team!** - ---- - -:::note Versioning for this article -At the time of this writing, this example works with the following versions: - -- rustc: `1.77.1` -- near-sdk-rs: `5.1.0` (with enabled `legacy` feature) -- cargo-near: `0.13.2` -- near-cli: `4.0.13` -::: diff --git a/docs/tutorials/nfts-js.md b/docs/tutorials/nfts-js.md new file mode 100644 index 00000000000..52ffc21b1ce --- /dev/null +++ b/docs/tutorials/nfts-js.md @@ -0,0 +1,52 @@ +--- +id: nfts-js +title: NFT Zero to Hero JavaScript Edition +sidebar_label: Build a NFT Contract from Scratch (JS) +description: "Learn NFTs from minting to building a full-featured smart contract in this Zero to Hero series." +--- + +> In this _Zero to Hero_ series, you'll find a set of tutorials that will cover every aspect of a non-fungible token (NFT) smart contract. +> You'll start by minting an NFT using a pre-deployed contract and by the end you'll end up building a fully-fledged NFT smart contract that supports every extension. + + + +## Prerequisites + +To complete these tutorials successfully, you'll need: + +- [Node.js](/smart-contracts/quickstart?code-tabs=js) +- [A NEAR Wallet](https://testnet.mynearwallet.com/create) +- [NEAR-CLI](/tools/near-cli#installation) + +--- + +## Overview + +These are the steps that will bring you from **_Zero_** to **_Hero_** in no time! πŸ’ͺ + +| Step | Name | Description | +|------|------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| +| 1 | [Pre-deployed contract](https://near-examples.github.io/nft-tutorial-js/predeployed-contract) | Mint an NFT without the need to code, create, or deploy a smart contract. | +| 2 | [Contract architecture](https://near-examples.github.io/nft-tutorial-js/skeleton) | Learn the basic architecture of the NFT smart contract and compile code. | +| 3 | [Minting](https://near-examples.github.io/nft-tutorial-js/minting) | Flesh out the skeleton so the smart contract can mint a non-fungible token. | +| 4 | [Upgrade a contract](https://near-examples.github.io/nft-tutorial-js/upgrade-contract) | Discover the process to upgrade an existing smart contract. | +| 5 | [Enumeration](https://near-examples.github.io/nft-tutorial-js/enumeration) | Explore enumeration methods that can be used to return the smart contract's states. | +| 6 | [Core](https://near-examples.github.io/nft-tutorial-js/core) | Extend the NFT contract using the core standard which allows token transfer | +| 7 | [Approvals](https://near-examples.github.io/nft-tutorial-js/approvals) | Expand the contract allowing other accounts to transfer NFTs on your behalf. | +| 8 | [Royalty](https://near-examples.github.io/nft-tutorial-js/royalty) | Add NFT royalties allowing for a set percentage to be paid out to the token creator. | +| 9 | [Events](https://near-examples.github.io/nft-tutorial-js/events) | in this tutorial you'll explore the events extension, allowing the contract to react on certain events. | +| 10 | [Marketplace](https://near-examples.github.io/nft-tutorial-js/marketplace) | Learn about how common marketplaces operate on NEAR and dive into some of the code that allows buying and selling NFTs | + +--- + +## Next steps + +Ready to start? Jump to the [Pre-deployed Contract](https://near-examples.github.io/nft-tutorial-js/predeployed-contract) tutorial and begin your learning journey! + +If you already know about non-fungible tokens and smart contracts, feel free to skip and jump directly to the tutorial of your interest. The tutorials have been designed so you can start at any given point! + +:::info Questions? + +πŸ‘‰ Join us on [Discord](https://near.chat/) and let us know in the `#development` channels. πŸ‘ˆ + +::: diff --git a/docs/tutorials/nfts.md b/docs/tutorials/nfts.md new file mode 100644 index 00000000000..80e6b0a5558 --- /dev/null +++ b/docs/tutorials/nfts.md @@ -0,0 +1,51 @@ +--- +id: nfts +title: NFT Zero to Hero +sidebar_label: Build a NFT Contract from Scratch +description: "Learn how to mint NFTs and build a full NFT contract step by step." +--- + +In this _Zero to Hero_ series, you'll find a set of tutorials that will cover every aspect of a non-fungible token (NFT) smart contract. +You'll start by minting an NFT using a pre-deployed contract and by the end you'll end up building a fully-fledged NFT smart contract that supports every extension. + +--- + +## Prerequisites + +To complete these tutorials successfully, you'll need: + +- [Rust](https://www.rust-lang.org/tools/install) +- [A Testnet wallet](https://testnet.mynearwallet.com/create) +- [NEAR-CLI](/tools/near-cli#installation) +- [cargo-near](https://github.com/near/cargo-near) + +:::info New to Rust? +If you are new to Rust and want to dive into smart contract development, our [Quick-start guide](../smart-contracts/quickstart.md) is a great place to start +::: + +--- + +## Overview + +These are the steps that will bring you from **_Zero_** to **_Hero_** in no time! πŸ’ͺ + +| Step | Name | Description | +|------|---------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| +| 1 | [Pre-deployed contract](https://near-examples.github.io/nft-tutorial/predeployed-contract) | Mint an NFT without the need to code, create, or deploy a smart contract. | +| 2 | [Contract architecture](https://near-examples.github.io/nft-tutorial/skeleton) | Learn the basic architecture of the NFT smart contract and compile code. | +| 3 | [Minting](https://near-examples.github.io/nft-tutorial/minting) | Flesh out the skeleton so the smart contract can mint a non-fungible token. | +| 4 | [Upgrade a contract](https://near-examples.github.io/nft-tutorial/upgrade-contract) | Discover the process to upgrade an existing smart contract. | +| 5 | [Enumeration](https://near-examples.github.io/nft-tutorial/enumeration) | Explore enumeration methods that can be used to return the smart contract's states. | +| 6 | [Core](https://near-examples.github.io/nft-tutorial/core) | Extend the NFT contract using the core standard which allows token transfer. | +| 7 | [Events](https://near-examples.github.io/nft-tutorial/events) | The events extension, allowing the contract to react on certain events. | +| 8 | [Approvals](https://near-examples.github.io/nft-tutorial/approvals) | Expand the contract allowing other accounts to transfer NFTs on your behalf. | +| 9 | [Royalty](https://near-examples.github.io/nft-tutorial/royalty) | Add NFT royalties allowing for a set percentage to be paid out to the token creator. | +| 10 | [Marketplace](https://near-examples.github.io/nft-tutorial/marketplace) | Learn about how common marketplaces operate on NEAR and dive into some of the code that allows buying and selling NFTs. | + +--- + +## Next steps + +Ready to start? Jump to the [Pre-deployed Contract](https://near-examples.github.io/nft-tutorial/predeployed-contract) tutorial and begin your learning journey! + +If you already know about non-fungible tokens and smart contracts, feel free to skip and jump directly to the tutorial of your interest. The tutorials have been designed so you can start at any given point! diff --git a/docs/tutorials/nfts/0-intro.md b/docs/tutorials/nfts/0-intro.md deleted file mode 100644 index dfa011abca9..00000000000 --- a/docs/tutorials/nfts/0-intro.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -id: introduction -title: NFT Zero to Hero -sidebar_label: Introduction -description: "Learn how to mint NFTs and build a full NFT contract step by step." ---- - -In this _Zero to Hero_ series, you'll find a set of tutorials that will cover every aspect of a non-fungible token (NFT) smart contract. -You'll start by minting an NFT using a pre-deployed contract and by the end you'll end up building a fully-fledged NFT smart contract that supports every extension. - ---- - -## Prerequisites - -To complete these tutorials successfully, you'll need: - -- [Rust](https://www.rust-lang.org/tools/install) -- [A Testnet wallet](https://testnet.mynearwallet.com/create) -- [NEAR-CLI](/tools/near-cli#installation) -- [cargo-near](https://github.com/near/cargo-near) - -:::info New to Rust? -If you are new to Rust and want to dive into smart contract development, our [Quick-start guide](../../smart-contracts/quickstart.md) is a great place to start -::: - ---- - -## Overview - -These are the steps that will bring you from **_Zero_** to **_Hero_** in no time! πŸ’ͺ - -| Step | Name | Description | -|------|---------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| -| 1 | [Pre-deployed contract](/tutorials/nfts/predeployed-contract) | Mint an NFT without the need to code, create, or deploy a smart contract. | -| 2 | [Contract architecture](/tutorials/nfts/skeleton) | Learn the basic architecture of the NFT smart contract and compile code. | -| 3 | [Minting](/tutorials/nfts/minting) | Flesh out the skeleton so the smart contract can mint a non-fungible token. | -| 4 | [Upgrade a contract](/tutorials/nfts/upgrade-contract) | Discover the process to upgrade an existing smart contract. | -| 5 | [Enumeration](/tutorials/nfts/enumeration) | Explore enumeration methods that can be used to return the smart contract's states. | -| 6 | [Core](/tutorials/nfts/core) | Extend the NFT contract using the core standard which allows token transfer. | -| 7 | [Events](/tutorials/nfts/events) | The events extension, allowing the contract to react on certain events. | -| 8 | [Approvals](/tutorials/nfts/approvals) | Expand the contract allowing other accounts to transfer NFTs on your behalf. | -| 9 | [Royalty](/tutorials/nfts/royalty) | Add NFT royalties allowing for a set percentage to be paid out to the token creator. | -| 10 | [Marketplace](/tutorials/nfts/marketplace) | Learn about how common marketplaces operate on NEAR and dive into some of the code that allows buying and selling NFTs. | - ---- - -## Next steps - -Ready to start? Jump to the [Pre-deployed Contract](/tutorials/nfts/predeployed-contract) tutorial and begin your learning journey! - -If you already know about non-fungible tokens and smart contracts, feel free to skip and jump directly to the tutorial of your interest. The tutorials have been designed so you can start at any given point! diff --git a/docs/tutorials/nfts/0-predeployed.md b/docs/tutorials/nfts/0-predeployed.md deleted file mode 100644 index bee52709857..00000000000 --- a/docs/tutorials/nfts/0-predeployed.md +++ /dev/null @@ -1,160 +0,0 @@ ---- -id: predeployed-contract -title: Pre-deployed Contract -sidebar_label: Pre-deployed Contract -description: "Mint your first NFT using a pre-deployed contract before building your own NFT smart contract." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Create your first non-fungible token by using a pre-deployed NFT smart contract which works exactly as the one you will build on this tutorial. - ---- - -## Prerequisites - -To complete this tutorial successfully, you'll need [a NEAR Wallet](https://testnet.mynearwallet.com/create) and [NEAR CLI](/tools/near-cli#installation) - ---- - -## Using the NFT contract - -Minting an NFT token on NEAR is a simple process that involves calling a smart contract function. - -To interact with the contract you will need to first login to your NEAR account through `near-cli`. - -
- -### Setup - -Log in to your newly created account with `near-cli` by running the following command in your terminal: - -```bash -near account import-account using-web-wallet network-config testnet -``` - -Set an environment variable for your account ID to make it easy to copy and paste commands from this tutorial: - -```bash -export NEARID=YOUR_ACCOUNT_NAME -``` - -
- -### Minting your NFTs - -We have already deployed an NFT contract to `nft.examples.testnet` which allows users to freely mint tokens. Let's use it to mint our first token. - -Run this command in your terminal, remember to replace the `token_id` with a string of your choice. This string will uniquely identify the token you mint. - - - - - ```bash - near call nft.examples.testnet nft_mint '{"token_id": "TYPE_A_UNIQUE_VALUE_HERE", "receiver_id": "'$NEARID'", "metadata": { "title": "GO TEAM", "description": "The Team Goes", "media": "https://bafybeidl4hjbpdr6u6xvlrizwxbrfcyqurzvcnn5xoilmcqbxfbdwrmp5m.ipfs.dweb.link/", "copies": 1}}' --gas 100000000000000 --deposit 0.1 --accountId $NEARID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction nft.examples.testnet nft_mint json-args '{"token_id": "TYPE_A_UNIQUE_VALUE_HERE", "receiver_id": "'$NEARID'", "metadata": { "title": "GO TEAM", "description": "The Team Goes", "media": "https://bafybeidl4hjbpdr6u6xvlrizwxbrfcyqurzvcnn5xoilmcqbxfbdwrmp5m.ipfs.dweb.link/", "copies": 1}}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $NEARID network-config testnet sign-with-keychain send - ``` - - - -
-Example response: -

- -```json -Log [nft.examples.testnet]: EVENT_JSON:{"standard":"nep171","version":"nft-1.0.0","event":"nft_mint","data":[{"owner_id":"benjiman.testnet","token_ids":["TYPE_A_UNIQUE_VALUE_HERE"]}]} -Transaction Id 8RFWrQvAsm2grEsd1UTASKpfvHKrjtBdEyXu7WqGBPUr -To see the transaction in the transaction explorer, please open this url in your browser -https://testnet.nearblocks.io/txns/8RFWrQvAsm2grEsd1UTASKpfvHKrjtBdEyXu7WqGBPUr -'' -``` - -

-
- -:::tip -You can also replace the `media` URL with a link to any image file hosted on your web server. -::: - -
- -### Querying your NFT - -To view tokens owned by an account you can call the NFT contract with the following `near-cli` command: - - - - - ```bash - near view nft.examples.testnet nft_tokens_for_owner '{"account_id": "'$NEARID'"}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only nft.examples.testnet nft_tokens_for_owner json-args '{"account_id": "'$NEARID'"}' network-config testnet now - ``` - - - -
-Example response: -

- -```json -[ - { - "token_id": "Goi0CZ", - "owner_id": "bob.testnet", - "metadata": { - "title": "GO TEAM", - "description": "The Team Goes", - "media": "https://bafybeidl4hjbpdr6u6xvlrizwxbrfcyqurzvcnn5xoilmcqbxfbdwrmp5m.ipfs.dweb.link/", - "media_hash": null, - "copies": 1, - "issued_at": null, - "expires_at": null, - "starts_at": null, - "updated_at": null, - "extra": null, - "reference": null, - "reference_hash": null - }, - "approved_account_ids": {} - } -] -``` - -

-
- -**Congratulations!** You just minted your first NFT token on the NEAR blockchain! πŸŽ‰ - -Now try going to your [NEAR Wallet](https://testnet.mynearwallet.com) and view your NFT in the "Collectibles" tab. - ---- - -## Final remarks - -This basic example illustrates all the required steps to call an NFT smart contract on NEAR and start minting your own non-fungible tokens. - -Now that you're familiar with the process, you can jump to [Contract Architecture](/tutorials/nfts/skeleton) and learn more about the smart contract structure and how you can build your own NFT contract from the ground up. - -***Happy minting!*** πŸͺ™ - -:::note Versioning for this article - -At the time of this writing, this example works with the following versions: - -- near-cli-rs: `0.17.0` -- NFT standard: [NEP171](https://github.com/near/NEPs/tree/master/neps/nep-0171.md), version `1.0.0` - -::: diff --git a/docs/tutorials/nfts/1-skeleton.md b/docs/tutorials/nfts/1-skeleton.md deleted file mode 100644 index a5f86687fcb..00000000000 --- a/docs/tutorials/nfts/1-skeleton.md +++ /dev/null @@ -1,270 +0,0 @@ ---- -id: skeleton -title: Skeleton and Rust Architecture -sidebar_label: Contract Architecture -description: "Learn the basic file structure of the NFT contract and how each Rust file works." ---- -import {Github} from "@site/src/components/UI/Codetabs" - -In this tutorial, you'll explore the architecture of the NFT contract and how Rust files are organized to build a fully-featured smart contract. - -You'll get a walkthrough of each file and its role, giving you the foundation to start building and compiling your own NFT contract. - -:::info Skeleton Contract -You can find the skeleton contract in our [GitHub repository](https://github.com/near-examples/nft-tutorial/tree/main/nft-contract-skeleton) -::: - -:::info New to Rust? -If you are new to Rust and want to dive into smart contract development, our [Quick-start guide](../../smart-contracts/quickstart.md) is a great place to start. -::: - ---- - -## Introduction - -This tutorial presents the code skeleton for the NFT smart contract and its file structure. - -Once every file and functions have been covered, we will guide you through the process of building the mock-up contract to confirm that your Rust setup works. - ---- - -## File structure - -Following a regular [Rust](https://www.rust-lang.org/) project, the file structure for this smart contract has: - -``` -nft-contract -β”œβ”€β”€ Cargo.lock -β”œβ”€β”€ Cargo.toml -β”œβ”€β”€ README.md -└── src - β”œβ”€β”€ approval.rs - β”œβ”€β”€ enumeration.rs - β”œβ”€β”€ lib.rs - β”œβ”€β”€ metadata.rs - β”œβ”€β”€ mint.rs - β”œβ”€β”€ nft_core.rs - β”œβ”€β”€ events.rs - └── royalty.rs -``` - -- The file `Cargo.toml` defines the code dependencies -- The `src` folder contains all the Rust source files - -
- -### Source files - -Here is a brief description of what each source file is responsible for: - -| File | Description | -|----------------------------------|---------------------------------------------------------------------------------| -| [approval.rs](#approvalrs) | Has the functions that controls the access and transfers of non-fungible tokens | -| [enumeration.rs](#enumerationrs) | Contains the methods to list NFT tokens and their owners | -| [lib.rs](#librs) | Holds the smart contract initialization functions | -| [metadata.rs](#metadatars) | Defines the token and metadata structure | -| [mint.rs](#mintrs) | Contains token minting logic | -| [nft_core.rs](#nft_corers) | Core logic that allows you to transfer NFTs between users. | -| [royalty.rs](#royaltyrs) | Contains payout-related functions | -| [events.rs](#eventsrs) | Contains events related structures | - -:::tip -Explore the code in our [GitHub repository](https://github.com/near-examples/nft-tutorial/). -::: - ---- - -## `approval.rs` - -> This allows people to approve other accounts to transfer NFTs on their behalf. - -This file contains the logic that complies with the standard's [approvals management](https://github.com/near/NEPs/tree/master/neps/nep-0178.md) extension. Here is a breakdown of the methods and their functions: - -| Method | Description | -|---------------------|-----------------------------------------------------------------------------------------------------------| -| **nft_approve** | Approves an account ID to transfer a token on your behalf. | -| **nft_is_approved** | Checks if the input account has access to approve the token ID. | -| **nft_revoke** | Revokes a specific account from transferring the token on your behalf. | -| **nft_revoke_all** | Revokes all accounts from transferring the token on your behalf. | -| **nft_on_approve** | This callback function, initiated during `nft_approve`, is a cross contract call to an external contract. | - - - -You'll learn more about these functions in the [approvals section](/tutorials/nfts/approvals) of the Zero to Hero series. - ---- - -## `enumeration.rs` - -> This file provides the functions needed to view information about NFTs, and follows the standard's [enumeration](https://github.com/near/NEPs/tree/master/neps/nep-0181.md) extension. - -| Method | Description | -|--------------------------|------------------------------------------------------------------------------------| -| **nft_total_supply** | Returns the total amount of NFTs stored on the contract | -| **nft_tokens** | Returns a paginated list of NFTs stored on the contract regardless of their owner. | -| **nft_supply_for_owner** | Allows you view the total number of NFTs owned by any given user | -| **nft_tokens_for_owner** | Returns a paginated list of NFTs owned by any given user | - - - -You'll learn more about these functions in the [enumeration section](/tutorials/nfts/enumeration) of the tutorial series. - ---- - -## `lib.rs` - -> This file outlines what information the contract stores and keeps track of. - -| Method | Description | -|----------------------|-------------------------------------------------------------------------------------------------| -| **new_default_meta** | Initializes the contract with default `metadata` so the user doesn't have to provide any input. | -| **new** | Initializes the contract with the user-provided `metadata`. | - -:::info Keep in mind -The initialization functions (`new`, `new_default_meta`) can only be called once. -::: - - - -You'll learn more about these functions in the [minting section](/tutorials/nfts/minting) of the tutorial series. - ---- - -## `metadata.rs` - -> This file is used to keep track of the information to be stored for tokens, and metadata. -> In addition, you can define a function to view the contract's metadata which is part of the standard's [metadata](https://github.com/near/NEPs/tree/master/neps/nep-0177.md) extension. - -| Name | Description | -|-------------------|---------------------------------------------------------------------------------------------------------------| -| **TokenMetadata** | This structure defines the metadata that can be stored for each token (title, description, media, etc.). | -| **Token** | This structure outlines what information will be stored on the contract for each token. | -| **JsonToken** | When querying information about NFTs through view calls, the return information is stored in this JSON token. | -| **nft_metadata** | This function allows users to query for the contact's internal metadata. | - - - -You'll learn more about these functions in the [minting section](/tutorials/nfts/minting) of the tutorial series. - ---- - -## `mint.rs` - -> Contains the logic to mint the non-fungible tokens - -| Method | Description | -|--------------|-------------------------------------------| -| **nft_mint** | This function mints a non-fungible token. | - - - ---- - -## `nft_core.rs` - -> Core logic that allows to transfer NFTs between users. - -| Method | Description | -|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **nft_transfer** | Transfers an NFT to a receiver ID. | -| **nft_transfer_call** | Transfers an NFT to a receiver and calls a function on the receiver ID's contract. The function returns `true` if the token was transferred from the sender's account. | -| **nft_token** | Allows users to query for the information about a specific NFT. | -| **nft_on_transfer** | Called by other contracts when an NFT is transferred to your contract account via the `nft_transfer_call` method. It returns `true` if the token should be returned back to the sender. | -| **nft_resolve_transfer** | When you start the `nft_transfer_call` and transfer an NFT, the standard also calls a method on the receiver's contract. If the receiver needs you to return the NFT to the sender (as per the return value of the `nft_on_transfer` method), this function allows you to execute that logic. | - - - -You'll learn more about these functions in the [core section](/tutorials/nfts/core) of the tutorial series. - ---- - -## `royalty.rs` - -> Contains payout-related functions. - -| Method | Description | -|-------------------------|---------------------------------------------------------------------------------------------------------------| -| **nft_payout** | This view method calculates the payout for a given token. | -| **nft_transfer_payout** | Transfers the token to the receiver ID and returns the payout object that should be paid for a given balance. | - - - -You'll learn more about these functions in the [royalty section](/tutorials/nfts/royalty) of the tutorial series. - ---- - -## `events.rs` - -> Contains events-related structures. - -| Method | Description | -|---------------------|-----------------------------------------------------| -| **EventLogVariant** | This enum represents the data type of the EventLog. | -| **EventLog** | Interface to capture data about an event. | -| **NftMintLog** | An event log to capture token minting. | -| **NftTransferLog** | An event log to capture token transfer. | - - - -You'll learn more about these functions in the [events section](/tutorials/nfts/events) of the tutorial series. - ---- - -## Building the skeleton - -If you haven't cloned the main repository yet, open a terminal and run: - -```sh -git clone https://github.com/near-examples/nft-tutorial/ -``` - -Next, go to the `nft-contract-skeleton/` folder and build the contract with `cargo-near`: - -```sh -cd nft-tutorial -cd nft-contract-skeleton/ -cargo near build -``` - -Since this source is just a skeleton you'll get many warnings about unused code, such as: - -``` - Compiling nft_contract_skeleton v0.1.0 (/Users/near-examples/Documents/my/projects/near/examples/nft-tutorial/nft-contract-basic) - β”‚ warning: unused imports: `LazyOption`, `LookupMap`, `UnorderedMap`, `UnorderedSet` - β”‚ --> src/lib.rs:3:29 - β”‚ | - β”‚ 3 | use near_sdk::collections::{LazyOption, LookupMap, UnorderedMap, UnorderedSet}; - β”‚ | ^^^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^ - β”‚ | - β”‚ = note: `#[warn(unused_imports)]` on by default - β”‚ - β”‚ warning: unused import: `Base64VecU8` - β”‚ --> src/lib.rs:4:28 - β”‚ | - β”‚ 4 | use near_sdk::json_types::{Base64VecU8, U128}; - β”‚ | - - β”‚ warning: `nft_contract_skeleton` (lib) generated 48 warnings (run `cargo fix --lib -p nft_contract_skeleton` to apply 45 suggestions) - β”‚ Finished release [optimized] target(s) in 11.01s - βœ“ Contract successfully built! -``` - -Don't worry about these warnings, you're not going to deploy this contract yet. -Building the skeleton is useful to validate that your Rust toolchain works properly and that you'll be able to compile improved versions of this NFT contract in the upcoming tutorials. - ---- - -## Conclusion - -You've seen the layout of this NFT smart contract, and how all the functions are laid out across the different source files. -Using `yarn`, you've been able to compile the contract, and you'll start fleshing out this skeleton in the next [Minting tutorial](2-minting.md). - -:::note Versioning for this article -At the time of this writing, this example works with the following versions: - -- rustc: `1.76.0` -- near-sdk-rs: `5.1.0` -- cargo-near: `0.13.2` -- NFT standard: [NEP171](https://github.com/near/NEPs/tree/master/neps/nep-0171.md), version `1.0.0` - -::: diff --git a/docs/tutorials/nfts/2-minting.md b/docs/tutorials/nfts/2-minting.md deleted file mode 100644 index 65430496228..00000000000 --- a/docs/tutorials/nfts/2-minting.md +++ /dev/null @@ -1,442 +0,0 @@ ---- -id: minting -title: Minting -sidebar_label: Minting -description: "Learn to mint NFTs from scratch with a smart contract that follows all NEAR NFT standards." ---- -import {Github} from "@site/src/components/UI/Codetabs"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -This is the first of many tutorials in a series where you'll be creating a complete NFT smart contract from scratch that conforms with all the NEAR [NFT standards](https://nomicon.io/Standards/NonFungibleToken/). - -Today you'll learn how to create the logic needed to mint NFTs and have them show up in your NEAR wallet. You will be filling a bare-bones [skeleton smart contract](1-skeleton.md) to add minting functionalities. - -:::info Contracts -You can find the skeleton contract in our [Skeleton folder](https://github.com/near-examples/nft-tutorial/tree/main/nft-contract-skeleton) - -A completed version of this tutorial can be found in the [Basic NFT folder](https://github.com/near-examples/nft-tutorial/tree/main/nft-contract-basic) -::: - ---- - -## Introduction - -To get started, go to the `nft-contract-skeleton` folder in our repo. If you haven't cloned the repository, refer to the [Contract Architecture](1-skeleton.md) to get started. - -``` -cd nft-contract-skeleton/ -``` - -If you wish to see the finished code of this step-by-step basic NFT contract tutorial, that can be found on the `nft-contract-basic` folder. - ---- - -## Modifications to the skeleton contract {#what-does-minting-mean} - -In order to implement the logic needed for minting, we should break it up into smaller tasks and handle those one-by-one. Let's step back and think about the best way to do this by asking ourselves a simple question: what does it mean to mint an NFT? - -To mint a non-fungible token, in the most simple way possible, a contract needs to be able to associate a token with an owner on the blockchain. This means you'll need: - -- A way to keep track of tokens and other information on the contract. -- A way to store information for each token such as `metadata` (more on that later). -- A way to link a token with an owner. - -That's it! We've now broken down the larger problem into some smaller, less daunting, subtasks. Let's start by tackling the first and work our way through the rest. - -
- -### Storing information on the contract {#storing-information} - -Start by navigating to `nft-contract-skeleton/src/lib.rs` and filling in some of the code blocks. -You need to be able to store important information on the contract such as the list of tokens that an account has. - -#### Contract Struct - -The first thing to do is modifying the contract `struct` as follows: - - -This allows you to get the information stored in these data structures from anywhere in the contract. The code above has created 3 token specific storages: - -- **tokens_per_owner**: allows you to keep track of the tokens owned by any account -- **tokens_by_id**: returns all the information about a specific token -- **token_metadata_by_id**: returns just the metadata for a specific token - -In addition, you'll keep track of the owner of the contract as well as the metadata for the contract. - -You might be confused as to some of the types that are being used. In order to make the code more readable, we've introduced custom data types which we'll briefly outline below: - -- **AccountId**: a string that ensures there are no special or unsupported characters. -- **TokenId**: simply a string. - -As for the `Token`, `TokenMetadata`, and `NFTContractMetadata` data types, those are structs that we'll define later in this tutorial. - -#### Initialization Functions - -Next, create what's called an initialization function; we will name it `new`, but you can choose any name you prefer. - -This function needs to be invoked when you first deploy the contract. It will initialize all the contract's fields that you've defined above with default values. -Don't forget to add the `owner_id` and `metadata` fields as parameters to the function, so only those can be customized. - -This function will default all the collections to be empty and set the `owner` and `metadata` equal to what you pass in. - - - -More often than not when doing development, you'll need to deploy contracts several times. You can imagine that it might get tedious to have to pass in metadata every single time you want to initialize the contract. For this reason, let's create a function that can initialize the contract with a set of default `metadata`. You can call it `new_default_meta` and it'll only take the `owner_id` as a parameter. - - - -This function is simply calling the previous `new` function and passing in the owner that you specify and also passes in some default metadata. - -
- -### Metadata and token information {#metadata-and-token-info} - -Now that you've defined what information to store on the contract itself and you've defined some ways to initialize the contract, you need to define what information should go in the `Token`, `TokenMetadata`, and `NFTContractMetadata` data types. - -Let's switch over to the `nft-contract-skeleton/src/metadata.rs` file as this is where that information will go. - -If you look at the [standards for metadata](https://nomicon.io/Standards/Tokens/NonFungibleToken/Metadata), you'll find all the necessary information that you need to store for both `TokenMetadata` and `NFTContractMetadata`. Simply fill in the following code. - - - -This now leaves you with the `Token` struct and something called a `JsonToken`. The `Token` struct will hold all the information directly related to the token excluding the metadata. The metadata, if you remember, is stored in a map on the contract in a data structure called `token_metadata_by_id`. This allows you to quickly get the metadata for any token by simply passing in the token's ID. - -For the `Token` struct, you'll just keep track of the owner for now. - - - -Since NEAR smart contracts receive and return data in JSON format, the purpose of the `JsonToken` is to act as output when the user asks information for an NFT. This means you'll want to store the owner, token ID, and metadata. - - - -:::tip -Some of you might be thinking _"how come we don't just store all the information in the `Token` struct?"_. -The reason behind this is that it's actually more efficient to construct the JSON token on the fly only when you need it rather than storing all the information in the token struct. -In addition, some operations might only need the metadata for a token and so having the metadata in a separate data structure is more optimal. -::: - -#### Function for querying contract metadata - -Now that you've defined some of the types that were used in the previous section, let's move on and create the first view function `nft_metadata`. This will allow users to query for the contract's metadata as per the [metadata standard](https://nomicon.io/Standards/Tokens/NonFungibleToken/Metadata). - - - -This function will get the `metadata` object from the contract which is of type `NFTContractMetadata` and will return it. - -Just like that, you've completed the first two tasks and are ready to move onto last part of the tutorial. - -
- -### Minting Logic {#minting-logic} - -Now that all the information and types are defined, let's start brainstorming how the minting logic will play out. In the end, you need to link a `Token` and `TokenId` to a specific owner. Let's look back at the `lib.rs` file to see how you can accomplish this. There are a couple data structures that might be useful: - -```rust -//keeps track of all the token IDs for a given account -pub tokens_per_owner: LookupMap>, - -//keeps track of the token struct for a given token ID -pub tokens_by_id: LookupMap, - -//keeps track of the token metadata for a given token ID -pub token_metadata_by_id: UnorderedMap, -``` - -Looking at these data structures, you could do the following: - -- Add the token ID into the set of tokens that the receiver owns. This will be done on the `tokens_per_owner` field. -- Create a token object and map the token ID to that token object in the `tokens_by_id` field. -- Map the token ID to it's metadata using the `token_metadata_by_id`. - -#### Storage Implications {#storage-implications} -With those steps outlined, it's important to take into consideration the storage costs of minting NFTs. Since you're adding bytes to the contract by creating entries in the data structures, the contract needs to cover the storage costs. If you just made it so any user could go and mint an NFT for free, that system could easily be abused and users could essentially "drain" the contract of all it's funds by minting thousands of NFTs. For this reason, you'll make it so that users need to attach a deposit to the call to cover the cost of storage. You'll measure the initial storage usage before anything was added and you'll measure the final storage usage after all the logic is finished. Then you'll make sure that the user has attached enough $NEAR to cover that cost and refund them if they've attached too much. - -This is how we do it in code: - - - - -You'll notice that we're using some internal methods such as `refund_deposit` and `internal_add_token_to_owner`. We've described the function of `refund_deposit` and as for `internal_add_token_to_owner`, this will add a token to the set of tokens an account owns for the contract's `tokens_per_owner` data structure. You can create these functions in a file called `internal.rs`. Go ahead and create the file. Your new contract architecture should look as follows: - -``` -nft-contract -β”œβ”€β”€ Cargo.lock -β”œβ”€β”€ Cargo.toml -β”œβ”€β”€ README.md -β”œβ”€β”€ build.sh -└── src - β”œβ”€β”€ approval.rs - β”œβ”€β”€ enumeration.rs - β”œβ”€β”€ internal.rs - β”œβ”€β”€ lib.rs - β”œβ”€β”€ metadata.rs - β”œβ”€β”€ mint.rs - β”œβ”€β”€ nft_core.rs - β”œβ”€β”€ events.rs - └── royalty.rs -``` - -Add the following to your newly created `internal.rs` file. - - - -:::note -You may notice more functions in the `internal.rs` file than we need for now. You may ignore them, we'll come back to them later. -::: - -Let's now quickly move to the `lib.rs` file and make the functions we just created invocable in other files. We'll add the internal crates and mod the file as shown below: - - - -At this point, the core logic is all in place so that you can mint NFTs. You can use the function `nft_mint` which takes the following parameters: - -- **token_id**: the ID of the token you're minting (as a string). -- **metadata**: the metadata for the token that you're minting (of type `TokenMetadata` which is found in the `metadata.rs` file). -- **receiver_id**: specifies who the owner of the token will be. - -Behind the scenes, the function will: - -1. Calculate the initial storage before adding anything to the contract -2. Create a `Token` object with the owner ID -3. Link the token ID to the newly created token object by inserting them into the `tokens_by_id` field. -4. Link the token ID to the passed in metadata by inserting them into the `token_metadata_by_id` field. -5. Add the token ID to the list of tokens that the owner owns by calling the `internal_add_token_to_owner` function. -6. Calculate the final and net storage to make sure that the user has attached enough NEAR to the call in order to cover those costs. - -
- -### Querying for token information - -If you were to go ahead and deploy this contract, initialize it, and mint an NFT, you would have no way of knowing or querying for the information about the token you just minted. Let's quickly add a way to query for the information of a specific NFT. You'll move to the `nft-contract-skeleton/src/nft_core.rs` file and edit the `nft_token` function. - -It will take a token ID as a parameter and return the information for that token. The `JsonToken` contains the token ID, the owner ID, and the token's metadata. - - - -With that finished, it's finally time to build and deploy the contract so you can mint your first NFT. - ---- - -## Interacting with the contract on-chain - -Now that the logic for minting is complete and you've added a way to query for information about specific tokens, it's time to build and deploy your contract to the blockchain. - -### Deploying the contract {#deploy-the-contract} - -For deployment, you will need a NEAR account with the keys stored on your local machine. Navigate to the [NEAR wallet](https://testnet.mynearwallet.com/) site and create an account. - -:::info -Please ensure that you deploy the contract to an account with no pre-existing contracts. It's easiest to simply create a new account or create a sub-account for this tutorial. -::: - -Log in to your newly created account with [`near-cli-rs`](../../tools/cli.md) by running the following command in your terminal. - -```bash -near account import-account using-web-wallet network-config testnet -``` - -To make this tutorial easier to copy/paste, we're going to set an environment variable for your account ID. In the command below, replace `YOUR_ACCOUNT_NAME` with the account name you just logged in with including the `.testnet` portion: - -```bash -export NFT_CONTRACT_ID="YOUR_ACCOUNT_NAME" -``` - -Test that the environment variable is set correctly by running: - -```bash -echo $NFT_CONTRACT_ID -``` - -Verify that the correct account ID is printed in the terminal. If everything looks correct you can now deploy your contract. -In the root of your NFT project run the following command to deploy your smart contract and answer questions: - -```bash -cargo near deploy build-non-reproducible-wasm $NFT_CONTRACT_ID - -> Select the need for initialization: with-init-call - Add an initialize -> What is the name of the function? new_default_meta -> How would you like to pass the function arguments? json-args -> Enter the arguments to this function: {"owner_id": ""} -> Enter gas for function call: 100 TeraGas -> Enter deposit for a function call (example: 10NEAR or 0.5near or 10000yoctonear): 0 NEAR -> What is the name of the network? testnet -> Select a tool for signing the transaction: sign-with-keychain -> How would you like to proceed? send -``` - -You don't need to answer these questions every time. If you look at the results you will find the message `Here is the console command if you ever need to re-run it again`. The next line is the command which you may use instead of answering to interactive questions: - -```bash -cargo near deploy build-non-reproducible-wasm $NFT_CONTRACT_ID with-init-call new_default_meta json-args '{"owner_id": "'$NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send -``` - -You've just deployed and initialized the contract with some default metadata and set your account ID as the owner. At this point, you're ready to call your first view function. - -
- -### Viewing the contract's metadata - -Now that the contract has been initialized, you can call some of the functions you wrote earlier. More specifically, let's test out the function that returns the contract's metadata: - - - - - ```bash - near view $NFT_CONTRACT_ID nft_metadata '{}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $NFT_CONTRACT_ID nft_metadata json-args {} network-config testnet now - ``` - - - -This should return an output similar to the following: - -```bash -{ - spec: 'nft-1.0.0', - name: 'NFT Tutorial Contract', - symbol: 'GOTEAM', - icon: null, - base_uri: null, - reference: null, - reference_hash: null -} -``` - -At this point, you're ready to move on and mint your first NFT. - -
- -### Minting our first NFT {#minting-our-first-nft} - -Let's now call the minting function that you've created. This requires a `token_id` and `metadata`. If you look back at the `TokenMetadata` struct you created earlier, there are many fields that could potentially be stored on-chain: - - - -Let's mint an NFT with a title, description, and media to start. The media field can be any URL pointing to a media file. We've got an excellent GIF to mint but if you'd like to mint a custom NFT, simply replace our media link with one of your choosing. If you run the following command, it will mint an NFT with the following parameters: - -- **token_id**: "token-1" -- **metadata**: - - _title_: "My Non Fungible Team Token" - - _description_: "The Team Most Certainly Goes :)" - - _media_: `https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif` - - **receiver_id**: "'$NFT_CONTRACT_ID'" - - - - - ```bash - near call $NFT_CONTRACT_ID nft_mint '{"token_id": "token-1", "metadata": {"title": "My Non Fungible Team Token", "description": "The Team Most Certainly Goes :)", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$NFT_CONTRACT_ID'"}' --gas 100000000000000 --deposit 0.1 --accountId $NFT_CONTRACT_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT_ID nft_mint json-args '{"token_id": "token-1", "metadata": {"title": "My Non Fungible Team Token", "description": "The Team Most Certainly Goes :)", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -:::info -The `amount` flag is specifying how much NEAR to attach to the call. Since you need to pay for storage, 0.1 NEAR is attached and you'll get refunded any excess that is unused at the end. -::: - -
- -### Viewing information about the NFT - -Now that the NFT has been minted, you can check and see if everything went correctly by calling the `nft_token` function. -This should return a `JsonToken` which should contain the `token_id`, `owner_id`, and `metadata`. - - - - - ```bash - near view $NFT_CONTRACT_ID nft_token '{"token_id": "token-1"}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $NFT_CONTRACT_ID nft_token json-args '{"token_id": "token-1"}' network-config testnet now - ``` - - - -
-Example response: -

- -```bash -{ - token_id: 'token-1', - owner_id: 'goteam.examples.testnet', - metadata: { - title: 'My Non Fungible Team Token', - description: 'The Team Most Certainly Goes :)', - media: 'https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif', - media_hash: null, - copies: null, - issued_at: null, - expires_at: null, - starts_at: null, - updated_at: null, - extra: null, - reference: null, - reference_hash: null - } -} -``` - -

-
- -**Go team!** You've now verified that everything works correctly and it's time to view your freshly minted NFT in the NEAR wallet's collectibles tab! - ---- - -## Viewing your NFTs in the wallet - -If you navigate to the [collectibles tab](https://testnet.mynearwallet.com/?tab=collectibles) in the NEAR wallet, this should list all the NFTs that you own. It should look something like the what's below. - -![empty-nft-in-wallet](/assets/docs/tutorials/nfts/empty-nft-in-wallet.png) - -We've got a problem. The wallet correctly picked up that you minted an NFT, however, the contract doesn't implement the specific view function that is being called. Behind the scenes, the wallet is trying to call `nft_tokens_for_owner` to get a list of all the NFTs owned by your account on the contract. The only function you've created, however, is the `nft_token` function. It wouldn't be very efficient for the wallet to call `nft_token` for every single NFT that a user has to get information and so they try to call the `nft_tokens_for_owner` function. - -In the next tutorial, you'll learn about how to deploy a patch fix to a pre-existing contract so that you can view the NFT in the wallet. - ---- - -## Conclusion - -In this tutorial, you went through the basics of setting up and understand the logic behind minting NFTs on the blockchain using a skeleton contract. - -You first looked at [what it means](#what-does-minting-mean) to mint NFTs and how to break down the problem into more feasible chunks. You then started modifying the skeleton contract chunk by chunk starting with solving the problem of [storing information / state](#storing-information) on the contract. You then looked at what to put in the [metadata and token information](#metadata-and-token-info). Finally, you looked at the logic necessary for [minting NFTs](#minting-logic). - -After the contract was written, it was time to deploy to the blockchain. You [deployed and initialized the contract](#deploy-the-contract). Finally, you [minted your very first NFT](#minting-our-first-nft) and saw that some changes are needed before you can view it in the wallet. - ---- - -## Next Steps - -In the [next tutorial](2-upgrade.md), you'll find out how to deploy a patch fix and what that means so that you can view your NFTs in the wallet. - -:::note Versioning for this article - -At the time of this writing, this example works with the following versions: - -- rustc: `1.77.1` -- near-cli-rs: `0.17.0` -- cargo-near `0.6.1` -- NFT standard: [NEP171](https://nomicon.io/Standards/Tokens/NonFungibleToken/Core), version `1.0.0` -- Metadata standard: [NEP177](https://nomicon.io/Standards/Tokens/NonFungibleToken/Metadata), version `2.1.0` - -::: diff --git a/docs/tutorials/nfts/2-upgrade.md b/docs/tutorials/nfts/2-upgrade.md deleted file mode 100644 index 9e1524fbacc..00000000000 --- a/docs/tutorials/nfts/2-upgrade.md +++ /dev/null @@ -1,160 +0,0 @@ ---- -id: upgrade-contract -title: Upgrading the Contract -sidebar_label: Upgrade a Contract -description: "Learn how to implement the nft_tokens_for_owner method." ---- -import {Github} from "@site/src/components/UI/Codetabs"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -In this tutorial, you'll build off the work you previously did to implement the [minting functionality](2-minting.md) on a skeleton smart contract. You got to the point where NFTs could be minted and the wallet correctly picked up on the fact that you owned an NFT. However, it had no way of displaying the tokens since your contract didn't implement the method that the wallet was trying to call. - ---- - -## Introduction - -Today you'll learn about deploying patch fixes to smart contracts and you'll use that knowledge to implement the `nft_tokens_for_owner` function on the contract you deployed in the previous tutorial. - ---- - -## Upgrading contracts overview {#upgrading-contracts} - -Upgrading contracts, when done right, can be an immensely powerful tool. If done wrong, it can lead to a lot of headaches. It's important to distinguish between the code and state of a smart contract. When a contract is deployed on top of an existing contract, the only thing that changes is the code. The state will remain the same and that's where a lot of developer's issues come to fruition. - -The NEAR Runtime will read the serialized state from disk and it will attempt to load it using the current contract code. When your code changes, it might not be able to figure out how to do this. - -You need to strategically upgrade your contracts and make sure that the runtime will be able to read your current state with the new contract code. For more information about upgrading contracts and some best practices, see the NEAR SDK's [upgrading contracts](../../smart-contracts/release/upgrade.md) write-up. - ---- - -## Modifications to our contract {#modifications-to-contract} - -In order for the wallet to properly display your NFTs, you need to implement the `nft_tokens_for_owner` method. This will allow anyone to query for a paginated list of NFTs owned by a given account ID. - -To accomplish this, let's break it down into some smaller subtasks. First, you need to get access to a list of all token IDs owned by a user. This information can be found in the `tokens_per_owner` data structure. Now that you have a set of token IDs, you need to convert them into `JsonToken` objects as that's what you'll be returning from the function. - -Luckily, you wrote a function `nft_token` which takes a token ID and returns a `JsonToken` in the `nft_core.rs` file. As you can guess, in order to get a list of `JsonToken` objects, you would need to iterate through the token IDs owned by the user and then convert each token ID into a `JsonToken` and store that in a list. - -As for the pagination, Rust has some awesome functions for skipping to a starting index and taking the first `n` elements of an iterator. - -Let's move over to the `enumeration.rs` file and implement that logic: - - - ---- - -## Redeploying the contract {#redeploying-contract} - -Now that you've implemented the necessary logic for `nft_tokens_for_owner`, it's time to build and re-deploy the contract to your account. Using the cargo-near, deploy the contract as you did in the previous tutorial: - -```bash -cargo near deploy build-non-reproducible-wasm $NFT_CONTRACT_ID without-init-call network-config testnet sign-with-keychain send -``` - -Once the contract has been redeployed, let's test and see if the state migrated correctly by running a simple view function: - - - - - ```bash - near view $NFT_CONTRACT_ID nft_metadata '{}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $NFT_CONTRACT_ID nft_metadata json-args {} network-config testnet now - ``` - - - -This should return an output similar to the following: - -```bash -{ - spec: 'nft-1.0.0', - name: 'NFT Tutorial Contract', - symbol: 'GOTEAM', - icon: null, - base_uri: null, - reference: null, - reference_hash: null -} -``` - -**Go team!** At this point, you can now test and see if the new function you wrote works correctly. Let's query for the list of tokens that you own: - - - - - ```bash - near view $NFT_CONTRACT_ID nft_tokens_for_owner '{"account_id": "'$NFT_CONTRACT_ID'", "limit": 5}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $NFT_CONTRACT_ID nft_tokens_for_owner json-args '{"account_id": "'$NFT_CONTRACT_ID'", "limit": 5}' network-config testnet now - ``` - - - -
-Example response: -

- -```bash -[ - { - token_id: 'token-1', - owner_id: 'goteam.examples.testnet', - metadata: { - title: 'My Non Fungible Team Token', - description: 'The Team Most Certainly Goes :)', - media: 'https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif', - media_hash: null, - copies: null, - issued_at: null, - expires_at: null, - starts_at: null, - updated_at: null, - extra: null, - reference: null, - reference_hash: null - } - } -] -``` - -

-
- ---- - -## Viewing NFTs in the wallet {#viewing-nfts-in-wallet} - -Now that your contract implements the necessary functions that the wallet uses to display NFTs, you should be able to see your tokens on display in the [collectibles tab](https://testnet.mynearwallet.com/?tab=collectibles). - -![filled-nft-in-wallet](/assets/docs/tutorials/nfts/filled-nft-in-wallet.png) - ---- - -## Conclusion - -In this tutorial, you learned about the basics of [upgrading contracts](#upgrading-contracts). Then, you implemented the necessary [modifications to your smart contract](#modifications-to-contract) and [redeployed it](#redeploying-contract). Finally you navigated to the wallet collectibles tab and [viewed your NFTs](#viewing-nfts-in-wallet). - -In the [next tutorial](3-enumeration.md), you'll implement the remaining functions needed to complete the [enumeration](https://nomicon.io/Standards/Tokens/NonFungibleToken/Enumeration) standard. - -:::note Versioning for this article - -At the time of this writing, this example works with the following versions: - -- rustc: `1.77.1` -- near-cli-rs: `0.17.0` -- cargo-near `0.6.1` -- NFT standard: [NEP171](https://nomicon.io/Standards/Tokens/NonFungibleToken/Core), version `1.0.0` - -::: diff --git a/docs/tutorials/nfts/3-enumeration.md b/docs/tutorials/nfts/3-enumeration.md deleted file mode 100644 index b3cef4a4818..00000000000 --- a/docs/tutorials/nfts/3-enumeration.md +++ /dev/null @@ -1,157 +0,0 @@ ---- -id: enumeration -title: Enumeration -sidebar_label: Enumeration -description: "Extend your NFT smart contract with enumeration methods to track and query tokens." ---- -import {Github} from "@site/src/components/UI/Codetabs"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -In this tutorial, you'll expand on and finish the rest of the enumeration methods as per the [standard](https://github.com/near/NEPs/tree/master/neps/nep-0181.md). - -In the previous tutorials, you looked at ways to integrate the minting functionality into a skeleton smart contract. In order to get your NFTs to show in the wallet, you also had to deploy a patch fix that implemented one of the enumeration methods. - -Now you'll extend the NFT smart contract and add a couple of enumeration methods that can be used to return the contract's state. - ---- - -## Introduction - -As mentioned in the [Upgrade a Contract](2-upgrade.md) tutorial, you can deploy patches and fixes to smart contracts. This time, you'll use that knowledge to implement the `nft_total_supply`, `nft_tokens` and `nft_supply_for_owner` enumeration functions. - ---- - -## Modifications to the contract - -Let's start by opening the `src/enumeration.rs` file and locating the empty `nft_total_supply` function. - -**nft_total_supply** - -This function should return the total number of NFTs stored on the contract. You can easily achieve this functionality by simply returning the length of the `nft_metadata_by_id` data structure. - - - -**nft_token** - -This function should return a paginated list of `JsonTokens` that are stored on the contract regardless of their owners. -If the user provides a `from_index` parameter, you should use that as the starting point for which to start iterating through tokens; otherwise it should start from the beginning. Likewise, if the user provides a `limit` parameter, the function shall stop after reaching either the limit or the end of the list. - -:::tip -Rust has useful methods for pagination, allowing you to skip to a starting index and taking the first `n` elements of an iterator. -::: - - - -**nft_supply_for_owner** - -This function should look for all the non-fungible tokens for a user-defined owner, and return the length of the resulting set. -If there isn't a set of tokens for the provided `AccountID`, then the function shall return `0`. - - - -Next, you can use the CLI to query these new methods and validate that they work correctly. - ---- - -## Redeploying the contract {#redeploying-contract} - -Now that you've implemented the necessary logic for `nft_tokens_for_owner`, it's time to build and re-deploy the contract to your account. Using the cargo-near, deploy the contract as you did in the previous tutorials: - -```bash -cargo near deploy build-non-reproducible-wasm $NFT_CONTRACT_ID without-init-call network-config testnet sign-with-keychain send -``` - ---- - -## Enumerating tokens - -Once the updated contract has been redeployed, you can test and see if these new functions work as expected. - -### NFT tokens - -Let's query for a list of non-fungible tokens on the contract. Use the following command to query for the information of up to 50 NFTs starting from the 10th item: - - - - - ```bash - near view $NFT_CONTRACT_ID nft_tokens '{"from_index": "10", "limit": 50}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $NFT_CONTRACT_ID nft_tokens json-args '{"from_index": "10", "limit": 50}' network-config testnet now - ``` - - - -This command should return an output similar to the following: - -
-Example response: -

- -```json -[] -``` - -

-
- -
- -### Tokens by owner - -To get the total supply of NFTs owned by the `goteam.testnet` account, call the `nft_supply_for_owner` function and set the `account_id` parameter: - - - - - ```bash - near view $NFT_CONTRACT_ID nft_supply_for_owner '{"account_id": "goteam.testnet"}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $NFT_CONTRACT_ID nft_supply_for_owner json-args '{"account_id": "goteam.testnet"}' network-config testnet now - ``` - - - -This should return an output similar to the following: - -
-Example response: -

- -```json -0 -``` - -

-
- ---- - -## Conclusion - -In this tutorial, you have added two [new enumeration functions](/tutorials/nfts/enumeration#modifications-to-the-contract), and now you have a basic NFT smart contract with minting and enumeration methods in place. After implementing these modifications, you redeployed the smart contract and tested the functions using the CLI. - -In the [next tutorial](4-core.md), you'll implement the core functions needed to allow users to transfer the minted tokens. - -:::note Versioning for this article - -At the time of this writing, this example works with the following versions: - -- rustc: `1.77.1` -- near-cli-rs: `0.17.0` -- cargo-near `0.6.1` -- NFT standard: [NEP171](https://github.com/near/NEPs/tree/master/neps/nep-0171.md), version `1.0.0` -- Enumeration standard: [NEP181](https://github.com/near/NEPs/tree/master/neps/nep-0181.md), version `1.0.0` - -::: diff --git a/docs/tutorials/nfts/4-core.md b/docs/tutorials/nfts/4-core.md deleted file mode 100644 index 8aa15fc9686..00000000000 --- a/docs/tutorials/nfts/4-core.md +++ /dev/null @@ -1,258 +0,0 @@ ---- -id: core -title: Transfers -description: "Learn how to implement NFT transfers, including simple and cross-contract transfer calls, in your smart contract." ---- -import {Github} from "@site/src/components/UI/Codetabs"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -In this tutorial you'll learn how to implement NFT transfers as defined in the [core standards](https://github.com/near/NEPs/tree/master/neps/nep-0171.md) into your smart contract. - -We will define two methods for transferring NFTs: -- `nft_transfer`: that transfers ownership of an NFT from one account to another -- `nft_transfer_call`: that transfers an NFT to a "receiver" and calls a method on the receiver's account - -:::tip Why two transfer methods? - -`nft_transfer` is a simple transfer between two user, while `nft_transfer_call` allows you to **attach an NFT to a function call** - -::: - ---- - -## Introduction {#introduction} - -Up until this point, you've created a simple NFT smart contract that allows users to mint tokens and view information using the [enumeration standards](https://github.com/near/NEPs/tree/master/neps/nep-0181.md). Today, you'll expand your smart contract to allow for users to not only mint tokens, but transfer them as well. - -As we did in the [minting tutorial](2-minting.md), let's break down the problem into multiple subtasks to make our lives easier. When a token is minted, information is stored in 3 places: - -- **tokens_per_owner**: set of tokens for each account. -- **tokens_by_id**: maps a token ID to a `Token` object. -- **token_metadata_by_id**: maps a token ID to its metadata. - -Let's now consider the following scenario. If Benji owns token A and wants to transfer it to Mike as a birthday gift, what should happen? First of all, token A should be removed from Benji's set of tokens and added to Mike's set of tokens. - -If that's the only logic you implement, you'll run into some problems. If you were to do a `view` call to query for information about that token after it's been transferred to Mike, it would still say that Benji is the owner. - -This is because the contract is still mapping the token ID to the old `Token` object that contains the `owner_id` field set to Benji's account ID. You still have to change the `tokens_by_id` data structure so that the token ID maps to a new `Token` object which has Mike as the owner. - -With that being said, the final process for when an owner transfers a token to a receiver should be the following: - -- Remove the token from the owner's set. -- Add the token to the receiver's set. -- Map a token ID to a new `Token` object containing the correct owner. - -:::note -You might be curious as to why we don't edit the `token_metadata_by_id` field. This is because no matter who owns the token, the token ID will always map to the same metadata. The metadata should never change and so we can just leave it alone. -::: - -At this point, you're ready to move on and make the necessary modifications to your smart contract. - ---- - -## Modifications to the contract - -Let's start our journey in the `nft-contract-skeleton/src/nft_core.rs` file. - -### Transfer function {#transfer-function} - -You'll start by implementing the `nft_transfer` logic. This function will transfer the specified `token_id` to the `receiver_id` with an optional `memo` such as `"Happy Birthday Mike!"`. - - - -There are a couple things to notice here. Firstly, we've introduced a new function called `assert_one_yocto()`, which ensures the user has attached exactly one yoctoNEAR to the call. This is a [security measure](../../smart-contracts/security/one_yocto.md) to ensure that the user is signing the transaction with a [full access key](../../protocol/access-keys.md). - -Since the transfer function is potentially transferring very valuable assets, you'll want to make sure that whoever is calling the function has a full access key. - -Secondly, we've introduced an `internal_transfer` method. This will perform all the logic necessary to transfer an NFT. - -
- -### Internal helper functions - -Let's quickly move over to the `nft-contract/src/internal.rs` file so that you can implement the `assert_one_yocto()` and `internal_transfer` methods. - -Let's start with the easier one, `assert_one_yocto()`. - -#### assert_one_yocto - - - -#### internal_transfer - -It's now time to explore the `internal_transfer` function which is the core of this tutorial. This function takes the following parameters: - -- **sender_id**: the account that's attempting to transfer the token. -- **receiver_id**: the account that's receiving the token. -- **token_id**: the token ID being transferred. -- **memo**: an optional memo to include. - -The first thing we have to do is to make sure that the sender is authorized to transfer the token. In this case, we just make sure that the sender is the owner of the token. We do that by getting the `Token` object using the `token_id` and making sure that the sender is equal to the token's `owner_id`. - -Second, we remove the token ID from the sender's list and add the token ID to the receiver's list of tokens. Finally, we create a new `Token` object with the receiver as the owner and remap the token ID to that newly created object. - -We want to create this function within the contract implementation (below the `internal_add_token_to_owner` you created in the minting tutorial). - - - -Now let's look at the function called `internal_remove_token_from_owner`. That function implements the functionality for removing a token ID from an owner's set. - -In the remove function, we get the set of tokens for a given account ID and then remove the passed in token ID. If the account's set is empty after the removal, we simply remove the account from the `tokens_per_owner` data structure. - - - -Your `internal.rs` file should now have the following outline: - -``` -internal.rs -β”œβ”€β”€ hash_account_id -β”œβ”€β”€ assert_one_yocto -β”œβ”€β”€ refund_deposit -└── impl Contract - β”œβ”€β”€ internal_add_token_to_owner - β”œβ”€β”€ internal_remove_token_from_owner - └── internal_transfer -``` - -
- -### Transfer call function {#transfer-call-function} - -The idea behind the `nft_transfer_call` function is to transfer an NFT to a receiver while calling a method on the receiver's contract all in the same transaction. - -This way, we can effectively **attach an NFT to a function call**. - - - -The function will first assert that the caller attached exactly 1 yocto for security purposes. It will then transfer the NFT using `internal_transfer` and start the cross contract call. It will call the method `nft_on_transfer` on the `receiver_id`'s contract, and create a promise to call back `nft_resolve_transfer` with the result. This is a very common workflow when dealing with [cross contract calls](../../smart-contracts/anatomy/crosscontract.md). - -As dictated by the core standard, the function we are calling (`nft_on_transfer`) needs to return a boolean stating whether or not you should return the NFT to its original owner. - - - -If `nft_on_transfer` returned true or the called failed, you should send the token back to its original owner. On the contrary, if false was returned, no extra logic is needed. - -As for the return value of our function `nft_resolve_transfer`, the standard dictates that the function should return a boolean indicating whether or not the receiver successfully received the token or not. - -This means that if `nft_on_transfer` returned true, you should return false. This is because if the token is being returned its original owner, the `receiver_id` didn't successfully receive the token in the end. On the contrary, if `nft_on_transfer` returned false, you should return true since we don't need to return the token and thus the `receiver_id` successfully owns the token. - -With that finished, you've now successfully added the necessary logic to allow users to transfer NFTs. It's now time to deploy and do some testing. - ---- - -## Redeploying the contract {#redeploying-contract} - -Using cargo-near, deploy the contract as you did in the previous tutorials: - -```bash -cargo near deploy build-non-reproducible-wasm $NFT_CONTRACT_ID without-init-call network-config testnet sign-with-keychain send -``` - -:::tip -If you haven't completed the previous tutorials and are just following along with this one, simply create an account and login with your CLI using `near login`. You can then export an environment variable `export NFT_CONTRACT_ID=YOUR_ACCOUNT_ID_HERE`. -::: - ---- - -## Testing the new changes {#testing-changes} - -Now that you've deployed a patch fix to the contract, it's time to move onto testing. Using the previous NFT contract where you had minted a token to yourself, you can test the `nft_transfer` method. If you transfer the NFT, it should be removed from your account's collectibles displayed in the wallet. In addition, if you query any of the enumeration functions, it should show that you are no longer the owner. - -Let's test this out by transferring an NFT to the account `benjiman.testnet` and seeing if the NFT is no longer owned by you. - -
- -### Testing the transfer function - -:::note -This means that the NFT won't be recoverable unless the account `benjiman.testnet` transfers it back to you. If you don't want your NFT lost, make a new account and transfer the token to that account instead. -::: - -If you run the following command, it will transfer the token `"token-1"` to the account `benjiman.testnet` with the memo `"Go Team :)"`. Take note that you're also attaching exactly 1 yoctoNEAR by using the `--depositYocto` flag. - -:::tip -If you used a different token ID in the previous tutorials, replace `token-1` with your token ID. -::: - - - - - ```bash - near call $NFT_CONTRACT_ID nft_transfer '{"receiver_id": "benjiman.testnet", "token_id": "token-1", "memo": "Go Team :)"}' --gas 100000000000000 --depositYocto 1 --accountId $NFT_CONTRACT_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT_ID nft_transfer json-args '{"receiver_id": "benjiman.testnet", "token_id": "token-1", "memo": "Go Team :)"}' prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -If you now query for all the tokens owned by your account, that token should be missing. Similarly, if you query for the list of tokens owned by `benjiman.testnet`, that account should now own your NFT. - -
- -### Testing the transfer call function - -Now that you've tested the `nft_transfer` function, it's time to test the `nft_transfer_call` function. If you try to transfer an NFT to a receiver that does **not** implement the `nft_on_transfer` function, the contract will panic and the NFT will **not** be transferred. Let's test this functionality below. - -First mint a new NFT that will be used to test the transfer call functionality. - - - - - ```bash - near call $NFT_CONTRACT_ID nft_mint '{"token_id": "token-2", "metadata": {"title": "NFT Tutorial Token", "description": "Testing the transfer call function", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$NFT_CONTRACT_ID'"}' --gas 100000000000000 --deposit 0.1 --accountId $NFT_CONTRACT_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT_ID nft_mint json-args '{"token_id": "token-2", "metadata": {"title": "NFT Tutorial Token", "description": "Testing the transfer call function", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -Now that you've minted the token, you can try to transfer the NFT to the account `no-contract.testnet` which as the name suggests, doesn't have a contract. This means that the receiver doesn't implement the `nft_on_transfer` function and the NFT should remain yours after the transaction is complete. - - - - - ```bash - near call $NFT_CONTRACT_ID nft_transfer_call '{"receiver_id": "no-contract.testnet", "token_id": "token-2", "msg": "foo"}' --gas 100000000000000 --depositYocto 1 --accountId $NFT_CONTRACT_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT_ID nft_transfer_call json-args '{"receiver_id": "no-contract.testnet", "token_id": "token-2", "msg": "foo"}' prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -If you query for your tokens, you should still have `token-2` and at this point, you're finished! - ---- - -## Conclusion - -In this tutorial, you learned how to expand an NFT contract past the minting functionality and you added ways for users to transfer NFTs. You [broke down](#introduction) the problem into smaller, more digestible subtasks and took that information and implemented both the [NFT transfer](#transfer-function) and [NFT transfer call](#transfer-call-function) functions. In addition, you deployed another [patch fix](#redeploying-contract) to your smart contract and [tested](#testing-changes) the transfer functionality. - -In the [next tutorial](5-approvals.md), you'll learn about the approval management system and how you can approve others to transfer tokens on your behalf. - -:::note Versioning for this article - -At the time of this writing, this example works with the following versions: - -- rustc: `1.77.1` -- near-cli-rs: `0.17.0` -- cargo-near `0.6.1` -- NFT standard: [NEP171](https://github.com/near/NEPs/tree/master/neps/nep-0171.md), version `1.0.0` -- Enumeration standard: [NEP181](https://github.com/near/NEPs/tree/master/neps/nep-0181.md), version `1.0.0` - -::: diff --git a/docs/tutorials/nfts/5-approvals.md b/docs/tutorials/nfts/5-approvals.md deleted file mode 100644 index 4e96f5efeb3..00000000000 --- a/docs/tutorials/nfts/5-approvals.md +++ /dev/null @@ -1,612 +0,0 @@ ---- -id: approvals -title: Approvals -sidebar_label: Approvals -description: "Learn how to manage NFT approvals so that others can transfer NFTs on your behalf." ---- -import {Github} from "@site/src/components/UI/Codetabs"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -In this tutorial you'll learn the basics of an approval management system which will allow you to grant others access to transfer NFTs on your behalf. - -This is the backbone of all NFT marketplaces and allows for some complex yet beautiful scenarios to happen. If you're joining us for the first time, feel free to clone [this repository](https://github.com/near-examples/nft-tutorial) and go to the `nft-contract-basic/` folder to follow along. - -```bash -cd nft-contract-basic/ -``` - -:::tip -If you wish to see the finished code for this _Approval_ tutorial, you can find it in the `nft-contract-approval/` folder. -::: - ---- - -## Introduction - -Up until this point you've created a smart contract that allows users to mint and transfer NFTs as well as query for information using the [enumeration standard](https://github.com/near/NEPs/tree/master/neps/nep-0181.md). As we've been doing in the previous tutorials, let's break down the problem into smaller, more digestible, tasks. - -Let's first define some of the end goals that we want to accomplish as per the [approval management](https://github.com/near/NEPs/tree/master/neps/nep-0178.md) extension of the standard. We want a user to have the ability to: - -- Grant other accounts access to transfer their NFTs on a per token basis. -- Check if an account has access to a specific token. -- Revoke a specific account the ability to transfer an NFT. -- Revoke **all** other accounts the ability to transfer an NFT. - -If you look at all these goals, they are all on a per token basis. This is a strong indication that you should change the `Token` struct which keeps track of information for each token. - ---- - -## Allow an account to transfer your NFT - -Let's start by trying to accomplish the first goal. How can you grant another account access to transfer an NFT on your behalf? - -The simplest way that you can achieve this is to add a list of approved accounts to the `Token` struct. When transferring the NFT, if the caller is not the owner, you could check if they're in the list. - -Before transferring, you would need to clear the list of approved accounts since the new owner wouldn't expect the accounts approved by the original owner to still have access to transfer their new NFT. - -
- -### The problem {#the-problem} - -On the surface, this would work, but if you start thinking about the edge cases, some problems arise. Often times when doing development, a common approach is to think about the easiest and most straightforward solution. Once you've figured it out, you can start to branch off and think about optimizations and edge cases. - -Let's consider the following scenario. Benji has an NFT and gives two separate marketplaces access to transfer his token. By doing so, he's putting the NFT for sale. Let's say he put the NFT for sale for 1 NEAR on both markets. The tokens list of approved account IDs would look like the following: - -``` -Token: { - owner_id: Benji - approved_accounts_ids: [marketplace A, marketplace B] -} -``` - -Josh then comes along and purchases the NFT on marketplace A for 1 NEAR. This would take the sale down from the marketplace A and clear the list of approved accounts. Marketplace B, however, still has the token listed for sale for 1 NEAR and has no way of knowing that the token was purchased on marketplace A by Josh. The new token struct would look as follows: - -``` -Token: { - owner_id: Josh - approved_accounts_ids: [] -} -``` - -Let's say Josh is low on cash and wants to flip this NFT and put it for sale for 10 times the price on marketplace B. He goes to put it for sale and for whatever reason, the marketplace is built in a way that if you try to put a token up for sale twice, it keeps the old sale data. This would mean that from marketplace B's perspective, the token is still for sale for 1 NEAR (which was the price that Benji had originally listed it for). - -Since Josh approved the marketplace to try and put it for sale, the token struct would look as follows: - -``` -Token: { - owner_id: Josh - approved_accounts_ids: [marketplace A, marketplace B] -} -``` - -If Mike then comes along and purchases the NFT for only 1 NEAR on marketplace B, the marketplace would go to try and transfer the NFT and since technically, Josh approved the marketplace and it's in the list of approved accounts, the transaction would go through properly. - -
- -### The solution {#the-solution} - -Now that we've identified a problem with the original solution, let's think about ways that we can fix it. What would happen now if, instead of just keeping track of a list of approved accounts, you introduced a specific ID that went along with each approved account. The new approved accounts would now be a map instead of a list. It would map an account to its `approval id`. - -For this to work, you need to make sure that the approval ID is **always** a unique, new ID. If you set it as an integer that always increases by 1 whenever u approve an account, this should work. Let's consider the same scenario with the new solution. - -Benji puts his NFT for sale for 1 NEAR on marketplace A and marketplace B by approving both marketplaces. The "next approval ID" would start off at 0 when the NFT was first minted and will increase from there. This would result in the following token struct: - -``` -Token: { - owner_id: Benji - approved_accounts_ids: { - marketplace A: 0 - marketplace B: 1 - } - next_approval_id: 2 -} -``` - -When Benji approved marketplace A, it took the original value of `next_approval_id` which started off at 0. The marketplace was then inserted into the map and the next approval ID was incremented. This process happened again for marketplace B and the next approval ID was again incremented where it's now 2. - -Josh comes along and purchases the NFT on marketplace A for 1 NEAR. Notice how the next approval ID stayed at 2: - -``` -Token: { - owner_id: Josh - approved_accounts_ids: {} - next_approval_id: 2 -} -``` - -Josh then flips the NFT because he's once again low on cash and approves marketplace B: - -``` -Token: { - owner_id: Josh - approved_accounts_ids: { - marketplace B: 2 - } - next_approval_id: 3 -} -``` - -The marketplace is inserted into the map and the next approval ID is incremented. From marketplace B's perspective it stores its original approval ID from when Benji put the NFT up for sale which has a value of 1. If Mike were to go and purchase the NFT on marketplace B for the original 1 NEAR sale price, the NFT contract should panic. This is because the marketplace is trying to transfer the NFT with an approval ID 1 but the token struct shows that it **should** have an approval ID of 2. - -
- -### Expanding the `Token` and `JsonToken` structs - -Now that you understand the proposed solution to the original problem of allowing an account to transfer your NFT, it's time to implement some of the logic. The first thing you should do is modify the `Token` and `JsonToken` structs to reflect the new changes. Let's switch over to the `nft-contract-basic/src/metadata.rs` file: - - - -You'll then need to initialize both the `approved_account_ids` and `next_approval_id` to their default values when a token is minted. Switch to the `nft-contract-basic/src/mint.rs` file and when creating the `Token` struct to store in the contract, let's set the next approval ID to be 0 and the approved account IDs to be an empty map: - - - -
- -### Approving accounts - -Now that you've added the support for approved account IDs and the next approval ID on the token level, it's time to add the logic for populating and changing those fields through a function called `nft_approve`. This function should approve an account to have access to a specific token ID. Let's move to the `nft-contract-basic/src/approval.rs` file and edit the `nft_approve` function: - - - -The function will first assert that the user has attached **at least** one yoctoNEAR (which we'll implement soon). This is both for security and to cover storage. When someone approves an account ID, they're storing that information on the contract. As you saw in the [minting tutorial](/tutorials/nfts/minting), you can either have the smart contract account cover the storage, or you can have the users cover that cost. The latter is more scalable and it's the approach you'll be working with throughout this tutorial. - -After the assertion comes back with no problems, you get the token object and make sure that only the owner is calling this method. Only the owner should be able to allow other accounts to transfer their NFTs. You then get the next approval ID and insert the passed in account into the map with the next approval ID. If it's a new approval ID, storage must be paid. If it's not a new approval ID, no storage needs to be paid and only attaching 1 yoctoNEAR would be enough. - -You then calculate how much storage is being used by adding that new account to the map and increment the tokens `next_approval_id` by 1. After inserting the token object back into the `tokens_by_id` map, you refund any excess storage. - -You'll notice that the function contains an optional `msg` parameter. This message can be used by NFT marketplaces. If a message was provided into the function, you're going to perform a cross contract call to the account being given access. This cross contract call will invoke the `nft_on_approve` function which will parse the message and act accordingly. - -It is up to the approving person to provide a properly encoded message that the marketplace can decode and use. This is usually done through the marketplace's frontend app which would know how to construct the `msg` in a useful way. - -
- -### Internal functions - -Now that the core logic for approving an account is finished, you need to implement the `assert_at_least_one_yocto` and `bytes_for_approved_account` functions. Move to the `nft-contract/src/internal.rs` file and copy the following function right below the `assert_one_yocto` function. - - - -Next, you'll need to copy the logic for calculating how many bytes it costs to store an account ID. Place this function at the very top of the page: - - - -Now that the logic for approving accounts is finished, you need to change the restrictions for transferring. - - -#### Changing the restrictions for transferring NFTs - -Currently, an NFT can **only** be transferred by its owner. You need to change that restriction so that people that have been approved can also transfer NFTs. In addition, you'll make it so that if an approval ID is passed, you can increase the security and check if both the account trying to transfer is in the approved list **and** they correspond to the correct approval ID. This is to address the problem we ran into earlier. - -In the `internal.rs` file, you need to change the logic of the `internal_transfer` method as that's where the restrictions are being made. Change the internal transfer function to be the following: - - - -This will check if the sender isn't the owner and then if they're not, it will check if the sender is in the approval list. If an approval ID was passed into the function, it will check if the sender's actual approval ID stored on the contract matches the one passed in. - -
- -#### Refunding storage on transfer - -While you're in the internal file, you're going to need to add methods for refunding users who have paid for storing approved accounts on the contract when an NFT is transferred. This is because you'll be clearing the `approved_account_ids` map whenever NFTs are transferred and so the storage is no longer being used. - -Right below the `bytes_for_approved_account_id` function, copy the following two functions: - - - -These will be useful in the next section where you'll be changing the `nft_core` functions to include the new approval logic. - -
- -### Changes to `nft_core.rs` - -Head over to the `nft-contract-basic/src/nft_core.rs` file and the first change that you'll want to make is to add an `approval_id` to both the `nft_transfer` and `nft_transfer_call` functions. This is so that anyone trying to transfer the token that isn't the owner must pass in an approval ID to address the problem seen earlier. If they are the owner, the approval ID won't be used as we saw in the `internal_transfer` function. - - - -You'll then need to add an `approved_account_ids` map to the parameters of `nft_resolve_transfer`. This is so that you can refund the list if the transfer went through properly. - - - -Moving over to `nft_transfer`, the only change that you'll need to make is to pass in the approval ID into the `internal_transfer` function and then refund the previous tokens approved account IDs after the transfer is finished - - - -Next, you need to do the same to `nft_transfer_call` but instead of refunding immediately, you need to attach the previous token's approved account IDs to `nft_resolve_transfer` instead as there's still the possibility that the transfer gets reverted. - - - -You'll also need to add the tokens approved account IDs to the `JsonToken` being returned by `nft_token`. - - - -Finally, you need to add the logic for refunding the approved account IDs in `nft_resolve_transfer`. If the transfer went through, you should refund the owner for the storage being released by resetting the tokens `approved_account_ids` field. If, however, you should revert the transfer, it wouldn't be enough to just not refund anybody. Since the receiver briefly owned the token, they could have added their own approved account IDs and so you should refund them if they did so. - - - -With that finished, it's time to move on and complete the next task. - ---- - -## Check if an account is approved - -Now that the core logic is in place for approving and refunding accounts, it should be smooth sailing from this point on. You now need to implement the logic for checking if an account has been approved. This should take an account and token ID as well as an optional approval ID. If no approval ID was provided, it should simply return whether or not the account is approved. - -If an approval ID was provided, it should return whether or not the account is approved and has the same approval ID as the one provided. Let's move to the `nft-contract-basic/src/approval.rs` file and add the necessary logic to the `nft_is_approved` function. - - - -Let's now move on and add the logic for revoking an account - ---- - -## Revoke an account - -The next step in the tutorial is to allow a user to revoke a specific account from having access to their NFT. The first thing you'll want to do is assert one yocto for security purposes. You'll then need to make sure that the caller is the owner of the token. If those checks pass, you'll need to remove the passed in account from the tokens approved account IDs and refund the owner for the storage being released. - - - ---- - -## Revoke all accounts - -The final step in the tutorial is to allow a user to revoke all accounts from having access to their NFT. This should also assert one yocto for security purposes and make sure that the caller is the owner of the token. You then refund the owner for releasing all the accounts in the map and then clear the `approved_account_ids`. - - - -With that finished, it's time to deploy and start testing the contract. - ---- - -## Testing the new changes {#testing-changes} - -Since these changes affect all the other tokens and the state won't be able to automatically be inherited by the new code, simply redeploying the contract will lead to errors. For this reason, it's best practice to create a new account and deploy the contract there. - -
- -### Deployment and initialization - -Next, you'll deploy this contract to the network. - - - - - ```bash - export APPROVAL_NFT_CONTRACT_ID= - near create-account $APPROVAL_NFT_CONTRACT_ID --useFaucet - ``` - - - - - ```bash - export APPROVAL_NFT_CONTRACT_ID= - near account create-account sponsor-by-faucet-service $APPROVAL_NFT_CONTRACT_ID autogenerate-new-keypair save-to-keychain network-config testnet create - ``` - - - -Using the cargo-near, deploy and initialize the contract as you did in the previous tutorials: - -```bash -cargo near deploy build-non-reproducible-wasm $APPROVAL_NFT_CONTRACT_ID with-init-call new_default_meta json-args '{"owner_id": "'$APPROVAL_NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send -``` - -
- -### Minting {#minting} - -Next, you'll need to mint a token. By running this command, you'll mint a token with a token ID `"approval-token"` and the receiver will be your new account. - - - - - ```bash - near call $APPROVAL_NFT_CONTRACT_ID nft_mint '{"token_id": "approval-token", "metadata": {"title": "Approval Token", "description": "testing out the new approval extension of the standard", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$APPROVAL_NFT_CONTRACT_ID'"}' --gas 100000000000000 --deposit 0.1 --accountId $APPROVAL_NFT_CONTRACT_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $APPROVAL_NFT_CONTRACT_ID nft_mint json-args '{"token_id": "approval-token", "metadata": {"title": "Approval Token", "description": "testing out the new approval extension of the standard", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$APPROVAL_NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $APPROVAL_NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -You can check to see if everything went through properly by calling one of the enumeration functions: - - - - - ```bash - near view $APPROVAL_NFT_CONTRACT_ID nft_tokens_for_owner '{"account_id": "'$APPROVAL_NFT_CONTRACT_ID'", "limit": 10}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $APPROVAL_NFT_CONTRACT_ID nft_tokens_for_owner json-args '{"account_id": "'$APPROVAL_NFT_CONTRACT_ID'", "limit": 10}' network-config testnet now - ``` - - - -This should return an output similar to the following: - -```json -[ - { - "token_id": "approval-token", - "owner_id": "approval.goteam.examples.testnet", - "metadata": { - "title": "Approval Token", - "description": "testing out the new approval extension of the standard", - "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif", - "media_hash": null, - "copies": null, - "issued_at": null, - "expires_at": null, - "starts_at": null, - "updated_at": null, - "extra": null, - "reference": null, - "reference_hash": null - }, - "approved_account_ids": {} - } -] -``` - -Notice how the approved account IDs are now being returned from the function? This is a great sign! You're now ready to move on and approve an account to have access to your token. - -
- -### Approving an account {#approving-an-account} - -At this point, you should have two accounts. One stored under `$NFT_CONTRACT_ID` and the other under the `$APPROVAL_NFT_CONTRACT_ID` environment variable. You can use both of these accounts to test things out. If you approve your old account, it should have the ability to transfer the NFT to itself. - -Execute the following command to approve the account stored under `$NFT_CONTRACT_ID` to have access to transfer your NFT with an ID `"approval-token"`. You don't need to pass a message since the old account didn't implement the `nft_on_approve` function. In addition, you'll need to attach enough NEAR to cover the cost of storing the account on the contract. 0.1 NEAR should be more than enough and you'll be refunded any excess that is unused. - - - - - ```bash - near call $APPROVAL_NFT_CONTRACT_ID nft_approve '{"token_id": "approval-token", "account_id": "'$NFT_CONTRACT_ID'"}' --gas 100000000000000 --deposit 0.1 --accountId $NFT_CONTRACT_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $APPROVAL_NFT_CONTRACT_ID nft_approve json-args '{"token_id": "approval-token", "account_id": "'$NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -If you call the same enumeration method as before, you should see the new approved account ID being returned. - - - - - ```bash - near view $APPROVAL_NFT_CONTRACT_ID nft_tokens_for_owner '{"account_id": "'$APPROVAL_NFT_CONTRACT_ID'", "limit": 10}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $APPROVAL_NFT_CONTRACT_ID nft_tokens_for_owner json-args '{"account_id": "'$APPROVAL_NFT_CONTRACT_ID'", "limit": 10}' network-config testnet now - ``` - - - -This should return an output similar to the following: - -```json -[ - { - "token_id": "approval-token", - "owner_id": "approval.goteam.examples.testnet", - "metadata": { - "title": "Approval Token", - "description": "testing out the new approval extension of the standard", - "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif", - "media_hash": null, - "copies": null, - "issued_at": null, - "expires_at": null, - "starts_at": null, - "updated_at": null, - "extra": null, - "reference": null, - "reference_hash": null - }, - "approved_account_ids": { "goteam.examples.testnet": 0 } - } -] -``` - -
- -### Transferring an NFT as an approved account {#transferring-the-nft} - -Now that you've approved another account to transfer the token, you can test that behavior. You should be able to use the other account to transfer the NFT to itself by which the approved account IDs should be reset. Let's test transferring the NFT with the wrong approval ID: - - - - - ```bash - near call $APPROVAL_NFT_CONTRACT_ID nft_transfer '{"receiver_id": "'$NFT_CONTRACT_ID'", "token_id": "approval-token", "approval_id": 1}' --gas 100000000000000 --depositYocto 1 --accountId $NFT_CONTRACT_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $APPROVAL_NFT_CONTRACT_ID nft_transfer json-args '{"receiver_id": "'$NFT_CONTRACT_ID'", "token_id": "approval-token", "approval_id": 1}' prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -
-Example response: -

- -```bash -kind: { - ExecutionError: "Smart contract panicked: panicked at 'assertion failed: `(left == right)`\n" + - ' left: `0`,\n' + - " right: `1`: The actual approval_id 0 is different from the given approval_id 1', src/internal.rs:165:17" - }, -``` - -

-
- -If you pass the correct approval ID which is `0`, everything should work fine. - - - - - ```bash - near call $APPROVAL_NFT_CONTRACT_ID nft_transfer '{"receiver_id": "'$NFT_CONTRACT_ID'", "token_id": "approval-token", "approval_id": 0}' --gas 100000000000000 --depositYocto 1 --accountId $NFT_CONTRACT_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $APPROVAL_NFT_CONTRACT_ID nft_transfer json-args '{"receiver_id": "'$NFT_CONTRACT_ID'", "token_id": "approval-token", "approval_id": 0}' prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -If you again call the enumeration method, you should see the owner updated and the approved account IDs reset. - -```json -[ - { - "token_id": "approval-token", - "owner_id": "goteam.examples.testnet", - "metadata": { - "title": "Approval Token", - "description": "testing out the new approval extension of the standard", - "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif", - "media_hash": null, - "copies": null, - "issued_at": null, - "expires_at": null, - "starts_at": null, - "updated_at": null, - "extra": null, - "reference": null, - "reference_hash": null - }, - "approved_account_ids": {} - } -] -``` - -Let's now test the approval ID incrementing across different owners. If you approve the account that originally minted the token, the approval ID should be 1 now. - - - - - ```bash - near call $APPROVAL_NFT_CONTRACT_ID nft_approve '{"token_id": "approval-token", "account_id": "'$APPROVAL_NFT_CONTRACT_ID'"}' --gas 100000000000000 --deposit 0.1 --accountId $NFT_CONTRACT_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $APPROVAL_NFT_CONTRACT_ID nft_approve json-args '{"token_id": "approval-token", "account_id": "'$APPROVAL_NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -Calling the view function again show now return an approval ID of 1 for the account that was approved. - - - - - ```bash - near view $APPROVAL_NFT_CONTRACT_ID nft_tokens_for_owner '{"account_id": "'$NFT_CONTRACT_ID'", "limit": 10}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $APPROVAL_NFT_CONTRACT_ID nft_tokens_for_owner json-args '{"account_id": "'$NFT_CONTRACT_ID'", "limit": 10}' network-config testnet now - ``` - - - -
-Example response: -

- -```json -[ - { - "token_id": "approval-token", - "owner_id": "goteam.examples.testnet", - "metadata": { - "title": "Approval Token", - "description": "testing out the new approval extension of the standard", - "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif", - "media_hash": null, - "copies": null, - "issued_at": null, - "expires_at": null, - "starts_at": null, - "updated_at": null, - "extra": null, - "reference": null, - "reference_hash": null - }, - "approved_account_ids": { "approval.goteam.examples.testnet": 1 } - } -] -``` - -

-
- -With the testing finished, you've successfully implemented the approvals extension to the standard! - ---- - -## Conclusion - -Today you went through a lot of logic to implement the [approvals extension](https://github.com/near/NEPs/tree/master/neps/nep-0178.md) so let's break down exactly what you did. - -First, you explored the [basic approach](#allow-an-account-to-transfer-your-nft) of how to solve the problem. You then went through and discovered some of the [problems](#the-problem) with that solution and learned how to [fix it](#the-solution). - -After understanding what you should do to implement the approvals extension, you started to [modify](#expanding-the-token-and-jsontoken-structs) the JsonToken and Token structs in the contract. You then implemented the logic for [approving accounts](#approving-accounts). - -After implementing the logic behind approving accounts, you went and [changed the restrictions](#changing-the-restrictions-for-transferring-nfts) needed to transfer NFTs. The last step you did to finalize the approving logic was to go back and edit the [nft_core](#changes-to-nft_corers) files to be compatible with the new changes. - -At this point, everything was implemented in order to allow accounts to be approved and you extended the functionality of the [core standard](https://github.com/near/NEPs/tree/master/neps/nep-0171.md) to allow for approved accounts to transfer tokens. - -You implemented a view method to [check](#check-if-an-account-is-approved) if an account is approved and to finish the coding portion of the tutorial, you implemented the logic necessary to [revoke an account](#revoke-an-account) as well as [revoke all accounts](#revoke-all-accounts). - -After this, the contract code was finished and it was time to move onto testing where you created an [account](#deployment-and-initialization) and tested the [approving](#approving-an-account) and [transferring](#transferring-the-nft) for your NFTs. - -In the [next tutorial](6-royalty.md), you'll learn about the royalty standards and how you can interact with NFT marketplaces. - -:::note Versioning for this article - -At the time of this writing, this example works with the following versions: - -- rustc: `1.77.1` -- near-cli-rs: `0.17.0` -- cargo-near `0.6.1` -- NFT standard: [NEP171](https://github.com/near/NEPs/tree/master/neps/nep-0171.md), version `1.0.0` -- Enumeration standard: [NEP181](https://github.com/near/NEPs/tree/master/neps/nep-0181.md), version `1.0.0` -- Approval standard: [NEP178](https://github.com/near/NEPs/tree/master/neps/nep-0178.md), version `1.1.0` - -::: diff --git a/docs/tutorials/nfts/6-royalty.md b/docs/tutorials/nfts/6-royalty.md deleted file mode 100644 index e4c9b3d3019..00000000000 --- a/docs/tutorials/nfts/6-royalty.md +++ /dev/null @@ -1,299 +0,0 @@ ---- -id: royalty -title: Royalty -sidebar_label: Royalty -description: "Learn how to add perpetual royalties to NFT so creators earn a percentage on every sale." ---- -import {Github} from "@site/src/components/UI/Codetabs"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -In this tutorial you'll continue building your non-fungible token (NFT) smart contract, and learn how to implement perpetual royalties into your NFTs. This will allow people to get a percentage of the purchase price when an NFT is sold. - -## Introduction - -By now, you should have a fully fledged NFT contract, except for the royalties support. -To get started, go to the `nft-contract-approval/` folder from our [GitHub repository](https://github.com/near-examples/nft-tutorial/), or continue your work from the previous tutorials. - -```bash -cd nft-contract-approval/ -``` - -:::tip -If you wish to see the finished code for this _Royalty_ tutorial, you can find it in the `nft-contract-royalty` folder. -::: - ---- - -## Thinking about the problem - -In order to implement the functionality, you first need to understand how NFTs are sold. In the previous tutorial, you saw how someone with an NFT could list it on a marketplace using the `nft_approve` function by passing in a message that could be properly decoded. When a user purchases your NFT on the marketplace, what happens? - -Using the knowledge you have now, a reasonable conclusion would be to say that the marketplace transfers the NFT to the buyer by performing a cross-contract call and invokes the NFT contract's `nft_transfer` method. Once that function finishes, the marketplace would pay the seller for the correct amount that the buyer paid. - -Let's now think about how this can be expanded to allow for a cut of the pay going to other accounts that aren't just the seller. - -
- -### Expanding the current solution - -Since perpetual royalties will be on a per-token basis, it's safe to assume that you should be changing the `Token` and `JsonToken` structs. You need some way of keeping track of what percentage each account with a royalty should have. If you introduce a map of an account to an integer, that should do the trick. - -Now, you need some way to relay that information to the marketplace. This method should be able to transfer the NFT exactly like the old solution but with the added benefit of telling the marketplace exactly what accounts should be paid what amounts. If you implement a method that transfers the NFT and then calculates exactly what accounts get paid and to what amount based on a passed-in balance, that should work nicely. - -This is what the [royalty standards](https://github.com/near/NEPs/blob/master/neps/nep-0199.md) outlined. Let's now move on and modify our smart contract to introduce this behavior. - ---- - -## Modifications to the contract - -The first thing you'll want to do is add the royalty information to the structs. Open the `nft-contract-approval/src/metadata.rs` file and add `royalty` to the `Token` struct: - -```rust -pub royalty: HashMap, -``` - -Second, you'll want to add `royalty` to the `JsonToken` struct as well: - -```rust -pub royalty: HashMap, -``` - -
- -### Internal helper function - -**royalty_to_payout** - -To simplify the payout calculation, let's add a helper `royalty_to_payout` function to `src/internal.rs`. This will convert a percentage to the actual amount that should be paid. In order to allow for percentages less than 1%, you can give 100% a value of `10,000`. This means that the minimum percentage you can give out is 0.01%, or `1`. For example, if you wanted the account `benji.testnet` to have a perpetual royalty of 20%, you would insert the pair `"benji.testnet": 2000` into the payout map. - - - -If you were to use the `royalty_to_payout` function and pass in `2000` as the `royalty_percentage` and an `amount_to_pay` of 1 NEAR, it would return a value of 0.2 NEAR. - -
- -### Royalties - -**nft_payout** - -Let's now implement a method to check what accounts will be paid out for an NFT given an amount, or balance. Open the `nft-contract/src/royalty.rs` file, and modify the `nft_payout` function as shown. - - - -This function will loop through the token's royalty map and take the balance and convert that to a payout using the `royalty_to_payout` function you created earlier. It will give the owner of the token whatever is left from the total royalties. As an example: - -You have a token with the following royalty field: - -```rust -Token { - owner_id: "damian", - royalty: { - "benji": 1000, - "josh": 500, - "mike": 2000 - } -} -``` - -If a user were to call `nft_payout` on the token and pass in a balance of 1 NEAR, it would loop through the token's royalty field and insert the following into the payout object: - -```rust -Payout { - payout: { - "benji": 0.1 NEAR, - "josh": 0.05 NEAR, - "mike": 0.2 NEAR - } -} -``` - -At the very end, it will insert `damian` into the payout object and give him `1 NEAR - 0.1 - 0.05 - 0.2 = 0.65 NEAR`. - -**nft_transfer_payout** - -Now that you know how payouts are calculated, it's time to create the function that will transfer the NFT and return the payout to the marketplace. - - - -
- -### Perpetual royalties - -To add support for perpetual royalties, let's edit the `src/mint.rs` file. First, add an optional parameter for perpetual royalties. This is what will determine what percentage goes to which accounts when the NFT is purchased. You will also need to create and insert the royalty to be put in the `Token` object: - - - -Next, you can use the CLI to query the new `nft_payout` function and validate that it works correctly. - -### Adding royalty object to struct implementations - -Since you've added a new field to your `Token` and `JsonToken` structs, you need to edit your implementations accordingly. Move to the `nft-contract/src/internal.rs` file and edit the part of your `internal_transfer` function that creates the new `Token` object: - - - -Once that's finished, move to the `nft-contract-approval/src/nft_core.rs` file. You need to edit your implementation of `nft_token` so that the `JsonToken` sends back the new royalty information. - - - ---- - -## Deploying the contract {#redeploying-contract} - -As you saw in the previous tutorial, adding changes like these will cause problems when redeploying. Since these changes affect all the other tokens and the state won't be able to automatically be inherited by the new code, simply redeploying the contract will lead to errors. For this reason, you'll create a new account again. - -### Deployment and initialization - -Next, you'll deploy this contract to the network. - - - - - ```bash - export ROYALTY_NFT_CONTRACT_ID= - near create-account $ROYALTY_NFT_CONTRACT_ID --useFaucet - ``` - - - - - ```bash - export ROYALTY_NFT_CONTRACT_ID= - near account create-account sponsor-by-faucet-service $ROYALTY_NFT_CONTRACT_ID autogenerate-new-keypair save-to-keychain network-config testnet create - ``` - - - -Using the cargo-near, deploy and initialize the contract as you did in the previous tutorials: - -```bash -cargo near deploy build-non-reproducible-wasm $ROYALTY_NFT_CONTRACT_ID with-init-call new_default_meta json-args '{"owner_id": "'$ROYALTY_NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send -``` - -### Minting {#minting} - -Next, you'll need to mint a token. By running this command, you'll mint a token with a token ID `"royalty-token"` and the receiver will be your new account. In addition, you're passing in a map with two accounts that will get perpetual royalties whenever your token is sold. - - - - - ```bash - near call $ROYALTY_NFT_CONTRACT_ID nft_mint '{"token_id": "royalty-token", "metadata": {"title": "Royalty Token", "description": "testing out the new royalty extension of the standard", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$ROYALTY_NFT_CONTRACT_ID'", "perpetual_royalties": {"benjiman.testnet": 2000, "mike.testnet": 1000, "josh.testnet": 500}}' --gas 100000000000000 --deposit 0.1 --accountId $ROYALTY_NFT_CONTRACT_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $ROYALTY_NFT_CONTRACT_ID nft_mint json-args '{"token_id": "royalty-token", "metadata": {"title": "Royalty Token", "description": "testing out the new royalty extension of the standard", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$ROYALTY_NFT_CONTRACT_ID'", "perpetual_royalties": {"benjiman.testnet": 2000, "mike.testnet": 1000, "josh.testnet": 500}}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $ROYALTY_NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -You can check to see if everything went through properly by calling one of the enumeration functions: - - - - - ```bash - near view $ROYALTY_NFT_CONTRACT_ID nft_tokens_for_owner '{"account_id": "'$ROYALTY_NFT_CONTRACT_ID'", "limit": 10}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $ROYALTY_NFT_CONTRACT_ID nft_tokens_for_owner json-args '{"account_id": "'$ROYALTY_NFT_CONTRACT_ID'", "limit": 10}' network-config testnet now - ``` - - - -This should return an output similar to the following: - -```json -[ - { - "token_id": "royalty-token", - "owner_id": "royalty.goteam.examples.testnet", - "metadata": { - "title": "Royalty Token", - "description": "testing out the new royalty extension of the standard", - "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif", - "media_hash": null, - "copies": null, - "issued_at": null, - "expires_at": null, - "starts_at": null, - "updated_at": null, - "extra": null, - "reference": null, - "reference_hash": null - }, - "approved_account_ids": {}, - "royalty": { - "josh.testnet": 500, - "benjiman.testnet": 2000, - "mike.testnet": 1000 - } - } -] -``` - -Notice how there's now a royalty field that contains the 3 accounts that will get a combined 35% of all sales of this NFT? Looks like it works! Go team :) - -### NFT payout - -Let's calculate the payout for the `"royalty-token"` NFT, given a balance of 100 yoctoNEAR. It's important to note that the balance being passed into the `nft_payout` function is expected to be in yoctoNEAR. - - - - - ```bash - near view $ROYALTY_NFT_CONTRACT_ID nft_payout '{"token_id": "royalty-token", "balance": "100", "max_len_payout": 100}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $ROYALTY_NFT_CONTRACT_ID nft_payout json-args '{"token_id": "royalty-token", "balance": "100", "max_len_payout": 100}' network-config testnet now - ``` - - - -This command should return an output similar to the following: - -```js -{ - payout: { - 'josh.testnet': '5', - 'royalty.goteam.examples.testnet': '65', - 'mike.testnet': '10', - 'benjiman.testnet': '20' - } -} -``` - -If the NFT was sold for 100 yoctoNEAR, josh would get 5, Benji would get 20, mike would get 10, and the owner, in this case `royalty.goteam.examples.testnet` would get the rest: 65. - -## Conclusion - -At this point you have everything you need for a fully functioning NFT contract to interact with marketplaces. -The last remaining standard that you could implement is the events standard. This allows indexers to know what functions are being called and makes it easier and more reliable to keep track of information that can be used to populate the collectibles tab in the wallet for example. - -:::info remember -If you want to see the finished code from this tutorial, you can go to the `nft-contract-royalty` folder. -::: - -:::note Versioning for this article - -At the time of this writing, this example works with the following versions: - -- rustc: `1.77.1` -- near-cli-rs: `0.17.0` -- cargo-near `0.6.1` -- NFT standard: [NEP171](https://github.com/near/NEPs/tree/master/neps/nep-0171.md), version `1.0.0` -- Enumeration standard: [NEP181](https://github.com/near/NEPs/tree/master/neps/nep-0181.md), version `1.0.0` -- Royalties standard: [NEP199](https://github.com/near/NEPs/tree/master/neps/nep-0171.md/Payout), version `2.0.0` - -::: diff --git a/docs/tutorials/nfts/7-events.md b/docs/tutorials/nfts/7-events.md deleted file mode 100644 index 3737def0e37..00000000000 --- a/docs/tutorials/nfts/7-events.md +++ /dev/null @@ -1,305 +0,0 @@ ---- -id: events -title: Events -description: "Learn about the events standard and how to implement it in your smart contract." ---- -import {Github} from "@site/src/components/UI/Codetabs"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -In this tutorial, you'll learn about the [events standard](https://github.com/near/NEPs/blob/master/neps/nep-0256.md) and how to implement it in your smart contract. - ---- - -## Understanding the use case {#understanding-the-use-case} - -Have you ever wondered how the wallet knows which NFTs you own and how it can display them in the [collectibles tab](https://testnet.mynearwallet.com/?tab=collectibles)? Originally, an indexer used to listen for any functions calls starting with `nft_` on your account. These contracts were then flagged on your account as likely NFT contracts. - -When you navigated to your collectibles tab, the wallet would then query all those contracts for the list of NFTs you owned using the `nft_tokens_for_owner` function you saw in the [enumeration tutorial](3-enumeration.md). - -
- -### The problem {#the-problem} - -This method of flagging contracts was not reliable as each NFT-driven application might have its own way of minting or transferring NFTs. In addition, it's common for apps to transfer or mint many tokens at a time using batch functions. - -
- -### The solution {#the-solution} - -A standard was introduced so that smart contracts could emit an event anytime NFTs were transferred, minted, or burnt. This event was in the form of a log. No matter how a contract implemented the functionality, an indexer could now listen for those standardized logs. - -As per the standard, you need to implement a logging functionality that gets fired when NFTs are transferred or minted. In this case, the contract doesn't support burning so you don't need to worry about that for now. - -It's important to note the standard dictates that the log should begin with `"EVENT_JSON:"`. The structure of your log should, however, always contain the 3 following things: - -- **standard**: the current name of the standard (e.g. nep171) -- **version**: the version of the standard you're using (e.g. 1.0.0) -- **event**: a list of events you're emitting. - -The event interface differs based on whether you're recording transfers or mints. The interface for both events is outlined below. - -**Transfer events**: -- *Optional* - **authorized_id**: the account approved to transfer on behalf of the owner. -- **old_owner_id**: the old owner of the NFT. -- **new_owner_id**: the new owner that the NFT is being transferred to. -- **token_ids**: a list of NFTs being transferred. -- *Optional* - **memo**: an optional message to include with the event. - -**Minting events**: -- **owner_id**: the owner that the NFT is being minted to. -- **token_ids**: a list of NFTs being transferred. -- *Optional* - **memo**: an optional message to include with the event. - -
- -### Examples {#examples} - -In order to solidify your understanding of the standard, let's walk through three scenarios and see what the logs should look like. - -#### Scenario A - simple mint - -In this scenario, Benji wants to mint an NFT to Mike with a token ID `"team-token"` and he doesn't include a message. The log should look as follows. - -```rust -EVENT_JSON:{ - "standard": "nep171", - "version": "1.0.0", - "event": "nft_mint", - "data": [ - {"owner_id": "mike.testnet", "token_ids": ["team-token"]} - ] -} -``` - -#### Scenario B - batch mint - -In this scenario, Benji wants to perform a batch mint. He will mint an NFT to Mike, Damian, Josh, and Dorian. Dorian, however, will get two NFTs. Each token ID will be `"team-token"` followed by an incrementing number. The log is as follows. - - -```rust -EVENT_JSON:{ - "standard": "nep171", - "version": "1.0.0", - "event": "nft_mint", - "data": [ - {"owner_id": "mike.testnet", "token_ids": ["team-token0"]}, - {"owner_id": "damian.testnet", "token_ids": ["team-token1"]}, - {"owner_id": "josh.testnet", "token_ids": ["team-token2"]} - {"owner_id": "dorian.testnet", "token_ids": ["team-token3", "team-token4"]}, - ] -} -``` - -#### Scenario C - transfer NFTs - -In this scenario, Mike is transferring both his team tokens to Josh. The log should look as follows. - -```rust -EVENT_JSON:{ - "standard": "nep171", - "version": "1.0.0", - "event": "nft_transfer", - "data": [ - {"old_owner_id": "mike.testnet", "new_owner_id": "josh.testnet", "token_ids": ["team-token", "team-token0"], "memo": "Go Team!"} - ] -} -``` - ---- - -## Modifications to the contract {#modifications-to-the-contract} - -At this point, you should have a good understanding of what the end goal should be so let's get to work! Open the repository and create a new file in the `nft-contract-basic/src` directory called `events.rs`. This is where your log structs will live. - -If you wish to see the finished code of the events implementation, that can be found on the `nft-contract-events` folder. - -### Creating the events file {#events-rs} - -Copy the following into your file. This will outline the structs for your `EventLog`, `NftMintLog`, and `NftTransferLog`. In addition, we've added a way for `EVENT_JSON:` to be prefixed whenever you log the `EventLog`. - - - -This requires the `serde_json` package which you can easily add to your `nft-contract-skeleton/Cargo.toml` file: - - - -
- -### Adding modules and constants {#lib-rs} - -Now that you've created a new file, you need to add the module to the `lib.rs` file. In addition, you can define two constants for the standard and version that will be used across our contract. - - - -
- -### Logging minted tokens {#logging-minted-tokens} - -Now that all the tools are set in place, you can now implement the actual logging functionality. Since the contract will only be minting tokens in one place, open the `nft-contract-basic/src/mint.rs` file and navigate to the bottom of the file. This is where you'll construct the log for minting. Anytime someone successfully mints an NFT, it will now correctly emit a log. - - - -
- -### Logging transfers {#logging-transfers} - -Let's open the `nft-contract-basic/src/internal.rs` file and navigate to the `internal_transfer` function. This is the location where you'll build your transfer logs. Whenever an NFT is transferred, this function is called and so you'll correctly be logging the transfers. - - - -This solution, unfortunately, has an edge case which will break things. If an NFT is transferred via the `nft_transfer_call` function, there's a chance that the transfer will be reverted if the `nft_on_transfer` function returns `true`. Taking a look at the logic for `nft_transfer_call`, you can see why this is a problem. - -When `nft_transfer_call` is invoked, it will: -- Call `internal_transfer` to perform the actual transfer logic. -- Initiate a cross-contract call and invoke the `nft_on_transfer` function. -- Resolve the promise and perform logic in `nft_resolve_transfer`. - - This will either return true meaning the transfer went fine or it will revert the transfer and return false. - -If you only place the log in the `internal_transfer` function, the log will be emitted and the indexer will think that the NFT was transferred. If the transfer is reverted during `nft_resolve_transfer`, however, that event should **also** be emitted. Anywhere that an NFT **could** be transferred, we should add logs. Replace the `nft_resolve_transfer` with the following code. - - - -In addition, you need to add an `authorized_id` and `memo` to the parameters for `nft_resolve_transfer` as shown below. - -:::tip - -We will talk more about this [`authorized_id`](./5-approvals.md) in the following chapter. - -::: - - - - -The last step is to modify the `nft_transfer_call` logic to include these new parameters: - - - -With that finished, you've successfully implemented the events standard and it's time to start testing. - ---- - -## Deploying the contract {#redeploying-contract} - -For the purpose of readability and ease of development, instead of redeploying the contract to the same account, let's create an account and deploy to that instead. You could have deployed to the same account as none of the changes you implemented in this tutorial would have caused errors. - -### Deployment - -Next, you'll deploy this contract to the network. - - - - - ```bash - export EVENTS_NFT_CONTRACT_ID= - near create-account $EVENTS_NFT_CONTRACT_ID --useFaucet - ``` - - - - - ```bash - export EVENTS_NFT_CONTRACT_ID= - near account create-account sponsor-by-faucet-service $EVENTS_NFT_CONTRACT_ID autogenerate-new-keypair save-to-keychain network-config testnet create - ``` - - - -Using the cargo-near, deploy and initialize the contract as you did in the previous tutorials: - -```bash -cargo near deploy build-non-reproducible-wasm $EVENTS_NFT_CONTRACT_ID with-init-call new_default_meta json-args '{"owner_id": "'$EVENTS_NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send -``` - -
- -### Minting {#minting} - -Next, you'll need to mint a token. By running this command, you'll mint a token with a token ID `"events-token"` and the receiver will be your new account. In addition, you're passing in a map with two accounts that will get perpetual royalties whenever your token is sold. - - - - - ```bash - near call $EVENTS_NFT_CONTRACT_ID nft_mint '{"token_id": "events-token", "metadata": {"title": "Events Token", "description": "testing out the new events extension of the standard", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$EVENTS_NFT_CONTRACT_ID'"}' --gas 100000000000000 --deposit 0.1 --accountId $EVENTS_NFT_CONTRACT_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $EVENTS_NFT_CONTRACT_ID nft_mint json-args '{"token_id": "events-token", "metadata": {"title": "Events Token", "description": "testing out the new events extension of the standard", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$EVENTS_NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $EVENTS_NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -You can check to see if everything went through properly by looking at the output in your CLI: - -```bash -Doing account.functionCall() -Receipts: F4oxNfv54cqwUwLUJ7h74H1iE66Y3H7QDfZMmGENwSxd, BJxKNFRuLDdbhbGeLA3UBSbL8UicU7oqHsWGink5WX7S - Log [events.goteam.examples.testnet]: EVENT_JSON:{"standard":"nep171","version":"1.0.0","event":"nft_mint","data":[{"owner_id":"events.goteam.examples.testnet","token_ids":["events-token"]}]} -Transaction Id 4Wy2KQVTuAWQHw5jXcRAbrz7bNyZBoiPEvLcGougciyk -To see the transaction in the transaction explorer, please open this url in your browser -https://testnet.nearblocks.io/txns/4Wy2KQVTuAWQHw5jXcRAbrz7bNyZBoiPEvLcGougciyk -'' -``` - -You can see that the event was properly logged! - -
- -### Transferring {#transferring} - -You can now test if your transfer log works as expected by sending `benjiman.testnet` your NFT. - - - - - ```bash - near call $EVENTS_NFT_CONTRACT_ID nft_transfer '{"receiver_id": "benjiman.testnet", "token_id": "events-token", "memo": "Go Team :)", "approval_id": 0}' --gas 100000000000000 --depositYocto 1 --accountId $EVENTS_NFT_CONTRACT_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $EVENTS_NFT_CONTRACT_ID nft_transfer json-args '{"receiver_id": "benjiman.testnet", "token_id": "events-token", "memo": "Go Team :)", "approval_id": 0}' prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' sign-as $EVENTS_NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -This should return an output similar to the following: - -```bash -Doing account.functionCall() -Receipts: EoqBxrpv9Dgb8KqK4FdeREawVVLWepEUR15KPNuZ4fGD, HZ4xQpbgc8EfU3PiV72LvfXb2f3dVC1n9aVTbQds9zfR - Log [events.goteam.examples.testnet]: Memo: Go Team :) - Log [events.goteam.examples.testnet]: EVENT_JSON:{"standard":"nep171","version":"1.0.0","event":"nft_transfer","data":[{"authorized_id":"events.goteam.examples.testnet","old_owner_id":"events.goteam.examples.testnet","new_owner_id":"benjiman.testnet","token_ids":["events-token"],"memo":"Go Team :)"}]} -Transaction Id 4S1VrepKzA6HxvPj3cK12vaT7Dt4vxJRWESA1ym1xdvH -To see the transaction in the transaction explorer, please open this url in your browser -https://testnet.nearblocks.io/txns/4S1VrepKzA6HxvPj3cK12vaT7Dt4vxJRWESA1ym1xdvH -'' -``` - -Hurray! At this point, your NFT contract is fully complete and the events standard has been implemented. - ---- - -## Conclusion - -Today you went through the [events standard](https://github.com/near/NEPs/blob/master/neps/nep-0256.md) and implemented the necessary logic in your smart contract. You created events for [minting](#logging-minted-tokens) and [transferring](#logging-transfers) NFTs. You then deployed and tested your changes by [minting](#minting) and [transferring](#transferring) NFTs. - -In the [next tutorial](8-marketplace.md), you'll look at the basics of a marketplace contract and how it was built. - -:::note Versioning for this article - -At the time of this writing, this example works with the following versions: - -- rustc: `1.77.1` -- near-cli-rs: `0.17.0` -- cargo-near `0.6.1` -- NFT standard: [NEP171](https://github.com/near/NEPs/tree/master/neps/nep-0171.md), version `1.0.0` -- Events standard: [NEP297 extension](https://github.com/near/NEPs/blob/master/neps/nep-0256.md), version `1.1.0` - -::: diff --git a/docs/tutorials/nfts/8-marketplace.md b/docs/tutorials/nfts/8-marketplace.md deleted file mode 100644 index 5c74375abf0..00000000000 --- a/docs/tutorials/nfts/8-marketplace.md +++ /dev/null @@ -1,406 +0,0 @@ ---- -id: marketplace -title: Marketplace -sidebar_label: Marketplace -description: "Learn how to build an NFT marketplace on NEAR to list, buy, and sell tokens." ---- -import {Github} from "@site/src/components/UI/Codetabs"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -In this tutorial, you'll learn the basics of an NFT marketplace contract where you can buy and sell non-fungible tokens for $NEAR. In the previous tutorials, you went through and created a fully fledged NFT contract that incorporates all the standards found in the [NFT standard](https://github.com/near/NEPs/tree/master/neps/nep-0171.md). - ---- - -## Introduction - -Throughout this tutorial, you'll learn how a marketplace contract **could** work on NEAR. This is meant to be **an example** as there is no **canonical implementation**. Feel free to branch off and modify this contract to meet your specific needs. - -```bash -cd market-contract/ -``` - -This folder contains both the actual contract code and dependencies as outlined below. - -``` -market-contract -β”œβ”€β”€ Cargo.lock -β”œβ”€β”€ Cargo.toml -β”œβ”€β”€ README.md -└── src - β”œβ”€β”€ external.rs - β”œβ”€β”€ internal.rs - β”œβ”€β”€ lib.rs - β”œβ”€β”€ nft_callbacks.rs - β”œβ”€β”€ sale.rs - └── sale_views.rs -``` - ---- - -## Understanding the contract - -At first, the contract can be quite overwhelming but if you strip away all the fluff and dig into the core functionalities, it's actually quite simple. This contract was designed for only one thing - to allow people to buy and sell NFTs for NEAR. This includes the support for paying royalties, updating the price of your sales, removing sales and paying for storage. - -Let's go through the files and take note of some of the important functions and what they do. - ---- - -## lib.rs {#lib-rs} - -This file outlines what information is stored on the contract as well as some other crucial functions that you'll learn about below. - -### Initialization function {#initialization-function} - -The first function you'll look at is the initialization function. This takes an `owner_id` as the only parameter and will default all the storage collections to their default values. - - - -
- -### Storage management model {#storage-management-model} - -Next, let's talk about the storage management model chosen for this contract. On the NFT contract, users attached $NEAR to the calls that needed storage paid for. For example, if someone was minting an NFT, they would need to attach `x` amount of NEAR to cover the cost of storing the data on the contract. - -On this marketplace contract, however, the storage model is a bit different. Users will need to deposit $NEAR onto the marketplace to cover the storage costs. Whenever someone puts an NFT for sale, the marketplace needs to store that information which costs $NEAR. Users can either deposit as much NEAR as they want so that they never have to worry about storage again or they can deposit the minimum amount to cover 1 sale on an as-needed basis. - -You might be thinking about the scenario when a sale is purchased. What happens to the storage that is now being released on the contract? This is why we've introduced a storage withdrawal function. This allows users to withdraw any excess storage that is not being used. Let's go through some scenarios to understand the logic. The required storage for 1 sale is 0.01 NEAR on the marketplace contract. - -**Scenario A** - -- Benji wants to list his NFT on the marketplace but has never paid for storage. -- He deposits exactly 0.01 NEAR using the `storage_deposit` method. This will cover 1 sale. -- He lists his NFT on the marketplace and is now using up 1 out of his prepaid 1 sales and has no more storage left. If he were to call `storage_withdraw`, nothing would happen. -- Dorian loves his NFT and quickly purchases it before anybody else can. This means that Benji's sale has now been taken down (since it was purchased) and Benji is using up 0 out of his prepaid 1 sales. In other words, he has an excess of 1 sale or 0.01 NEAR. -- Benji can now call `storage_withdraw` and will be transferred his 0.01 NEAR back. On the contract's side, after withdrawing, he will have 0 sales paid for and will need to deposit storage before trying to list anymore NFTs. - -**Scenario B** - -- Dorian owns one hundred beautiful NFTs and knows that he wants to list all of them. -- To avoid having to call `storage_deposit` everytime he wants to list an NFT, he calls it once. Since Dorian is a baller, he attaches 10 NEAR which is enough to cover 1000 sales. Then he lists his 100 NFTs and now he has an excess of 9 NEAR or 900 sales. -- Dorian needs the 9 NEAR for something else but doesn't want to take down his 100 listings. Since he has an excess of 9 NEAR, he can easily withdraw and still have his 100 listings. After calling `storage_withdraw` and being transferred 9 NEAR, he will have an excess of 0 sales. - -With this behavior in mind, the following two functions outline the logic. - - - - -In this contract, the storage required for each sale is 0.01 NEAR but you can query that information using the `storage_minimum_balance` function. In addition, if you wanted to check how much storage a given account has paid, you can query the `storage_balance_of` function. - -With that out of the way, it's time to move onto the `sale.rs` file where you'll look at how NFTs are put for sale. - ---- - -## sale.rs {#sale} - -This file is responsible for the internal marketplace logic. - -### Listing logic {#listing-logic} - -In order to put an NFT on sale, a user should: - -1. Approve the marketplace contract on an NFT token (by calling `nft_approve` method on the NFT contract) -2. Call the `list_nft_for_sale` method on the marketplace contract. - -#### nft_approve -This method has to be called by the user to [approve our marketplace](5-approvals.md), so it can transfer the NFT on behalf of the user. In our contract, we only need to implement the `nft_on_approve` method, which is called by the NFT contract when the user approves our contract. - -In our case, we left it blank, but you could implement it to do some additional logic when the user approves your contract. - - - - -#### list_nft_for_sale -The `list_nft_for_sale` method lists an nft for sale, for this, it takes the id of the NFT contract (`nft_contract_id`), the `token_id` to know which token is listed, the [`approval_id`](5-approvals.md), and the price in yoctoNEAR at which we want to sell the NFT. - - - -The function first checks if the user has [enough storage available](#storage-management-model), and makes two calls in parallel to the NFT contract. The first is to check if this marketplace contract is authorized to transfer the NFT. The second is to make sure that the caller (`predecessor`) is actually the owner of the NFT, otherwise, anyone could call this method to create fake listings. This second call is mostly a measure to avoid spam, since anyways, only the owner could approve the marketplace contract to transfer the NFT. - -Both calls return their results to the `process_listing` function, which executes the logic to store the sale object on the contract. - -#### process_listing - -The `process_listing` function will receive if our marketplace is authorized to list the NFT on sale, and if this was requested by the NFTs owner. If both conditions are met, it will proceed to check if the user has enough storage, and store the sale object on the contract. - - - -
- -### Sale object {#sale-object} - -It's important to understand what information the contract is storing for each sale object. Since the marketplace has many NFTs listed that come from different NFT contracts, simply storing the token ID would not be enough to distinguish between different NFTs. This is why you need to keep track of both the token ID and the contract by which the NFT came from. In addition, for each listing, the contract must keep track of the approval ID it was given to transfer the NFT. Finally, the owner and sale conditions are needed. - - - -
- -### Removing sales {#removing-sales} - -In order to remove a listing, the owner must call the `remove_sale` function and pass the NFT contract and token ID. Behind the scenes, this calls the `internal_remove_sale` function which you can find in the `internal.rs` file. This will assert one yoctoNEAR for security reasons. - - - -
- -### Updating price {#updating-price} - -In order to update the list price of a token, the owner must call the `update_price` function and pass in the contract, token ID, and desired price. This will get the sale object, change the sale conditions, and insert it back. For security reasons, this function will assert one yoctoNEAR. - - - -
- -### Purchasing NFTs {#purchasing-nfts} - -For purchasing NFTs, you must call the `offer` function. It takes an `nft_contract_id` and `token_id` as parameters. You must attach the correct amount of NEAR to the call in order to purchase. Behind the scenes, this will make sure your deposit is greater than the list price and call a private method `process_purchase` which will perform a cross-contract call to the NFT contract to invoke the `nft_transfer_payout` function. This will transfer the NFT using the [approval management](https://github.com/near/NEPs/tree/master/neps/nep-0178.md) standard that you learned about and it will return the `Payout` object which includes royalties. - -The marketplace will then call `resolve_purchase` where it will check for malicious payout objects and then if everything went well, it will pay the correct accounts. - - - ---- - -## sale_view.rs {#sale_view-rs} - -The final file is [`sale_view.rs`](https://github.com/near-examples/nft-tutorial/blob/main/market-contract/src/sale_view.rs) file. This is where some of the enumeration methods are outlined. It allows users to query for important information regarding sales. - ---- - -## Deployment and Initialization - -Next, you'll deploy this contract to the network. - - - - - ```bash - export MARKETPLACE_CONTRACT_ID= - near create-account $MARKETPLACE_CONTRACT_ID --useFaucet - ``` - - - - - ```bash - export MARKETPLACE_CONTRACT_ID= - near account create-account sponsor-by-faucet-service $MARKETPLACE_CONTRACT_ID autogenerate-new-keypair save-to-keychain network-config testnet create - ``` - - - -Using the build script, deploy the contract as you did in the previous tutorials: - -```bash -cargo near deploy build-non-reproducible-wasm $MARKETPLACE_CONTRACT_ID with-init-call new json-args '{"owner_id": "'$MARKETPLACE_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send -``` - -
- -### Minting and approving - -Let's mint a new NFT token and approve a marketplace contract: - - - - - ```bash - near call $NFT_CONTRACT_ID nft_mint '{"token_id": "token-1", "metadata": {"title": "My Non Fungible Team Token", "description": "The Team Most Certainly Goes :)", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$NFT_CONTRACT_ID'"}' --gas 100000000000000 --deposit 0.1 --accountId $NFT_CONTRACT_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT_ID nft_mint json-args '{"token_id": "token-1", "metadata": {"title": "My Non Fungible Team Token", "description": "The Team Most Certainly Goes :)", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - - - - - ```bash - near call $NFT_CONTRACT_ID nft_approve '{"token_id": "token-1", "account_id": "'$MARKETPLACE_CONTRACT_ID'"}' --gas 100000000000000 --deposit 0.1 --accountId $NFT_CONTRACT_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT_ID nft_approve json-args '{"token_id": "token-1", "account_id": "'$MARKETPLACE_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -
- -### Listing NFT on sale - - - - - ```bash - near call $MARKETPLACE_CONTRACT_ID list_nft_for_sale '{"nft_contract_id": "'$NFT_CONTRACT_ID'", "token_id": "token-1", "approval_id": 0, "msg": "{\"sale_conditions\": \"1\"}"}' --gas 300000000000000 --accountId $NFT_CONTRACT_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $MARKETPLACE_CONTRACT_ID list_nft_for_sale json-args '{"nft_contract_id": "'$NFT_CONTRACT_ID'", "token_id": "token-1", "approval_id": 0, "msg": "{\"sale_conditions\": \"1\"}"}' prepaid-gas '300.0 Tgas' attached-deposit '0 NEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -
- -### Total supply {#total-supply} - -To query for the total supply of NFTs listed on the marketplace, you can call the `get_supply_sales` function. An example can be seen below. - - - - - ```bash - near view $MARKETPLACE_CONTRACT_ID get_supply_sales '{}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $MARKETPLACE_CONTRACT_ID get_supply_sales json-args {} network-config testnet now - ``` - - - - -
- -### Total supply by owner {#total-supply-by-owner} - -To query for the total supply of NFTs listed by a specific owner on the marketplace, you can call the `get_supply_by_owner_id` function. An example can be seen below. - - - - - ```bash - near view $MARKETPLACE_CONTRACT_ID get_supply_by_owner_id '{"account_id": "'$NFT_CONTRACT_ID'"}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $MARKETPLACE_CONTRACT_ID get_supply_by_owner_id json-args '{"account_id": "'$NFT_CONTRACT_ID'"}' network-config testnet now - ``` - - - -
- -### Total supply by contract {#total-supply-by-contract} - -To query for the total supply of NFTs that belong to a specific contract, you can call the `get_supply_by_nft_contract_id` function. An example can be seen below. - - - - - ```bash - near view $MARKETPLACE_CONTRACT_ID get_supply_by_nft_contract_id '{"nft_contract_id": "'$NFT_CONTRACT_ID'"}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $MARKETPLACE_CONTRACT_ID get_supply_by_nft_contract_id json-args '{"nft_contract_id": "'$NFT_CONTRACT_ID'"}' network-config testnet now - ``` - - - -
- -### Query for listing information {#query-listing-information} - -To query for important information for a specific listing, you can call the `get_sale` function. This requires that you pass in the `nft_contract_token`. This is essentially the unique identifier for sales on the market contract as explained earlier. It consists of the NFT contract followed by a `DELIMITER` followed by the token ID. In this contract, the `DELIMITER` is simply a period: `.`. An example of this query can be seen below. - - - - - ```bash - near view $MARKETPLACE_CONTRACT_ID get_sale '{"nft_contract_token": "'$NFT_CONTRACT_ID'.token-1"}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $MARKETPLACE_CONTRACT_ID get_sale json-args '{"nft_contract_token": "'$NFT_CONTRACT_ID'.token-1"}' network-config testnet now - ``` - - - -In addition, you can query for paginated information about the listings for a given owner by calling the `get_sales_by_owner_id` function. - - - - - ```bash - near view $MARKETPLACE_CONTRACT_ID get_sales_by_owner_id '{"account_id": "'$NFT_CONTRACT_ID'", "from_index": "0", "limit": 5}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $MARKETPLACE_CONTRACT_ID get_sales_by_owner_id json-args '{"account_id": "'$NFT_CONTRACT_ID'", "from_index": "0", "limit": 5}' network-config testnet now - ``` - - - -Finally, you can query for paginated information about the listings that originate from a given NFT contract by calling the `get_sales_by_nft_contract_id` function. - - - - - ```bash - near view $MARKETPLACE_CONTRACT_ID get_sales_by_nft_contract_id '{"nft_contract_id": "'$NFT_CONTRACT_ID'", "from_index": "0", "limit": 5}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $MARKETPLACE_CONTRACT_ID get_sales_by_nft_contract_id json-args '{"nft_contract_id": "'$NFT_CONTRACT_ID'", "from_index": "0", "limit": 5}' network-config testnet now - ``` - - - ---- - -## Conclusion - -In this tutorial, you learned about the basics of a marketplace contract and how it works. You went through the [lib.rs](#lib-rs) file and learned about the [initialization function](#initialization-function) in addition to the [storage management](#storage-management-model) model. - -You went through the [NFTs listing process](#listing-logic). In addition, you went through some important functions needed after you've listed an NFT. This includes [removing sales](#removing-sales), [updating the price](#updating-price), and [purchasing NFTs](#purchasing-nfts). - -Finally, you went through the enumeration methods found in the [`sale_view`](#sale_view-rs) file. These allow you to query for important information found on the marketplace contract. - -You should now have a solid understanding of NFTs and marketplaces on NEAR. Feel free to branch off and expand on these contracts to create whatever cool applications you'd like. In the [next tutorial](9-series.md), you'll learn how to take the existing NFT contract and optimize it to allow for: -- Lazy Minting -- Creating Collections -- Allowlisting functionalities -- Optimized Storage Models - -:::note Versioning for this article - -At the time of this writing, this example works with the following versions: - -- rustc: `1.77.1` -- near-cli-rs: `0.17.0` -- cargo-near `0.6.1` -- NFT standard: [NEP171](https://github.com/near/NEPs/tree/master/neps/nep-0171.md), version `1.0.0` - -::: diff --git a/docs/tutorials/nfts/9-series.md b/docs/tutorials/nfts/9-series.md deleted file mode 100644 index 0e1f3d19abe..00000000000 --- a/docs/tutorials/nfts/9-series.md +++ /dev/null @@ -1,719 +0,0 @@ ---- -id: series -title: Customizing the NFT Contract -sidebar_label: Lazy Minting, Collections, and More! -description: "Learn how to create NFT series." ---- -import {Github} from "@site/src/components/UI/Codetabs"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -In this tutorial, you'll learn how to take the [existing NFT contract](https://github.com/near-examples/nft-tutorial) you've been working with and modify it to meet some of the most common needs in the ecosystem.This includes [Lazy Minting NFTs](#lazy-minting), [Creating Collections](#nft-collections-and-series), [Restricting Minting Access](#restricted-access), and [Highly Optimizing Storage](#modifying-view-calls-for-optimizations), and hacking enumeration methods. - ---- - -## Introduction - -Now that you have a deeper understanding of basic NFT smart contracts, we can start to get creative and implement more unique features. The basic contract works really well for simple use-cases but as you begin to explore the potential of NFTs, you can use it as a foundation to build upon. - -A fun analogy would be that you now have a standard muffin recipe and it's now up to you to decide how to alter it to create your own delicious varieties, may I suggest blueberry perhaps. - -Below we've created a few of these new varieties by showing potential solutions to the problems outlined above. As we demonstrate how to customize the basic NFT contract, we hope it activates your ingenuity thus introducing you to what's possible and helping you discover the true potential of NFTs. πŸ’ͺ - -
- -### NFT Collections and Series - -NFT Collections help solve two common problems when dealing with the basic NFT contract: -- Storing repeated data. -- Organizing data and code. - -The concept of a collection in the NFT space has a very loose meaning and can be interpreted in many different ways. In our case, we'll define a collection as a set of tokens that share **similar metadata**. For example, you could create a painting and want 100 identical copies to be put for sale. In this case, all one hundred pieces would be part of the same *collection*. Each piece would have the same artist, title, description, media etc. - -One of the biggest problems with the basic NFT contract is that you store similar data many times. If you mint NFTs, the contract will store the metadata individually for **every single token ID**. We can fix this by introducing the idea of NFT series, or NFT collection. - -A series can be thought of as a bucket of token IDs that *all* share similar information. This information is specified when the series is **created** and can be the metadata, royalties, price etc. Rather than storing this information for **every token ID**, you can simply store it once in the series and then associate token IDs with their respective buckets. - -
- -### Restricted Access - -Currently, the NFT contract allows anyone to mint NFTs. While this works well for some projects, the vast majority of dApps and creators want to restrict who can create NFTs on the contract. This is why you'll introduce an allowlist functionality for both series and for NFTs. You'll have two data structures customizable by the contract owner: -- Approved Minters -- Approved Creators - -If you're an approved minter, you can freely mint NFTs for any given series. You cannot, however, create new series. - -On the other hand, you can also be an approved creator. This allows you to define new series that NFTs can be minted from. It's important to note that if you're an approved creator, you're not automatically an approved minter as well. Each of these permissions need to be given by the owner of the contract and they can be revoked at any time. - -
- -### Lazy Minting - -Lazy minting allows users to mint *on demand*. Rather than minting all the NFTs and spending $NEAR on storage, you can instead mint the tokens **when they are purchased**. This helps to avoid burning unnecessary Gas and saves on storage for when not all the NFTs are purchased. Let's look at a common scenario to help solidify your understanding: - -Benji has created an amazing digital painting of the famous Go Team gif. He wants to sell 1000 copies of it for 1 $NEAR each. Using the traditional approach, he would have to mint each copy individually and pay for the storage himself. He would then need to either find or deploy a marketplace contract and pay for the storage to put 1000 copies up for sale. He would need to burn Gas putting each token ID up for sale 1 by 1. - -After that, people would purchase the NFTs, and there would be no guarantee that all or even any would be sold. There's a real possibility that nobody buys a single piece of his artwork, and Benji spent all that time, effort and money on nothing. - -Lazy minting would allow the NFTs to be *automatically minted on-demand*. Rather than having to purchase NFTs from a marketplace, Benji could specify a price on the NFT contract and a user could directly call the `nft_mint` function whereby the funds would be distributed to Benji's account directly. - -Using this model, NFTs would **only** be minted when they're actually purchased and there wouldn't be any upfront fee that Benji would need to pay in order to mint all 1000 NFTs. In addition, it removes the need to have a separate marketplace contract. - -With this example laid out, a high level overview of lazy minting is that it gives the ability for someone to mint "on-demand" - they're lazily minting the NFTs instead of having to mint everything up-front even if they're unsure if there's any demand for the NFTs. With this model, you don't have to waste Gas or storage fees because you're only ever minting when someone actually purchases the artwork. - ---- - -## New Contract File Structure - -Let's now take a look at how we've implemented solutions to the issues we've discussed so far. - -In your locally cloned example of the [`nft-tutorial`](https://github.com/near-examples/nft-tutorial) check out the `main` branch and be sure to pull the most recent version. - -```bash -git checkout main && git pull -``` - -You'll notice that there's a folder at the root of the project called `nft-series`. This is where the smart contract code lives. If you open the `src` folder, it should look similar to the following: - -``` -src -β”œβ”€β”€ approval.rs -β”œβ”€β”€ enumeration.rs -β”œβ”€β”€ events.rs -β”œβ”€β”€ internal.rs -β”œβ”€β”€ lib.rs -β”œβ”€β”€ metadata.rs -β”œβ”€β”€ nft_core.rs -β”œβ”€β”€ owner.rs -β”œβ”€β”€ royalty.rs -β”œβ”€β”€ series.rs -``` - ---- - -## Differences - -You'll notice that most of this code is the same, however, there are a few differences between this contract and the basic NFT contract. - -### Main Library File - -Starting with `lib.rs`, you'll notice that the contract struct has been modified to now store the following information. - -```diff -pub owner_id: AccountId, -+ pub approved_minters: LookupSet, -+ pub approved_creators: LookupSet, -pub tokens_per_owner: LookupMap>, -pub tokens_by_id: UnorderedMap, -- pub token_metadata_by_id: UnorderedMap, -+ pub series_by_id: UnorderedMap, -pub metadata: LazyOption, -``` - -As you can see, we've replaced `token_metadata_by_id` with `series_by_id` and added two lookup sets: - -- **series_by_id**: Map a series ID (u64) to its Series object. -- **approved_minters**: Keeps track of accounts that can call the `nft_mint` function. -- **approved_creators**: Keeps track of accounts that can create new series. - -
- -### Series Object {#series-object} -In addition, we're now keeping track of a new object called a `Series`. - -```rust -pub struct Series { - // Metadata including title, num copies etc.. that all tokens will derive from - metadata: TokenMetadata, - // Royalty used for all tokens in the collection - royalty: Option>, - // Set of tokens in the collection - tokens: UnorderedSet, - // What is the price of each token in this series? If this is specified, when minting, - // Users will need to attach enough $NEAR to cover the price. - price: Option, - // Owner of the collection - owner_id: AccountId, -} -``` - -This object stores information that each token will inherit from. This includes: -- The [metadata](2-minting.md#metadata-and-token-info). -- The [royalties](6-royalty.md). -- The price. - -:::caution -If a price is specified, there will be no restriction on who can mint tokens in the series. In addition, if the `copies` field is specified in the metadata, **only** that number of NFTs can be minted. If the field is omitted, an unlimited amount of tokens can be minted. -::: - -We've also added a field `tokens` which keeps track of all the token IDs that have been minted for this series. This allows us to deal with the potential `copies` cap by checking the length of the set. It also allows us to paginate through all the tokens in the series. - -
- -### Creating Series - -`series.rs` is a new file that replaces the old [minting](2-minting.md) logic. This file has been created to combine both the series creation and minting logic into one. - - - -The function takes in a series ID in the form of a [u64](https://doc.rust-lang.org/std/primitive.u64.html), the metadata, royalties, and the price for tokens in the series. It will then create the [Series object](#series-object) and insert it into the contract's series_by_id data structure. It's important to note that the caller must be an approved creator and they must attach enough $NEAR to cover storage costs. - -
- -### Minting NFTs - -Next, we'll look at the minting function. If you remember from before, this used to take the following parameters: -- Token ID -- Metadata -- Receiver ID -- Perpetual Royalties - -With the new and improved minting function, these parameters have been changed to just two: -- The series ID -- The receiver ID. - -The mint function might look complicated at first but let's break it down to understand what's happening. The first thing it does is get the [series object](#series-object) from the specified series ID. From there, it will check that the number of copies won't be exceeded if one is specified in the metadata. - -It will then store the token information on the contract as explained in the [minting section](2-minting.md#storage-implications) of the tutorial and map the token ID to the series. Once this is finished, a mint log will be emitted and it will ensure that enough deposit has been attached to the call. This amount differs based on whether or not the series has a price. - -#### Required Deposit - -As we went over in the [minting section](2-minting.md#storage-implications) of this tutorial, all information stored on the contract costs $NEAR. When minting, there is a required deposit to pay for this storage. For *this contract*, a series price can also be specified by the owner when the series is created. This price will be used for **all** NFTs in the series when they are minted. If the price is specified, the deposit must cover both the storage as well as the price. - -If a price **is specified** and the user attaches more deposit than what is necessary, the excess is sent to the **series owner**. There is also *no restriction* on who can mint tokens for series that have a price. The caller does **not** need to be an approved minter. - -If **no price** was specified in the series and the user attaches more deposit than what is necessary, the excess is *refunded to them*. In addition, the contract makes sure that the caller is an approved minter in this case. - -:::info -Notice how the token ID isn't required? This is because the token ID is automatically generated when minting. The ID stored on the contract is `${series_id}:${token_id}` where the token ID is a nonce that increases each time a new token is minted in a series. This not only reduces the amount of information stored on the contract but it also acts as a way to check the specific edition number. -::: - - - -
- -### View Functions - -Now that we've introduced the idea of series, more view functions have also been added. - -:::info -Notice how we've also created a new struct `JsonSeries` instead of returning the regular `Series` struct. This is because the `Series` struct contains an `UnorderedSet` which cannot be serialized. - -The common practice is to return everything **except** the `UnorderedSet` in a separate struct and then have entirely different methods for accessing the data from the `UnorderedSet` itself. - -::: - - - -The view functions are listed below. -- **[get_series_total_supply](https://github.com/near-examples/nft-tutorial/blob/main/nft-series/src/enumeration.rs#L92)**: Get the total number of series currently on the contract. - - Arguments: None. - - - -- **[get_series](https://github.com/near-examples/nft-tutorial/blob/main/nft-series/src/enumeration.rs#L97)**: Paginate through all the series in the contract and return a vector of `JsonSeries` objects. - - Arguments: `from_index: String | null`, `limit: number | null`. - - - -- **[get_series_details](https://github.com/near-examples/nft-tutorial/blob/main/nft-series/src/enumeration.rs#L115)**: Get the `JsonSeries` details for a specific series. - - Arguments: `id: number`. - - - -- **[nft_supply_for_series](https://github.com/near-examples/nft-tutorial/blob/main/nft-series/src/enumeration.rs#L133)**: View the total number of NFTs minted for a specific series. - - Arguments: `id: number`. - - - -- **[nft_tokens_for_series](https://github.com/near-examples/nft-tutorial/blob/main/nft-series/src/enumeration.rs#L146)**: Paginate through all NFTs for a specific series and return a vector of `JsonToken` objects. - - Arguments: `id: number`, `from_index: String | null`, `limit: number | null`. - - - -:::info -Notice how with every pagination function, we've also included a getter to view the total supply. This is so that you can use the `from_index` and `limit` parameters of the pagination functions in conjunction with the total supply so you know where to end your pagination. -::: - -
- -### Modifying View Calls for Optimizations - -Storing information on-chain can be very expensive. As you level up in your smart contract development skills, one area to look into is reducing the amount of information stored. View calls are a perfect example of this optimization. - -For example, if you wanted to relay the edition number for a given NFT in its title, you don't necessarily need to store this on-chain for every token. Instead, you could modify the view functions to manually append this information to the title before returning it. - -To do this, here's a way of modifying the `nft_token` function as it's central to all enumeration methods. - - - -For example if a token had a title `"My Amazing Go Team Gif"` and the NFT was edition 42, the new title returned would be `"My Amazing Go Team Gif - 42"`. If the NFT didn't have a title in the metadata, the series and edition number would be returned in the form of `Series {} : Edition {}`. - -While this is a small optimization, this idea is extremely powerful as you can potentially save on a ton of storage. As an example: most of the time NFTs don't utilize the following fields in their metadata. -- `issued_at` -- `expires_at` -- `starts_at` -- `updated_at` - -As an optimization, you could change the token metadata that's **stored** on the contract to not include these fields but then when returning the information in `nft_token`, you could simply add them in as `null` values. - -
- -### Owner File - -The last file we'll look at is the owner file found at `owner.rs`. This file simply contains all the functions for getting and setting approved creators and approved minters which can only be called by the contract owner. - -:::info -There are some other smaller changes made to the contract that you can check out if you'd like. The most notable are: -- The `Token` and `JsonToken` objects have been [changed](https://github.com/near-examples/nft-tutorial/blob/main/nft-series/src/metadata.rs#L40) to reflect the new series IDs. -- All references to `token_metadata_by_id` have been [changed](https://github.com/near-examples/nft-tutorial/blob/main/nft-series/src/enumeration.rs#L23) to `tokens_by_id` -- Royalty functions [now](https://github.com/near-examples/nft-tutorial/blob/main/nft-series/src/royalty.rs#L43) calculate the payout objects by using the series' royalties rather than the token's royalties. -::: - ---- - -## Building the Contract - -Now that you hopefully have a good understanding of the contract, let's get started building it. Run the following build command to compile the contract to wasm. - -```bash -cargo near build -``` - ---- - -## Deployment and Initialization - -Next, you'll deploy this contract to the network. - - - - - ```bash - export NFT_CONTRACT_ID= - near create-account $NFT_CONTRACT_ID --useFaucet - ``` - - - - - ```bash - export NFT_CONTRACT_ID= - near account create-account sponsor-by-faucet-service $NFT_CONTRACT_ID autogenerate-new-keypair save-to-keychain network-config testnet create - ``` - - - -Check if this worked correctly by echoing the environment variable. -```bash -echo $NFT_CONTRACT_ID -``` -This should return your ``. The next step is to initialize the contract with some default metadata. - -```bash -cargo near deploy build-non-reproducible-wasm $NFT_CONTRACT_ID with-init-call new_default_meta json-args '{"owner_id": "'$NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send -``` - -If you now query for the metadata of the contract, it should return our default metadata. - - - - - ```bash - near view $NFT_CONTRACT_ID nft_metadata '{}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $NFT_CONTRACT_ID nft_metadata json-args {} network-config testnet now - ``` - - - ---- - -## Creating The Series - -The next step is to create two different series. One will have a price for lazy minting and the other will simply be a basic series with no price. The first step is to create an owner [sub-account](/tools/near-cli#create) that you can use to create both series - - - - - ```bash - export SERIES_OWNER=owner.$NFT_CONTRACT_ID - - near create-account $SERIES_OWNER --use-account $NFT_CONTRACT_ID --initial-balance 3 --network-id testnet - ``` - - - - - ```bash - export SERIES_OWNER=owner.$NFT_CONTRACT_ID - - near account create-account fund-myself $SERIES_OWNER '3 NEAR' autogenerate-new-keypair save-to-keychain sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -### Basic Series - -You'll now need to create the simple series with no price and no royalties. If you try to run the following command before adding the owner account as an approved creator, the contract should throw an error. - - - - - ```bash - near call $NFT_CONTRACT_ID create_series '{"id": 1, "metadata": {"title": "SERIES!", "description": "testing out the new series contract", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}}' --gas 100000000000000 --deposit 1 --accountId $SERIES_OWNER --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT_ID create_series json-args '{"id": 1, "metadata": {"title": "SERIES!", "description": "testing out the new series contract", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}}' prepaid-gas '100.0 Tgas' attached-deposit '1 NEAR' sign-as $SERIES_OWNER network-config testnet sign-with-keychain send - ``` - - - -The expected output is an error thrown: `ExecutionError: 'Smart contract panicked: only approved creators can add a type`. If you now add the series owner as a creator, it should work. - - - - - ```bash - near call $NFT_CONTRACT_ID add_approved_creator '{"account_id": "'$SERIES_OWNER'"}' --gas 100000000000000 --accountId $SERIES_OWNER --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT_ID add_approved_creator json-args '{"account_id": "'$SERIES_OWNER'"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - - - - - ```bash - near call $NFT_CONTRACT_ID create_series '{"id": 1, "metadata": {"title": "SERIES!", "description": "testing out the new series contract", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}}' --gas 100000000000000 --deposit 1 --accountId $SERIES_OWNER --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT_ID create_series json-args '{"id": 1, "metadata": {"title": "SERIES!", "description": "testing out the new series contract", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}}' prepaid-gas '100.0 Tgas' attached-deposit '1 NEAR' sign-as $SERIES_OWNER network-config testnet sign-with-keychain send - ``` - - - -If you now query for the series information, it should work! - - - - - ```bash - near view $NFT_CONTRACT_ID get_series '{}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $NFT_CONTRACT_ID get_series json-args {} network-config testnet now - ``` - - - -Which should return something similar to: - -```js -[ - { - series_id: 1, - metadata: { - title: 'SERIES!', - description: 'testing out the new series contract', - media: 'https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif', - media_hash: null, - copies: null, - issued_at: null, - expires_at: null, - starts_at: null, - updated_at: null, - extra: null, - reference: null, - reference_hash: null - }, - royalty: null, - owner_id: 'owner.nft_contract.testnet' - } -] -``` - -
- -### Series With a Price - -Now that you've created the first, simple series, let's create the second one that has a price of 1 $NEAR associated with it. - - - - - ```bash - near call $NFT_CONTRACT_ID create_series '{"id": 2, "metadata": {"title": "COMPLEX SERIES!", "description": "testing out the new contract with a complex series", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "price": "500000000000000000000000"}' --gas 100000000000000 --deposit 1 --accountId $SERIES_OWNER --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT_ID create_series json-args '{"id": 2, "metadata": {"title": "COMPLEX SERIES!", "description": "testing out the new contract with a complex series", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "price": "500000000000000000000000"}' prepaid-gas '100.0 Tgas' attached-deposit '1 NEAR' sign-as $SERIES_OWNER network-config testnet sign-with-keychain send - ``` - - - -If you now paginate through the series again, you should see both appear. - - - - - ```bash - near view $NFT_CONTRACT_ID get_series '{}' --networkId testnet - ``` - - - - - ```bash - near contract call-function as-read-only $NFT_CONTRACT_ID get_series json-args {} network-config testnet now - ``` - - - - -Which has - -```js -[ - { - series_id: 1, - metadata: { - title: 'SERIES!', - description: 'testing out the new series contract', - media: 'https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif', - media_hash: null, - copies: null, - issued_at: null, - expires_at: null, - starts_at: null, - updated_at: null, - extra: null, - reference: null, - reference_hash: null - }, - royalty: null, - owner_id: 'owner.nft_contract.testnet' - }, - { - series_id: 2, - metadata: { - title: 'COMPLEX SERIES!', - description: 'testing out the new contract with a complex series', - media: 'https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif', - media_hash: null, - copies: null, - issued_at: null, - expires_at: null, - starts_at: null, - updated_at: null, - extra: null, - reference: null, - reference_hash: null - }, - royalty: null, - owner_id: 'owner.nft_contract.testnet' - } -] -``` - ---- - -## Minting NFTs - -Now that you have both series created, it's time to now mint some NFTs. You can either login with an existing NEAR wallet using [`near login`](/tools/near-cli#import) or you can create a sub-account of the NFT contract. In our case, we'll use a sub-account. - - - - - ```bash - export BUYER_ID=buyer.$NFT_CONTRACT_ID - - near create-account $BUYER_ID --use-account $NFT_CONTRACT_ID --initial-balance 1 --network-id testnet - ``` - - - - - ```bash - export BUYER_ID=buyer.$NFT_CONTRACT_ID - - near account create-account fund-myself $BUYER_ID '1 NEAR' autogenerate-new-keypair save-to-keychain sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -### Lazy Minting - -The first workflow you'll test out is [lazy minting](#lazy-minting) NFTs. If you remember, the second series has a price associated with it of 1 $NEAR. This means that there are no minting restrictions and anyone can try and purchase the NFT. Let's try it out. - -In order to view the NFT in the NEAR wallet, you'll want the `receiver_id` to be an account you have currently available in the wallet site. Let's export it to an environment variable. Run the following command but replace `YOUR_ACCOUNT_ID_HERE` with your actual NEAR account ID. - -```bash -export NFT_RECEIVER_ID=YOUR_ACCOUNT_ID_HERE -``` -Now if you try and run the mint command but don't attach enough $NEAR, it should throw an error. - - - - - ```bash - near call $NFT_CONTRACT_ID nft_mint '{"id": "2", "receiver_id": "'$NFT_RECEIVER_ID'"}' --gas 100000000000000 --accountId $BUYER_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT_ID nft_mint json-args '{"id": "2", "receiver_id": "'$NFT_RECEIVER_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as $BUYER_ID network-config testnet sign-with-keychain send - ``` - - - -Run the command again but this time, attach 1.5 $NEAR. - - - - - ```bash - near call $NFT_CONTRACT_ID nft_mint '{"id": "2", "receiver_id": "'$NFT_RECEIVER_ID'"}' --gas 100000000000000 --deposit 1.5 --accountId $BUYER_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT_ID nft_mint json-args '{"id": "2", "receiver_id": "'$NFT_RECEIVER_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '1.5 NEAR' sign-as $BUYER_ID network-config testnet sign-with-keychain send - ``` - - - -This should output the following logs. - -```bash -Receipts: BrJLxCVmxLk3yNFVnwzpjZPDRhiCinNinLQwj9A7184P, 3UwUgdq7i1VpKyw3L5bmJvbUiqvFRvpi2w7TfqmnPGH6 - Log [nft_contract.testnet]: EVENT_JSON:{"standard":"nep171","version":"nft-1.0.0","event":"nft_mint","data":[{"owner_id":"benjiman.testnet","token_ids":["2:1"]}]} -Transaction Id FxWLFGuap7SFrUPLskVr7Uxxq8hpDtAG76AvshWppBVC -To see the transaction in the transaction explorer, please open this url in your browser -https://testnet.nearblocks.io/txns/FxWLFGuap7SFrUPLskVr7Uxxq8hpDtAG76AvshWppBVC -'' -``` - -If you check the explorer link, it should show that the owner received on the order of `0.59305 $NEAR`. - - - -
- -### Becoming an Approved Minter - -If you try to mint the NFT for the simple series with no price, it should throw an error saying you're not an approved minter. - - - - - ```bash - near call $NFT_CONTRACT_ID nft_mint '{"id": "1", "receiver_id": "'$NFT_RECEIVER_ID'"}' --gas 100000000000000 --deposit 0.1 --accountId $BUYER_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT_ID nft_mint json-args '{"id": "1", "receiver_id": "'$NFT_RECEIVER_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $BUYER_ID network-config testnet sign-with-keychain send - ``` - - - -Go ahead and run the following command to add the buyer account as an approved minter. - - - - - ```bash - near call $NFT_CONTRACT_ID add_approved_minter '{"account_id": "'$BUYER_ID'"}' --gas 100000000000000 --accountId $NFT_CONTRACT_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT_ID add_approved_minter json-args '{"account_id": "'$BUYER_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -If you now run the mint command again, it should work. - - - - - ```bash - near call $NFT_CONTRACT_ID nft_mint '{"id": "1", "receiver_id": "'$NFT_RECEIVER_ID'"}' --gas 100000000000000 --deposit 0.1 --accountId $BUYER_ID --networkId testnet - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT_ID nft_mint json-args '{"id": "1", "receiver_id": "'$NFT_RECEIVER_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $BUYER_ID network-config testnet sign-with-keychain send - ``` - - - -
- -### Viewing the NFTs in the Wallet - -Now that you've received both NFTs, they should show up in the NEAR wallet. Open the collectibles tab and search for the contract with the title `NFT Series Contract` and you should own two NFTs. One should be the complex series and the other should just be the simple version. Both should have ` - 1` appended to the end of the title because the NFTs are the first editions for each series. - - - -Hurray! You've successfully deployed and tested the series contract! **GO TEAM!**. - ---- - -## Conclusion - -In this tutorial, you learned how to take the basic NFT contract and iterate on it to create a complex and custom version to meet the needs of the community. You optimized the storage, introduced the idea of collections, created a lazy minting functionality, hacked the enumeration functions to save on storage, and created an allowlist functionality. - -You then built the contract and deployed it on chain. Once it was on-chain, you initialized it and created two sets of series. One was complex with a price and the other was a regular series. You lazy minted an NFT and purchased it for `1.5 $NEAR` and then added yourself as an approved minter. You then minted an NFT from the regular series and viewed them both in the NEAR wallet. - -Thank you so much for going through this journey with us! I wish you all the best and am eager to see what sorts of neat and unique use-cases you can come up with. If you have any questions, feel free to ask on our [Discord](https://near.chat) or any other social media channels we have. If you run into any issues or have feedback, feel free to use the `Feedback` button on the right. - -:::note Versioning for this article - -At the time of this writing, this example works with the following versions: - -- rustc: `1.77.1` -- near-cli-rs: `0.17.0` -- cargo-near `0.6.1` -- NFT standard: [NEP171](https://nomicon.io/Standards/Tokens/NonFungibleToken/Core), version `1.0.0` - -::: diff --git a/docs/tutorials/nfts/js/0-intro.md b/docs/tutorials/nfts/js/0-intro.md deleted file mode 100644 index d2d1577a63b..00000000000 --- a/docs/tutorials/nfts/js/0-intro.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -id: introduction -title: NFT Zero to Hero JavaScript Edition -sidebar_label: Introduction -description: "Learn NFTs from minting to building a full-featured smart contract in this Zero to Hero series." ---- - -> In this _Zero to Hero_ series, you'll find a set of tutorials that will cover every aspect of a non-fungible token (NFT) smart contract. -> You'll start by minting an NFT using a pre-deployed contract and by the end you'll end up building a fully-fledged NFT smart contract that supports every extension. - - - -## Prerequisites - -To complete these tutorials successfully, you'll need: - -- [Node.js](/smart-contracts/quickstart?code-tabs=js) -- [A NEAR Wallet](https://testnet.mynearwallet.com/create) -- [NEAR-CLI](/tools/near-cli#installation) - ---- - -## Overview - -These are the steps that will bring you from **_Zero_** to **_Hero_** in no time! πŸ’ͺ - -| Step | Name | Description | -|------|------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| -| 1 | [Pre-deployed contract](/tutorials/nfts/js/predeployed-contract) | Mint an NFT without the need to code, create, or deploy a smart contract. | -| 2 | [Contract architecture](/tutorials/nfts/js/skeleton) | Learn the basic architecture of the NFT smart contract and compile code. | -| 3 | [Minting](/tutorials/nfts/js/minting) | Flesh out the skeleton so the smart contract can mint a non-fungible token. | -| 4 | [Upgrade a contract](/tutorials/nfts/js/upgrade-contract) | Discover the process to upgrade an existing smart contract. | -| 5 | [Enumeration](/tutorials/nfts/js/enumeration) | Explore enumeration methods that can be used to return the smart contract's states. | -| 6 | [Core](/tutorials/nfts/js/core) | Extend the NFT contract using the core standard which allows token transfer | -| 7 | [Approvals](/tutorials/nfts/js/approvals) | Expand the contract allowing other accounts to transfer NFTs on your behalf. | -| 8 | [Royalty](/tutorials/nfts/js/royalty) | Add NFT royalties allowing for a set percentage to be paid out to the token creator. | -| 9 | [Events](/tutorials/nfts/js/events) | in this tutorial you'll explore the events extension, allowing the contract to react on certain events. | -| 10 | [Marketplace](/tutorials/nfts/js/marketplace) | Learn about how common marketplaces operate on NEAR and dive into some of the code that allows buying and selling NFTs | - ---- - -## Next steps - -Ready to start? Jump to the [Pre-deployed Contract](/tutorials/nfts/js/predeployed-contract) tutorial and begin your learning journey! - -If you already know about non-fungible tokens and smart contracts, feel free to skip and jump directly to the tutorial of your interest. The tutorials have been designed so you can start at any given point! - -:::info Questions? -πŸ‘‰ Join us on [Discord](https://near.chat/) and let us know in the `#development` channels. πŸ‘ˆ - -We also host daily [Office Hours](https://pages.near.org/developers/get-help/office-hours/) live where the DevRel team will answer any questions you may have. πŸ€” - -Monday – Friday 11AM – 12PM Pacific (6PM – 7PM UTC) -::: diff --git a/docs/tutorials/nfts/js/0-predeployed.md b/docs/tutorials/nfts/js/0-predeployed.md deleted file mode 100644 index 5a25ec33fba..00000000000 --- a/docs/tutorials/nfts/js/0-predeployed.md +++ /dev/null @@ -1,161 +0,0 @@ ---- -id: predeployed-contract -title: Pre-deployed Contract -sidebar_label: Pre-deployed Contract -description: "Learn how to mint NFTs on NEAR using a pre-deployed NFT smart contract without any coding." ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -> Learn how to easily create your own non-fungible tokens without doing any software development by using a readily-available NFT smart contract. - - -## Prerequisites - -To complete this tutorial successfully, you'll need: - -- [A NEAR Wallet](https://testnet.mynearwallet.com/create) -- [NEAR-CLI](/tools/near-cli#installation) - -## Using the NFT contract - -### Setup - -- Log in to your newly created account with `near-cli` by running the following command in your terminal: - - - - - - ```bash - near login --networkId testnet - ``` - - - - - ```bash - near account import-account using-web-wallet network-config testnet - ``` - - - - - Set an environment variable for your account ID to make it easy to copy and paste commands from this tutorial: - -```bash -export NEARID=YOUR_ACCOUNT_NAME -``` -:::note - -Be sure to replace `YOUR_ACCOUNT_NAME` with the account name you just logged in with including the `.testnet` (or `.near` for `mainnet`). - -::: - -- Test that the environment variable is set correctly by running: - -```bash -echo $NEARID -``` - -### Minting your NFTs - -NEAR has deployed an NFT contract to the account `nft.examples.testnet` which allows users to freely mint tokens. Using this pre-deployed contract, let's mint our first token! - - -- Run this command in your terminal, however you **must replace the `token_id` value with an UNIQUE string**. - - - - - ```bash - near call nft.examples.testnet nft_mint '{"token_id": "TYPE_A_UNIQUE_VALUE_HERE", "receiver_id": "'$NEARID'", "metadata": { "title": "GO TEAM", "description": "The Team Goes", "media": "https://bafybeidl4hjbpdr6u6xvlrizwxbrfcyqurzvcnn5xoilmcqbxfbdwrmp5m.ipfs.dweb.link/", "copies": 1}}' --accountId $NEARID --deposit 0.1 - ``` - - - - - ```bash - near contract call-function as-transaction nft.examples.testnet nft_mint json-args '{"token_id": "TYPE_A_UNIQUE_VALUE_HERE", "receiver_id": "'$NEARID'", "metadata": { "title": "GO TEAM", "description": "The Team Goes", "media": "https://bafybeidl4hjbpdr6u6xvlrizwxbrfcyqurzvcnn5xoilmcqbxfbdwrmp5m.ipfs.dweb.link/", "copies": 1}}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $NEARID network-config testnet sign-with-keychain send - ``` - - - -:::tip -You can also replace the `media` URL with a link to any image file hosted on your web server. -::: - -
-Example response: -

- -```json -Log [nft.examples.testnet]: EVENT_JSON:{"standard":"nep171","version":"nft-1.0.0","event":"nft_mint","data":[{"owner_id":"benjiman.testnet","token_ids":["TYPE_A_UNIQUE_VALUE_HERE"]}]} -Transaction Id 8RFWrQvAsm2grEsd1UTASKpfvHKrjtBdEyXu7WqGBPUr -To see the transaction in the transaction explorer, please open this url in your browser -https://testnet.nearblocks.io/txns/8RFWrQvAsm2grEsd1UTASKpfvHKrjtBdEyXu7WqGBPUr -'' -``` - -

-
- -- To view tokens owned by an account you can call the NFT contract with the following `near-cli` command: - -```bash -near view nft.examples.testnet nft_tokens_for_owner '{"account_id": "'$NEARID'"}' -``` - -
-Example response: -

- -```json -[ - { - "token_id": "0", - "owner_id": "dev-xxxxxx-xxxxxxx", - "metadata": { - "title": "Some Art", - "description": "My NFT media", - "media": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/00/Olympus_Mons_alt.jpg/1024px-Olympus_Mons_alt.jpg", - "media_hash": null, - "copies": 1, - "issued_at": null, - "expires_at": null, - "starts_at": null, - "updated_at": null, - "extra": null, - "reference": null, - "reference_hash": null - }, - "approved_account_ids": {} - } -] -``` - -

-
- -***Congratulations! You just minted your first NFT token on the NEAR blockchain!*** πŸŽ‰ - -πŸ‘‰ Now try going to your [NEAR Wallet](https://testnet.mynearwallet.com) and view your NFT in the "Collectibles" tab. πŸ‘ˆ - ---- - -## Final remarks - -This basic example illustrates all the required steps to call an NFT smart contract on NEAR and start minting your own non-fungible tokens. - -Now that you're familiar with the process, you can jump to [Contract Architecture](/tutorials/nfts/js/skeleton) and learn more about the smart contract structure and how you can build your own NFT contract from the ground up. - -***Happy minting!*** πŸͺ™ - -:::note Versioning for this article - -At the time of this writing, this example works with the following versions: - -- near-cli: `3.0.0` -- NFT standard: [NEP171](https://github.com/near/NEPs/tree/master/neps/nep-0171.md), version `1.0.0` - -::: diff --git a/docs/tutorials/nfts/js/1-skeleton.md b/docs/tutorials/nfts/js/1-skeleton.md deleted file mode 100644 index d334b210cf9..00000000000 --- a/docs/tutorials/nfts/js/1-skeleton.md +++ /dev/null @@ -1,227 +0,0 @@ ---- -id: skeleton -title: Skeleton and JavaScript Architecture -sidebar_label: Contract Architecture -description: "Discover the architecture and layout of the NFT smart contract in this Zero to Hero series." ---- -import {Github} from "@site/src/components/UI/Codetabs" - -> In this article, you'll learn about the basic architecture behind the NFT contract that you'll develop while following this _"Zero to Hero"_ series. -> You'll discover the contract's layout and you'll see how the JavaScript files are structured in order to build a feature-complete smart contract. - - - - -## Introduction - -This tutorial presents the code skeleton for the NFT smart contract and its file structure. -You'll find how all the functions are laid out as well as the missing JS code that needs to be filled in. -Once every file and function has been covered, you'll go through the process of building the mock-up contract to confirm that everything is working correctly. - -## File structure - -Following a regular [JavaScript](https://www.javascript.com/) project, the file structure for this smart contract has: - -- `package.json` file to define the packages and scripts used in the project. -- `src` folder where all the JavaScript source files are stored -- `build` folder where the compiled `wasm` will output to. - -### Source files - -| File | Description | -| -------------------------------- | -------------------------------------------------------------------------------- | -| [approval.ts](#approvalts) | Has the internal functions that controls the access and transfers of non-fungible tokens. | -| [enumeration.ts](#enumerationts) | Contains the internal methods to query for NFT tokens and their owners. | -| [index.ts](#indexts) | Holds the exposed smart contract functions. | -| [metadata.ts](#metadatats) | Defines the token and metadata structures. | -| [mint.ts](#mintts) | Contains the internal token minting logic. | -| [nft_core.ts](#nft_corets) | Has the internal core logic that allows you to transfer NFTs between users. | -| [royalty.ts](#royaltyts) | Contains the internal payout-related functions. | - -``` -nft-tutorial-js -└── src - market-contract - nft-contract - β”œβ”€β”€ approval.ts - β”œβ”€β”€ enumeration.ts - β”œβ”€β”€ index.ts - β”œβ”€β”€ metadata.ts - β”œβ”€β”€ mint.ts - β”œβ”€β”€ nft_core.ts - └── royalty.ts -``` - -:::tip -Explore the code in our [GitHub repository](https://github.com/near-examples/nft-tutorial-js/tree/1.skeleton). -::: - ---- - -## `approval.ts` - -> This allows people to approve other accounts to transfer NFTs on their behalf. - -This file contains the internal logic that complies with the standard's [approvals management](https://github.com/near/NEPs/tree/master/neps/nep-0178.md) extension. Here is a breakdown of the methods and their functions: - -| Method | Description | -| ------------------- | --------------------------------------------------------------------------------------------------------- | -| **internalNftApprove** | Approves an account ID to transfer a token on your behalf. Called during **nft_approve**. | -| **internalNftIsApproved** | Checks if the input account has access to approve the token ID. Called during **nft_is_approved**. | -| **internalNftRevoke** | Revokes a specific account from transferring the token on your behalf. Called during **nft_revoke**. | -| **internalNftRevokeAll** | Revokes all accounts from transferring the token on your behalf. Called during **nft_revoke_all**. | - - - -You'll learn more about these functions in the [approvals section](/tutorials/nfts/js/approvals) of the Zero to Hero series. - ---- - -## `enumeration.ts` - -> This file provides the internal functions needed to view information about NFTs, and follows the standard's [enumeration](https://github.com/near/NEPs/tree/master/neps/nep-0181.md) extension. - -| Method | Description | -| ------------------------ | ---------------------------------------------------------------------------------- | -| **internalNftTotalSupply** | Returns the total amount of NFTs stored on the contract. Called during **nft_total_supply**. | -| **internalNftTokens** | Returns a paginated list of NFTs stored on the contract regardless of their owner. Called during **nft_tokens**. | -| **internalNftSupplyForOwner** | Allows you view the total number of NFTs owned by any given user. Called during **nft_supply_for_owner**. | -| **internalNftTokensForOwner** | Returns a paginated list of NFTs owned by any given user. Called during **nft_tokens_for_owner**. | - - - -You'll learn more about these functions in the [enumeration section](/tutorials/nfts/js/enumeration) of the tutorial series. - ---- - -## `metadata.ts` - -> This file is used to keep track of the information to be stored for tokens, and metadata. -> In addition, you can define a function to view the contract's metadata which is part of the standard's [metadata](https://github.com/near/NEPs/tree/master/neps/nep-0177.md) extension. - -| Name | Description | -| ----------------- | ------------------------------------------------------------------------------------------------------------- | -| **TokenMetadata** | This structure defines the metadata that can be stored for each token. (title, description, media, etc. | -| **Token** | This structure outlines what information will be stored on the contract for each token. | -| **JsonToken** | When querying information about NFTs through view calls, the return information is stored in this JSON token. | -| **internalNftMetadata** | This function allows users to query for the contact's internal metadata. Called during **nft_metadata**. | - - - -You'll learn more about these functions in the [minting section](/tutorials/nfts/js/minting) of the tutorial series. - ---- - -## `mint.ts` - -> Contains the internal token minting logic. - -| Method | Description | -| ------------ | ----------------------------------------- | -| **internalNftMint** | This function mints a non-fungible token. Called during **nft_mint**. | - - - ---- - -## `nft_core.ts` - -> Core logic that allows you to transfer NFTs between users. - -| Method | Description | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **internalNftTransfer** | Transfers an NFT to a receiver ID. Called during **nft_transfer**. | -| **internalNftTransferCall** | Transfers an NFT to a receiver and calls a function on the receiver ID's contract. The function returns `true` if the token was transferred from the sender's account. Called during **nft_transfer_call**. | -| **internalNftToken** | Allows users to query for the information about a specific NFT. Called during **nft_token**. | | -| **internalNftResolveTransfer** | When you start the `nft_transfer_call` and transfer an NFT, the standard dictates that you should also call a method on the receiver's contract. If the receiver needs you to return the NFT to the sender (as per the return value of the `nft_on_transfer` method), this function allows you to execute that logic. Called during **nft_resolve_transfer**. | - - - -You'll learn more about these functions in the [minting section](/tutorials/nfts/js/minting) of the tutorial series. - ---- - -## `royalty.ts` - -> Contains the internal payout-related functions. - -| Method | Description | -| ----------------------- | ------------------------------------------------------------------------------------------------------------- | -| **internalNftPayout** | This internal method calculates the payout for a given token. Called during **nft_payout**. | -| **internalNftTransferPayout** | Internal method to transfer the token to the receiver ID and return the payout object that should be paid for a given balance. Called during **nft_transfer_payout**. | - - - -You'll learn more about these functions in the [royalty section](/tutorials/nfts/js/royalty) of the tutorial series. - ---- - -## `index.ts` - -> This file outlines the smart contract class and what information it stores and keeps track of. In addition, it exposes all public facing methods that are callable by the user. - -| Method | Description | -| -------------------- | ----------------------------------------------------------------------------------------------- | -| **init** | Constructor function used to initialize the contract with some metadata and default state. | -| **nft_mint** | Calls the internal mint function to mint an NFT. | -| **nft_token** | Calls the internal function to query for info on a specific NFT | -| **nft_transfer** | Calls the internal function to transfer an NFT | -| **nft_transfer_call** | Calls the internal function to transfer an NFT and call `nft_on_transfer` on the receiver's contract | -| **nft_resolve_transfer** | Calls the internal function to resolve the transfer call promise.| -| **nft_is_approved** | Calls the internal function to check whether someone is approved for an NFT| -| **nft_approve** | Calls the internal function to approve someone to transfer your NFT| -| **nft_payout** | Calls the internal function to query for the payout object for an NFT| -| **nft_transfer_payout** | Calls the internal function to transfer an NFT and return the payout object. | -| **nft_revoke** | Calls the internal function to revoke someone access to transfer your NFT| -| **nft_revoke_all** | Calls the internal function to revoke everyone's access to transfer your NFT| -| **nft_total_supply** | Calls the internal function to query the total supply of NFTs on the contract.| -| **nft_tokens** | Calls the internal function to paginate through NFTs on the contract| -| **nft_tokens_for_owner** | Calls the internal function to paginate through NFTs for a given owner| -| **nft_supply_for_owner** | Calls the internal function to query for the total number of NFTs owned by someone.| -| **nft_metadata** | Calls the internal function to query for the contract's metadata| - - - - - -You'll learn more about these functions in the [minting section](/tutorials/nfts/js/minting) of the tutorial series. - ---- - -## Building the skeleton - -- If you haven't cloned the main repository yet, open a terminal and run: - -```sh -git clone https://github.com/near-examples/nft-tutorial-js/ -``` - -- Next, switch to the `1.skeleton` branch. -- Install the dependencies (including the JS SDK): `yarn` -- Build the contract with `yarn build`: - -```sh -git clone https://github.com/near-examples/nft-tutorial-js/ -cd nft-tutorial-js -git checkout 1.skeleton -yarn && yarn build -``` - -Once this finishes, the `nft-tutorial-js/build` directory should contain the `nft.wasm` smart contract! - -Building the skeleton is useful to validate that everything works properly and that you'll be able to compile improved versions of this NFT contract in the upcoming tutorials. - ---- - -## Conclusion - -You've seen the layout of this NFT smart contract, and how all the functions are laid out across the different source files. -Using `yarn`, you've been able to compile the contract, and you'll start fleshing out this skeleton in the next [Minting tutorial](/tutorials/nfts/js/minting). - -:::note Versioning for this article -At the time of this writing, this example works with the following versions: - -- near-sdk-js: `0.4.0-5` -- NFT standard: [NEP171](https://github.com/near/NEPs/tree/master/neps/nep-0171.md), version `1.0.0` - -::: diff --git a/docs/tutorials/nfts/js/2-minting.md b/docs/tutorials/nfts/js/2-minting.md deleted file mode 100644 index 4f9926b06eb..00000000000 --- a/docs/tutorials/nfts/js/2-minting.md +++ /dev/null @@ -1,386 +0,0 @@ ---- -id: minting -title: Minting -sidebar_label: Minting -description: "Learn how to implement the logic to mint NFTs on NEAR, store token and metadata, and link tokens to owners using a skeleton smart contract." ---- -import {Github} from "@site/src/components/UI/Codetabs"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -This is the first of many tutorials in a series where you'll be creating a complete NFT smart contract from scratch that conforms with all the NEAR [NFT standards](https://github.com/near/NEPs/tree/master/neps/nep-0171.md). Today you'll learn how to create the logic needed to mint NFTs and have them show up in your NEAR wallet. You will be modifying a bare-bones [skeleton smart contract](/tutorials/nfts/js/skeleton) by filling in the necessary code snippets needed to add minting functionalities. - - - - -## Introduction - -To get started, switch to the `1.skeleton` branch in our repo. If you haven't cloned the repository, refer to the [Contract Architecture](/tutorials/nfts/js/skeleton) to get started. - -``` -git checkout 1.skeleton -``` - -If you wish to see the finished code for the minting portion of the tutorial, that can be found on the `2.minting` branch. - -## Modifications to the skeleton contract {#what-does-minting-mean} - -In order to implement the logic needed for minting, we should break it up into smaller tasks and handle those one-by-one. Let's step back and think about the best way to do this by asking ourselves a simple question: what does it mean to mint an NFT? - -To mint a non-fungible token, in the most simple way possible, a contract needs to be able to associate a token with an owner on the blockchain. This means you'll need: - -- A way to keep track of tokens and other information on the contract. -- A way to store information for each token such as `metadata` (more on that later). -- A way to link a token with an owner. - -That's it! We've now broken down the larger problem into some smaller, less daunting, subtasks. Let's start by tackling the first and work our way through the rest. - -### Storing information on the contract {#storing-information} - -Start by navigating to `nft-contract/src/index.ts` and filling in some of the code blocks. -You need to be able to store important information on the contract such as the list of tokens that an account has. - - -The first thing to do is add the information to the contract class. - - - -This allows you to get the information stored in these data structures from anywhere in the contract. The code above has created 3 token specific storages: - -- **tokensPerOwner**: allows you to keep track of the tokens owned by any account. It will map the account address to a set of token ID strings owned by that account. -- **tokensById**: returns all the information about a specific token. It will map a token ID string to a `Token` object. -- **tokenMetadataById**: returns just the metadata for a specific token. It wil map a token ID string to a `TokenMetadata` object. - -In addition, you'll keep track of the owner of the contract as well as the metadata for the contract. - -#### Constructor Function - -Next, you'll add the logic to the constructor function. This function needs to be invoked when you first deploy the contract. It will initialize all the contract's fields that you've defined above with default values. -We've added the `ownerId` and `metadata` fields as parameters to the function because those are the only ones that can be customized. - -This function will default all the collections to be empty and set the `owner` and `metadata` equal to what you pass in. - - - -More often than not when doing development, you'll need to deploy contracts several times. You can imagine that it might get tedious to have to pass in metadata every single time you want to initialize the contract. For this reason, the metadata has been defaulted with some initial data if it wasn't passed in by the user. - -### Metadata and token information {#metadata-and-token-info} - -Now that you've defined what information to store on the contract itself and you've defined some ways to initialize the contract, you need to define what information should go in the `Token`, `TokenMetadata`, and `NFTContractMetadata` data types. - -Let's switch over to the `nft-contract/src/metadata.ts` file as this is where that information will go. If you look at the [standards for metadata](https://github.com/near/NEPs/tree/master/neps/nep-0177.md), you'll find all the necessary information that you need to store for both `TokenMetadata` and `NFTContractMetadata`. Simply fill in the following code. - - - -This now leaves you with the `Token` struct and something called a `JsonToken`. The `Token` struct will hold all the information directly related to the token excluding the metadata. The metadata, if you remember, is stored in a map on the contract in a data structured called `tokenMetadataById`. This allows you to quickly get the metadata for any token by simply passing in the token's ID. - -For the `Token` struct, you'll just keep track of the owner for now. - - - -The purpose of the `JsonToken` is to hold all the information for an NFT that you want to send back as JSON whenever someone does a view call. This means you'll want to store the owner, token ID, and metadata. - - - -:::tip -Some of you might be thinking _"how come we don't just store all the information in the `Token` struct?"_. -The reason behind this is that it's actually more efficient to construct the JSON token on the fly only when you need it rather than storing all the information in the token struct. -In addition, some operations might only need the metadata for a token and so having the metadata in a separate data structure is more optimal. -::: - -#### Function for querying contract metadata - -Now that you've defined some of the types that were used in the previous section, let's move on and create the first view function `internalNftMetadata`. This will allow users to query for the contract's metadata as per the [metadata standard](https://github.com/near/NEPs/tree/master/neps/nep-0177.md). - - - -This function will get the `metadata` object from the contract which is of type `NFTContractMetadata` and will return it. - -Just like that, you've completed the first two tasks and are ready to move onto last part of the tutorial. - -### Minting Logic {#minting-logic} - -Now that all the information and types are defined, let's start brainstorming how the minting logic will play out. In the end, you need to link a `Token` and `TokenId` to a specific owner. Let's look back at a couple data structures that might be useful: - -```ts -//keeps track of all the token IDs for a given account -tokensPerOwner: LookupMap>; - -//keeps track of the token struct for a given token ID -tokensById: LookupMap; - -//keeps track of the token metadata for a given token ID -tokenMetadataById: UnorderedMap; -``` - -Looking at these data structures, you could do the following: - -- Add the token ID into the set of tokens that the receiver owns. This will be done on the `tokensPerOwner` field. -- Create a token object and map the token ID to that token object in the `tokensById` field. -- Map the token ID to it's metadata using the `tokenMetadataById`. - -With those steps outlined, it's important to take into consideration the storage costs of minting NFTs. Since you're adding bytes to the contract by creating entries in the data structures, the contract needs to cover the storage costs. If you just made it so any user could go and mint an NFT for free, that system could easily be abused and users could essentially "drain" the contract of all it's funds by minting thousands of NFTs. For this reason, you'll make it so that users need to attach a deposit to the call to cover the cost of storage. You'll measure the initial storage usage before anything was added and you'll measure the final storage usage after all the logic is finished. Then you'll make sure that the user has attached enough $NEAR to cover that cost and refund them if they've attached too much. - -Now that you've got a good understanding of how everything should play out, let's fill in the necessary code. - - - -You'll notice that we're using some internal methods such as `refundDeposit` and `internalAddTokenToOwner`. We've described the function of `refundDeposit` and as for `internalAddTokenToOwner`, this will add a token to the set of tokens an account owns for the contract's `tokensPerOwner` data structure. You can create these functions in a file called `internal.ts`. Go ahead and create the file. Your new contract architecture should look as follows: - -``` -nft-contract -└── src - β”œβ”€β”€ approval.ts - β”œβ”€β”€ enumeration.ts - β”œβ”€β”€ internal.ts - β”œβ”€β”€ lib.ts - β”œβ”€β”€ metadata.ts - β”œβ”€β”€ mint.ts - β”œβ”€β”€ nft_core.ts - └── royalty.ts -``` - -Add the following to your newly created `internal.ts` file. - - - -At this point, the core logic is all in place so that you can mint NFTs. You can use the function `nft_mint` which takes the following parameters: - -- **token_id**: the ID of the token you're minting (as a string). -- **metadata**: the metadata for the token that you're minting (of type `TokenMetadata` which is found in the `metadata.ts` file). -- **receiver_id**: specifies who the owner of the token will be. - -Behind the scenes, the function will: - -1. Call the internal mint function. -2. Calculate the initial storage before adding anything to the contract -3. Create a `Token` object with the owner ID -4. Link the token ID to the newly created token object by inserting them into the `tokensById` field. -5. Link the token ID to the passed in metadata by inserting them into the `tokenMetadataById` field. -6. Add the token ID to the list of tokens that the owner owns by calling the `internalAddTokenToOwner` function. -7. Calculate the final and net storage to make sure that the user has attached enough NEAR to the call in order to cover those costs. - -### Querying for token information - -If you were to go ahead and deploy this contract, initialize it, and mint an NFT, you would have no way of knowing or querying for the information about the token you just minted. Let's quickly add a way to query for the information of a specific NFT. You'll move to the `nft-contract/src/nft_core.ts` file and edit the `internalNftToken` function. - -It will take a token ID as a parameter and return the information for that token. The `JsonToken` contains the token ID, the owner ID, and the token's metadata. - - - -With that finished, it's finally time to build and deploy the contract so you can mint your first NFT. - -## Interacting with the contract on-chain - -Now that the logic for minting is complete and you've added a way to query for information about specific tokens, it's time to build and deploy your contract to the blockchain. - -### Deploying the contract {#deploy-the-contract} - -We've included a very simple way to build the smart contracts throughout this tutorial using `yarn`. The following command will build the contract and copy over the `.wasm` file to a folder `build/nft.wasm`. - -```bash -yarn build:nft -``` - -For deployment, you will need a NEAR account with the keys stored on your local machine. Navigate to the [NEAR wallet](https://testnet.mynearwallet.com//) site and create an account. - -:::info -Please ensure that you deploy the contract to an account with no pre-existing contracts. It's easiest to simply create a new account or create a sub-account for this tutorial. -::: - -Log in to your newly created account with `near-cli` by running the following command in your terminal. - - - - - - ```bash - near login --networkId testnet - ``` - - - - - ```bash - near account import-account using-web-wallet network-config testnet - ``` - - - -To make this tutorial easier to copy/paste, we're going to set an environment variable for your account ID. In the command below, replace `YOUR_ACCOUNT_NAME` with the account name you just logged in with including the `.testnet` portion: - -```bash -export NFT_CONTRACT_ID="YOUR_ACCOUNT_NAME" -``` - -Test that the environment variable is set correctly by running: - -```bash -echo $NFT_CONTRACT_ID -``` - -Verify that the correct account ID is printed in the terminal. If everything looks correct you can now deploy your contract. -In the root of your NFT project run the following command to deploy your smart contract. - -```bash -cargo near deploy build-non-reproducible-wasm $NFT_CONTRACT_ID -``` - -At this point, the contract should have been deployed to your account and you're ready to move onto testing and minting NFTs. - -### Initializing the contract {#initialize-contract} - -The very first thing you need to do once the contract has been deployed is to initialize it. For simplicity, let's call the default metadata initialization function you wrote earlier so that you don't have to type the metadata manually in the CLI. - - - - - ```bash - near call $NFT_CONTRACT_ID init '{"owner_id": "'$NFT_CONTRACT_ID'"}' --accountId $NFT_CONTRACT_ID - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT_ID init json-args '{"owner_id": "'$NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -You've just initialized the contract with some default metadata and set your account ID as the owner. At this point, you're ready to call your first view function. - -### Viewing the contract's metadata - -Now that the contract has been initialized, you can call some of the functions you wrote earlier. More specifically, let's test out the function that returns the contract's metadata: - - - - - ```bash - near view $NFT_CONTRACT_ID nft_metadata - ``` - - - - - ```bash - near contract call-function as-read-only $NFT_CONTRACT_ID nft_metadata json-args '{}' network-config testnet now - ``` - - - -This should return an output similar to the following: - -```bash -{ spec: 'nft-1.0.0', name: 'NFT Tutorial Contract', symbol: 'GOTEAM' } -``` - -At this point, you're ready to move on and mint your first NFT. - -### Minting our first NFT {#minting-our-first-nft} - -Let's now call the minting function that you've created. This requires a `token_id` and `metadata`. If you look back at the `TokenMetadata` struct you created earlier, there are many fields that could potentially be stored on-chain: - - - -Let's mint an NFT with a title, description, and media to start. The media field can be any URL pointing to a media file. We've got an excellent GIF to mint but if you'd like to mint a custom NFT, simply replace our media link with one of your choosing. If you run the following command, it will mint an NFT with the following parameters: - -- **token_id**: "token-1" -- **metadata**: - - _title_: "My Non Fungible Team Token" - - _description_: "The Team Most Certainly Goes :)" - - _media_: `https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif` -- **receiver_id**: "'$NFT_CONTRACT_ID'" - - - - - ```bash - near call $NFT_CONTRACT_ID nft_mint '{"token_id": "token-1", "metadata": {"title": "My Non Fungible Team Token", "description": "The Team Most Certainly Goes :)", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$NFT_CONTRACT_ID'"}' --accountId $NFT_CONTRACT_ID --amount 0.1 - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT_ID nft_mint json-args '{"token_id": "token-1", "metadata": {"title": "My Non Fungible Team Token", "description": "The Team Most Certainly Goes :)", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -:::info -The `amount` flag is specifying how much NEAR to attach to the call. Since you need to pay for storage, 0.1 NEAR is attached and you'll get refunded any excess that is unused at the end. -::: - -### Viewing information about the NFT - -Now that the NFT has been minted, you can check and see if everything went correctly by calling the `nft_token` function. -This should return a `JsonToken` which should contain the `token_id`, `owner_id`, and `metadata`. - - - - - ```bash - near view $NFT_CONTRACT_ID nft_token '{"token_id": "token-1"}' - ``` - - - - - ```bash - near contract call-function as-read-only $NFT_CONTRACT_ID nft_tokens_for_owner json-args '{"token_id": "token-1"}' network-config testnet now - ``` - - - -
-Example response: -

- -```bash -{ - token_id: 'token-1', - owner_id: 'goteam.examples.testnet', - metadata: { - title: 'My Non Fungible Team Token', - description: 'The Team Most Certainly Goes :)', - media: 'https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif' - } -} -``` - -

-
- -**Go team!** You've now verified that everything works correctly and it's time to view your freshly minted NFT in the NEAR wallet's collectibles tab! - -## Viewing your NFTs in the wallet - -If you navigate to the [collectibles tab](https://testnet.mynearwallet.com//?tab=collectibles) in the NEAR wallet, this should list all the NFTs that you own. Currently, It should be empty. - -We've got a problem. The wallet correctly picked up that you minted an NFT, however, the contract doesn't implement the specific view function that is being called. Behind the scenes, the wallet is trying to call `nft_tokens_for_owner` to get a list of all the NFTs owned by your account on the contract. The only function you've created, however, is the `nft_token` function. It wouldn't be very efficient for the wallet to call `nft_token` for every single NFT that a user has to get information and so they try to call the `nft_tokens_for_owner` function instead. - -In the next tutorial, you'll learn about how to deploy a patch fix to a pre-existing contract so that you can view the NFT in the wallet. - -## Conclusion - -In this tutorial, you went through the basics of setting up and understand the logic behind minting NFTs on the blockchain using a skeleton contract. - -You first looked at [what it means](#what-does-minting-mean) to mint NFTs and how to break down the problem into more feasible chunks. You then started modifying the skeleton contract chunk by chunk starting with solving the problem of [storing information / state](#storing-information) on the contract. You then looked at what to put in the [metadata and token information](#metadata-and-token-info). Finally, you looked at the logic necessary for [minting NFTs](#minting-logic). - -After the contract was written, it was time to deploy to the blockchain. You [deployed the contract](#deploy-the-contract) and [initialized it](#initialize-contract). Finally, you [minted your very first NFT](#minting-our-first-nft) and saw that some changes are needed before you can view it in the wallet. - -## Next Steps - -In the [next tutorial](/tutorials/nfts/js/upgrade-contract), you'll find out how to deploy a patch fix and what that means so that you can view your NFTs in the wallet. - -:::note Versioning for this article - -At the time of this writing, this example works with the following versions: - -- near-cli: `3.0.0` -- NFT standard: [NEP171](https://github.com/near/NEPs/tree/master/neps/nep-0171.md), version `1.0.0` -- Metadata standard: [NEP177](https://github.com/near/NEPs/tree/master/neps/nep-0177.md), version `2.1.0` - -::: diff --git a/docs/tutorials/nfts/js/2-upgrade.md b/docs/tutorials/nfts/js/2-upgrade.md deleted file mode 100644 index 7e0868369c6..00000000000 --- a/docs/tutorials/nfts/js/2-upgrade.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -id: upgrade-contract -title: Upgrading the Contract -sidebar_label: Upgrade a Contract -description: "Learn how to upgrade your NEAR NFT contract so minted NFTs can be properly displayed in wallets." ---- -import {Github} from "@site/src/components/UI/Codetabs" - -In this tutorial, you'll build off the work you previously did to implement the [minting functionality](/tutorials/nfts/js/minting) on a skeleton smart contract. You got to the point where NFTs could be minted, however, the wallet had no way of displaying the tokens since your contract didn't implement the method that the wallet was trying to call. - - - - -## Introduction - -Today you'll learn about deploying patch fixes to smart contracts and you'll use that knowledge to implement the `nft_tokens_for_owner` function on the contract you deployed in the previous tutorial. - -## Upgrading contracts overview {#upgrading-contracts} - -Upgrading contracts, when done right, can be an immensely powerful tool. If done wrong, it can lead to a lot of headaches. It's important to distinguish between the code and state of a smart contract. When a contract is deployed on top of an existing contract, the only thing that changes is the code. The state will remain the same and that's where a lot of developer's issues come to fruition. - -The NEAR Runtime will read the serialized state from disk and it will attempt to load it using the current contract code. When your code changes, it might not be able to figure out how to do this. - -You need to strategically upgrade your contracts and make sure that the runtime will be able to read your current state with the new contract code. For more information about upgrading contracts and some best practices, see the NEAR SDK's [upgrading contracts](../../../smart-contracts/release/upgrade.md) write-up. - -## Modifications to our contract {#modifications-to-contract} - -In order for the wallet to properly display your NFTs, you need to implement the `nft_tokens_for_owner` method. This will allow anyone to query for a paginated list of NFTs owned by a given account ID. - -To accomplish this, let's break it down into some smaller subtasks. First, you need to get access to a list of all token IDs owned by a user. This information can be found in the `tokensPerOwner` data structure. Now that you have a set of token IDs, you need to convert them into `JsonToken` objects as that's what you'll be returning from the function. - -Luckily, you wrote a function `nft_token` which takes a token ID and returns a `JsonToken` in the `nft_core.ts` file. As you can guess, in order to get a list of `JsonToken` objects, you would need to iterate through the token IDs owned by the user and then convert each token ID into a `JsonToken` and store that in a list. - -As for the pagination, you can use some basic JavaScript to get that done. Let's move over to the `enumeration.ts` file and implement that logic: - - - -## Redeploying the contract {#redeploying-contract} - -Now that you've implemented the necessary logic for `nft_tokens_for_owner`, it's time to build and re-deploy the contract to your account. Using the build script, deploy the contract as you did in the previous tutorial: - -```bash -yarn build && near deploy --wasmFile build/nft.wasm --accountId $NFT_CONTRACT_ID -``` - -This should output a warning saying that the account has a deployed contract and will ask if you'd like to proceed. Simply type `y` and hit enter. - -```bash -This account already has a deployed contract [ AKJK7sCysrWrFZ976YVBnm6yzmJuKLzdAyssfzK9yLsa ]. Do you want to proceed? (y/n) -``` - -Once the contract has been redeployed, let's test and see if the state migrated correctly by running a simple view function: - -```bash -near view $NFT_CONTRACT_ID nft_metadata -``` - -This should return an output similar to the following: - -```bash -{ spec: 'nft-1.0.0', name: 'NFT Tutorial Contract', symbol: 'GOTEAM' } -``` - -**Go team!** At this point, you can now test and see if the new function you wrote works correctly. Let's query for the list of tokens that you own: - -```bash -near view $NFT_CONTRACT_ID nft_tokens_for_owner '{"account_id": "'$NFT_CONTRACT_ID'", "limit": 5}' -``` - -
-Example response: -

- -```bash -[ - { - token_id: 'token-1', - owner_id: 'goteam.examples.testnet', - metadata: { - title: 'My Non Fungible Team Token', - description: 'The Team Most Certainly Goes :)', - media: 'https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif' - } - } -] -``` - -

-
- -## Viewing NFTs in the wallet {#viewing-nfts-in-wallet} - -Now that your contract implements the necessary functions that the wallet uses to display NFTs, you should be able to see your tokens on display in the [collectibles tab](https://testnet.mynearwallet.com//?tab=collectibles). - -![filled-nft-in-wallet](/assets/docs/tutorials/nfts/filled-nft-in-wallet.png) - -## Conclusion - -In this tutorial, you learned about the basics of [upgrading contracts](#upgrading-contracts). Then, you implemented the necessary [modifications to your smart contract](#modifications-to-contract) and [redeployed it](#redeploying-contract). Finally you navigated to the wallet collectibles tab and [viewed your NFTs](#viewing-nfts-in-wallet). - -In the [next tutorial](/tutorials/nfts/js/enumeration), you'll implement the remaining functions needed to complete the [enumeration](https://nomicon.io/Standards/Tokens/NonFungibleToken/Enumeration) standard. - -:::note Versioning for this article - -At the time of this writing, this example works with the following versions: - -- near-cli: `3.0.0` -- NFT standard: [NEP171](https://nomicon.io/Standards/Tokens/NonFungibleToken/Core), version `1.0.0` - -::: diff --git a/docs/tutorials/nfts/js/3-enumeration.md b/docs/tutorials/nfts/js/3-enumeration.md deleted file mode 100644 index 04ac10d057b..00000000000 --- a/docs/tutorials/nfts/js/3-enumeration.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -id: enumeration -title: Enumeration -sidebar_label: Enumeration -description: "In this tutorial, you'll extend your NFT contract with enumeration methods." ---- -import {Github} from "@site/src/components/UI/Codetabs" - -In the previous tutorials, you looked at ways to integrate the minting functionality into a skeleton smart contract. In order to get your NFTs to show in the wallet, you also had to deploy a patch fix that implemented one of the enumeration methods. In this tutorial, you'll expand on and finish the rest of the enumeration methods as per the [standard](https://github.com/near/NEPs/tree/master/neps/nep-0181.md) -Now you'll extend the NFT smart contract and add a couple of enumeration methods that can be used to return the contract's state. - - - - -## Introduction - -As mentioned in the [Upgrade a Contract](/tutorials/nfts/js/upgrade-contract/) tutorial, you can deploy patches and fixes to smart contracts. This time, you'll use that knowledge to implement the `nft_total_supply`, `nft_tokens` and `nft_supply_for_owner` enumeration functions. - -To get started, either switch to the `2.minting` branch from our [GitHub repository](https://github.com/near-examples/nft-tutorial/), or continue your work from the previous tutorials. -If you haven't cloned it yet, refer to the [Contract Architecture](/tutorials/nfts/js/skeleton#building-the-skeleton) to check out the repository. - -```bash -git checkout 2.minting -``` - -:::tip -If you wish to see the finished code for this _Enumeration_ tutorial, you can find it on the `3.enumeration` branch. -::: - -## Modifications to the contract - -Let's start by opening the `src/enumeration.ts` file and locating the empty `internalNftTotalSupply` function. - -### NFT Total Supply - -This function should return the total number of NFTs stored on the contract. You can easily achieve this functionality by simply returning the length of the `nftMetadataById` data structure. - - - -### NFT Tokens - -This function should return a paginated list of `JsonTokens` that are stored on the contract regardless of their owners. -If the user provides a `from_index` parameter, you should use that as the starting point for which to start iterating through tokens; otherwise it should start from the beginning. Likewise, if the user provides a `limit` parameter, the function shall stop after reaching either the limit or the end of the list. - - - -### NFT Supply For Owner - -This function should look for all the non-fungible tokens for a user-defined owner, and return the length of the resulting set. -If there isn't a set of tokens for the provided Account ID, then the function shall return `0`. - - - -Next, you can use the CLI to query these new methods and validate that they work correctly. - -## Redeploying the contract {#redeploying-contract} - -Now that you've implemented the necessary logic for `nft_tokens_for_owner`, it's time to build and re-deploy the contract to your account. Using the build script, deploy the contract as you did in the previous tutorials: - -```bash -yarn build && near deploy --wasmFile build/nft.wasm --accountId $NFT_CONTRACT_ID -``` - -This should output a warning saying that the account has a deployed contract and will ask if you'd like to proceed. Simply type `y` and hit enter. - -``` -This account already has a deployed contract [ AKJK7sCysrWrFZ976YVBnm6yzmJuKLzdAyssfzK9yLsa ]. Do you want to proceed? (y/n) -``` - -## Enumerating tokens - -Once the updated contract has been redeployed, you can test and see if these new functions work as expected. - -### NFT tokens - -Let's query for a list of non-fungible tokens on the contract. Use the following command to query for the information of up to 50 NFTs starting from the 10th item: - -```bash -near view $NFT_CONTRACT_ID nft_tokens '{"from_index": "10", "limit": 50}' -``` - -This command should return an output similar to the following: - -
-Example response: -

- -```json -[] -``` - -

-
- -### Tokens by owner - -To get the total supply of NFTs owned by the `goteam.testnet` account, call the `nft_supply_for_owner` function and set the `account_id` parameter: - -```bash -near view $NFT_CONTRACT_ID nft_supply_for_owner '{"account_id": "goteam.testnet"}' -``` - -This should return an output similar to the following: - -
-Example response: -

- -```json -0 -``` - -

-
- -## Conclusion - -In this tutorial, you have added two [new enumeration functions](/tutorials/nfts/js/enumeration#modifications-to-the-contract), and now you have a basic NFT smart contract with minting and enumeration methods in place. After implementing these modifications, you redeployed the smart contract and tested the functions using the CLI. - -In the [next tutorial](/tutorials/nfts/js/core), you'll implement the core functions needed to allow users to transfer the minted tokens. - -:::info Remember -If you want to see the finished code from this tutorial, you can checkout the `3.enumeration` branch. -::: - -:::note Versioning for this article - -At the time of this writing, this example works with the following versions: - -- near-cli: `3.0.0` -- NFT standard: [NEP171](https://github.com/near/NEPs/tree/master/neps/nep-0171.md), version `1.0.0` -- Enumeration standard: [NEP181](https://github.com/near/NEPs/tree/master/neps/nep-0181.md), version `1.0.0` - -::: diff --git a/docs/tutorials/nfts/js/4-core.md b/docs/tutorials/nfts/js/4-core.md deleted file mode 100644 index 274be8a9901..00000000000 --- a/docs/tutorials/nfts/js/4-core.md +++ /dev/null @@ -1,232 +0,0 @@ ---- -id: core -title: Core -sidebar_label: Core -description: "Learn how to implement the core standards into your smart contract." ---- -import {Github} from "@site/src/components/UI/Codetabs"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -In this tutorial you'll learn how to implement the [core standards](https://github.com/near/NEPs/tree/master/neps/nep-0171.md) into your smart contract. -If you're joining us for the first time, feel free to clone [this repo](https://github.com/near-examples/nft-tutorial) and checkout the `3.enumeration` branch to follow along. - - -```bash -git checkout 3.enumeration -``` - -:::tip -If you wish to see the finished code for this _Core_ tutorial, you can find it on the `4.core` branch. -::: - -## Introduction {#introduction} - -Up until this point, you've created a simple NFT smart contract that allows users to mint tokens and view information using the [enumeration standards](https://github.com/near/NEPs/tree/master/neps/nep-0181.md). Today, you'll expand your smart contract to allow for users to not only mint tokens, but transfer them as well. - -As we did in the [minting tutorial](/tutorials/nfts/js/minting), let's break down the problem into multiple subtasks to make our lives easier. When a token is minted, information is stored in 3 places: - -- **tokensPerOwner**: set of tokens for each account. -- **tokensById**: maps a token ID to a `Token` object. -- **tokenMetadataById**: maps a token ID to its metadata. - -Let's now consider the following scenario. If Benji owns token A and wants to transfer it to Mike as a birthday gift, what should happen? First of all, token A should be removed from Benji's set of tokens and added to Mike's set of tokens. - -If that's the only logic you implement, you'll run into some problems. If you were to do a `view` call to query for information about that token after it's been transferred to Mike, it would still say that Benji is the owner. - -This is because the contract is still mapping the token ID to the old `Token` object that contains the `owner_id` field set to Benji's account ID. You still have to change the `tokensById` data structure so that the token ID maps to a new `Token` object which has Mike as the owner. - -With that being said, the final process for when an owner transfers a token to a receiver should be the following: - -- Remove the token from the owner's set. -- Add the token to the receiver's set. -- Map a token ID to a new `Token` object containing the correct owner. - -:::note -You might be curious as to why we don't edit the `tokenMetadataById` field. This is because no matter who owns the token, the token ID will always map to the same metadata. The metadata should never change and so we can just leave it alone. -::: - -At this point, you're ready to move on and make the necessary modifications to your smart contract. - -## Modifications to the contract - -Let's start our journey in the `nft-contract/src/nft_core.ts` file. - -### Transfer function {#transfer-function} - -You'll start by implementing the `nft_transfer` logic. This function will transfer the specified `token_id` to the `receiver_id` with an optional `memo` such as `"Happy Birthday Mike!"`. The core logic will be found in the `internalNftTransfer` function. - - - -There are a couple things to notice here. Firstly, we've introduced a new function called `assertOneYocto()`. This method will ensure that the user has attached exactly one yoctoNEAR to the call. If a function requires a deposit, you need a full access key to sign that transaction. By adding the one yoctoNEAR deposit requirement, you're essentially forcing the user to sign the transaction with a full access key. - -Since the transfer function is potentially transferring very valuable assets, you'll want to make sure that whoever is calling the function has a full access key. - -Secondly, we've introduced an `internalTransfer` method. This will perform all the logic necessary to transfer an NFT. - -### Internal helper functions - -Let's quickly move over to the `nft-contract/src/internal.ts` file so that you can implement the `assertOneYocto()` and `internalTransfer` methods. - -Let's start with the easier one, `assertOneYocto()`. - -#### assertOneYocto - - - -#### internal_transfer - -It's now time to implement the `internalTransfer` function which is the core of this tutorial. This function will take the following parameters: - -- **senderId**: the account that's attempting to transfer the token. -- **receiverId**: the account that's receiving the token. -- **tokenId**: the token ID being transferred. -- **memo**: an optional memo to include. - -The first thing you'll want to do is to make sure that the sender is authorized to transfer the token. In this case, you'll just make sure that the sender is the owner of the token. You'll do that by getting the `Token` object using the `token_id` and making sure that the sender is equal to the token's `owner_id`. - -Second, you'll remove the token ID from the sender's list and add the token ID to the receiver's list of tokens. Finally, you'll create a new `Token` object with the receiver as the owner and remap the token ID to that newly created object. - - - -You've previously implemented functionality for adding a token ID to an owner's set but you haven't created the functionality for removing a token ID from an owner's set. Let's do that now by created a new function called `internalRemoveTokenFromOwner` which we'll place right above our `internalTransfer` and below the `internalAddTokenToOwner` function. - -In the remove function, you'll get the set of tokens for a given account ID and then remove the passed in token ID. If the account's set is empty after the removal, you'll simply remove the account from the `tokensPerOwner` data structure. - - - -With these internal functions complete, the logic for transferring NFTs is finished. It's now time to move on and implement `nft_transfer_call`, one of the most integral yet complicated functions of the standard. - -### Transfer call function {#transfer-call-function} - -Let's consider the following scenario. An account wants to transfer an NFT to a smart contract for performing a service. The traditional approach would be to use an approval management system, where the receiving contract is granted the ability to transfer the NFT to themselves after completion. You'll learn more about the approval management system in the [approvals section](/tutorials/nfts/js/approvals) of the tutorial series. - -This allowance workflow takes multiple transactions. If we introduce a β€œtransfer and call” workflow baked into a single transaction, the process can be greatly improved. - -For this reason, we have a function `nft_transfer_call` which will transfer an NFT to a receiver and also call a method on the receiver's contract all in the same transaction. - - - -The function will first assert that the caller attached exactly 1 yocto for security purposes. It will then transfer the NFT using `internalTransfer` and start the cross contract call. It will call the method `nft_on_transfer` on the `receiver_id`'s contract which returns a promise. After the promise finishes executing, the function `nft_resolve_transfer` is called. This is a very common workflow when dealing with cross contract calls. You first initiate the call and wait for it to finish executing. You then invoke a function that resolves the result of the promise and act accordingly. - -In our case, when calling `nft_on_transfer`, that function will return whether or not you should return the NFT to it's original owner in the form of a boolean. This is logic will be executed in the `internalResolveTransfer` function. - - - -If `nft_on_transfer` returned true, you should send the token back to it's original owner. On the contrary, if false was returned, no extra logic is needed. As for the return value of `nft_resolve_transfer`, the standard dictates that the function should return a boolean indicating whether or not the receiver successfully received the token or not. - -This means that if `nft_on_transfer` returned true, you should return false. This is because if the token is being returned to its original owner. The `receiver_id` didn't successfully receive the token in the end. On the contrary, if `nft_on_transfer` returned false, you should return true since we don't need to return the token and thus the `receiver_id` successfully owns the token. - -With that finished, you've now successfully added the necessary logic to allow users to transfer NFTs. It's now time to deploy and do some testing. - -## Redeploying the contract {#redeploying-contract} - -Using the build script, build and deploy the contract as you did in the previous tutorials: - -```bash -yarn build && near deploy --wasmFile build/nft.wasm --accountId $NFT_CONTRACT_ID -``` - -This should output a warning saying that the account has a deployed contract and will ask if you'd like to proceed. Simply type `y` and hit enter. - -``` -This account already has a deployed contract [ AKJK7sCysrWrFZ976YVBnm6yzmJuKLzdAyssfzK9yLsa ]. Do you want to proceed? (y/n) -``` - -:::tip -If you haven't completed the previous tutorials and are just following along with this one, simply create an account and login with your CLI using `near login`. You can then export an environment variable `export NFT_CONTRACT_ID=YOUR_ACCOUNT_ID_HERE`. -::: - -## Testing the new changes {#testing-changes} - -Now that you've deployed a patch fix to the contract, it's time to move onto testing. Using the previous NFT contract where you had minted a token to yourself, you can test the `nft_transfer` method. If you transfer the NFT, it should be removed from your account's collectibles displayed in the wallet. In addition, if you query any of the enumeration functions, it should show that you are no longer the owner. - -Let's test this out by transferring an NFT to the account `benjiman.testnet` and seeing if the NFT is no longer owned by you. - -### Testing the transfer function - -:::note -This means that the NFT won't be recoverable unless the account `benjiman.testnet` transfers it back to you. If you don't want your NFT lost, make a new account and transfer the token to that account instead. -::: - -If you run the following command, it will transfer the token `"token-1"` to the account `benjiman.testnet` with the memo `"Go Team :)"`. Take note that you're also attaching exactly 1 yoctoNEAR by using the `--depositYocto` flag. - -:::tip -If you used a different token ID in the previous tutorials, replace `token-1` with your token ID. -::: - - - - - ```bash - near call $NFT_CONTRACT_ID nft_transfer '{"receiver_id": "benjiman.testnet", "token_id": "token-1", "memo": "Go Team :)"}' --accountId $NFT_CONTRACT_ID --depositYocto 1 - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT_ID nft_transfer json-args '{"receiver_id": "benjiman.testnet", "token_id": "token-1", "memo": "Go Team :)"}' prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -If you now query for all the tokens owned by your account, that token should be missing. Similarly, if you query for the list of tokens owned by `benjiman.testnet`, that account should now own your NFT. - -### Testing the transfer call function - -Now that you've tested the `nft_transfer` function, it's time to test the `nft_transfer_call` function. If you try to transfer an NFT to a receiver that does **not** implement the `nft_on_transfer` function, the contract will panic and the NFT will **not** be transferred. Let's test this functionality below. - -First mint a new NFT that will be used to test the transfer call functionality. - - - - - ```bash - near call $NFT_CONTRACT_ID nft_mint '{"token_id": "token-2", "metadata": {"title": "NFT Tutorial Token", "description": "Testing the transfer call function", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$NFT_CONTRACT_ID'"}' --accountId $NFT_CONTRACT_ID --amount 0.1 - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT_ID nft_mint json-args '{"token_id": "token-2", "metadata": {"title": "NFT Tutorial Token", "description": "Testing the transfer call function", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -Now that you've minted the token, you can try to transfer the NFT to the account `no-contract.testnet` which as the name suggests, doesn't have a contract. This means that the receiver doesn't implement the `nft_on_transfer` function and the NFT should remain yours after the transaction is complete. - - - - - ```bash - near call $NFT_CONTRACT_ID nft_transfer_call '{"receiver_id": "no-contract.testnet", "token_id": "token-2", "msg": "foo"}' --accountId $NFT_CONTRACT_ID --depositYocto 1 --gas 200000000000000 - ``` - - - - - ```bash - near contract call-function as-transaction $NFT_CONTRACT_ID nft_transfer_call json-args '{"receiver_id": "no-contract.testnet", "token_id": "token-2", "msg": "foo"}' prepaid-gas '200.0 Tgas' attached-deposit '1 yoctoNEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -If you query for your tokens, you should still have `token-2` and at this point, you're finished! - -## Conclusion - -In this tutorial, you learned how to expand an NFT contract past the minting functionality and you added ways for users to transfer NFTs. You [broke down](#introduction) the problem into smaller, more digestible subtasks and took that information and implemented both the [NFT transfer](#transfer-function) and [NFT transfer call](#transfer-call-function) functions. In addition, you deployed another [patch fix](#redeploying-contract) to your smart contract and [tested](#testing-changes) the transfer functionality. - -In the [next tutorial](/tutorials/nfts/js/approvals), you'll learn about the approval management system and how you can approve others to transfer tokens on your behalf. - -:::note Versioning for this article - -At the time of this writing, this example works with the following versions: - -- near-cli: `3.0.0` -- NFT standard: [NEP171](https://github.com/near/NEPs/tree/master/neps/nep-0171.md), version `1.0.0` -- Enumeration standard: [NEP181](https://github.com/near/NEPs/tree/master/neps/nep-0181.md), version `1.0.0` - -::: diff --git a/docs/tutorials/nfts/js/5-approval.md b/docs/tutorials/nfts/js/5-approval.md deleted file mode 100644 index ea0d1e0235e..00000000000 --- a/docs/tutorials/nfts/js/5-approval.md +++ /dev/null @@ -1,550 +0,0 @@ ---- -id: approvals -title: Approvals -sidebar_label: Approvals -description: "Learn the basics of an approval management system for NFTs, allowing you to grant others access to transfer NFTs on your behalf." ---- -import {Github} from "@site/src/components/UI/Codetabs"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -In this tutorial you'll learn the basics of an approval management system which will allow you to grant others access to transfer NFTs on your behalf. This is the backbone of all NFT marketplaces and allows for some complex yet beautiful scenarios to happen. If you're joining us for the first time, feel free to clone [this repository](https://github.com/near-examples/nft-tutorial) and checkout the `4.core` branch to follow along. - - - - -```bash -git checkout 4.core -``` - -:::tip -If you wish to see the finished code for this _Approval_ tutorial, you can find it on the `5.approval` branch. -::: - -## Introduction - -Up until this point you've created a smart contract that allows users to mint and transfer NFTs as well as query for information using the [enumeration standard](https://github.com/near/NEPs/tree/master/neps/nep-0181.md). As we've been doing in the previous tutorials, let's break down the problem into smaller, more digestible, tasks. Let's first define some of the end goals that we want to accomplish as per the [approval management](https://github.com/near/NEPs/tree/master/neps/nep-0178.md) extension of the standard. We want a user to have the ability to: - -- Grant other accounts access to transfer their NFTs on a per token basis. -- Check if an account has access to a specific token. -- Revoke a specific account the ability to transfer an NFT. -- Revoke **all** other accounts the ability to transfer an NFT. - -If you look at all these goals, they are all on a per token basis. This is a strong indication that you should change the `Token` struct which keeps track of information for each token. - -## Allow an account to transfer your NFT - -Let's start by trying to accomplish the first goal. How can you grant another account access to transfer an NFT on your behalf? - -The simplest way that you can achieve this is to add a list of approved accounts to the `Token` struct. When transferring the NFT, if the caller is not the owner, you could check if they're in the list. - -Before transferring, you would need to clear the list of approved accounts since the new owner wouldn't expect the accounts approved by the original owner to still have access to transfer their new NFT. - -### The problem {#the-problem} - -On the surface, this would work, but if you start thinking about the edge cases, some problems arise. Often times when doing development, a common approach is to think about the easiest and most straightforward solution. Once you've figured it out, you can start to branch off and think about optimizations and edge cases. - -Let's consider the following scenario. Benji has an NFT and gives two separate marketplaces access to transfer his token. By doing so, he's putting the NFT for sale (more about that in the [marketplace integrations](#marketplace-integrations) section). Let's say he put the NFT for sale for 1 NEAR on both markets. The token's list of approved account IDs would look like the following: - -``` -Token: { - owner_id: Benji - approved_accounts_ids: [marketplace A, marketplace B] -} -``` - -Josh then comes along and purchases the NFT on marketplace A for 1 NEAR. This would take the sale down from the marketplace A and clear the list of approved accounts. Marketplace B, however, still has the token listed for sale for 1 NEAR and has no way of knowing that the token was purchased on marketplace A by Josh. The new token struct would look as follows: - -``` -Token: { - owner_id: Josh - approved_accounts_ids: [] -} -``` - -Let's say Josh is low on cash and wants to flip this NFT and put it for sale for 10 times the price on marketplace B. He goes to put it for sale and for whatever reason, the marketplace is built in a way that if you try to put a token up for sale twice, it keeps the old sale data. This would mean that from marketplace B's perspective, the token is still for sale for 1 NEAR (which was the price that Benji had originally listed it for). - -Since Josh approved the marketplace to try and put it for sale, the token struct would look as follows: - -``` -Token: { - owner_id: Josh - approved_accounts_ids: [marketplace A, marketplace B] -} -``` - -If Mike then comes along and purchases the NFT for only 1 NEAR on marketplace B, the marketplace would go to try and transfer the NFT and since technically, Josh approved the marketplace and it's in the list of approved accounts, the transaction would go through properly. - -### The solution {#the-solution} - -Now that we've identified a problem with the original solution, let's think about ways that we can fix it. What would happen now if, instead of just keeping track of a list of approved accounts, you introduced a specific ID that went along with each approved account. The new approved accounts would now be a map instead of a list. It would map an account to it's `approval id`. - -For this to work, you need to make sure that the approval ID is **always** a unique, new ID. If you set it as an integer that always increases by 1 whenever u approve an account, this should work. Let's consider the same scenario with the new solution. - -Benji puts his NFT for sale for 1 NEAR on marketplace A and marketplace B by approving both marketplaces. The "next approval ID" would start off at 0 when the NFT was first minted and will increase from there. This would result in the following token struct: - -``` -Token: { - owner_id: Benji - approved_accounts_ids: { - marketplace A: 0 - marketplace B: 1 - } - next_approval_id: 2 -} -``` - -When Benji approved marketplace A, it took the original value of `next_approval_id` which started off at 0. The marketplace was then inserted into the map and the next approval ID was incremented. This process happened again for marketplace B and the next approval ID was again incremented where it's now 2. - -Josh comes along and purchases the NFT on marketplace A for 1 NEAR. Notice how the next approval ID stayed at 2: - -``` -Token: { - owner_id: Josh - approved_accounts_ids: {} - next_approval_id: 2 -} -``` - -Josh then flips the NFT because he's once again low on cash and approves marketplace B: - -``` -Token: { - owner_id: Josh - approved_accounts_ids: { - marketplace B: 2 - } - next_approval_id: 3 -} -``` - -The marketplace is inserted into the map and the next approval ID is incremented. From marketplace B's perspective it stores it's original approval ID from when Benji put the NFT up for sale which has a value of 1. If Mike were to go and purchase the NFT on marketplace B for the original 1 NEAR sale price, the NFT contract should panic. This is because the marketplace is trying to transfer the NFT with an approval ID 1 but the token struct shows that it **should** have an approval ID of 2. - -### Expanding the `Token` and `JsonToken` structs - -Now that you understand the proposed solution to the original problem of allowing an account to transfer your NFT, it's time to implement some of the logic. The first thing you should do is modify the `Token` and `JsonToken` structs to reflect the new changes. Let's switch over to the `nft-contract/src/metadata.ts` file: - - - -You'll then need to initialize both the `approved_account_ids` and `next_approval_id` to their default values when a token is minted. Switch to the `nft-contract/src/mint.ts` file and when creating the `Token` struct to store in the contract, let's set the next approval ID to be 0 and the approved account IDs to be an empty object: - - - -### Approving accounts - -Now that you've added the support for approved account IDs and the next approval ID on the token level, it's time to add the logic for populating and changing those fields through a function called `nft_approve`. This function should approve an account to have access to a specific token ID. Let's move to the `nft-contract/src/approval.ts` file and edit the `internalNftApprove` function: - - - -The function will first assert that the user has attached **at least** one yoctoNEAR (which we'll implement soon). This is both for security and to cover storage. When someone approves an account ID, they're storing that information on the contract. As you saw in the [minting tutorial](/tutorials/nfts/js/minting), you can either have the smart contract account cover the storage, or you can have the users cover that cost. The latter is more scalable and it's the approach you'll be working with throughout this tutorial. - -After the assertion comes back with no problems, you get the token object and make sure that only the owner is calling this method. Only the owner should be able to allow other accounts to transfer their NFTs. You then get the next approval ID and insert the passed in account into the map with the next approval ID. If it's a new approval ID, storage must be paid. If it's not a new approval ID, no storage needs to be paid and only attaching 1 yoctoNEAR would be enough. - -You then calculate how much storage is being used by adding that new account to the map and increment the tokens `next_approval_id` by 1. After inserting the token object back into the `tokensById` map, you refund any excess storage. - -You'll notice that the function contains an optional `msg` parameter. This message is actually the foundation of all NFT marketplaces on NEAR. - -#### Marketplace Integrations {#marketplace-integrations} - -If a message was provided into the function, you're going to perform a cross contract call to the account being given access. This cross contract call will invoke the `nft_on_approve` function which will parse the message and act accordingly. Let's consider a general use case. - -We have a marketplace that expects it's sale conditions to be passed in through the message field. Benji approves the marketplace with the `nft_approve` function and passes in a stringified JSON to the message which will outline sale conditions. These sale conditions could look something like the following: - -```json -sale_conditions: { - price: 5 -} -``` - -By leaving the message field type as just a string, this generalizes the process and allows users to input sale conditions for many different marketplaces. It is up to the person approving to pass in an appropriate message that the marketplace can properly decode and use. This is usually done through the marketplace's frontend app which would know how to construct the `msg` in a useful way. - -#### Internal functions - -Now that the core logic for approving an account is finished, you need to implement the `assertAtLeastOneYocto` and `bytesForApprovedAccountId` functions. Move to the `nft-contract/src/internal.ts` file and copy the following function right below the `assertOneYocto` function. - - - -Next, you'll need to copy the logic for calculating how many bytes it costs to store an account ID. Place this function at the very top of the page: - - - -Now that the logic for approving accounts is finished, you need to change the restrictions for transferring. - -### Changing the restrictions for transferring NFTs - -Currently, an NFT can **only** be transferred by its owner. You need to change that restriction so that people that have been approved can also transfer NFTs. In addition, you'll make it so that if an approval ID is passed, you can increase the security and check if both the account trying to transfer is in the approved list **and** they correspond to the correct approval ID. This is to address the problem we ran into earlier. - -In the `internal.ts` file, you need to change the logic of the `internalTransfer` method as that's where the restrictions are being made. Change the internal transfer function to be the following: - - - -This will check if the sender isn't the owner and then if they're not, it will check if the sender is in the approval list. If an approval ID was passed into the function, it will check if the sender's actual approval ID stored on the contract matches the one passed in. - -#### Refunding storage on transfer - -While you're in the internal file, you're going to need to add methods for refunding users who have paid for storing approved accounts on the contract when an NFT is transferred. This is because you'll be clearing the `approved_account_ids` object whenever NFTs are transferred and so the storage is no longer being used. - - - -These will be useful in the next section where you'll be changing the `nft_core` functions to include the new approval logic. - -### Changes to `nft_core.ts` - -Head over to the `nft-contract/src/nft_core.ts` file and the first change that you'll want to make is to add an `approval_id` to the `internalTransfer` function. This is so that anyone trying to transfer the token that isn't the owner must pass in an approval ID to address the problem seen earlier. If they are the owner, the approval ID won't be used as we saw in the `internalTransfer` function. - - -For the `nft_transfer` function, the only change that you'll need to make is to pass in the approval ID into the `internalTransfer` function and then refund the previous tokens approved account IDs after the transfer is finished - - - -Next, you need to do the same to `nft_transfer_call` but instead of refunding immediately, you need to attach the previous token's approved account IDs to `nft_resolve_transfer` instead as there's still the possibility that the transfer gets reverted. - - - -You'll also need to add the tokens approved account IDs to the `JsonToken` being returned by `nft_token`. - - - -Finally, you need to add the logic for refunding the approved account IDs in `internalResolveTransfer`. If the transfer went through, you should refund the owner for the storage being released by resetting the tokens `approved_account_ids` field. If, however, you should revert the transfer, it wouldn't be enough to just not refund anybody. Since the receiver briefly owned the token, they could have added their own approved account IDs and so you should refund them if they did so. - - - -With that finished, it's time to move on and complete the next task. - -## Check if an account is approved - -Now that the core logic is in place for approving and refunding accounts, it should be smooth sailing from this point on. You now need to implement the logic for checking if an account has been approved. This should take an account and token ID as well as an optional approval ID. If no approval ID was provided, it should simply return whether or not the account is approved. - -If an approval ID was provided, it should return whether or not the account is approved and has the same approval ID as the one provided. Let's move to the `nft-contract/src/approval.ts` file and add the necessary logic to the `internalNftIsApproved` function. - - - -Let's now move on and add the logic for revoking an account - -## Revoke an account - -The next step in the tutorial is to allow a user to revoke a specific account from having access to their NFT. The first thing you'll want to do is assert one yocto for security purposes. You'll then need to make sure that the caller is the owner of the token. If those checks pass, you'll need to remove the passed in account from the tokens approved account IDs and refund the owner for the storage being released. - - - -## Revoke all accounts - -The final step in the tutorial is to allow a user to revoke all accounts from having access to their NFT. This should also assert one yocto for security purposes and make sure that the caller is the owner of the token. You then refund the owner for releasing all the accounts in the map and then clear the `approved_account_ids`. - - - -With that finished, it's time to deploy and start testing the contract. - -## Testing the new changes {#testing-changes} - -Since these changes affect all the other tokens and the state won't be able to automatically be inherited by the new code, simply redeploying the contract will lead to errors. For this reason, it's best practice to create a sub-account and deploy the contract there. - -### Creating a sub-account {#creating-sub-account} - -Run the following command to create a sub-account `approval` of your main account with an initial balance of 25 NEAR which will be transferred from the original to your new account. - -```bash -near create-account approval.$NFT_CONTRACT_ID --masterAccount $NFT_CONTRACT_ID --initialBalance 25 -``` - -Next, you'll want to export an environment variable for ease of development: - -```bash -export APPROVAL_NFT_CONTRACT_ID=approval.$NFT_CONTRACT_ID -``` - -Using the build script, build the deploy the contract as you did in the previous tutorials: - -```bash -yarn build && near deploy --wasmFile build/nft.wasm --accountId $APPROVAL_NFT_CONTRACT_ID -``` - -### Initialization and minting {#initialization-and-minting} - -Since this is a new contract, you'll need to initialize and mint a token. Use the following command to initialize the contract: - - - - - ```bash - near call $APPROVAL_NFT_CONTRACT_ID init '{"owner_id": "'$APPROVAL_NFT_CONTRACT_ID'"}' --accountId $APPROVAL_NFT_CONTRACT_ID - ``` - - - - - ```bash - near contract call-function as-transaction $APPROVAL_NFT_CONTRACT_ID init json-args '{"owner_id": "'$APPROVAL_NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as $APPROVAL_NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -Next, you'll need to mint a token. By running this command, you'll mint a token with a token ID `"approval-token"` and the receiver will be your new account. - - - - - ```bash - near call $APPROVAL_NFT_CONTRACT_ID nft_mint '{"token_id": "approval-token", "metadata": {"title": "Approval Token", "description": "testing out the new approval extension of the standard", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$APPROVAL_NFT_CONTRACT_ID'"}' --accountId $APPROVAL_NFT_CONTRACT_ID --amount 0.1 - ``` - - - - - ```bash - near contract call-function as-transaction $APPROVAL_NFT_CONTRACT_ID nft_mint json-args '{"token_id": "approval-token", "metadata": {"title": "Approval Token", "description": "testing out the new approval extension of the standard", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$APPROVAL_NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $APPROVAL_NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -You can check to see if everything went through properly by calling one of the enumeration functions: - - - - - ```bash - near view $APPROVAL_NFT_CONTRACT_ID nft_tokens_for_owner '{"account_id": "'$APPROVAL_NFT_CONTRACT_ID'", "limit": 10}' - ``` - - - - - ```bash - near contract call-function as-read-only $APPROVAL_NFT_CONTRACT_ID nft_tokens_for_owner json-args '{"account_id": "'$APPROVAL_NFT_CONTRACT_ID'", "limit": 10}' network-config testnet now - ``` - - - -This should return an output similar to the following: - -```json -[ - { - "token_id": "approval-token", - "owner_id": "approval.goteam.examples.testnet", - "metadata": { - "title": "Approval Token", - "description": "testing out the new approval extension of the standard", - "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif", - }, - "approved_account_ids": {} - } -] -``` - -Notice how the approved account IDs are now being returned from the function? This is a great sign! You're now ready to move on and approve an account to have access to your token. - -### Approving an account {#approving-an-account} - -At this point, you should have two accounts. One stored under `$NFT_CONTRACT_ID` and the other under the `$APPROVAL_NFT_CONTRACT_ID` environment variable. You can use both of these accounts to test things out. If you approve your old account, it should have the ability to transfer the NFT to itself. - -Execute the following command to approve the account stored under `$NFT_CONTRACT_ID` to have access to transfer your NFT with an ID `"approval-token"`. You don't need to pass a message since the old account didn't implement the `nft_on_approve` function. In addition, you'll need to attach enough NEAR to cover the cost of storing the account on the contract. 0.1 NEAR should be more than enough and you'll be refunded any excess that is unused. - - - - - ```bash - near call $APPROVAL_NFT_CONTRACT_ID nft_approve '{"token_id": "approval-token", "account_id": "'$NFT_CONTRACT_ID'"}' --accountId $APPROVAL_NFT_CONTRACT_ID --deposit 0.1 - ``` - - - - - ```bash - near contract call-function as-transaction $APPROVAL_NFT_CONTRACT_ID nft_approve json-args '{"token_id": "approval-token", "account_id": "'$NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $APPROVAL_NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -If you call the same enumeration method as before, you should see the new approved account ID being returned. - - - - - ```bash - near view $APPROVAL_NFT_CONTRACT_ID nft_tokens_for_owner '{"account_id": "'$APPROVAL_NFT_CONTRACT_ID'", "limit": 10}' - ``` - - - - - ```bash - near contract call-function as-read-only $APPROVAL_NFT_CONTRACT_ID nft_tokens_for_owner json-args '{"account_id": "'$APPROVAL_NFT_CONTRACT_ID'", "limit": 10}' network-config testnet now - ``` - - - -This should return an output similar to the following: - -```json -[ - { - "token_id": "approval-token", - "owner_id": "approval.goteam.examples.testnet", - "metadata": { - "title": "Approval Token", - "description": "testing out the new approval extension of the standard", - "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif" - }, - "approved_account_ids": { "goteam.examples.testnet": 0 } - } -] -``` - -### Transferring an NFT as an approved account {#transferring-the-nft} - -Now that you've approved another account to transfer the token, you can test that behavior. You should be able to use the other account to transfer the NFT to itself by which the approved account IDs should be reset. Let's test transferring the NFT with the wrong approval ID: - - - - - ```bash - near call $APPROVAL_NFT_CONTRACT_ID nft_transfer '{"receiver_id": "'$NFT_CONTRACT_ID'", "token_id": "approval-token", "approval_id": 1}' --accountId $NFT_CONTRACT_ID --depositYocto 1 - ``` - - - - - ```bash - near contract call-function as-transaction $APPROVAL_NFT_CONTRACT_ID nft_transfer json-args '{"receiver_id": "'$NFT_CONTRACT_ID'", "token_id": "approval-token", "approval_id": 1}' prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -
-Example response: -

- -```bash -kind: { - ExecutionError: "Smart contract panicked: panicked at 'assertion failed: `(left == right)`\n" + - ' left: `0`,\n' + - " right: `1`: The actual approval_id 0 is different from the given approval_id 1', src/internal.ts:165:17" - }, -``` - -

-
- -If you pass the correct approval ID which is `0`, everything should work fine. - - - - - ```bash - near call $APPROVAL_NFT_CONTRACT_ID nft_transfer '{"receiver_id": "'$NFT_CONTRACT_ID'", "token_id": "approval-token", "approval_id": 0}' --accountId $NFT_CONTRACT_ID --depositYocto 1 - ``` - - - - - ```bash - near contract call-function as-transaction $APPROVAL_NFT_CONTRACT_ID nft_transfer json-args '{"receiver_id": "'$NFT_CONTRACT_ID'", "token_id": "approval-token", "approval_id": 0}' prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -If you again call the enumeration method, you should see the owner updated and the approved account IDs reset. - -```json -[ - { - "token_id": "approval-token", - "owner_id": "goteam.examples.testnet", - "metadata": { - "title": "Approval Token", - "description": "testing out the new approval extension of the standard", - "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif" - }, - "approved_account_ids": {} - } -] -``` - -Let's now test the approval ID incrementing across different owners. If you approve the sub-account that originally minted the token, the approval ID should be 1 now. - - - - - ```bash - near call $APPROVAL_NFT_CONTRACT_ID nft_approve '{"token_id": "approval-token", "account_id": "'$APPROVAL_NFT_CONTRACT_ID'"}' --accountId $NFT_CONTRACT_ID --deposit 0.1 - ``` - - - - - ```bash - near contract call-function as-transaction $APPROVAL_NFT_CONTRACT_ID nft_approve json-args '{"token_id": "approval-token", "account_id": "'$APPROVAL_NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -Calling the view function again show now return an approval ID of 1 for the sub-account that was approved. - - - - ```bash - near view $APPROVAL_NFT_CONTRACT_ID nft_tokens_for_owner '{"account_id": "'$NFT_CONTRACT_ID'", "limit": 10}' - ``` - - - - - ```bash - near contract call-function as-read-only $APPROVAL_NFT_CONTRACT_ID nft_tokens_for_owner json-args '{"account_id": "'$NFT_CONTRACT_ID'", "limit": 10}' network-config testnet now - ``` - - - -
-Example response: -

- -```json -[ - { - "token_id": "approval-token", - "owner_id": "goteam.examples.testnet", - "metadata": { - "title": "Approval Token", - "description": "testing out the new approval extension of the standard", - "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif" - }, - "approved_account_ids": { "approval.goteam.examples.testnet": 1 } - } -] -``` - -

-
- -With the testing finished, you've successfully implemented the approvals extension to the standard! - -## Conclusion - -Today you went through a lot of logic to implement the [approvals extension](https://github.com/near/NEPs/tree/master/neps/nep-0178.md) so let's break down exactly what you did. - -First, you explored the [basic approach](#allow-an-account-to-transfer-your-nft) of how to solve the problem. You then went through and discovered some of the [problems](/tutorials/nfts/js/approvals#the-problem) with that solution and learned how to [fix it](#the-solution). - -After understanding what you should do to implement the approvals extension, you started to [modify](#expanding-the-token-and-jsontoken-structs) the JsonToken and Token structs in the contract. You then implemented the logic for [approving accounts](#approving-accounts) and saw how [marketplaces](#marketplace-integrations) are integrated. - -After implementing the logic behind approving accounts, you went and [changed the restrictions](#changing-the-restrictions-for-transferring-nfts) needed to transfer NFTs. The last step you did to finalize the approving logic was to go back and edit the [nft_core](#changes-to-nft_corets) files to be compatible with the new changes. - -At this point, everything was implemented in order to allow accounts to be approved and you extended the functionality of the [core standard](https://github.com/near/NEPs/tree/master/neps/nep-0171.md) to allow for approved accounts to transfer tokens. - -You implemented a view method to [check](#check-if-an-account-is-approved) if an account is approved and to finish the coding portion of the tutorial, you implemented the logic necessary to [revoke an account](#revoke-an-account) as well as [revoke all accounts](#revoke-all-accounts). - -After this, the contract code was finished and it was time to move onto testing where you created a [subaccount](#creating-sub-account) and tested the [approving](/tutorials/nfts/js/approvals#approving-an-account) and [transferring](#transferring-the-nft) for your NFTs. - -In the next tutorial, you'll learn about the royalty standards and how you can interact with NFT marketplaces. - -:::note Versioning for this article - -At the time of this writing, this example works with the following versions: - -- near-cli: `3.0.0` -- NFT standard: [NEP171](https://github.com/near/NEPs/tree/master/neps/nep-0171.md), version `1.0.0` -- Enumeration standard: [NEP181](https://github.com/near/NEPs/tree/master/neps/nep-0181.md), version `1.0.0` -- Approval standard: [NEP178](https://github.com/near/NEPs/tree/master/neps/nep-0178.md), version `1.0.0` - -::: diff --git a/docs/tutorials/nfts/js/6-royalty.md b/docs/tutorials/nfts/js/6-royalty.md deleted file mode 100644 index cfb901f5d5c..00000000000 --- a/docs/tutorials/nfts/js/6-royalty.md +++ /dev/null @@ -1,284 +0,0 @@ ---- -id: royalty -title: Royalty -sidebar_label: Royalty -description: "Learn how to add perpetual royalties to NFTs so creators earn from every sale." ---- -import {Github} from "@site/src/components/UI/Codetabs"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -In this tutorial you'll continue building your non-fungible token (NFT) smart contract, and learn how to implement perpetual royalties into your NFTs. This will allow people to get a percentage of the purchase price when an NFT is sold. - - - -## Introduction - -By now, you should have a fully fledged NFT contract, except for the royalties support. -To get started, either switch to the `5.approval` branch from our [GitHub repository](https://github.com/near-examples/nft-tutorial-js/), or continue your work from the previous tutorials. - -```bash -git checkout 5.approval -``` - -:::tip -If you wish to see the finished code for this _Royalty_ tutorial, you can find it on the `6.royalty` branch. -::: - -## Thinking about the problem - -In order to implement the functionality, you first need to understand how NFTs are sold. In the previous tutorial, you saw how someone with an NFT could list it on a marketplace using the `nft_approve` function by passing in a message that could be properly decoded. When a user purchases your NFT on the marketplace, what happens? - -Using the knowledge you have now, a reasonable conclusion would be to say that the marketplace transfers the NFT to the buyer by performing a cross-contract call and invokes the NFT contract's `nft_transfer` method. Once that function finishes, the marketplace would pay the seller for the correct amount that the buyer paid. - -Let's now think about how this can be expanded to allow for a cut of the pay going to other accounts that aren't just the seller. - -### Expanding the current solution - -Since perpetual royalties will be on a per-token basis, it's safe to assume that you should be changing the `Token` and `JsonToken` structs. You need some way of keeping track of what percentage each account with a royalty should have. If you introduce a map of an account to an integer, that should do the trick. - -Now, you need some way to relay that information to the marketplace. This method should be able to transfer the NFT exactly like the old solution but with the added benefit of telling the marketplace exactly what accounts should be paid what amounts. If you implement a method that transfers the NFT and then calculates exactly what accounts get paid and to what amount based on a passed-in balance, that should work nicely. - -This is what the [royalty standards](https://github.com/near/NEPs/blob/master/neps/nep-0199.md) outlined. Let's now move on and modify our smart contract to introduce this behavior. - -## Modifications to the contract - -The first thing you'll want to do is add the royalty information to the structs. Open the `nft-contract/src/metadata.ts` file and add `royalty` to the `Token` and `JsonToken` structs: - -```js -royalty: { [accountId: string]: number }; -``` - -Second, you'll want to add `royalty` to the `JsonToken` struct as well: - - - -### Internal helper function - -**royaltyToPayout** - -To simplify the payout calculation, let's add a helper `royaltyToPayout` function to `src/internal.ts`. This will convert a percentage to the actual amount that should be paid. In order to allow for percentages less than 1%, you can give 100% a value of `10,000`. This means that the minimum percentage you can give out is 0.01%, or `1`. For example, if you wanted the account `benji.testnet` to have a perpetual royalty of 20%, you would insert the pair `"benji.testnet": 2000` into the payout map. - - - -If you were to use the `royaltyToPayout` function and pass in `2000` as the `royaltyPercentage` and an `amountToPay` of 1 NEAR, it would return a value of 0.2 NEAR. - -### Royalties - -**nft_payout** - -Let's now implement a method to check what accounts will be paid out for an NFT given an amount, or balance. Open the `nft-contract/src/royalty.ts` file, and modify the `internalNftPayout` function as shown. - - - -This function will loop through the token's royalty map and take the balance and convert that to a payout using the `royaltyToPayout` function you created earlier. It will give the owner of the token whatever is left from the total royalties. As an example: - -You have a token with the following royalty field: - -```js -Token { - owner_id: "damian", - royalty: { - "benji": 1000, - "josh": 500, - "mike": 2000 - } -} -``` - -If a user were to call `nft_payout` on the token and pass in a balance of 1 NEAR, it would loop through the token's royalty field and insert the following into the payout object: - -```js -Payout { - payout: { - "benji": 0.1 NEAR, - "josh": 0.05 NEAR, - "mike": 0.2 NEAR - } -} -``` - -At the very end, it will insert `damian` into the payout object and give him `1 NEAR - 0.1 - 0.05 - 0.2 = 0.65 NEAR`. - -**nft_transfer_payout** - -Now that you know how payouts are calculated, it's time to create the function that will transfer the NFT and return the payout to the marketplace. - - - -### Perpetual royalties - -To add support for perpetual royalties, let's edit the `src/mint.ts` file. First, add an optional parameter for perpetual royalties. This is what will determine what percentage goes to which accounts when the NFT is purchased. You will also need to create and insert the royalty to be put in the `Token` object: - - - -### Adding royalty object to struct implementations - -Since you've added a new field to your `Token` and `JsonToken` structs, you need to edit your implementations accordingly. Move to the `nft-contract/src/internal.ts` file and edit the part of your `internalTransfer` function that creates the new `Token` object: - - - -Once that's finished, move to the `nft-contract/src/nft_core.ts` file. You need to edit your implementation of `internalNftToken` so that the `JsonToken` sends back the new royalty information. - - - -Next, you can use the CLI to query the new `nft_payout` function and validate that it works correctly. - -## Deploying the contract {#redeploying-contract} - -As you saw in the previous tutorial, adding changes like these will cause problems when redeploying. Since these changes affect all the other tokens and the state won't be able to automatically be inherited by the new code, simply redeploying the contract will lead to errors. For this reason, you'll create a new sub-account again. - -### Creating a sub-account - -Run the following command to create a sub-account `royalty` of your main account with an initial balance of 25 NEAR which will be transferred from the original to your new account. - -```bash -near create-account royalty.$NFT_CONTRACT_ID --masterAccount $NFT_CONTRACT_ID --initialBalance 25 -``` - -Next, you'll want to export an environment variable for ease of development: - -```bash -export ROYALTY_NFT_CONTRACT_ID=royalty.$NFT_CONTRACT_ID -``` - -Using the build script, build the deploy the contract as you did in the previous tutorials: - -```bash -yarn build && near deploy --wasmFile build/nft.wasm --accountId $ROYALTY_NFT_CONTRACT_ID -``` - -### Initialization and minting {#initialization-and-minting} - -Since this is a new contract, you'll need to initialize and mint a token. Use the following command to initialize the contract: - - - - - ```bash - near call $ROYALTY_NFT_CONTRACT_ID init '{"owner_id": "'$ROYALTY_NFT_CONTRACT_ID'"}' --accountId $ROYALTY_NFT_CONTRACT_ID - ``` - - - - - ```bash - near contract call-function as-transaction $ROYALTY_NFT_CONTRACT_ID init json-args '{"owner_id": "'$ROYALTY_NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as $ROYALTY_NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -Next, you'll need to mint a token. By running this command, you'll mint a token with a token ID `"royalty-token"` and the receiver will be your new account. In addition, you're passing in a map with two accounts that will get perpetual royalties whenever your token is sold. - - - - - ```bash - near call $ROYALTY_NFT_CONTRACT_ID nft_mint '{"token_id": "approval-token", "metadata": {"title": "Approval Token", "description": "testing out the new approval extension of the standard", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$ROYALTY_NFT_CONTRACT_ID'", "perpetual_royalties": {"benjiman.testnet": 2000, "mike.testnet": 1000, "josh.testnet": 500}}' --accountId $ROYALTY_NFT_CONTRACT_ID --amount 0.1 - ``` - - - - - ```bash - near contract call-function as-transaction $ROYALTY_NFT_CONTRACT_ID nft_mint json-args '{"token_id": "approval-token", "metadata": {"title": "Approval Token", "description": "testing out the new approval extension of the standard", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$ROYALTY_NFT_CONTRACT_ID'", "perpetual_royalties": {"benjiman.testnet": 2000, "mike.testnet": 1000, "josh.testnet": 500}}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $ROYALTY_NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -You can check to see if everything went through properly by calling one of the enumeration functions: - - - - - ```bash - near view $ROYALTY_NFT_CONTRACT_ID nft_tokens_for_owner '{"account_id": "'$ROYALTY_NFT_CONTRACT_ID'", "limit": 10}' - ``` - - - - - ```bash - near contract call-function as-read-only $ROYALTY_NFT_CONTRACT_ID nft_tokens_for_owner json-args '{"account_id": "'$ROYALTY_NFT_CONTRACT_ID'", "limit": 10}' network-config testnet now - ``` - - - -This should return an output similar to the following: - -```json -[ - { - "token_id": "approval-token", - "owner_id": "royalty.goteam.examples.testnet", - "metadata": { - "title": "Approval Token", - "description": "testing out the new approval extension of the standard", - "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif" - }, - "approved_account_ids": {}, - "royalty": { - "josh.testnet": 500, - "benjiman.testnet": 2000, - "mike.testnet": 1000 - } - } -] -``` - -Notice how there's now a royalty field that contains the 3 accounts that will get a combined 35% of all sales of this NFT? Looks like it works! Go team :) - -### NFT payout - -Let's calculate the payout for the `"approval-token"` NFT, given a balance of 100 yoctoNEAR. It's important to note that the balance being passed into the `nft_payout` function is expected to be in yoctoNEAR. - - - - - ```bash - near view $ROYALTY_NFT_CONTRACT_ID nft_payout '{"token_id": "approval-token", "balance": "100", "max_len_payout": 100}' - ``` - - - - - ```bash - near contract call-function as-read-only $ROYALTY_NFT_CONTRACT_ID nft_payout json-args '{"token_id": "approval-token", "balance": "100", "max_len_payout": 100}' network-config testnet now - ``` - - - -This command should return an output similar to the following: - -```bash -{ - payout: { - 'josh.testnet': '5', - 'royalty.goteam.examples.testnet': '65', - 'mike.testnet': '10', - 'benjiman.testnet': '20' - } -} -``` - -If the NFT was sold for 100 yoctoNEAR, josh would get 5, benji would get 20, mike would get 10, and the owner, in this case `royalty.goteam.examples.testnet` would get the rest: 65. - -## Conclusion - -At this point you have everything you need for a fully functioning NFT contract to interact with marketplaces. -The last remaining standard that you could implement is the events standard. This allows indexers to know what functions are being called and makes it easier and more reliable to keep track of information that can be used to populate the collectibles tab in the wallet for example. - -:::info remember -If you want to see the finished code from this tutorial, you can checkout the `6.royalty` branch. -::: - -:::note Versioning for this article - -At the time of this writing, this example works with the following versions: - -- near-cli: `3.0.0` -- NFT standard: [NEP171](https://github.com/near/NEPs/tree/master/neps/nep-0171.md), version `1.0.0` -- Enumeration standard: [NEP181](https://github.com/near/NEPs/tree/master/neps/nep-0181.md), version `1.0.0` -- Royalties standard: [NEP199](https://github.com/near/NEPs/tree/master/neps/nep-0171.md/Payout), version `2.0.0` - -::: diff --git a/docs/tutorials/nfts/js/7-events.md b/docs/tutorials/nfts/js/7-events.md deleted file mode 100644 index 6aaee5b2cff..00000000000 --- a/docs/tutorials/nfts/js/7-events.md +++ /dev/null @@ -1,322 +0,0 @@ ---- -id: events -title: Events -sidebar_label: Events -description: "Learn about the events standard and how to implement it in your smart contract." ---- -import {Github} from "@site/src/components/UI/Codetabs"; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -In this tutorial, you'll learn about the [events standard](https://github.com/near/NEPs/blob/master/neps/nep-0256.md) and how to implement it in your smart contract. - - - -## Introduction - -To get started, either switch to the `6.royalty` branch from our [GitHub repository](https://github.com/near-examples/nft-tutorial/), or continue your work from the previous tutorials. - -```bash -git checkout 6.royalty -``` - -:::tip -If you wish to see the finished code for this _Events_ tutorial, you can find it on the `7.events` branch. -::: - -## Understanding the use case {#understanding-the-use-case} - -Have you ever wondered how the wallet knows which NFTs you own and how it can display them in the [collectibles tab](https://testnet.mynearwallet.com//?tab=collectibles)? Originally, an indexer used to listen for any functions calls starting with `nft_` on your account. These contracts were then flagged on your account as likely NFT contracts. - -When you navigated to your collectibles tab, the wallet would then query all those contracts for the list of NFTs you owned using the `nft_tokens_for_owner` function you saw in the [enumeration tutorial](/tutorials/nfts/js/enumeration). - -### The problem {#the-problem} - -This method of flagging contracts was not reliable as each NFT-driven application might have its own way of minting or transferring NFTs. In addition, it's common for apps to transfer or mint many tokens at a time using batch functions. - -### The solution {#the-solution} - -A standard was introduced so that smart contracts could emit an event anytime NFTs were transferred, minted, or burnt. This event was in the form of a log. No matter how a contract implemented the functionality, an indexer could now listen for those standardized logs. - -As per the standard, you need to implement a logging functionality that gets fired when NFTs are transferred or minted. In this case, the contract doesn't support burning so you don't need to worry about that for now. - -It's important to note the standard dictates that the log should begin with `"EVENT_JSON:"`. The structure of your log should, however, always contain the 3 following things: - -- **standard**: the current name of the standard (e.g. nep171) -- **version**: the version of the standard you're using (e.g. 1.0.0) -- **event**: a list of events you're emitting. - -The event interface differs based on whether you're recording transfers or mints. The interface for both events is outlined below. - -**Transfer events**: -- *Optional* - **authorized_id**: the account approved to transfer on behalf of the owner. -- **old_owner_id**: the old owner of the NFT. -- **new_owner_id**: the new owner that the NFT is being transferred to. -- **token_ids**: a list of NFTs being transferred. -- *Optional* - **memo**: an optional message to include with the event. - -**Minting events**: -- **owner_id**: the owner that the NFT is being minted to. -- **token_ids**: a list of NFTs being transferred. -- *Optional* - **memo**: an optional message to include with the event. - -### Examples {#examples} - -In order to solidify your understanding of the standard, let's walk through three scenarios and see what the logs should look like. - -#### Scenario A - simple mint - -In this scenario, Benji wants to mint an NFT to Mike with a token ID `"team-token"` and he doesn't include a message. The log should look as follows. - -```js -EVENT_JSON:{ - "standard": "nep171", - "version": "1.0.0", - "event": "nft_mint", - "data": [ - {"owner_id": "mike.testnet", "token_ids": ["team-token"]} - ] -} -``` - -#### Scenario B - batch mint - -In this scenario, Benji wants to perform a batch mint. He will mint an NFT to Mike, Damian, Josh, and Dorian. Dorian, however, will get two NFTs. Each token ID will be `"team-token"` followed by an incrementing number. The log is as follows. - - -```js -EVENT_JSON:{ - "standard": "nep171", - "version": "1.0.0", - "event": "nft_mint", - "data": [ - {"owner_id": "mike.testnet", "token_ids": ["team-token0"]}, - {"owner_id": "damian.testnet", "token_ids": ["team-token1"]}, - {"owner_id": "josh.testnet", "token_ids": ["team-token2"]} - {"owner_id": "dorian.testnet", "token_ids": ["team-token3", "team-token4"]}, - ] -} -``` - -#### Scenario C - transfer NFTs - -In this scenario, Mike is transferring both his team tokens to Josh. The log should look as follows. - -```js -EVENT_JSON:{ - "standard": "nep171", - "version": "1.0.0", - "event": "nft_transfer", - "data": [ - {"old_owner_id": "mike.testnet", "new_owner_id": "josh.testnet", "token_ids": ["team-token", "team-token0"], "memo": "Go Team!"} - ] -} -``` - -## Modifications to the contract {#modifications-to-the-contract} - -At this point, you should have a good understanding of what the end goal should be so let's get to work! - -### Logging minted tokens {#logging-minted-tokens} - -Since the contract will only be minting tokens in one place, it's trivial where you should place the log. Open the `nft-contract/src/mint.ts` file and navigate to the bottom of the file. This is where you'll construct the log for minting. Anytime someone successfully mints an NFT, it will now correctly emit a log. - -```js -// Construct the mint log as per the events standard. -let nftMintLog = { - // Standard name ("nep171"). - standard: NFT_STANDARD_NAME, - // Version of the standard ("nft-1.0.0"). - version: NFT_METADATA_SPEC, - // The data related with the event stored in a vector. - event: "nft_mint", - data: [ - { - // Owner of the token. - owner_id: token.owner_id, - // Vector of token IDs that were minted. - token_ids: [tokenId], - } - ] -} - -// Log the json. -near.log(`EVENT_JSON:${JSON.stringify(nftMintLog)}`); -``` - - - -### Logging transfers {#logging-transfers} - -Let's open the `nft-contract/src/internal.ts` file and navigate to the `internalTransfer` function. This is the location where you'll build your transfer logs. Whenever an NFT is transferred, this function is called and so you'll correctly be logging the transfers. - -```js -// Construct the transfer log as per the events standard. -let nftTransferLog = { - // Standard name ("nep171"). - standard: NFT_STANDARD_NAME, - // Version of the standard ("nft-1.0.0"). - version: NFT_METADATA_SPEC, - // The data related with the event stored in a vector. - event: "nft_transfer", - data: [ - { - // The optional authorized account ID to transfer the token on behalf of the old owner. - authorized_id: authorizedId, - // The old owner's account ID. - old_owner_id: token.owner_id, - // The account ID of the new owner of the token. - new_owner_id: receiverId, - // A vector containing the token IDs as strings. - token_ids: [tokenId], - // An optional memo to include. - memo, - } - ] -} - -// Log the serialized json. -near.log(`EVENT_JSON:${JSON.stringify(nftTransferLog)}`); -``` - - -This solution, unfortunately, has an edge case which will break things. If an NFT is transferred via the `nft_transfer_call` function, there's a chance that the transfer will be reverted if the `nft_on_transfer` function returns `true`. Taking a look at the logic for `nft_transfer_call`, you can see why this is a problem. - -When `nft_transfer_call` is invoked, it will: -- Call `internalTransfer` to perform the actual transfer logic. -- Initiate a cross-contract call and invoke the `nft_on_transfer` function. -- Resolve the promise and perform logic in `internalResolveTransfer`. - - This will either return true meaning the transfer went fine or it will revert the transfer and return false. - -If you only place the log in the `internalTransfer` function, the log will be emitted and the indexer will think that the NFT was transferred. If the transfer is reverted during `internalResolveTransfer`, however, that event should **also** be emitted. Anywhere that an NFT **could** be transferred, we should add logs. Replace the `internalResolveTransfer` with the following code. - - - -With that finished, you've successfully implemented the events standard and it's time to start testing. - -## Deploying the contract {#redeploying-contract} - -For the purpose of readability and ease of development, instead of redeploying the contract to the same account, let's create a sub-account and deploy to that instead. You could have deployed to the same account as none of the changes you implemented in this tutorial would have caused errors. - -### Creating a sub-account - -Run the following command to create a sub-account `events` of your main account with an initial balance of 25 NEAR which will be transferred from the original to your new account. - -```bash -near create-account events.$NFT_CONTRACT_ID --masterAccount $NFT_CONTRACT_ID --initialBalance 25 -``` - -Next, you'll want to export an environment variable for ease of development: - -```bash -export EVENTS_NFT_CONTRACT_ID=events.$NFT_CONTRACT_ID -``` - -Using the build script, build the deploy the contract as you did in the previous tutorials: - -```bash -yarn build && near deploy --wasmFile build/nft.wasm --accountId $EVENTS_NFT_CONTRACT_ID -``` - -### Initialization and minting {#initialization-and-minting} - -Since this is a new contract, you'll need to initialize and mint a token. Use the following command to initialize the contract: - - - - - ```bash - near call $EVENTS_NFT_CONTRACT_ID init '{"owner_id": "'$EVENTS_NFT_CONTRACT_ID'"}' --accountId $EVENTS_NFT_CONTRACT_ID - ``` - - - - - ```bash - near contract call-function as-transaction $EVENTS_NFT_CONTRACT_ID init json-args '{"owner_id": "'$EVENTS_NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as $EVENTS_NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -Next, you'll need to mint a token. By running this command, you'll mint a token with a token ID `"events-token"` and the receiver will be your new account. In addition, you're passing in a map with two accounts that will get perpetual royalties whenever your token is sold. - - - - - ```bash - near call $EVENTS_NFT_CONTRACT_ID nft_mint '{"token_id": "events-token", "metadata": {"title": "Events Token", "description": "testing out the new events extension of the standard", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$EVENTS_NFT_CONTRACT_ID'"}' --accountId $EVENTS_NFT_CONTRACT_ID --amount 0.1 - ``` - - - - - ```bash - near contract call-function as-transaction $EVENTS_NFT_CONTRACT_ID nft_mint json-args '{"token_id": "events-token", "metadata": {"title": "Events Token", "description": "testing out the new events extension of the standard", "media": "https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif"}, "receiver_id": "'$EVENTS_NFT_CONTRACT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0.1 NEAR' sign-as $EVENTS_NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -You can check to see if everything went through properly by looking at the output in your CLI: - -```bash -Doing account.functionCall() -Receipts: F4oxNfv54cqwUwLUJ7h74H1iE66Y3H7QDfZMmGENwSxd, BJxKNFRuLDdbhbGeLA3UBSbL8UicU7oqHsWGink5WX7S - Log [events.goteam.examples.testnet]: EVENT_JSON:{"standard":"nep171","version":"1.0.0","event":"nft_mint","data":[{"owner_id":"events.goteam.examples.testnet","token_ids":["events-token"]}]} -Transaction Id 4Wy2KQVTuAWQHw5jXcRAbrz7bNyZBoiPEvLcGougciyk -To see the transaction in the transaction explorer, please open this url in your browser -https://testnet.nearblocks.io/txns/4Wy2KQVTuAWQHw5jXcRAbrz7bNyZBoiPEvLcGougciyk -'' -``` - -You can see that the event was properly logged! - -### Transferring {#transferring} - -You can now test if your transfer log works as expected by sending `benjiman.testnet` your NFT. - - - - - ```bash - near call $EVENTS_NFT_CONTRACT_ID nft_transfer '{"receiver_id": "benjiman.testnet", "token_id": "events-token", "memo": "Go Team :)", "approval_id": 0}' --accountId $EVENTS_NFT_CONTRACT_ID --depositYocto 1 - ``` - - - - - ```bash - near contract call-function as-transaction $EVENTS_NFT_CONTRACT_ID nft_transfer json-args '{"receiver_id": "benjiman.testnet", "token_id": "events-token", "memo": "Go Team :)", "approval_id": 0}' prepaid-gas '100.0 Tgas' attached-deposit '1 yoctoNEAR' sign-as $EVENTS_NFT_CONTRACT_ID network-config testnet sign-with-keychain send - ``` - - - -This should return an output similar to the following: - -```bash -Doing account.functionCall() -Receipts: EoqBxrpv9Dgb8KqK4FdeREawVVLWepEUR15KPNuZ4fGD, HZ4xQpbgc8EfU3PiV72LvfXb2f3dVC1n9aVTbQds9zfR - Log [events.goteam.examples.testnet]: Memo: Go Team :) - Log [events.goteam.examples.testnet]: EVENT_JSON:{"standard":"nep171","version":"1.0.0","event":"nft_transfer","data":[{"authorized_id":"events.goteam.examples.testnet","old_owner_id":"events.goteam.examples.testnet","new_owner_id":"benjiman.testnet","token_ids":["events-token"],"memo":"Go Team :)"}]} -Transaction Id 4S1VrepKzA6HxvPj3cK12vaT7Dt4vxJRWESA1ym1xdvH -To see the transaction in the transaction explorer, please open this url in your browser -https://testnet.nearblocks.io/txns/4S1VrepKzA6HxvPj3cK12vaT7Dt4vxJRWESA1ym1xdvH -'' -``` - -Hurray! At this point, your NFT contract is fully complete and the events standard has been implemented. - -## Conclusion - -Today you went through the [events standard](https://github.com/near/NEPs/blob/master/neps/nep-0256.md) and implemented the necessary logic in your smart contract. You created events for [minting](#logging-minted-tokens) and [transferring](#logging-transfers) NFTs. You then deployed and [tested](#initialization-and-minting) your changes by minting and transferring NFTs. - -In the next tutorial, you'll look at the basics of a marketplace contract and how it was built. - -:::note Versioning for this article - -At the time of this writing, this example works with the following versions: - -- near-cli: `3.0.0` -- NFT standard: [NEP171](https://github.com/near/NEPs/tree/master/neps/nep-0171.md), version `1.0.0` -- Events standard: [NEP297 extension](https://github.com/near/NEPs/blob/master/neps/nep-0256.md), version `1.0.0` - -::: diff --git a/docs/tutorials/nfts/js/8-marketplace.md b/docs/tutorials/nfts/js/8-marketplace.md deleted file mode 100644 index 41d90c1c3bb..00000000000 --- a/docs/tutorials/nfts/js/8-marketplace.md +++ /dev/null @@ -1,202 +0,0 @@ ---- -id: marketplace -title: Marketplace -sidebar_label: Marketplace -description: "Learn how to build an NFT marketplace on NEAR to list, buy, and sell NFTs." ---- -import {Github} from "@site/src/components/UI/Codetabs" - -In this tutorial, you'll learn the basics of an NFT marketplace contract where you can buy and sell non-fungible tokens for $NEAR. In the previous tutorials, you went through and created a fully fledged NFT contract that incorporates all the standards found in the [NFT standard](https://github.com/near/NEPs/tree/master/neps/nep-0171.md). - - - -## Introduction - -Throughout this tutorial, you'll learn how a marketplace contract could work on NEAR. This is meant to be an example and there is no canonical implementation. Feel free to branch off and modify this contract to meet your specific needs. - -Using the same repository as the previous tutorials, if you checkout the `8.marketplace` branch, you should have the necessary files to complete the tutorial. - -```bash -git checkout 8.marketplace -``` - -## File structure {#file-structure} - -``` -market-contract -└── src - β”œβ”€β”€ internal.ts - β”œβ”€β”€ index.ts - β”œβ”€β”€ nft_callbacks.ts - β”œβ”€β”€ sale.ts - └── sale_views.ts -``` - -Usually, when doing work on multiple smart contracts that all pertain to the same repository, it's a good idea to structure them in their own folders as done in this tutorial. To make your work easier when building the smart contracts, we've also modified the repository's `package.json` file so that building both smart contracts can be easily done by running the following command. - -```bash -yarn build -``` -This will install the dependencies for both contracts and compile them to `wasm` files that are stored in the following directory. - -``` -nft-tutorial-js -└── build - β”œβ”€β”€ nft.wasm - └── market.wasm -``` - -## Understanding the contract - -At first, the contract can be quite overwhelming but if you strip away all the fluff and dig into the core functionalities, it's actually quite simple. This contract was designed for only one thing - to allow people to buy and sell NFTs for NEAR. This includes the support for paying royalties, updating the price of your sales, removing sales and paying for storage. - -Let's go through the files and take note of some of the important functions and what they do. - -## index.ts {#index-ts} - -This file outlines what information is stored on the contract as well as some other crucial functions that you'll learn about below. - -### Constructor logic {#constructor-logic} - -The first function you'll look at is the constructor function. This takes an `owner_id` as the only parameter and will default all the storage collections to their default values. - - - -### Storage management model {#storage-management-model} - -Next, let's talk about the storage management model chosen for this contract. On the NFT contract, users attached $NEAR to the calls that needed storage paid for. For example, if someone was minting an NFT, they would need to attach `x` amount of NEAR to cover the cost of storing the data on the contract. - -On this marketplace contract, however, the storage model is a bit different. Users will need to deposit $NEAR onto the marketplace to cover the storage costs. Whenever someone puts an NFT for sale, the marketplace needs to store that information which costs $NEAR. Users can either deposit as much NEAR as they want so that they never have to worry about storage again or they can deposit the minimum amount to cover 1 sale on an as-needed basis. - -You might be thinking about the scenario when a sale is purchased. What happens to the storage that is now being released on the contract? This is why we've introduced a storage withdrawal function. This allows users to withdraw any excess storage that is not being used. Let's go through some scenarios to understand the logic. The required storage for 1 sale is 0.01 NEAR on the marketplace contract. - -**Scenario A** - -- Benji wants to list his NFT on the marketplace but has never paid for storage. -- He deposits exactly 0.01 NEAR using the `storage_deposit` method. This will cover 1 sale. -- He lists his NFT on the marketplace and is now using up 1 out of his prepaid 1 sales and has no more storage left. If he were to call `storage_withdraw`, nothing would happen. -- Dorian loves his NFT and quickly purchases it before anybody else can. This means that Benji's sale has now been taken down (since it was purchased) and Benji is using up 0 out of his prepaid 1 sales. In other words, he has an excess of 1 sale or 0.01 NEAR. -- Benji can now call `storage_withdraw` and will be transferred his 0.01 NEAR back. On the contract's side, after withdrawing, he will have 0 sales paid for and will need to deposit storage before trying to list anymore NFTs. - -**Scenario B** - -- Dorian owns one hundred beautiful NFTs and knows that he wants to list all of them. -- To avoid having to call `storage_deposit` everytime he wants to list an NFT, he calls it once. Since Dorian is a baller, he attaches 10 NEAR which is enough to cover 1000 sales. He now has an excess of 9 NEAR or 900 sales. -- Dorian needs the 9 NEAR for something else but doesn't want to take down his 100 listings. Since he has an excess of 9 NEAR, he can easily withdraw and still have his 100 listings. After calling `storage_withdraw` and being transferred 9 NEAR, he will have an excess of 0 sales. - -With this behavior in mind, the following two functions outline the logic. - - - -In this contract, the storage required for each sale is 0.01 NEAR but you can query that information using the `storage_minimum_balance` function. In addition, if you wanted to check how much storage a given account has paid, you can query the `storage_balance_of` function. - -With that out of the way, it's time to move onto the `nft_callbacks.ts` file where you'll look at how NFTs are put for sale. - -## nft_callbacks.ts {#nft_callbacks-ts} - -This file is responsible for the logic used to put NFTs for sale. If you remember from the [marketplaces section](/tutorials/nfts/js/approvals#marketplace-integrations) of the approvals tutorial, when users call `nft_approve` and pass in a message, it will perform a cross-contract call to the `receiver_id`'s contract and call the method `nft_on_approve`. This `nft_callbacks.ts` file will implement that function. - -### Listing logic {#listing-logic} - -The market contract is expecting the message that the user passes into `nft_approve` on the NFT contract to be JSON stringified sale arguments. This outlines the sale price in yoctoNEAR for the NFT that is listed. - -The `nft_on_approve` function is called via a cross-contract call by the NFT contract. It will make sure that the signer has enough storage to cover adding another sale. It will then attempt to get the sale conditions from the message and create the listing. - - - -## sale.ts {#sale-ts} - -Now that you're familiar with the process of both adding storage and listing NFTs on the marketplace, let's go through what you can do once a sale has been listed. The `sale.ts` file outlines the functions for updating the price, removing, and purchasing NFTs. - -### Sale object {#sale-object} - -It's important to understand what information the contract is storing for each sale object. Since the marketplace has many NFTs listed that come from different NFT contracts, simply storing the token ID would not be enough to distinguish between different NFTs. This is why you need to keep track of both the token ID and the contract by which the NFT came from. In addition, for each listing, the contract must keep track of the approval ID it was given to transfer the NFT. Finally, the owner and sale conditions are needed. - - - -### Removing sales {#removing-sales} - -In order to remove a listing, the owner must call the `remove_sale` function and pass the NFT contract and token ID. Behind the scenes, this calls the `internallyRemoveSale` function which you can find in the `internal.ts` file. This will assert one yoctoNEAR for security reasons. - - - -### Updating price {#updating-price} - -In order to update the list price of a token, the owner must call the `update_price` function and pass in the contract, token ID, and desired price. This will get the sale object, change the sale conditions, and insert it back. For security reasons, this function will assert one yoctoNEAR. - - - -### Purchasing NFTs {#purchasing-nfts} - -For purchasing NFTs, you must call the `offer` function. It takes an `nft_contract_id` and `token_id` as parameters. You must attach the correct amount of NEAR to the call in order to purchase. Behind the scenes, this will make sure your deposit is greater than the list price and call a private method `processPurchase` which will perform a cross-contract call to the NFT contract to invoke the `nft_transfer_payout` function. This will transfer the NFT using the [approval management](https://github.com/near/NEPs/tree/master/neps/nep-0178.md) standard that you learned about and it will return the `Payout` object which includes royalties. - -The marketplace will then call `resolve_purchase` where it will check for malicious payout objects and then if everything went well, it will pay the correct accounts. - - - -## sale_view.ts {#sale_view-ts} - -The final file we'll go through is the `sale_view.ts` file. This is where some of the enumeration methods are outlined. It allows users to query for important information regarding sales. - -### Total supply {#total-supply} - -To query for the total supply of NFTs listed on the marketplace, you can call the `get_supply_sales` function. An example can be seen below. - -```bash -near view $MARKETPLACE_CONTRACT_ID get_supply_sales -``` - -### Total supply by owner {#total-supply-by-owner} - -To query for the total supply of NFTs listed by a specific owner on the marketplace, you can call the `get_supply_by_owner_id` function. An example can be seen below. - -```bash -near view $MARKETPLACE_CONTRACT_ID get_supply_by_owner_id '{"account_id": "benji.testnet"}' -``` - -### Total supply by contract {#total-supply-by-contract} - -To query for the total supply of NFTs that belong to a specific contract, you can call the `get_supply_by_nft_contract_id` function. An example can be seen below. - -```bash -near view $MARKETPLACE_CONTRACT_ID get_supply_by_nft_contract_id '{"nft_contract_id": "fayyr-nft.testnet"}' -``` - -### Query for listing information {#query-listing-information} - -To query for important information for a specific listing, you can call the `get_sale` function. This requires that you pass in the `nft_contract_token`. This is essentially the unique identifier for sales on the market contract as explained earlier. It consists of the NFT contract followed by a `DELIMITER` followed by the token ID. In this contract, the `DELIMITER` is simply a period: `.`. An example of this query can be seen below. - -```bash -near view $MARKETPLACE_CONTRACT_ID get_sale '{"nft_contract_token": "fayyr-nft.testnet.token-42"}' -``` - -In addition, you can query for paginated information about the listings for a given owner by calling the `get_sales_by_owner_id` function. - -```bash -near view $MARKETPLACE_CONTRACT_ID get_sales_by_owner_id '{"account_id": "benji.testnet", "from_index": "5", "limit": 10}' -``` - -Finally, you can query for paginated information about the listings that originate from a given NFT contract by calling the `get_sales_by_nft_contract_id` function. - -```bash -near view $MARKETPLACE_CONTRACT_ID get_sales_by_nft_contract_id '{"nft_contract_id": "fayyr-nft.testnet, "from_index": "5", "limit": 10}' -``` - -## Conclusion - -In this tutorial, you learned about the basics of a marketplace contract and how it works. You went through the [index.ts](#index-ts) file and learned about the initialization function in addition to the [storage management](#storage-management-model) model. - -You then went through the [nft_callbacks](#nft_callbacks-ts) file to understand how to [list NFTs](#listing-logic). In addition, you went through some important functions needed for after you've listed an NFT. This includes [removing sales](#removing-sales), [updating the price](#updating-price), and [purchasing NFTs](#purchasing-nfts). - -Finally, you went through the enumeration methods found in the [`sale_view`](#sale_view-ts) file. These allow you to query for important information found on the marketplace contract. - -You should now have a solid understanding of NFTs and marketplaces on NEAR. Feel free to branch off and expand on these contracts to create whatever cool applications you'd like. The world is your oyster! Thanks for joining on this journey and don't forget, **Go Team!** - -:::note Versioning for this article - -At the time of this writing, this example works with the following versions: - -- near-cli: `3.0.0` -- NFT standard: [NEP171](https://github.com/near/NEPs/tree/master/neps/nep-0171.md), version `1.0.0` - -::: diff --git a/website/sidebars.js b/website/sidebars.js index c010638b103..643630baff8 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -300,62 +300,9 @@ const sidebar = { 'tutorials/examples/advanced-xcc', 'tutorials/examples/global-contracts', 'tutorials/examples/update-contract-migrate-state', - { - "Build a FT Contract from Scratch": [ - 'tutorials/fts/introduction', - 'tutorials/fts/predeployed-contract', - 'tutorials/fts/skeleton', - 'tutorials/fts/defining-a-token', - 'tutorials/fts/circulating-supply', - 'tutorials/fts/registering-accounts', - 'tutorials/fts/transfers', - 'tutorials/fts/marketplace', - ] - }, - { - "Build a NFT Contract from Scratch": [ - 'tutorials/nfts/introduction', - { - Basic: [ - 'tutorials/nfts/predeployed-contract', - 'tutorials/nfts/skeleton', - 'tutorials/nfts/minting', - 'tutorials/nfts/upgrade-contract', - 'tutorials/nfts/enumeration', - 'tutorials/nfts/core', - ], - }, - 'tutorials/nfts/events', - { - Marketplace: ['tutorials/nfts/approvals', 'tutorials/nfts/marketplace'], - }, - 'tutorials/nfts/royalty', - 'tutorials/nfts/series', - ] - }, - { - "Build a NFT Contract from Scratch (JS)": [ - 'tutorials/nfts/js/introduction', - { - Basic: [ - 'tutorials/nfts/js/predeployed-contract', - 'tutorials/nfts/js/skeleton', - 'tutorials/nfts/js/minting', - 'tutorials/nfts/js/upgrade-contract', - 'tutorials/nfts/js/enumeration', - 'tutorials/nfts/js/core', - ], - }, - 'tutorials/nfts/js/events', - { - Marketplace: [ - 'tutorials/nfts/js/approvals', - 'tutorials/nfts/js/marketplace' - ], - }, - 'tutorials/nfts/js/royalty', - ] - }, + 'tutorials/fts', + 'tutorials/nfts', + 'tutorials/nfts-js', ] }, {