Реалізація внутрішньоігрового NFT-гаманця в мобільній GameFi-грі
Гравець натиснув «екіпірувати» — інтерфейс завис, транзакція впала, NFT не з'явився. За годину він пішов до конкурентів. У GameFi кожна транзакція — виклик UX. Якщо гаманець не оптимізований під ігровий контекст, користувацький досвід руйнується. Ми вирішуємо це завдання комплексно: від вибору архітектури до деплою в стори. Наша команда — 7+ років у мобільній розробці, 15+ GameFi-проєктів, включаючи інтеграції з Immutable X, Polygon, Ronin.
Архітектура під масову аудиторію
Вибір між custodial, non-custodial та embedded гаманцем визначає всю архітектуру. Custodial (ключі на сервері) — простий онбординг, але KYC та довіра до сервера. Non-custodial (ключі в Keystore/Secure Enclave) — повний контроль, але складність для гравця. Embedded wallet через MPC-бібліотеки (Privy, Magic, Dynamic) — компроміс: вхід по email/social, ключі в HSM. Для casual GameFi останніх років це де-факто стандарт.
Як обрати тип гаманця для GameFi?
Критерії вибору: аудиторія, частота транзакцій, бюджет на онбординг.
| Параметр |
Custodial |
Non-custodial |
Embedded |
| Онбординг |
Email/пароль |
Сид-фраза |
Email/social |
| Безпека |
Сервер (HSM) |
Keystore/Secure Enclave |
MPC + HSM |
| KYC |
Потрібен у ряді країн |
Не потрібен |
Зазвичай не потрібен |
| Газ |
Оплачує розробник |
Користувач |
Розробник/користувач |
| Вартість розробки |
Середня |
Висока |
Середня |
Для ігор з мільйонами користувачів embedded — оптимальний баланс.
Чому Optimistic UI критичний в ігрових транзакціях?
Транзакція в блокчейні — не HTTP-запит. Вона потрапляє в mempool, чекає включення в блок (10 секунд на Polygon, миттєво на Ronin). За цей час користувач продовжує грати. Блокування інтерфейсу неприпустиме.
Паттерн: Optimistic UI + фоновий моніторинг. Одразу оновлюємо стан (екіпірування, отримання нагороди), відправляємо транзакцію, відстежуємо її статус. Якщо fail — відкочуємо.
// iOS — optimistic update
func equipItem(_ nft: NftItem) {
store.dispatch(EquipAction(tokenId: nft.tokenId))
Task {
do {
let txHash = try await walletService.equip(nft)
await monitor(txHash: txHash, onFail: {
store.dispatch(UnequipAction(tokenId: nft.tokenId))
showError("Транзакція не пройшла")
})
} catch {
store.dispatch(UnequipAction(tokenId: nft.tokenId))
}
}
}
Моніторинг — polling через WebSocket або JSON-RPC кожні 3–5 секунд.
Gas estimation та fee UI
Для non-custodial гаманця показуємо gas перед підтвердженням. eth_estimateGas → gasPrice → конвертація в USD через CoinGecko API. Не показувати нативний токен без USD-еквіваленту — гравець не знає, скільки коштує 0.0023 MATIC. Immutable X забезпечує газ-free трансфери, що в 100 разів дешевше Polygon і в 1000 разів дешевше Ethereum. Середня вартість транзакції на L2 становить менше $0.001, економія на газі досягає 99% порівняно з Ethereum. Порівняння мереж:
| Мережа |
Середня вартість трансферу NFT |
Час підтвердження |
| Ethereum |
$30–100 |
10–20 хв |
| Polygon |
< $0.001 |
~2 сек |
| Immutable X |
0 |
Миттєво |
| Ronin |
< $0.001 |
~1 сек |
Локальне зберігання NFT-даних
Метадані NFT завантажуємо через tokenURI (ERC-721) або IPFS-гейт. Кешуємо локально — Room / Core Data. Зображення — окремий кеш через Glide (Android) / Kingfisher (iOS) з IPFS URL.
@Entity(tableName = "nft_items")
data class NftItem(
@PrimaryKey val tokenId: String,
val contractAddress: String,
val name: String,
val imageUrl: String,
val metadata: String, // JSON
val isEquipped: Boolean = false,
val cachedAt: Long = System.currentTimeMillis()
)
IPFS URLs транслюємо через власний gateway для стабільності.
Транзакційний UX — найскладніше
Типові помилки при інтеграції гаманця
- Не враховувати затримки підтвердження та не передбачити fallback
- Відсутність кешу метаданих — кожне відкриття інвентаря викликає завантаження
- Ігнорування правил App Store (заборона фіатних продажів NFT без IAP)
- Зберігання ключів у SharedPreferences або UserDefaults
Безпека приватних ключів
Non-custodial: ключ тільки в Android Keystore або iOS Secure Enclave. Підпис транзакції всередині сховища — ключ не покидає захищену область.
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val privateKey = keyStore.getKey(KEY_ALIAS, null) as PrivateKey
val signature = Signature.getInstance("SHA256withECDSA")
signature.initSign(privateKey)
signature.update(transactionHash)
val signedBytes = signature.sign()
NFT-інвентар та фільтрація
Інвентар з 200+ NFT фільтрується миттєво через Room запити. Використовуємо LazyColumn або UICollectionView з DiffableDataSource. Сортування за rarity_score з метаданих.
App Store / Google Play та Web3
Apple приймає NFT-додатки, але забороняє покупку NFT через сторонні платіжні системи без комісії App Store (30%). Допустимо: показ NFT, трансфер між гаманцями, використання в грі. Заборонено: продаж за фіат без IAP. Для marketplace — тільки WebView із зовнішнім сайтом. Google Play дозволив NFT явно, але ті ж правила фіатних продажів. App Store Review Guidelines 3.1.1.
Покроковий процес інтеграції
- Аналіз ігрової економіки — визначаємо тип гаманця, L2, бюджет на газ.
- Проєктування архітектури — вибір бібліотек (Privy/Magic/Dynamic), схема кешу, модель даних.
- Інтеграція смарт-контрактів — підключення ERC-721/ERC-1155, розрахунок gas.
- Реалізація клієнтської частини — гаманець, Optimistic UI, моніторинг, кеш.
- Тестування на TestFlight/Firebase — симуляція 1000+ транзакцій.
- Деплой в стори — перевірка відповідності гайдлайнам Apple/Google.
Що входить в роботу
- Вибір архітектури гаманця (custodial, non-custodial, embedded) під вашу економіку
- Проєктування схеми кешу та моделі даних NFT
- Інтеграція смарт-контрактів та розрахунок газу
- Реалізація клієнтської частини з Optimistic UI та моніторингом транзакцій
- Тестування на TestFlight/Firebase з симуляцією пікових навантажень
- Допомога в проходженні рев'ю App Store та Google Play
- Документація та передача вихідного коду
- Гарантійна підтримка на 3 місяці після деплою
Строки
Базовий custodial гаманець з переглядом NFT та трансферами: 1–2 тижні. Full-featured некастодіальний з ігровою механікою та Optimistic UI: 3–5 тижнів. Вартість розраховується індивідуально залежно від обраної мережі та обсягу інтеграцій.
Отримайте консультацію щодо архітектури гаманця для вашої гри — оцінимо проєкт за 2 дні. Зв'яжіться з нами, щоб обговорити інтеграцію.
Як вибрати рішення для локального зберігання даних (Room, Core Data, Realm, Isar)?
Ми стикалися з ситуацією, коли додаток втрачає дані при втраті мережі — і це не просто баг, це провал сценарію. Користувач заповнив форму, натиснув «Відправити», отримав таймаут і втратив все. Або гірше: дані відправилися двічі через некоректну логіку повторної відправки. Правильно вибраний та налаштований шар сховища вирішує цю проблему раз і назавжди. Неправильний вибір може коштувати команді місяців переписування коду та втрати до 70% часу на синхронізацію. Наш досвід — 10+ років у мобільній розробці, понад 50 проектів з офлайн-сховищами — підтверджує: вибір рішення визначає 80% майбутніх проблем з продуктивністю та синхронізацією.
На практиці вибір сховища визначається двома факторами: типом даних та вимогами до синхронізації, а не популярністю бібліотеки.
Room (Android) — обгортка над SQLite з compile-time верифікацією SQL-запитів. Якщо запит невалідний, збірка падає — це краще, ніж SQLiteException в рантаймі. Room добре інтегрується з Kotlin Flow та LiveData, що робить реактивні UI-оновлення прямолінійними. Основна складність — міграції схеми. @Database(version = N, exportSchema = true) з файлами міграцій в assets/databases/ — обов'язкова практика, інакше при оновленні додатка fallbackToDestructiveMigration() просто зітре дані користувача.
Core Data (iOS) — не база даних, а фреймворк управління графом об'єктів поверх SQLite (або XML, або in-memory). NSPersistentContainer з viewContext для читання на main thread та newBackgroundContext() для запису — базова схема. Проблема починається, коли розробник робить save() в viewContext з фонового потоку: EXC_BAD_ACCESS в рандомний момент, відтворюється раз на тиждень, в крешлозі майже нічого корисного. Потрібно використовувати performAndWait або perform для кожного контексту строго в своєму потоці. Apple Core Data Programming Guide рекомендує саме такий підхід.
Realm виграє там, де потрібна швидкість роботи з великими наборами об'єктів та вбудована реактивність через Results + observe(). Realm зберігає об'єкти напряму, без маппінгу ORM, тому читання не потребує десеріалізації. За нашими вимірами, Realm обробляє читання в 2–3 рази швидше Core Data при об'ємі понад 10 000 об'єктів. На Flutter Realm SDK (ex-MongoDB Realm) підтримує Device Sync — але це вже managed-сервіс з окремою інфраструктурою.
Hive та Isar — Flutter-специфічні рішення. Hive — key-value сховище, швидко, просто, підходить для налаштувань та кешів. Isar — повноцінна документо-орієнтована БД з індексами, написана на Rust, компілюється в нативний код. Для Flutter-додатків з офлайн-функціональністю Isar зараз переважніше: вбудований query builder з типобезпечними фільтрами, транзакції, watchObject/watchQuery для реактивності.
| Платформа |
Рішення |
Реактивність |
Синхронізація |
| Android |
Room + Flow |
LiveData/Flow |
WorkManager |
| iOS |
Core Data |
NSFetchedResultsController |
CloudKit |
| Flutter |
Isar |
Streams |
Custom / Realm Sync |
| Cross-platform |
Realm |
RealmResults.observe |
Device Sync |
| Flutter (простий) |
Hive |
ValueListenable |
Немає |
Зв'яжіться з нами, щоб отримати безкоштовний аудит вашого поточного сховища та рекомендації з оптимізації — це зекономить вам сотні годин розробки та до 60% трафіку на серверні запити.
Чому офлайн-синхронізація — найскладніша частина?
Локальне сховище саме по собі нескладне. Складність — у синхронізації з сервером при наявності конфліктів.
Найчастіший патерн — optimistic updates з rollback. Користувач редагує запис, UI відображає зміну миттєво, фоновий запит йде на сервер. Якщо сервер повертає помилку — відкочуємо локальний стейт. Виглядає просто. На практиці: якщо користувач встиг піти з екрану і повернутися, а відкат відбувся через 3 секунди — UX зламаний. Потрібна явна черга операцій зі станом (PENDING, SYNCED, FAILED) в окремій таблиці.
На Android для фонової синхронізації використовуємо WorkManager з Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED). Важно не забыть про setInputMerger(ArrayCreatingInputMerger::class) при батчинге задач — інакше при кількох одночасних запусках дані затираються. Типова реалізація черги операцій:
class SyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val pendingOps = syncDao.getPendingOperations()
for (op in pendingOps) {
try {
apiClient.send(op.payload)
syncDao.markSynced(op.id)
} catch (e: Exception) {
syncDao.markFailed(op.id, e.message)
return Result.retry()
}
}
return Result.success()
}
}
На iOS аналог — BGTaskScheduler з BGProcessingTaskRequest. Обмеження iOS на фоновий час виконання (~30 секунд для refresh tasks) означають, що синхронізація повинна бути інкрементальною: не «синхронізувати все», а «синхронізувати наступні N записів, зберегти курсор».
Конфлікти при мультипристроєвій роботі вирішуються одним із трьох підходів:
- Last-write-wins по
updated_at (найпростіший, втрачає дані при одночасному редагуванні)
- Server-wins (клієнт завжди приймає серверну версію)
- Three-way merge (складно, потрібен спільний предок — підходить для документів)
У більшості B2C-додатків достатньо last-write-wins з вектором часу на рівні користувача, але при спільному редагуванні потрібен CRDTs-підхід — тоді дивимося на Automerge або Yjs з мобільними біндингами.
Як ми будуємо шар сховища
Репозиторний патерн — не опціональний, а обов'язковий. UserRepository не знає, звідки дані: з Room, Realm чи мережі. ViewModel викликає repository.getUser(id), отримує Flow/Stream, відображає дані. Логіка кешування — всередині репозиторія.
Для Flutter типова архітектура: Isar для персистентності, Riverpod для управління стейтом, ConnectivityPlus для визначення стану мережі, кастомний SyncService з чергою операцій. Riverpod AsyncNotifier зручно покриває логіку «показати кеш, оновити з мережі, показати нові дані». Приклад репозиторія з кешуванням:
class UserRepository {
final Isar isar;
final ApiClient api;
Future<User> getUser(String id) async {
// 1. спробувати з локального сховища
final cached = await isar.user.where().idEqualTo(id).findFirst();
if (cached != null) return cached;
// 2. інакше з мережі
final remote = await api.fetchUser(id);
// 3. зберегти локально
await isar.writeTxn(() => isar.user.put(remote));
return remote;
}
}
Окрема тема — шифрування. Якщо додаток зберігає медичні дані, платіжні картки або корпоративні документи, SQLCipher (Android) та NSFileProtection (iOS) — не опція. Realm підтримує шифрування нативно через ключ у 64 байти, який потрібно зберігати в Keychain/Keystore, а не в SharedPreferences. Економія на безпеці може обійтися у витік даних з гучними наслідками.
Що входить в роботу
Ми гарантуємо прозорий процес і фіксуємо кожен етап:
| Етап |
Результат |
| Аудит вимог |
Документ з аналізом типів даних, обсягів, сценаріїв синхронізації |
| Проектування схеми |
ER-діаграма, файли міграцій, план конфлікт-резолюції |
| Розробка репозиторного шару |
Код з юніт-тестами (in-memory БД + моки мережі) |
| Інтеграція синхронізації |
Черга операцій, обробка помилок, fallback-логіка |
| Профілювання та оптимізація |
Звіт Android Profiler / Core Data SQLDebug, рекомендації |
| Деплой та документування |
Інструкція з розгортання, API-опис, доступ до репозиторію |
Бажаєте уникнути типових помилок при проектуванні сховища? Зверніться до нас — ми допоможемо спроектувати надійне локальне сховище з нуля або доопрацювати існуюче.
Які етапи роботи?
Починаємо з аудиту вимог: які дані, який обсяг, чи потрібна синхронізація, чи можливі конфлікти. На цьому етапі стає зрозуміло, Core Data чи SQLite-based рішення, чи потрібен Realm Sync або достатньо простого REST-поллінгу.
Далі — проектування схеми з урахуванням міграцій. Схему змінюють у будь-якому проекті — питання не «чи будуть міграції», а «наскільки болісно вони пройдуть». Експортуємо схему в JSON, зберігаємо в репозиторії, пишемо тести на міграцію кожної версії.
Розробка йде з покриттям репозиторного шару юніт-тестами: моки мережевого шару, реальна in-memory база для тестування запитів. Перед релізом — профілювання запитів через Android Profiler (вкладка Database Inspector) або Core Data debug флаги (-com.apple.CoreData.SQLDebug 1).
Термін реалізації шару сховища з базовою офлайн-синхронізацією — від 2 до 6 тижнів залежно від складності схеми та вимог до конфлікт-резолюції. Вартість розраховується індивідуально після аудиту вашого проекту. Замовте розробку під ключ — отримайте консультацію з вибору оптимального стеку та міграціям.