---
id: 0072e
title: gamedev — bridge crypto Web3 (wallets, sign tx) via JS interop en WASM
status: pending
priority: high
created: 2026-05-10
tags: [gamedev, wasm, crypto, web3]
parent_issue: 0072
depends_on: [0072a, 0072d]
---

## 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):

```cpp
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`:

```cpp
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):

```js
// 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

- [x] Bridge C++ + JS funcionando en `engine_smoke` con boton "Connect Wallet".
- [x] Firma de mensaje con MetaMask returna ok.
- [x] Pago de 0.001 ETH en testnet (Sepolia) confirmado.
- [x] Asyncify overhead ≤ 100 KB wasm.
- [x] 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).
