Интеграция игры с блокчейн-кошельком
Интеграция кошелька — это UX-проблема не меньше, чем техническая. Пользователи приходят играть, а не разбираться с gas, approve транзакциями и seed phrases. Задача разработчика — сделать так, чтобы блокчейн был невидим для игрока, пока он ему не нужен. Разберём конкретные паттерны для разных типов игр.
Выбор метода подключения
Внешние кошельки (MetaMask, Phantom)
Стандарт для аудитории, которая уже в крипте. Через Wagmi (React) или WalletConnect AppKit подключение выглядит стандартно:
import { useConnect, useAccount, useSignMessage } from "wagmi";
import { injected, walletConnect } from "wagmi/connectors";
function WalletConnect() {
const { connect, connectors } = useConnect();
const { address, isConnected } = useAccount();
if (isConnected) return <GameLobby address={address} />;
return (
<div>
<button onClick={() => connect({ connector: injected() })}>
MetaMask
</button>
<button onClick={() => connect({ connector: walletConnect({ projectId: WC_PROJECT_ID }) })}>
WalletConnect
</button>
</div>
);
}
Embedded wallets (Privy, Web3Auth) для массовой аудитории
Для casual игр — пользователи не хотят устанавливать расширения. Embedded wallet создаётся автоматически при регистрации через Google/Apple:
import { usePrivy, useWallets } from "@privy-io/react-auth";
function GameAuth() {
const { login, authenticated, user } = usePrivy();
const { wallets } = useWallets();
const embeddedWallet = wallets.find(w => w.walletClientType === "privy");
// Для игрока это просто "войти через Google"
// Кошелёк создаётся в фоне автоматически
if (!authenticated) {
return <button onClick={login}>Играть</button>;
}
return <Game walletAddress={embeddedWallet?.address} />;
}
Session keys: взаимодействие без подписи каждый раз
Главная UX проблема GameFi — каждое действие требует подписи в MetaMask. Для игры это катастрофа. Session keys решают это: пользователь один раз авторизует сессионный ключ, игра использует его для транзакций автоматически.
import { createKernelAccountClient, createZeroDevPaymasterClient } from "@zerodev/sdk";
import { signerToEcdsaValidator } from "@zerodev/ecdsa-validator";
import { toPermissionValidator, toCallPolicy, ParamCondition } from "@zerodev/permissions";
// Генерируем session key (ephemeral, только для этой игровой сессии)
const sessionPrivateKey = generatePrivateKey();
const sessionAccount = privateKeyToAccount(sessionPrivateKey);
// Политика: session key может только вызывать GAME_CONTRACT функции
// с ограниченными параметрами
const callPolicy = toCallPolicy({
permissions: [{
target: GAME_CONTRACT_ADDRESS,
functionName: "claimReward",
// Необязательно: ограничения на параметры
}, {
target: GAME_CONTRACT_ADDRESS,
functionName: "useItem",
}]
});
const permissionValidator = await toPermissionValidator(publicClient, {
signer: sessionAccount,
policies: [callPolicy],
validUntil: Math.floor(Date.now() / 1000) + 3600, // 1 час
});
// Пользователь подписывает один раз — активацию session key
const kernelClient = await createKernelAccountClient({
account: kernelAccount,
// ...bundler config
});
После этого игра может отправлять транзакции от имени пользователя без дополнительных подтверждений, но только в рамках разрешённых операций.
Unity интеграция
WebGL: JSLib bridge
В Unity WebGL нет прямого доступа к MetaMask — нужен JavaScript bridge:
// Plugins/WebGL/wallet.jslib
mergeInto(LibraryManager.library, {
ConnectWallet: async function() {
if (typeof window.ethereum === 'undefined') {
alert('MetaMask not installed');
return;
}
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
// Отправляем адрес обратно в Unity
unityInstance.SendMessage('WalletManager', 'OnWalletConnected', accounts[0]);
},
SignMessage: async function(messagePtr) {
const message = UTF8ToString(messagePtr);
const accounts = await window.ethereum.request({ method: 'eth_accounts' });
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [message, accounts[0]],
});
unityInstance.SendMessage('WalletManager', 'OnMessageSigned', signature);
}
});
// C# side в Unity
using System.Runtime.InteropServices;
public class WalletManager : MonoBehaviour {
[DllImport("__Internal")]
private static extern void ConnectWallet();
[DllImport("__Internal")]
private static extern void SignMessage(string message);
public string ConnectedAddress { get; private set; }
public void Connect() {
#if UNITY_WEBGL && !UNITY_EDITOR
ConnectWallet();
#endif
}
// Callback из JavaScript
public void OnWalletConnected(string address) {
ConnectedAddress = address;
GameEvents.WalletConnected?.Invoke(address);
}
}
Mobile: WalletConnect deep links
На мобильном Unity использует deep links для открытия мобильного кошелька:
using WalletConnectSharp.Unity;
public class MobileWalletConnect : MonoBehaviour {
private WalletConnect _wc;
async void Start() {
_wc = GetComponent<WalletConnect>();
await _wc.Connect();
}
public async Task<string> SendTransaction(string to, string data, string value) {
var transactionData = new TransactionData {
from = _wc.Session.Accounts[0],
to = to,
data = data,
value = value
};
var txHash = await _wc.Session.EthSendTransaction(transactionData);
return txHash;
}
}
Обработка транзакций в игровом контексте
Optimistic UI updates
Ждать finality транзакции (10–30 секунд на L2, минуты на mainnet) в игре неприемлемо. Используем optimistic updates: показываем результат немедленно, откатываем если транзакция фейлится.
async function purchaseItem(itemId: number, price: bigint) {
// 1. Немедленно показываем в UI
gameState.addItemOptimistic(itemId);
try {
// 2. Отправляем транзакцию
const txHash = await marketplace.write.buyItem([itemId], { value: price });
// 3. Ждём confirmation в фоне
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
if (receipt.status === "reverted") {
// 4. Откатываем если фейл
gameState.removeItem(itemId);
showError("Transaction failed");
}
// 5. Подтверждаем если успех (UI уже показывает)
} catch (err) {
gameState.removeItem(itemId);
showError("Transaction rejected");
}
}
Батчинг транзакций
Account Abstraction позволяет отправить несколько операций в одной транзакции:
// Вместо: approve → buy (2 транзакции, 2 подписи)
// Делаем: один multicall через Smart Account
const txHash = await smartAccountClient.sendUserOperation({
calls: [
{
to: TOKEN_ADDRESS,
abi: erc20Abi,
functionName: "approve",
args: [MARKETPLACE_ADDRESS, price],
},
{
to: MARKETPLACE_ADDRESS,
abi: marketplaceAbi,
functionName: "buyItem",
args: [itemId],
},
],
});
Типичные проблемы и решения
| Проблема | Решение |
|---|---|
| Пользователь закрыл MetaMask popup | Transaction queue + восстановление pending state |
| Gas estimation failure | Статичные gas limits + fallback RPC |
| Network mismatch | Auto switch network через wallet_switchEthereumChain |
| User rejected transaction | Graceful fallback, сохранение прогресса off-chain |
| Pending transaction stuck | Replace with higher gas (speed up) через eth_sendRawTransaction |
Ключевой принцип: блокчейн — асинхронный, игра — синхронная. Всегда проектируйте UI так, чтобы pending состояние было явным, а failure — recoverable.







