Розробка Dynamic NFT (оновлювані метаданні)
Статичний NFT — це файл з картинкою, прив'язаний до токена навічно. Dynamic NFT — це токен, чиї метаданні змінюються у відповідь на події: минає час, грається гра, змінюється ціна активу, користувач досягає нового рівня. Технічно це означає, що tokenURI() повертає різні дані в різні моменти часу. Існує кілька реалізацій, і вибір між ними визначає ступінь децентралізації, вартість оновлень та складність розробки.
Три архітектури Dynamic NFT
On-chain метаданні через SVG генерацію
Найбільш децентралізований варіант: метаданні та зображення генеруються прямо в смарт-контракті, без зовнішнього сховища. tokenURI() повертає base64-кодований JSON з base64-кодованим SVG усередину.
function tokenURI(uint256 tokenId) public view override returns (string memory) {
uint256 level = playerLevel[tokenId];
string memory svg = generateSVG(level);
string memory json = Base64.encode(bytes(string(abi.encodePacked(
'{"name": "Warrior #', tokenId.toString(), '",',
'"attributes": [{"trait_type": "Level", "value": ', level.toString(), '}],',
'"image": "data:image/svg+xml;base64,', Base64.encode(bytes(svg)), '"}'
))));
return string(abi.encodePacked("data:application/json;base64,", json));
}
Перевага: немає залежності від IPFS, серверів чи оракулів. Недолік: складна візуальна складова обмежена можливостями SVG. Підходить для ігрових NFT, бейджів, токенів досягнень.
Типова помилка при on-chain SVG: конкатенація рядків через abi.encodePacked з користувацькими даними. Якщо playerName містить лапки — JSON розбивається. Завжди санітизуйте рядкові дані перед включенням у JSON.
Chainlink Functions для зовнішніх даних
Коли метаданні повинні відбивати реальні дані — ціна ETH, результат матчу, погода — потрібен оракул. Chainlink Functions дозволяє виконувати довільний JavaScript off-chain та доставляти результат on-chain.
// Запит через Chainlink Functions
function requestMetadataUpdate(uint256 tokenId) external {
FunctionsRequest.Request memory req;
req.initializeRequestForInlineJavaScript(
"const price = await fetch('https://api.coingecko.com/...')..."
);
bytes32 requestId = _sendRequest(req.encodeCBOR(), subscriptionId, gasLimit, donId);
requestToToken[requestId] = tokenId;
}
// Отримання результату
function fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err)
internal override {
uint256 tokenId = requestToToken[requestId];
tokenData[tokenId] = abi.decode(response, (uint256));
emit MetadataUpdate(tokenId); // EIP-4906
}
EIP-4906 — стандарт для сповіщення маркетплейсів про зміни метаданих. OpenSea та інші прослуховують подію MetadataUpdate(tokenId) та оновлюють кеш. Без цієї события маркетплейс показує застарілі дані до наступної ручної синхронізації.
Вартість одного запиту через Chainlink Functions: 0.2-2 LINK в залежності від обчислень. При щодобових оновленнях 1 000 токенів — близько $150-1500/місяць. Це потрібно закласти в токеноміку проекту.
Upgradeable URI з IPFS та Timelock
Менш децентралізований, але найбільш гнучкий варіант: контракт зберігає baseURI, який може оновлюватися власником. При кожному оновленні метаданих — нова версія завантажується на IPFS, власник змінює baseURI на новий CID.
Цей підхід несе ризик довіри: власник може підмінити метаданні довільно (rug the art). Для пом'якшення — timelock на setBaseURI() з затримкою 48-72 години. Користувачі бачать запланову зміну заздалегідь.
Хороша практика: зберігати історію всіх baseURI on-chain (append-only масив) — користувачи завжди можуть перевірити попередні версії метаданих.
Ігрові NFT: State Machine On-chain
Для ігрових проектів dynamic NFT часто реалізує state machine: Egg → Baby → Adult → Legendary. Кожен перехід — окрема транзакція, що перевіряє умови (минув час, отримано досвід, завершено квест).
enum State { Egg, Baby, Adult, Legendary }
mapping(uint256 => State) public tokenState;
mapping(uint256 => uint256) public experience;
function evolve(uint256 tokenId) external {
require(ownerOf(tokenId) == msg.sender, "Not owner");
State current = tokenState[tokenId];
if (current == State.Egg) {
require(block.timestamp >= hatchTime[tokenId], "Not ready");
tokenState[tokenId] = State.Baby;
} else if (current == State.Baby) {
require(experience[tokenId] >= 1000, "Insufficient XP");
tokenState[tokenId] = State.Adult;
}
emit MetadataUpdate(tokenId); // EIP-4906
}
State зберігається on-chain, зображення для кожного state — на IPFS. tokenURI() повертає різні CID в залежності від tokenState[tokenId].
Особливості інтеграції з маркетплейсами
OpenSea кешує метаданні агресивно. Навіть з правильною подією MetadataUpdate оновлення в UI може тривати до 24 годин. Для проектів з критичними оновленнями (наприклад, результати турніру) рекомендуємо:
- Додати endpoint
forceUpdateчерез OpenSea API - Показувати актуальний стан на власному сайті через прямий RPC виклик
Blur, Gem, Reservoir обробляють события EIP-4906 швидше, зазвичай протягом години.
Процес розробки
Аналітика (1-2 дні). Визначіть тригери оновлення (час, подія, зовнішні дані), частоту, джерела даних. Виберіть архітектуру: on-chain SVG, Chainlink Functions чи upgradeable URI.
Розробка (3-6 днів). Залежить від архітектури: on-chain SVG з простою генерацією — 3 дні; інтеграція Chainlink Functions з зовнішнім API — 5-6 днів; ігрова state machine — 4-5 днів.
Тестування (1-2 дні). Fork-тести з Chainlink Functions на testnet (Fuji для Avalanche, Sepolia для Ethereum), перевірка событій EIP-4906 на OpenSea testnet.
Вартість розраховується після вибору архітектури та визначення джерел даних.







