Files
fn_registry/dev/issues/0072e-gamedev-crypto-bridge-web3.md
T

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
gamedev
multi-app alta
0072a
0072d
2026-05-10 2026-05-17
gamedev
wasm
crypto
web3

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

  1. Nunca empotrar private keys en WASM. El juego solo PIDE firmas, nunca las tiene.
  2. Mensaje a firmar SIEMPRE con dominio explicito (EIP-191 personal_sign con prefijo o EIP-712 typed data) para evitar phishing cross-app.
  3. Validar chain_id antes de cada accion. Si user cambio de red, abortar.
  4. Rate limit de bridge_sign_message desde el juego: max 1 firma cada 5s para evitar spam UI.
  5. 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.fnGameBridge para 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_smoke con 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.md seccion 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).