Nhảy đến nội dung chính

Integrating FT Payments into an NFT Marketplace

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.


Giới thiệu

Thông qua hướng dẫn này, bạn sẽ học cách một marketplace contract sẽ làm việc trên NEAR. Đây được xem là một ví dụ và không có triển khai chính tắc nào. Vui lòng tách branch và sửa đổi contract này để đáp ứng nhu cầu cụ thể của bạn.

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.


Cấu trúc file

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:

cd market-contract && cargo near build && 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

Hiểu về 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.

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 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

Function đầu tiên bạn sẽ xem là 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.


Model quản lý storage

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 muốn đưa NFT lên marketplace nhưng anh ấy chưa bao giờ trả tiền cho storage.
  • Anh ấy nạp chính xác 0.01 NEAR sử dụng method storage_deposit. Nó sẽ thanh toán cho 1 lần bán.
  • Anh ấy đưa NFT lên marketplace và bây giờ đang sử dụng hết 1 trong số 1 lần sale đã được thanh toán trước và không còn storage. Nếu anh ấy call storage_withdraw, sẽ không có gì xảy ra.
  • Dorian thích NFT của anh ấy và nhanh chóng mua nó trước bất cứ ai. Điều này có nghĩa rằng đơn hàng của Benji bây giờ đã bị gỡ xuống (kể từ khi nó được mua) và Benji đã sử dụng 0 trong số 1 lần sale đã thanh toán trước. Nói cách khác, anh ấy đang thừa 1 lần sale hay 0.01 NEAR.
  • Benji bây giờ có thể call storage_withdraw và sẽ được chuyển lại 0.01 NEAR cho anh ấy. Về phía contract, sau khi rút tiền, anh ấy sẽ có 0 lần sale được thanh toán sẽ cần phải nạp tiền storage trước khi niêm yết thêm NFT.

Scenario B

  • Dorian sở hữu một trăm NFT rất đẹp và anh ta muốn niêm yết toàn bộ.
  • Để tránh phải gọi storage_deposit mỗi khi muốn niêm yết một NFT, anh ấy sẽ gọi nó một lần. Vì Dorian là một người có tiền, anh đã đã đính kèm 10 NEAR đủ để thanh toán cho 1000 lần sale. Bây giờ anh ấy thừa 9 NEAR hay 900 lần sale.
  • Dorian cần 9 NEAR để để làm gì đó nhưng anh ấy không muốn gỡ 100 NFT đang niêm yết. Bởi vì anh ấy có thừa 9 NEAR, anh ấy có thể dễ dàng rút và vẫn có 100 NFT đang niêm yết. Sau khi call storage_withdraw và được chuyển 9 NEAR, anh ấy có 0 lần sale đang thừa.

With this behavior in mind, the following two functions outline the logic.

Trong contract này, storage yêu cầu 0.01 NEAR cho mỗi lần sale nhưng bạn có thể truy vấn thông tin đó sử dụng function storage_minimum_balance. Ngoài ra, bạn có thể truy vấn function storage_balance_of để kiểm tra một tài khoản nào đó đã thanh toán bao nhiêu storage.


FT Deposits

If you want to learn more about how NFTs are put for sale, check out the NFT zero to hero tutorial. 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.


Mua các NFT

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.

Để mua các NFT, bạn phải call function offer. 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 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.

cd market-contract && cargo near build && cd ..

To deploy the FT contract and export an environment variable, run the following command:

export FT_CONTRACT=<new-ft-account-id>
near create-account $FT_CONTRACT --useFaucet
cd 5.transfers/ && cargo near deploy $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.

export NFT_CONTRACT=<new-nft-account-id>
near create-account $NFT_CONTRACT --useFaucet
near deploy $NFT_CONTRACT out/nft-contract.wasm
export MARKETPLACE_CONTRACT=<new-marketplace-account-id>
near create-account $MARKETPLACE_CONTRACT --useFaucet
cd market-contract/ && cargo near deploy $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.

echo $FT_CONTRACT && echo $MARKETPLACE_CONTRACT && echo $NFT_CONTRACT

An example output is:

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).

near call $NFT_CONTRACT new_default_meta '{"owner_id": "'$NFT_CONTRACT'"}' --accountId $NFT_CONTRACT

Let's check if each contract was initialized correctly. You can do this by checking the metadata of the FT and NFT contracts:

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.

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.

near create-account buyer.$FT_CONTRACT --masterAccount $FT_CONTRACT --initialBalance 2 && export BUYER_ID=buyer.$FT_CONTRACT
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.

echo $BUYER_ID && echo $SELLER_ID

This should return something similar to:

buyer.ft-contract.testnet
seller.ft-contract.testnet

The next thing you'll want to do is mint a token to the seller.

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

Now you'll need to place the token for sale. This requires paying for storage as well as calling the nft_approve function.

near call $MARKETPLACE_CONTRACT storage_deposit --accountId $SELLER_ID --amount 0.1

In this case, we'll place the token for sale for 10 gtNEAR.

near call $NFT_CONTRACT nft_approve '{"token_id": "market-token", "account_id": "'$MARKETPLACE_CONTRACT'", "msg": "{\"sale_conditions\":\"10000000000000000000000000\"}"}' --accountId $SELLER_ID --amount 0.1

If you now query for the supply of sales again on the marketplace, it should be 1.

near view $MARKETPLACE_CONTRACT get_supply_sales

In addition, if you query for the sales by the owner ID, it should reflect the 10 gtNEAR price.

near view $MARKETPLACE_CONTRACT get_sales_by_owner_id '{"account_id": "'$SELLER_ID'"}'

Expected output:

[
{
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.

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

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.

near call $FT_CONTRACT ft_transfer '{"receiver_id": "'$BUYER_ID'", "amount": "50000000000000000000000000", "memo": "Go Team!"}' --accountId $FT_CONTRACT --depositYocto 1

You'll now need to deposit those tokens into the marketplace contract.

near call $FT_CONTRACT ft_transfer_call '{"receiver_id": "'$MARKETPLACE_CONTRACT'", "amount": "50000000000000000000000000", "msg": "Wooooooo!"}' --accountId $BUYER_ID --depositYocto 1 --gas 200000000000000

If you now query for your balance on the marketplace contract, it should be 50 gtNEAR.

near view $MARKETPLACE_CONTRACT ft_deposits_of '{"account_id": "'$BUYER_ID'"}'

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.

near call $FT_CONTRACT storage_deposit '{"account_id": "'$SELLER_ID'"}' --accountId $FT_CONTRACT --amount 0.1

Now let's make an offer!

near call $MARKETPLACE_CONTRACT offer '{"nft_contract_id": "'$NFT_CONTRACT'", "token_id": "market-token", "amount": "20000000000000000000000000"}' --accountId $BUYER_ID --depositYocto 1 --gas 300000000000000

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.

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.

near view $FT_CONTRACT ft_balance_of '{"account_id": "'$SELLER_ID'"}'

Next, let's check if the buyer has 30 gtNEAR left to withdraw.

near view $MARKETPLACE_CONTRACT ft_deposits_of '{"account_id": "'$BUYER_ID'"}'

Finally, let's check if the NFT is now owned by the buyer.

near view $NFT_CONTRACT nft_token '{"token_id": "market-token"}'

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.

near call $MARKETPLACE_CONTRACT ft_withdraw '{"amount": "30000000000000000000000000"}' --accountId $BUYER_ID --depositYocto 1 --gas 300000000000000

If you now query for the buyer's balance, it should be 30 gtNEAR.

near view $FT_CONTRACT ft_balance_of '{"account_id": "'$BUYER_ID'"}'

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 🚀


Tổng kết

Trong hướng dẫn này, bạn đã học về những thứ cơ bản của một marketplace contract và nó làm việc như thế nào. 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!


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.6.1
  • near-cli: 4.0.13
Was this page helpful?