Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.0 KiB
id, title, status, type, domain, scope, priority, depends, blocks, related, created, updated, tags
| id | title | status | type | domain | scope | priority | depends | blocks | related | created | updated | tags | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0072e | gamedev — bridge crypto Web3 (wallets, sign tx) via JS interop en WASM | pendiente | feature |
|
multi-app | alta |
|
2026-05-10 | 2026-05-17 |
|
Objetivo
Permitir que el juego (corriendo en WASM dentro de un navegador) interactue con wallets crypto del usuario (MetaMask, Phantom, Coinbase Wallet, WalletConnect) sin empotrar libs Web3 en el bundle WASM. Toda la logica Web3 vive en JS, el WASM la invoca via EM_JS / EM_ASM.
Razon: las libs Web3 nativas C++ son escasas, inmaduras y pesadas. JS tiene ethers.js (~120KB gz), viem (~50KB gz), @solana/web3.js (~60KB gz) maduras y testeadas. Cargar JS en navegador es gratis, empotrar C++ Web3 en wasm cuesta MB.
Alcance
| Caso de uso | Soporte fase 1 |
|---|---|
| Conectar wallet (MetaMask, Phantom) | Si |
| Leer direccion del usuario | Si |
| Firmar mensaje (login passwordless) | Si |
| Firmar transaccion (in-game payment) | Si |
| Leer balance ETH/SOL/token | Si |
| Mintear NFT | No (sub-issue 0072f) |
| Listar NFTs del wallet (asset loading) | No (0072f) |
| Eventos on-chain (leaderboards) | No (0072f) |
Arquitectura
┌────────────────────────────────────────┐
│ WASM (juego C++) │
│ ┌──────────────────────────────────┐ │
│ │ crypto_bridge.h │ │
│ │ - bridge_connect_wallet() │ │
│ │ - bridge_get_address() │ │
│ │ - bridge_sign_message(msg) │ │
│ │ - bridge_sign_tx(tx_json) │ │
│ │ - bridge_get_balance() │ │
│ └──────────────────────────────────┘ │
│ ↕ EM_ASYNC_JS / EM_JS │
└────────────────────────────────────────┘
↕
┌────────────────────────────────────────┐
│ JS host (browser) │
│ - ethers.js / viem │
│ - @solana/web3.js │
│ - WalletConnect SDK │
│ - window.ethereum / window.solana │
└────────────────────────────────────────┘
Funciones a crear
En WASM (C++)
cpp/functions/crypto/bridge_web3.{cpp,h,md} (impure, WASM only):
namespace fn::crypto {
enum class Chain { Ethereum, Polygon, Arbitrum, Base, Solana };
struct ConnectResult {
bool ok;
std::string address;
std::string chain_id;
std::string error;
};
struct SignResult {
bool ok;
std::string signature; // hex 0x...
std::string error;
};
// Async via Emscripten promises. Usa Asyncify o callbacks.
void bridge_connect_wallet(Chain c, void (*cb)(const ConnectResult&, void*), void* user);
void bridge_sign_message(const std::string& msg, void (*cb)(const SignResult&, void*), void* user);
void bridge_sign_tx(const std::string& tx_json, void (*cb)(const SignResult&, void*), void* user);
void bridge_get_balance(const std::string& token_addr, void (*cb)(uint64_t, void*), void* user);
}
Implementacion via EM_ASYNC_JS:
EM_ASYNC_JS(char*, js_connect_wallet, (int chain), {
const result = await window.fnGameBridge.connectWallet(chain);
return stringToNewUTF8(JSON.stringify(result));
});
En JS host (no en registry, en cpp/apps/<app>/web/)
Cada app que use crypto incluye su web/bridge.js (compartible via plantilla):
// cpp/apps/engine_demo/web/bridge.js
import { ethers } from "https://esm.sh/ethers@6";
window.fnGameBridge = {
async connectWallet(chain) {
if (!window.ethereum) return { ok: false, error: "no_provider" };
try {
const accounts = await window.ethereum.request({ method: "eth_requestAccounts" });
const chainId = await window.ethereum.request({ method: "eth_chainId" });
return { ok: true, address: accounts[0], chain_id: chainId };
} catch (e) {
return { ok: false, error: e.message };
}
},
async signMessage(msg) {
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
try {
const sig = await signer.signMessage(msg);
return { ok: true, signature: sig };
} catch (e) {
return { ok: false, error: e.message };
}
},
async signTx(txJson) { /* ... */ },
async getBalance(tokenAddr) { /* ... */ }
};
Tamaño JS: ethers.js gzip ~120KB. Cargado lazy (solo cuando el usuario abre menu wallet), no afecta el TTI inicial del juego.
Asyncify vs JSPI
Emscripten ofrece dos formas de manejar async desde C++:
| Opcion | Pros | Contras |
|---|---|---|
| Asyncify | Funciona en todos los navegadores | +50-200KB al wasm, llamadas mas lentas |
| JSPI (JS Promise Integration) | 0 overhead, tamaño minimo | Solo Chrome/Edge desde 2024, requiere flag |
Decision: Asyncify por compatibilidad. Permitir JSPI via build flag opcional para usuarios Chrome.
-sASYNCIFY -sASYNCIFY_IMPORTS=['js_connect_wallet','js_sign_message',...]
Wallet support matrix
| Wallet | Chain | Mecanismo |
|---|---|---|
| MetaMask | EVM (ETH, Polygon, BSC, ...) | window.ethereum (EIP-1193) |
| Coinbase Wallet | EVM | window.ethereum (mismo provider) |
| Phantom | Solana | window.solana |
| WalletConnect | EVM + Solana | SDK + QR / deep link (mobile) |
| Trust Wallet (mobile) | EVM + Solana | WalletConnect |
Para mobile (Android/iOS) en sub-issue futuro: WalletConnect deep links + universal links.
Seguridad
- Nunca empotrar private keys en WASM. El juego solo PIDE firmas, nunca las tiene.
- Mensaje a firmar SIEMPRE con dominio explicito (EIP-191 personal_sign con prefijo o EIP-712 typed data) para evitar phishing cross-app.
- Validar chain_id antes de cada accion. Si user cambio de red, abortar.
- Rate limit de
bridge_sign_messagedesde el juego: max 1 firma cada 5s para evitar spam UI. - Auditar JS host code — es donde vive el riesgo. Versiones pinneadas de ethers/viem, no CDN sin SRI.
Login passwordless con firma
Patron canonico para identificar al jugador sin password:
1. Servidor genera nonce: "Sign this to login: <random_uuid>" + timestamp
2. Cliente llama bridge_sign_message(nonce)
3. Servidor verifica firma → recupera address
4. Servidor emite JWT con sub=address
Funcion auxiliar en registry: crypto_verify_eth_signature_go_crypto (ya existe? si no, crear) para el backend.
Pago in-game
1. Juego pide a JS: "send 0.01 ETH to <addr>"
2. JS abre MetaMask popup con tx
3. Usuario aprueba en wallet
4. JS devuelve tx_hash al WASM
5. WASM polleea balance / espera confirmacion via JS bridge
Tamaño
- Bridge C++: ~3 KB.
- Asyncify overhead: ~80 KB wasm (cuando se activa).
- JS host (ethers.js): ~120 KB gz, lazy loaded (no cuenta para TTI).
Tests
- Mock de
window.fnGameBridgepara tests unit del bridge C++ (responde con respuestas predefinidas). - Test e2e en navegador (Playwright) que abre el juego con MetaMask en un wallet de prueba, firma un mensaje, valida la respuesta. Sub-issue 0072f probablemente.
Criterio de exito
- Bridge C++ + JS funcionando en
engine_smokecon boton "Connect Wallet". - Firma de mensaje con MetaMask returna ok.
- Pago de 0.001 ETH en testnet (Sepolia) confirmado.
- Asyncify overhead ≤ 100 KB wasm.
- Documentacion
cpp/GAMEDEV.mdseccion crypto.
No-objetivos
- Wallets nativos (todo via JS).
- Soporte mobile profundo (en 0072g/0072h con WalletConnect).
- NFTs / on-chain assets (0072f).
- Backend de leaderboards firmado (0072f).