From b84a013e7a016cc5d4196a2bba2a5e34a473b469 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:02:17 +0100 Subject: [PATCH 1/9] feat(appkit): how to work with nfts --- docs.json | 3 +- ecosystem/appkit/jettons.mdx | 15 +- ecosystem/appkit/nfts.mdx | 482 +++++++++++++++++++++++++++++++ ecosystem/appkit/overview.mdx | 7 + ecosystem/walletkit/web/nfts.mdx | 2 +- 5 files changed, 506 insertions(+), 3 deletions(-) create mode 100644 ecosystem/appkit/nfts.mdx diff --git a/docs.json b/docs.json index 7bea52056..3aa91b2b1 100644 --- a/docs.json +++ b/docs.json @@ -141,7 +141,8 @@ "ecosystem/appkit/overview", "ecosystem/appkit/init", "ecosystem/appkit/toncoin", - "ecosystem/appkit/jettons" + "ecosystem/appkit/jettons", + "ecosystem/appkit/nfts" ] }, { diff --git a/ecosystem/appkit/jettons.mdx b/ecosystem/appkit/jettons.mdx index 654ca9ca1..bdb3dd63f 100644 --- a/ecosystem/appkit/jettons.mdx +++ b/ecosystem/appkit/jettons.mdx @@ -145,7 +145,7 @@ USDT has a decimal precision of 6, meaning that the fractional balance string `' #### Single jetton @@ -412,6 +412,8 @@ Modify the following example according to the application logic: Mitigation: Verify the correct `decimals` value before calculating transfer amounts. For USDTs, the decimals value is 6. +Jetton transfers require various Toncoin [transaction fees](/foundations/fees). Before making a transfer, make sure there is enough Toncoin in the balance to cover the fees. + Modify the following examples according to the application logic: @@ -559,6 +561,17 @@ Modify the following examples according to the application logic: ``` +## Next steps + + + + + ## See also Jettons: diff --git a/ecosystem/appkit/nfts.mdx b/ecosystem/appkit/nfts.mdx new file mode 100644 index 000000000..14f861ef5 --- /dev/null +++ b/ecosystem/appkit/nfts.mdx @@ -0,0 +1,482 @@ +--- +title: "How to work with NFTs using AppKit" +sidebarTitle: "Work with NFTs" +--- + +import { Aside } from '/snippets/aside.jsx'; + + + +[NFTs](/standard/tokens/nft/overview) (non-fungible tokens) are unique digital assets on TON, similar to ERC-721 tokens on Ethereum. Unlike [jettons](/ecosystem/appkit/jettons), which are fungible and interchangeable, each NFT is unique and represents ownership of a specific item. NFTs consist of a collection contract and individual NFT item contracts for each token. + + + + + +## Ownership + +NFT ownership is tracked through individual NFT item contracts. Unlike jettons, which have a balance, one either owns a specific NFT item or does not. + +Similar to other asset queries, [discrete one-off checks](#on-demand-ownership-check) have limited value on their own and [continuous monitoring](#continuous-ownership-monitoring) should be used for UI display. +### On-demand ownership check + + + +#### Single NFT + +Obtain the information of a specific NFT by its address and check the ownership: + + + ```tsx title="React" icon="react" + import { + useNft, + useSelectedWallet, + } from '@ton/appkit-react'; + + export const NftCard = ({ nftAddress }) => { + const [wallet, _] = useSelectedWallet(); + const { + data: nft, + isLoading, + error, + } = useNft({ + // NFT contract address + address: nftAddress ?? '', + }); + + if (isLoading) { + return
Loading...
; + } + + if (error) { + return
Error: {error.message}
; + } + + return ( +
+

NFT info

+

Name: {nft?.info?.name}

+

Collection: {nft?.collection?.name}

+

Owner address: {nft?.ownerAddress?.toString()}

+

Am I the owner: {nft?.ownerAddress === wallet?.getAddress() ? 'yes' : 'no'}

+
+ ); + }; + ``` + + ```ts title="TypeScript" icon="globe" + import { + type AppKit, + type NFT, + getNft, + getSelectedWallet, + } from '@ton/appkit'; + + async function fetchNft( + /** Initialized AppKit instance */ + kit: AppKit, + /** NFT contract address */ + nftAddress: string, + ): Promise { + const selectedWallet = getSelectedWallet(kit); + const nft = await getNft(kit, { + address: nftAddress, + }); + console.log('NFT info'); + console.log(`Name: ${nft?.info?.name}`); + console.log(`Collection: ${nft?.collection?.name}`); + console.log(`Owner address: ${nft?.ownerAddress?.toString()}`); + console.log( + `Am I the owner: ${nft?.ownerAddress === selectedWallet?.getAddress() ? 'yes' : 'no'}` + ); + return nft; + } + ``` +
+ +#### All NFTs + +Retrieve every NFT held by the connected TON wallet or an arbitrary address: + + + ```tsx title="React" icon="react" + import { + useNftsByAddress, + useJettonsByAddress, + useSelectedWallet, + // Helper function targeting the connected wallet + useNfts, + } from '@ton/appkit-react'; + + export const NftListByAddress = () => { + const [wallet, _] = useSelectedWallet(); + const { + data: nfts, + isLoading, + error, + } = useNftsByAddress({ + // TON wallet address of the NFT holder + address: wallet?.getAddress() ?? '', + }); + + // Alternatively, query the connected wallet directly + // const { data: nfts, isLoading, error } = useNfts(); + + if (isLoading) { + return
Loading...
; + } + + if (error) { + return
Error: {error.message}
; + } + + return ( +
+

NFTs

+
    + {nfts?.nfts.map((nft) => ( +
  • + {nft.info?.name}: {nft.info?.description ?? '—.'} +
  • + ))} +
+
+ ); + }; + ``` + + ```ts title="TypeScript" icon="globe" + import { + type AppKit, + type NFT, + getJettonsByAddress, + getSelectedWallet, + // Helper function targeting the connected wallet + getNfts, + } from '@ton/appkit'; + + async function fetchNftsByAddress( + /** AppKit instance */ + kit: AppKit, + ): Promise { + const selectedWallet = getSelectedWallet(kit); + const response = await getNftsByAddress(kit, { + address: selectedWallet?.getAddress() ?? '', + }); + + // Alternatively, query the connected wallet directly + // const response = await getNfts(); + + console.log('NFTs by address:', response.nfts.length); + return response.nfts; + } + ``` +
+ +### Continuous ownership monitoring + +Poll the NFT ownership at regular intervals to keep the displayed value up to date. Use an appropriate interval based on UX requirements — shorter intervals provide fresher data but increase API usage. + +Modify the following example according to the application logic: + + + ```tsx title="React" icon="react" + import { + useNfts, + } from '@ton/appkit-react'; + + export const NftsCard = () => { + const { + data: nfts, + isLoading, + error, + refetch, + } = useNfts({ + // Only looks for up to 100 NFTs at a time + limit: 100, + }); + + if (isLoading) { + return
Loading...
; + } + + if (error) { + return ( +
+

Error: {error.message}

+ +
+ ); + } + + return
NFTs by address: {nfts?.nfts.length}
; + }; + ``` + + ```ts title="TypeScript" icon="globe" expandable + // Not runnable: implement the updateNftGallery() + import { + type AppKit, + type NFT, + getNfts, + } from '@ton/appkit'; + + /** + * Starts the monitoring of a given wallet's NFT ownership, + * calling `onNftsUpdate()` every `intervalMs` milliseconds + * + * @returns a function to stop monitoring + */ + export function startNftOwnershipMonitoring( + kit: AppKit, + onNftsUpdate: (nfts: NFT[]) => void, + intervalMs: number = 10_000, + ): () => void { + let isRunning = true; + + const poll = async () => { + while (isRunning) { + // Only looks for up to 100 NFTs. + // To get more, call the `getNfts()` function + // multiple times with increasing offsets + const { nfts } = await getNfts(kit, { limit: 100 }); + onNftsUpdate(nfts); + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + } + }; + + // Start monitoring + poll(); + + // Return a cleanup function to stop monitoring + return () => { + isRunning = false; + }; + } + + // Usage + const stopMonitoring = startNftOwnershipMonitoring( + kit, + // The updateNftGallery() function is exemplary and should + // be replaced by an app function that refreshes the NFT gallery + // displayed in the interface + (balance) => updateNftGallery(balance), + ); + + // Stop monitoring once it is no longer needed + stopMonitoring(); + ``` +
+ +## Transfers + + + +NFT transfers require various Toncoin [transaction fees](/foundations/fees). Before making a transfer, make sure there is enough Toncoin in the balance to cover the fees. + +Modify the following examples according to the application logic: + + + ```tsx title="React" icon="react" + import { useTransferNft } from '@ton/appkit-react'; + + export const SendNft = ({ recipientAddress, nftAddress }) => { + const { mutate: transfer, isPending, error, data } = useTransferNft(); + + const handleTransfer = () => { + transfer({ + // New owner of the sent NFT. + // For example: 'UQ...' + recipientAddress, + + // NFT contract address. + nftAddress, + + // (optional) Additional Toncoin sent to recipient. + // An amount string in fractional units. + // For example, '0.1' or '1' Toncoin. + amount: '', + + // (optional) Comment string. Defaults to none if not provided. + comment: 'Hello from AppKit!', + }); + }; + + return ( +
+ + {error &&
Error: {error.message}
} + {data && ( +
+

Transfer successful: {data.boc}

+
+ )} +
+ ); + }; + ``` + + ```ts title="TypeScript" icon="globe" + import { + type AppKit, + type Base64String, + // Single-call transfer + transferNft, + // Two-step transfer: create a transaction object separately from sending it + createTransferNftTransaction, + sendTransaction, + } from '@ton/appkit'; + + async function sendNft( + /** Initialized AppKit instance */ + kit: AppKit, + /** Recipient's TON wallet address as a string */ + recipientAddress: string, + /** NFT contract address */ + nftAddress: string, + /** + * Optional additional Toncoin sent to recipient. + * An amount string in fractional units. + * E.g., '0.1' or '1' Toncoin. + */ + amount?: string, + /** Optional comment string */ + comment?: string, + ) { + // Sign and send via TON Connect + const result = await transferNft(kit, { + recipientAddress, + nftAddress, + ...(amount && { amount }), + ...(comment && { comment }), + }); + console.log('Transaction sent:', result.boc); + + // Alternatively, build the transaction first with createTransferNftTransaction, + // then pass the resulting object to the sendTransaction function. + } + ``` +
+ +## `NFT` type + +NFT-related queries produce objects that conform to the following interface: + +```ts title="TypeScript" +/** + * Non-fungible token (NFT) on the TON blockchain. + */ +export interface NFT { + /** + * Contract address of the NFT item + */ + address: string; + + /** + * Index of the item within its collection + */ + index?: string; + + /** + * Display information about the NFT (name, description, images, etc.) + */ + info?: TokenInfo; + + /** + * Custom attributes/traits of the NFT (e.g., rarity, properties) + */ + attributes?: NFTAttribute[]; + + /** + * Information about the collection this item belongs to + */ + collection?: NFTCollection; + + /** + * Address of the auction contract, if the NFT is being auctioned + */ + auctionContractAddress?: string; + + /** + * Hash of the NFT smart contract code + */ + codeHash?: string; // hexadecimal characters + + /** + * Hash of the NFT's on-chain data + */ + dataHash?: string; // hexadecimal characters + + /** + * Whether the NFT contract has been initialized + */ + isInited?: boolean; + + /** + * Whether the NFT is soulbound (non-transferable) + */ + isSoulbound?: boolean; + + /** + * Whether the NFT is currently listed for sale + */ + isOnSale?: boolean; + + /** + * Current owner address of the NFT + */ + ownerAddress?: string; + + /** + * Real owner address when NFT is on sale (sale contract becomes temporary owner) + */ + realOwnerAddress?: string; + + /** + * Address of the sale contract, if the NFT is listed for sale + */ + saleContractAddress?: string; + + /** + * Off-chain metadata of the NFT (key-value pairs) + */ + extra?: { [key: string]: unknown }; +} +``` + +## See also + +NFTs: + +- [NFT overview](/standard/tokens/nft/overview) +- [NFT metadata](/standard/tokens/nft/metadata) + +General: + +- [Transaction fees](/foundations/fees) +- [AppKit overview](/ecosystem/appkit/overview) +- [TON Connect overview](/ecosystem/ton-connect) diff --git a/ecosystem/appkit/overview.mdx b/ecosystem/appkit/overview.mdx index 6ec799bc0..fe4ca7c99 100644 --- a/ecosystem/appkit/overview.mdx +++ b/ecosystem/appkit/overview.mdx @@ -50,6 +50,13 @@ TON Connect's **AppKit** is an open-source SDK that integrates web2 and web3 app horizontal="true" href="/ecosystem/appkit/jettons" /> + + ## See also diff --git a/ecosystem/walletkit/web/nfts.mdx b/ecosystem/walletkit/web/nfts.mdx index ad1719392..0e754cbf8 100644 --- a/ecosystem/walletkit/web/nfts.mdx +++ b/ecosystem/walletkit/web/nfts.mdx @@ -9,7 +9,7 @@ import { Aside } from '/snippets/aside.jsx'; [Initialize the WalletKit](/ecosystem/walletkit/web/init), [set up at least one TON wallet](/ecosystem/walletkit/web/wallets), handle [connection requests](/ecosystem/walletkit/web/connections) and [transaction requests](/ecosystem/walletkit/web/events) before using examples on this page. -[NFTs](/standard/tokens/nft/overview) (non-fungible tokens) are unique digital assets on TON, similar to ERC-721 tokens on Ethereum. Unlike jettons, which are fungible and interchangeable, each NFT is unique and represents ownership of a specific item. NFTs consist of a collection contract and individual NFT item contracts for each token. +[NFTs](/standard/tokens/nft/overview) (non-fungible tokens) are unique digital assets on TON, similar to ERC-721 tokens on Ethereum. Unlike [jettons](/ecosystem/walletkit/jettons), which are fungible and interchangeable, each NFT is unique and represents ownership of a specific item. NFTs consist of a collection contract and individual NFT item contracts for each token. To work with NFTs, the wallet service needs to handle [NFT ownership queries](#ownership) and perform transfers initiated [from dApps](#transfers-from-dapps) and [from within the wallet service itself](#transfers-in-the-wallet-service). From eb09a5c73d0444155ba8dbb732a88d86cf67bc4f Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:06:06 +0100 Subject: [PATCH 2/9] fmt --- ecosystem/appkit/nfts.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/ecosystem/appkit/nfts.mdx b/ecosystem/appkit/nfts.mdx index 14f861ef5..4b11b3718 100644 --- a/ecosystem/appkit/nfts.mdx +++ b/ecosystem/appkit/nfts.mdx @@ -29,6 +29,7 @@ import { Aside } from '/snippets/aside.jsx'; NFT ownership is tracked through individual NFT item contracts. Unlike jettons, which have a balance, one either owns a specific NFT item or does not. Similar to other asset queries, [discrete one-off checks](#on-demand-ownership-check) have limited value on their own and [continuous monitoring](#continuous-ownership-monitoring) should be used for UI display. + ### On-demand ownership check -[NFTs](/standard/tokens/nft/overview) (non-fungible tokens) are unique digital assets on TON, similar to ERC-721 tokens on Ethereum. Unlike [jettons](/ecosystem/appkit/jettons), which are fungible and interchangeable, each NFT is unique and represents ownership of a specific item. NFTs consist of a collection contract and individual NFT item contracts for each token. +[NFTs](/standard/tokens/nft/overview) (non-fungible tokens) are unique digital assets on TON, similar to ERC-721 tokens on Ethereum. Unlike [jettons](/standard/tokens/jettons/overview), which are fungible and interchangeable, each NFT is unique and represents ownership of a specific item. NFTs consist of a collection contract and individual NFT item contracts for each token. - - ## Ownership NFT ownership is tracked through individual NFT item contracts. Unlike jettons, which have a balance, one either owns a specific NFT item or does not. From 053d4cc4a0a6fc23adf400edecbf059a464d0573 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Fri, 13 Mar 2026 18:06:10 +0100 Subject: [PATCH 7/9] correct code sample --- ecosystem/appkit/nfts.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ecosystem/appkit/nfts.mdx b/ecosystem/appkit/nfts.mdx index b9e0f6815..a63939fa0 100644 --- a/ecosystem/appkit/nfts.mdx +++ b/ecosystem/appkit/nfts.mdx @@ -250,8 +250,8 @@ Modify the following example according to the application logic: // Only looks for up to 100 NFTs. // To get more, call the `getNfts()` function // multiple times with increasing offsets - const { nfts } = await getNfts(kit, { limit: 100 }); - onNftsUpdate(nfts); + const nfts = await getNfts(kit, { limit: 100 }); + onNftsUpdate(nfts?.nfts ?? []); await new Promise((resolve) => setTimeout(resolve, intervalMs)); } }; From 93cd315b30f79f3888f8aa4e808246104c7bb7d9 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Fri, 13 Mar 2026 18:06:37 +0100 Subject: [PATCH 8/9] prefer `useAddress` to `useSelectedWallet` --- ecosystem/appkit/nfts.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ecosystem/appkit/nfts.mdx b/ecosystem/appkit/nfts.mdx index a63939fa0..8e648d0b0 100644 --- a/ecosystem/appkit/nfts.mdx +++ b/ecosystem/appkit/nfts.mdx @@ -42,11 +42,11 @@ Obtain the information of a specific NFT by its address and check the ownership: ```tsx title="React" icon="react" import { useNft, - useSelectedWallet, + useAddress, } from '@ton/appkit-react'; export const NftCard = ({ nftAddress }) => { - const [wallet, _] = useSelectedWallet(); + const address = useAddress(); const { data: nft, isLoading, @@ -70,7 +70,7 @@ Obtain the information of a specific NFT by its address and check the ownership:

Name: {nft?.info?.name}

Collection: {nft?.collection?.name}

Owner address: {nft?.ownerAddress?.toString()}

-

Am I the owner: {nft?.ownerAddress === wallet?.getAddress() ? 'yes' : 'no'}

+

Am I the owner: {address && nft?.ownerAddress === address ? 'yes' : 'no'}

); }; @@ -114,20 +114,20 @@ Retrieve every NFT held by the connected TON wallet or an arbitrary address: ```tsx title="React" icon="react" import { useNftsByAddress, - useSelectedWallet, + useAddress, // Helper function targeting the connected wallet useNfts, } from '@ton/appkit-react'; export const NftListByAddress = () => { - const [wallet, _] = useSelectedWallet(); + const address = useAddress(); const { data: nfts, isLoading, error, } = useNftsByAddress({ // TON wallet address of the NFT holder - address: wallet?.getAddress() ?? '', + address: address ?? '', }); // Alternatively, query the connected wallet directly From ca6eaf9d79f1064335e9c839f88391f29994ce89 Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Fri, 13 Mar 2026 18:09:26 +0100 Subject: [PATCH 9/9] `useAddress` over `useSelectedWallet` for jettons page too --- ecosystem/appkit/jettons.mdx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/ecosystem/appkit/jettons.mdx b/ecosystem/appkit/jettons.mdx index bdb3dd63f..13440a8c1 100644 --- a/ecosystem/appkit/jettons.mdx +++ b/ecosystem/appkit/jettons.mdx @@ -79,18 +79,18 @@ Each jetton holder has a dedicated jetton wallet contract. To resolve its addres ```tsx title="React" icon="react" expandable import { useJettonWalletAddress, - useSelectedWallet, + useAddress, } from '@ton/appkit-react'; export const JettonWalletAddressCard = ({ jettonAddress }) => { - const [wallet, _] = useSelectedWallet(); + const ownerAddress = useAddress(); const { data: walletAddress, isLoading, error, } = useJettonWalletAddress({ // TON wallet address of the jetton holder - ownerAddress: wallet?.getAddress() ?? '', + ownerAddress: ownerAddress ?? '', // Jetton master (minter) contract address jettonAddress, @@ -156,18 +156,18 @@ Check the balance of a specific jetton for the connected TON wallet or an arbitr ```tsx title="React" icon="react" import { useJettonBalanceByAddress, - useSelectedWallet, + useAddress, } from '@ton/appkit-react'; export const JettonBalanceCard = ({ jettonAddress }) => { - const [wallet, _] = useSelectedWallet(); + const ownerAddress = useAddress(); const { data: balance, isLoading, error, } = useJettonBalanceByAddress({ // TON wallet address of the jetton holder - ownerAddress: wallet?.getAddress() ?? '', + ownerAddress: ownerAddress ?? '', // Jetton master (minter) contract address jettonAddress, @@ -222,20 +222,20 @@ Retrieve every jetton held by the connected TON wallet or an arbitrary address: ```tsx title="React" icon="react" import { useJettonsByAddress, - useSelectedWallet, + useAddress, // Helper function targeting the connected wallet useJettons, } from '@ton/appkit-react'; export const JettonListByAddress = () => { - const [wallet, _] = useSelectedWallet(); + const address = useAddress(); const { data: jettons, isLoading, error, } = useJettonsByAddress({ // TON wallet address of the jetton holder - address: wallet?.getAddress() ?? '', + address: address ?? '', }); // Alternatively, query the connected wallet directly @@ -303,11 +303,11 @@ Modify the following example according to the application logic: ```tsx title="React" icon="react" import { useJettonBalanceByAddress, - useSelectedWallet, + useAddress, } from '@ton/appkit-react'; export const JettonBalanceCard = ({ jettonAddress }) => { - const [wallet, _] = useSelectedWallet(); + const ownerAddress = useAddress(); const { data: balance, isLoading, @@ -315,7 +315,7 @@ Modify the following example according to the application logic: refetch, } = useJettonBalanceByAddress({ // TON wallet address of the jetton holder - ownerAddress: wallet?.getAddress() ?? '', + ownerAddress: ownerAddress ?? '', // Jetton master (minter) contract address jettonAddress,