Game-Blockchain Wallet Integration
Wallet integration is UX problem as much as technical. Users come to play, not deal with gas, approve transactions, seed phrases. Developer's job—make blockchain invisible until player needs it. Different game types need different patterns.
Connection Methods
External Wallets (MetaMask, Phantom)
Standard for crypto-native audience. Via Wagmi (React) or 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() })}>
WalletConnect
</button>
</div>
);
}
Embedded Wallets (Privy, Web3Auth) for Mass Audience
For casual games—users don't want extensions. Embedded wallet created auto on signup via 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");
if (!authenticated) {
return <button onClick={login}>Play</button>;
}
return <Game walletAddress={embeddedWallet?.address} />;
}
Session Keys: Auto Transactions
Main UX problem GameFi—each action requires MetaMask signature. Session keys solve it: user authorizes once, game uses session key for auto transactions.
const sessionPrivateKey = generatePrivateKey();
const permissionValidator = await toPermissionValidator(publicClient, {
signer: sessionAccount,
policies: [callPolicy],
validUntil: Math.floor(Date.now() / 1000) + 3600,
});
Unity Integration
WebGL: JSLib Bridge
// Plugins/WebGL/wallet.jslib
mergeInto(LibraryManager.library, {
ConnectWallet: async function() {
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
unityInstance.SendMessage('WalletManager', 'OnWalletConnected', accounts[0]);
}
});
Mobile: WalletConnect 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
};
return await _wc.Session.EthSendTransaction(transactionData);
}
}
Transaction Handling
Optimistic UI Updates
Can't wait 10–30 seconds for finality. Use optimistic updates: show result immediately, rollback if transaction fails.
async function purchaseItem(itemId: number, price: bigint) {
// 1. Show immediately
gameState.addItemOptimistic(itemId);
try {
// 2. Send transaction
const txHash = await marketplace.write.buyItem([itemId], { value: price });
// 3. Wait confirmation in background
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
if (receipt.status === "reverted") {
// 4. Rollback if fail
gameState.removeItem(itemId);
showError("Transaction failed");
}
} catch (err) {
gameState.removeItem(itemId);
showError("Transaction rejected");
}
}
Transaction Batching
Account Abstraction allows multiple operations in one transaction:
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],
},
],
});
Common Issues
| Problem | Solution |
|---|---|
| User closed MetaMask popup | Transaction queue + recover pending state |
| Gas estimation failure | Static limits + fallback RPC |
| Network mismatch | Auto switch via wallet_switchEthereumChain |
| User rejected | Graceful fallback, save progress off-chain |
| Pending tx stuck | Replace with higher gas (speed up) |
Key principle: blockchain is async, game is sync. Design UI so pending state is explicit, failure is recoverable.







