Non-Fungible Tokens (NFTs)
This page lists common examples of working with NFTs.
Accepting NFT ownership assignment
Notification message of assigned NFT ownership has the following structure:
message(0x05138d91) NFTOwnershipAssigned { queryId: Int as uint64; previousOwner:Address; forwardPayload:Slice as remaining;}
Use receiver function to accept notification message.
Validation can be done in two ways:
- Directly store the NFT item address and validate against it.
import "@stdlib/deploy";
message(0x05138d91) NFTOwnershipAssigned { queryId: Int as uint64; previousOwner:Address; forwardPayload:Slice as remaining;}
contract SingleNft with Deployable { nftItemAddress: Address;
init(nftItemAddress: Address) { self.nftItemAddress = nftItemAddress; }
receive(msg: NFTOwnershipAssigned) { require(self.nftItemAddress == sender(), "NFT contract is not the sender");
// your logic of processing nft ownership assign notification }}
- Use
StateInit
and derived address of the NFT item.
import "@stdlib/deploy";
message(0x05138d91) NFTOwnershipAssigned { queryId: Int as uint64; previousOwner:Address; forwardPayload:Slice as remaining;}
struct NFTItemInitData { index: Int as uint64; collectionAddress: Address;}
inline fun calculateNFTAddress(index: Int, collectionAddress: Address, nftCode: Cell): Address { let initData = NFTItemInitData{ index, collectionAddress, };
return contractAddress(StateInit{code: nftCode, data: initData.toCell()});}
contract NftInCollection with Deployable { nftCollectionAddress: Address; nftItemIndex: Int as uint64; nftCode: Cell;
init(nftCollectionAddress: Address, nftItemIndex: Int, nftCode: Cell) { self.nftCollectionAddress = nftCollectionAddress; self.nftItemIndex = nftItemIndex; self.nftCode = nftCode; }
receive(msg: NFTOwnershipAssigned) { let expectedNftAddress = calculateNFTAddress(self.nftItemIndex, self.nftCollectionAddress, self.nftCode); // or you can even store expectedNftAddress require(expectedNftAddress == sender(), "NFT contract is not the sender");
// your logic of processing nft ownership assign notification }}
Since the initial data layout of the NFT item can vary, the first approach is often more suitable.
Transferring NFT item
To send NFT item transfer use send()
function.
import "@stdlib/deploy";
message(0x5fcc3d14) NFTTransfer { queryId: Int as uint64; newOwner: Address; // address of the new owner of the NFT item. responseDestination: Address; // address where to send a response with confirmation of a successful transfer and the rest of the incoming message coins. customPayload: Cell? = null; // optional custom data. In most cases should be null forwardAmount: Int as coins; // the amount of nanotons to be sent to the new owner. forwardPayload: Slice as remaining; // optional custom data that should be sent to the new owner.}
contract Example with Deployable { nftItemAddress: Address;
init(nftItemAddress: Address) { self.nftItemAddress = nftItemAddress; }
// ... add more code from previous examples ...
receive("transfer") { send(SendParameters{ to: self.nftItemAddress, value: ton("0.1"), body: NFTTransfer{ queryId: 42, // FIXME: Change this according to your needs newOwner: sender(), responseDestination: myAddress(), customPayload: null, forwardAmount: 1, forwardPayload: rawSlice("F"), // precomputed beginCell().storeUint(0xF, 4).endCell().beginParse() }.toCell(), }); }}
Get NFT static info
Note, that TON Blockchain does not allow contracts to call each other getters. In order to receive data from another contract, you must exchange messages.
import "@stdlib/deploy";
message(0x2fcb26a2) NFTGetStaticData { queryId: Int as uint64;}
message(0x8b771735) NFTReportStaticData { queryId: Int as uint64; index: Int as uint256; collection: Address;}
struct NFTItemInitData { index: Int as uint64; collectionAddress: Address;}
inline fun calculateNFTAddress(index: Int, collectionAddress: Address, nftCode: Cell): Address { let initData = NFTItemInitData{ index, collectionAddress, };
return contractAddress(StateInit{code: nftCode, data: initData.toCell()});}
contract Example with Deployable { nftCollectionAddress: Address; nftItemIndex: Int as uint64; nftCode: Cell;
init(nftCollectionAddress: Address, nftItemIndex: Int, nftCode: Cell) { self.nftCollectionAddress = nftCollectionAddress; self.nftItemIndex = nftItemIndex; self.nftCode = nftCode; }
// ... add more code from previous examples ...
receive("get static data") { // FIXME: Put proper address("[NFT_ADDRESS]") here let nftAddress = sender(); send(SendParameters{ to: nftAddress, value: ton("0.1"), body: NFTGetStaticData{ queryId: 42, }.toCell(), }); }
receive(msg: NFTReportStaticData) { let expectedNftAddress = calculateNFTAddress(msg.index, msg.collection, self.nftCode); require(expectedNftAddress == sender(), "NFT contract is not the sender");
// Save nft static data or do something }}
Get NFT royalty params
NFT royalty params are described here.
import "@stdlib/deploy";
message(0x693d3950) NFTGetRoyaltyParams { queryId: Int as uint64;}
message(0xa8cb00ad) NFTReportRoyaltyParams { queryId: Int as uint64; numerator: Int as uint16; denominator: Int as uint16; destination: Address;}
contract Example with Deployable { nftCollectionAddress: Address;
init(nftCollectionAddress: Address) { self.nftCollectionAddress = nftCollectionAddress; }
// ... add more code from previous examples ...
receive("get royalty params") { send(SendParameters{ to: self.nftCollectionAddress, value: ton("0.1"), body: NFTGetRoyaltyParams{ queryId: 42, }.toCell(), }); }
receive(msg: NFTReportRoyaltyParams) { require(self.nftCollectionAddress == sender(), "NFT collection contract is not the sender");
// Do something with msg }}
NFT Collection methods
Note that only NFT owners are allowed to use these methods.
Deploy NFT
import "@stdlib/deploy";
message(0x1) NFTDeploy { queryId: Int as uint64; itemIndex: Int as uint64; amount: Int as coins; // amount to sent when deploying nft nftContent: Cell;}
contract Example with Deployable { nftCollectionAddress: Address;
init(nftCollectionAddress: Address) { self.nftCollectionAddress = nftCollectionAddress; }
// ... add more code from previous examples ...
receive("deploy") { send(SendParameters{ to: self.nftCollectionAddress, value: ton("0.14"), body: NFTDeploy{ queryId: 42, itemIndex: 42, amount: ton("0.1"), nftContent: beginCell().endCell() // FIXME: Should be your content, mostly generated offchain }.toCell(), }); }}
Change owner
import "@stdlib/deploy";
message(0x3) NFTChangeOwner { queryId: Int as uint64; newOwner: Address;}
contract Example with Deployable { nftCollectionAddress: Address;
init(nftCollectionAddress: Address) { self.nftCollectionAddress = nftCollectionAddress; }
// ... add more code from previous examples ...
receive("change owner") { send(SendParameters{ to: self.nftCollectionAddress, value: ton("0.05"), body: NFTChangeOwner{ queryId: 42, // FIXME: Put proper address("NEW_OWNER_ADDRESS") here newOwner: sender(), }.toCell(), }); }}
Onchain metadata creation
NFT Collection
/// https://github.com/ton-blockchain/TEPs/blob/master/text/0064-token-data-standard.md#nft-metadata-attributesfun composeCollectionMetadata( name: String, // full name description: String, // text description of the NFT image: String, // link to the image // There could be other data, see: // https://github.com/ton-blockchain/TEPs/blob/master/text/0064-token-data-standard.md#nft-metadata-attributes): Cell { let dict: map<Int as uint256, Cell> = emptyMap(); dict.set(sha256("name"), name.asMetadataCell()); dict.set(sha256("description"), description.asMetadataCell()); dict.set(sha256("image"), image.asMetadataCell());
return beginCell() .storeUint(0, 8) // a null byte prefix .storeMaybeRef(dict.asCell()!!) // 1 as a single bit, then a reference .endCell();}
// Taking flight!fun poorMansLaunchPad() { let collectionMetadata = composeCollectionMetadata( "Best Collection", "A very descriptive description describing the collection descriptively", "...link to ipfs or somewhere trusted...", );}
// Prefixes the String with a single null byte and converts it to a Cell// The null byte prefix is used to express metadata in various standards, like NFT or Jettoninline extends fun asMetadataCell(self: String): Cell { return beginTailString().concat(self).toCell();}
NFT Item
/// https://github.com/ton-blockchain/TEPs/blob/master/text/0064-token-data-standard.md#nft-metadata-attributesfun composeItemMetadata( name: String, // full name description: String, // text description of the NFT image: String, // link to the image // There could be other data, see: // https://github.com/ton-blockchain/TEPs/blob/master/text/0064-token-data-standard.md#nft-metadata-attributes): Cell { let dict: map<Int as uint256, Cell> = emptyMap(); dict.set(sha256("name"), name.asMetadataCell()); dict.set(sha256("description"), description.asMetadataCell()); dict.set(sha256("image"), image.asMetadataCell());
return beginCell() .storeUint(0, 8) // a null byte prefix .storeMaybeRef(dict.asCell()!!) // 1 as a single bit, then a reference .endCell();}
// Taking flight!fun poorMansLaunchPad() { let itemMetadata = composeItemMetadata( "Best Item", "A very descriptive description describing the item descriptively", "...link to ipfs or somewhere trusted...", );}
// Prefixes the String with a single null byte and converts it to a Cell// The null byte prefix is used to express metadata in various standards, like NFT or Jettoninline extends fun asMetadataCell(self: String): Cell { return beginTailString().concat(self).toCell();}