Метаданные NFT: off-chain JSON

Соберёшь off-chain метаданные NFT по TEP-64: полный JSON коллекции и айтема, on-chain ссылка и get_nft_content.

Где лежат метаданные

TEP-64 допускает off-chain (в контракте — URI на JSON) и on-chain (поля в cell). Off-chain — основной путь для картинок и атрибутов: collection хранит common prefix, item — суффикс (0.json), get_nft_content склеивает полный URL.

Кошельки и маркетплейсы (Getgems, Tonkeeper) читают JSON по HTTP(S)/IPFS. Битая ссылка = «пустой» NFT в UI при живом on-chain владении.


JSON коллекции и айтема + ссылка в контракте

Полный off-chain JSON коллекции (collection.json):

json
{
  "name": "Gramdocs Demo Collection",
  "description": "Учебная NFT-коллекция для документации gramdocs.tech",
  "image": "https://example.com/nft/collection.png",
  "cover_image": "https://example.com/nft/cover.png",
  "social_links": [
    "https://t.me/gramdocs",
    "https://gramdocs.tech"
  ]
}

Полный off-chain JSON айтема (0.json):

json
{
  "name": "Gramdocs Demo #0",
  "description": "Первый айтем учебной коллекции",
  "image": "https://example.com/nft/0.png",
  "content_url": "https://example.com/nft/0.png",
  "attributes": [
    { "trait_type": "Rarity", "value": "Common" },
    { "trait_type": "Edition", "value": "1" }
  ]
}

On-chain ссылка (TEP-64 off-chain marker 0x01 + URI):

typescript
// @ton/core ^0.59.0
// TEP-64: off-chain content = 0x01 + URL string
import { beginCell, Cell } from "@ton/core";

/** Off-chain content cell: первый байт 0x01, далее UTF-8 URI */
export function offchainContent(uri: string): Cell {
  return beginCell()
    .storeUint(0x01, 8)
    .storeStringTail(uri)
    .endCell();
}

// Collection content: часто два ref — collection meta + common item prefix
export function buildCollectionContent(opts: {
  collectionMetaUrl: string; // https://example.com/nft/collection.json
  commonItemPrefix: string; // https://example.com/nft/
}) {
  return beginCell()
    .storeRef(offchainContent(opts.collectionMetaUrl))
    .storeRef(offchainContent(opts.commonItemPrefix))
    .endCell();
}

// Item individual content — только суффикс; collection.get_nft_content склеит
export function buildItemIndividualContent(suffix: string): Cell {
  // suffix = "0.json" → полный URL = commonPrefix + "0.json"
  return offchainContent(suffix);
}

Проверка полного контента айтема:

typescript
// get_nft_content(index, individual_content) на collection → full URI cell
const full = await client.runMethod(collection, "get_nft_content", [
  { type: "int", value: 0n },
  { type: "cell", cell: individualContent },
]);
const fullContent = full.stack.readCell();
// Парсишь 0x01 + string → https://example.com/nft/0.json

Как это работает

  • 0x01 в начале content cell = off-chain; 0x00 = on-chain dict (реже для картинок).
  • Collection дедуплицирует prefix: меняешь common URL один раз — обновляются все айтемы (если item хранит только суффикс).
  • name / image / attributes читает индексатор; on-chain хранит лишь ссылку.
  • IPFS (ipfs://...) тоже off-chain: нужен gateway у клиента или CDN перед ним.

Частые ошибки

Кладешь полный URL в item и другой prefix в collectionget_nft_content даёт кашу или двойной путь. Либо полный URI в item, либо prefix+suffix — единообразно.

JSON без image / с относительным путём → кошелёк не рисует превью.

HTTP без CORS / 404 после минта → on-chain owner есть, в UI «Unknown NFT». Проверь URL до деплоя.


Что дальше

Материалы gramdocs.tech носят образовательный характер и не являются финансовой, юридической или инвестиционной рекомендацией. Работа с блокчейном TON и токеном Gram связана с рисками потери средств. Правовая информация