Image

Как я добавил on-chain награды и NFT в Solana Quiz: практические находки, ошибки и рабочие советы

В прошлой статье на Хабре я рассказывал о том, как построил небольшое приложение — Solana Quiz, где пользователи отвечают на ежедневные вопросы и получают награды.

Вот ссылка на ту статью — чтобы не повторять архитектуру и базовые вещи:

👉 https://habr.com/ru/articles/956186/

С тех пор я значительно расширил функционал: я добавил on-chain награды, 7-дневные стрики и самое интересное — NFT за серию полностью правильных ответов.

Казалось бы, задача простая: получил событие — начислил токены — иногда выдал NFT.

Но за этим «иногда» скрывается целая гора нюансов: от Solana PDA до порядка вызовов в метадате и странных ошибок, которые не объяснены ни в одном официальном гайде.

В этой статье я поделюсь:

  • как я реализовал on-chain/off-chain награды,

  • почему стрики работают по-разному в этих режимах,

  • как устроено NFT-вознаграждение,

  • какие технические грабли я собрал,

  • и что нужно знать о последовательности команд при создании NFT (если поменять порядок — всё ломается).

🟡 Коротко о том, как сейчас работает Solana Quiz

Когда пользователь проходит квиз (пока — всегда 5 вопросов, значение зашито в коде, позже вынесу в .env), приложение:

  1. Отправляет результат в Kafka.

  2. Rust-сервис получает событие и начисляет награду.

  3. В зависимости от режима (SOLANA_ON_CHAIN=true/false) награда выдаётся:

    • либо on-chain — транзакцией в Solana,

    • либо off-chain — через локальное API.

  4. Rust подтверждает награду через Kafka.

  5. Node.js помечает награду как выданную.

  6. Если достигнут 7-дневный стрик полностью правильных ответов — генерируется NFT.

Solana Quiz Streaker: 7 Days
Solana Quiz Streaker: 7 Days

🧰 Работа с Solana test-validator + симуляция транзакций

🚀 Зачем вообще нужен solana-test-validator

Когда я начал писать on-chain часть для квиза, я быстро понял одну вещь:
тестировать всё сразу на devnet — боль.

  • Транзакции подтверждаются дольше.

  • Не всегда понятно, это я ошибся или сеть.

  • Некоторые ошибки (особенно PDA / metadata) ловить невозможно в «боевом» сложнее.

Поэтому в Solana есть потрясающая штука — локальный валидатор:

solana-test-validator

Это полноценная локальная цепочка Solana, запускаемая одной командой.

Она работает быстро, даёт бесконечные токены и позволяет деплоить программы локально без ограничений.

После запуска RPC доступен по умолчанию на: http://127.0.0.1:8899

И теперь вы можете направить туда ваше приложение:

SOLANA_NETWORK=local SOLANA_RPC_ENDPOINT=http://127.0.0.1:8899 # http://host.docker.internal:8899

Когда это полезно:

  • Когда вы пишете свои Solana программы (Anchor или чистый Rust).

  • Когда вы тестируете mint NFT / PDA / metadata.

  • Когда вы хотите массово симулировать транзакции.

  • Когда нужно воспроизвести ошибку быстро и повторяемо.

🧪 Devnet, Localnet, Mainnet — в чём разница

Среда

Для чего

Плюсы

Минусы

Localnet (solana-test-validator)

Мгновенная разработка

мгновенные блоки, полный контроль

не отражает реальную сеть

Devnet

Предпрод / публичные тесты

реальная нагрузка, публичный RPC

иногда тормозит, имеет лимиты

Mainnet

Прод

стабильность, реальная токеномика

дорогие транзакции

При разработке on-chain функционала я могу уверенно сказать:

👉 80% времени стоит работать на test-validator
👉 20% — финальная проверка на devnet

🔍 Как симулировать транзакции и получать читабельные ошибки

Если вы работаете с Solana без симуляций, вы теряете половину дебаг-информации.

Симуляция позволяет:

  • поймать ошибку ещё до отправки,

  • увидеть логи программ,

  • прочитать ошибки SPL-токенов, Metaplex и т. д.,

  • понять, какие PDA не совпадают,

  • проверить, хватает ли signer-ов,

  • увидеть stack trace on-chain вызовов.

let simulation = self.rpc_client.simulate_transaction(&transaction).await?; println!(«Simulation logs: {:#?}», simulation.value.logs);

Примеры логов:

Program log: Incorrect mint authority Program log: Owner does not match Program log: Failed to deserialize metadata Program log: Account is not rent-exempt Program log: Missing required signature Program log: Edition already exists

Эти строки спасли мне массу времени.

🔄 On-chain и Off-chain: что реально регулирует SOLANA_ON_CHAIN

Самая частая ошибка при проектировании — думать, что переменная включит или выключит выдачу наград.

Нет — награды начисляются всегда.

SOLANA_ON_CHAIN влияет только на то, как награда будет применена:

SOLANA_ON_CHAIN

Как считается токен

Как считается стрик

true

On-chain (Solana transaction)

На блокчейне, по факту успешных транзакций

false

Off-chain (через API, Rust)

Локально в Rust, без взаимодействия с Solana

Почему так?

Потому что логика стрика должна быть консистентной.
Если я работаю on-chain — я полагаюсь на реальные транзакции.
Если off-chain — быстрее и дешевле считать локально.

🧩 Самая интересная часть: NFT за стрики

NFT выдаётся за 7 дней полностью правильных ответов (SOLANA_STREAK_DAYS=7).

В будущем у меня в планах расширять этот функционал — например, редкости, уровни, разные типы NFT, но пока реализован только один тип.

Технически NFT создаётся следующим образом:

  • создаётся новый mint,

  • создаётся ATA для пользователя,

  • минтится 1 токен на ATA,

  • создаётся metadata (вместе с master edition).

И вот здесь начинается магия.
А точнее — боль.

🔥 Главная боль: последовательность команд имеет значение

Если вы просто посмотрите примеры Metaplex или Solana SDK — вы не увидите предупреждений о порядке вызовов.
Я тоже не видел.

Но когда я попробовал в реальном проекте, вот что произошло:

❌ Если делать так:

create_metadata(); // создаётся metadata (вместе с master edition) mint_token();

То NFT часто не создавался, а иногда вылетала ошибка:

«mint must have exactly 1 token minted before metadata creation»

или ещё веселее:

«MasterEdition account does not match mint supply»

Или просто транзакция падала без объяснения причин.

✔ Рабочий порядок (который я вывел экспериментами)

Вот финальная версия корректного рабочего flow:

/// Full workflow: mint NFT → create ATA → send NFT → create metadata pub async fn mint_nft_to_recipient(&self, recipient_pubkey: &Pubkey) -> Result<()> { // 1) Create mint let (mint_keypair, mint_signature) = self.create_mint().await?; println!( «✅ Mint: {}, Signature: {}», &mint_keypair.pubkey(), mint_signature ); // 2) Create user’s ATA let token_account_signature = self .create_token_account(&mint_keypair.pubkey(), &recipient_pubkey) .await?; println!(«✅ Token Account, Signature: {}», token_account_signature); // 3) Mint 1 token to recipient let one_token_signature = self .mint_token(&mint_keypair, &recipient_pubkey) .await?; println!(«✅ Token (NFT), Signature: {}», one_token_signature); // 4) Create metadata + master edition let metadata_signature = self.create_metadata(&mint_keypair).await?; println!(«✅ Metadata, Signature: {}», metadata_signature); Ok(()) }

Почему именно так?

  • Mint должен иметь supply = 1 до создания metadata.

    Если supply = 0 — это ещё «пустой» mint, Metadata не разрешит создавать NFT.

  • ATA должен существовать заранее, иначе mint_to_checked просто некуда будет отправить токен.

  • MasterEdition можно создать только после успешного mint_to_checked, что следует из логики стандартов Metaplex. Мы не делаем отдельный вызов — всё инкапсулировано в create_metadata().

Я перепробовал множество вариантов, и только эта последовательность работает стабильно — независимо от RPC и окружения.

⚠️ Очень важно быть внимательным с подписантами (signers):

  • Ошибки часто случаются, если неправильно указать authority или не подписан нужный Keypair.

  • Любая транзакция без корректного подписанта просто упадёт или вернёт непонятную ошибку.

💡 Совет:

Если возникли проблемы, обязательно воспользуйтесь симуляцией транзакции (описано выше). Это позволяет увидеть все ошибки и предупреждения в читаемом виде, не тратя драгоценное время и SOL на devnet или mainnet.

🧠 Почему вообще NFT?

Причин несколько:

1. Это подкрепляет мотивацию пользователя

Стрик из 7 дней — небольшое достижение, но приятно получить что-то осязаемое в обмен на ежедневные усилия

2. Это отличный повод изучить Metaplex и Solana SDK

Проходя через боль поиска PDA, ошибок mint_to_checked и order-sensitive API, я стал понимать архитектуру куда глубже.

3. Это хороший showcase для GitHub

Сейчас весь код опубликован — и это отличная демонстрация Rust + Solana + Node.js + Kafka взаимодействия.

👉 Репозиторий: https://github.com/di-zed/solana-quiz

🧵 Какие ещё технические моменты были интересными

✔ PDA для Metadata и MasterEdition надо считать вручную

Никакого удобного модуля вроде mpl_token_metadata::pda нет.
Я использую такие вычисления:

let (metadata_pubkey, _) = Metadata::find_pda(mint_pubkey); let (master_edition_pda, _) = Pubkey::find_program_address( &[ b»metadata», mpl_token_metadata::ID.as_ref(), mint_pubkey.as_ref(), b»edition», ], &mpl_token_metadata::ID, );

✔ Mint authority должен существовать только до выпуска токена

✔ ATA для NFT всегда уникальный

NFT = 1 токен, 0 decimals.
Ошибки часто возникают, если случайно выставить decimals != 0.

🎯 Что можно расширить дальше

  • разные типы NFT за разные streak lengths;

  • on-chain подсчёт стрика полностью в программе Anchor;

  • уровни наград;

  • коллекции NFT;

  • поддержка mainnet-beta.

Пока что я ограничился одним типом NFT и стриком в 7 дней, но вся архитектура уже позволяет безболезненно расширять функциональность — хотя для этого потребуется доработка логики и шаблонов.

⚡ Итог

Я прошёл через:

  • on-chain/off-chain логику начисления наград,

  • создание полноценных NFT на Solana,

  • ручное вычисление PDA,

  • ошибки Metaplex, которые нигде не описаны,

  • экспериментально вывел корректный порядок команд при создании NFT.

Теперь Solana Quiz стал не просто игрой, а сервисом, где:

  • токены начисляются честно и прозрачно,

  • streaks работают и учитываются корректно,

  • и за упорство можно получить свой небольшой, но настоящий NFT.

Если вам хочется посмотреть рабочий код, разобрать Rust-сервис, или сделать форк — буду рад:

👉 https://github.com/di-zed/solana-quiz

А так же:

Solana программа https://github.com/di-zed/solana-quiz/blob/main/rust/programs/solana_quiz_rewards/programs/solana_quiz_rewards/src/lib.rs
Пример логов: https://solscan.io/tx/2nr9QGfE2WVj2udFMdjmUWYvvatMT1BoHuouwEqnGg7VKwLtoWkZcsGvK5bMH6SbDgN6T7n5hNi1WuQcFdutBXoY?cluster=devnet

NFT API: https://github.com/di-zed/solana-quiz/blob/main/rust/src/services/nft_api.rs
Пример логов: https://solscan.io/tx/uomvJ7LJBDrQp74RUAgRve1MbweyPjJSDvnivkAk93AggygDnbrKKBMLu3TYEgQ4RkihKzfbdjURBz8wojkPNhb?cluster=devnet

Спасибо за внимание! 🚀

Источник: habr.com

✅ Найденные теги: Как, новости

ОСТАВЬТЕ СВОЙ КОММЕНТАРИЙ

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Каталог бесплатных опенсорс-решений, которые можно развернуть локально и забыть о подписках

галерея

Фото сгенерированных лиц: исследование показывает, что люди не могут отличить настоящие лица от сгенерированных
Нейросети построили капитализм за трое суток: 100 агентов Claude заперли…
Скетч: цифровой осьминог и виртуальный мир внутри компьютера с человечком.
Сцена с жестами пальцами, где один жест символизирует "VPN", а другой "KHP".
‼️Paramount купила Warner Bros. Discovery — сумма сделки составила безумные…
Скриншот репозитория GitHub "Claude Scientific Skills" AI для научных исследований.
Структура эффективного запроса Claude с элементами задачи, контекста и референса.
Эскиз и готовая веб-страница платформы для AI-дизайна в современном темном режиме.
ideipro logotyp
Image Not Found
Звёздное небо с галактиками и туманностями, космос, Вселенная, астрофотография.

Система оповещения обсерватории Рубина отправила 800 000 сигналов в первую ночь наблюдений.

Астрономы будут получать оповещения о небесных явлениях в течение нескольких минут после их обнаружения. Теренс О'Брайен, редактор раздела «Выходные». Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной…

Мар 2, 2026
Женщина с длинными тёмными волосами в синем свете, нейтральный фон.

Расследование в отношении 61-фунтовой машины, которая «пожирает» пластик и выплевывает кирпичи.

Обзор компактного пресса для мягкого пластика Clear Drop — и что будет дальше. Шон Холлистер, старший редактор Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной странице вашего…

Мар 2, 2026
Черный углеродное волокно с текстурой плетения, отражающий свет.

Материал будущего: как работает «бессмертный» композит

Учёные из Университета штата Северная Каролина представили композит нового поколения, способный самостоятельно восстанавливаться после серьёзных повреждений.  Речь идёт о модифицированном армированном волокном полимере (FRP), который не просто сохраняет прочность при малом весе, но и способен «залечивать» внутренние…

Мар 2, 2026
Круглый экран с изображением замка и горы, рядом электронная плата.

Круглый дисплей Waveshare для креативных проектов

Круглый 7-дюймовый сенсорный дисплей от Waveshare создан для разработчиков и дизайнеров, которым нужен нестандартный экран.  Это IPS-панель с разрешением 1 080×1 080 пикселей, поддержкой 10-точечного ёмкостного сенсора, оптической склейкой и защитным закалённым стеклом, выполненная в круглом форм-факторе.…

Мар 2, 2026

Впишите свой почтовый адрес и мы будем присылать вам на почту самые свежие новости в числе самых первых