docs: add AfterCoin documentation and MetaMask guides
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
Comprehensive docs covering architecture, all components, Docker services, environment variables, MetaMask connection (desktop + mobile), administration commands, and troubleshooting. Also adds Lua scripts to repo for version control, including the periodic chain sync loop (every 30s) in the mainframe. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
483
docs/aftercoin.md
Normal file
483
docs/aftercoin.md
Normal file
@@ -0,0 +1,483 @@
|
|||||||
|
# AfterCoin (AFC) - Blockchain Privada para el Casino de Minecraft
|
||||||
|
|
||||||
|
## Tabla de Contenidos
|
||||||
|
|
||||||
|
- [Resumen General](#resumen-general)
|
||||||
|
- [Arquitectura](#arquitectura)
|
||||||
|
- [Componentes](#componentes)
|
||||||
|
- [1. Nodo Geth (blockchain/)](#1-nodo-geth-blockchain)
|
||||||
|
- [2. Contrato AfterCoin (blockchain/contracts/AfterCoin.sol)](#2-contrato-aftercoin-blockchaincontractsaftercoinsol)
|
||||||
|
- [3. Bridge API (services/afc-bridge/)](#3-bridge-api-servicesafc-bridge)
|
||||||
|
- [4. Proxy SSL para RPC (docker/nginx/rpc-ssl.conf)](#4-proxy-ssl-para-rpc-dockernginxrpc-sslconf)
|
||||||
|
- [5. Mainframe Lua (Computer 7)](#5-mainframe-lua-computer-7)
|
||||||
|
- [6. Generador de Tarjetas Lua (Computer 4)](#6-generador-de-tarjetas-lua-computer-4)
|
||||||
|
- [Servicios Docker](#servicios-docker)
|
||||||
|
- [Variables de Entorno](#variables-de-entorno)
|
||||||
|
- [Guia de Conexion con MetaMask](#guia-de-conexion-con-metamask)
|
||||||
|
- [Escritorio (Extension)](#escritorio-extension)
|
||||||
|
- [Movil (App)](#movil-app)
|
||||||
|
- [Vincular una Wallet Personal](#vincular-una-wallet-personal)
|
||||||
|
- [Administracion](#administracion)
|
||||||
|
- [Comandos de Verificacion](#comandos-de-verificacion)
|
||||||
|
- [Operaciones Directas con Tokens](#operaciones-directas-con-tokens)
|
||||||
|
- [Renovacion de Certificado SSL](#renovacion-de-certificado-ssl)
|
||||||
|
- [Whitelist HTTP de CC:Tweaked](#whitelist-http-de-cctweaked)
|
||||||
|
- [Despliegue de Scripts Lua](#despliegue-de-scripts-lua)
|
||||||
|
- [Detalles del Contrato](#detalles-del-contrato)
|
||||||
|
- [Solucion de Problemas](#solucion-de-problemas)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resumen General
|
||||||
|
|
||||||
|
**AfterCoin (AFC)** es un token ERC-20 desplegado en una blockchain privada de Ethereum que utiliza el mecanismo de consenso **Clique PoA** (Proof of Authority) con chain ID **8888**.
|
||||||
|
|
||||||
|
Principios fundamentales:
|
||||||
|
|
||||||
|
- **1 AFC = 1 diamante** en el casino de Minecraft
|
||||||
|
- **0 decimales** (solo valores enteros, sin fracciones)
|
||||||
|
- Los jugadores pueden ver su saldo del casino en **MetaMask** como tokens reales en la blockchain
|
||||||
|
- El **mainframe del casino** sincroniza los saldos on-chain a traves de un **Bridge API**
|
||||||
|
|
||||||
|
Esto permite que los jugadores tengan una experiencia tangible de sus fondos del casino: pueden abrir MetaMask en su telefono o navegador y ver exactamente cuantos diamantes tienen, con la seguridad y transparencia de una blockchain real.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arquitectura
|
||||||
|
|
||||||
|
```
|
||||||
|
+---------------------+ +-----------------------------+
|
||||||
|
| | HTTPS | |
|
||||||
|
| MetaMask |--------->| Nginx SSL Proxy |
|
||||||
|
| (Escritorio/Movil) | :8443 | (rpc-ssl) |
|
||||||
|
| | +-------------+---------------+
|
||||||
|
+--------+------------+ |
|
||||||
|
| | HTTP :8545
|
||||||
|
| HTTP :8545 v
|
||||||
|
| (solo escritorio) +--------+---------------+
|
||||||
|
+--------------------------->| |
|
||||||
|
| Geth Node |
|
||||||
|
| (Clique PoA, ID 8888) |
|
||||||
|
| |
|
||||||
|
+--------+---------------+
|
||||||
|
^
|
||||||
|
| ethers.js (HTTP RPC)
|
||||||
|
|
|
||||||
|
+--------+---------------+
|
||||||
|
| |
|
||||||
|
| Bridge API |
|
||||||
|
| (Node.js, :3001) |
|
||||||
|
| |
|
||||||
|
| +------------------+ |
|
||||||
|
| | SQLite DB | |
|
||||||
|
| | (wallets, config)| |
|
||||||
|
| +------------------+ |
|
||||||
|
+--------+---------------+
|
||||||
|
^
|
||||||
|
| HTTP (red Docker interna)
|
||||||
|
|
|
||||||
|
+----------------+------------------+
|
||||||
|
| |
|
||||||
|
| CC:Tweaked Mainframe |
|
||||||
|
| (Computer 7, Minecraft) |
|
||||||
|
| |
|
||||||
|
+---+----------+----------+---------+
|
||||||
|
| | |
|
||||||
|
rednet rednet rednet
|
||||||
|
| | |
|
||||||
|
+---+--+ +---+---+ +---+---+
|
||||||
|
| Slots| | BJ | | Poker |
|
||||||
|
+------+ +-------+ +-------+
|
||||||
|
|
||||||
|
(Juegos del casino)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Flujo de datos:**
|
||||||
|
|
||||||
|
1. **MetaMask** se conecta al nodo Geth via HTTPS (puerto 8443, movil) o HTTP (puerto 8545, escritorio)
|
||||||
|
2. El **Bridge API** (puerto 3001) se comunica con Geth mediante ethers.js y mantiene una base de datos SQLite con las wallets de los jugadores
|
||||||
|
3. El **mainframe CC:Tweaked** (Computer 7) se comunica con el Bridge API via HTTP dentro de la red Docker interna
|
||||||
|
4. El **mainframe** se comunica con los juegos del casino (slots, blackjack, poker, etc.) via **rednet** (protocolo de red inalambrica de CC:Tweaked)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Componentes
|
||||||
|
|
||||||
|
### 1. Nodo Geth (blockchain/)
|
||||||
|
|
||||||
|
El nodo Geth ejecuta la blockchain privada donde vive el token AfterCoin.
|
||||||
|
|
||||||
|
| Parametro | Valor |
|
||||||
|
|---|---|
|
||||||
|
| Consenso | Clique PoA (Proof of Authority) |
|
||||||
|
| Chain ID | 8888 |
|
||||||
|
| Tiempo de bloque | 5 segundos |
|
||||||
|
| Version de Geth | v1.13.15 (ultima version con soporte para Clique PoA) |
|
||||||
|
| Limite de memoria | 1 GB |
|
||||||
|
| Puerto HTTP RPC | 8545 |
|
||||||
|
| Puerto WebSocket | 8546 |
|
||||||
|
|
||||||
|
**Archivos clave:**
|
||||||
|
|
||||||
|
- `genesis.json` -- Configuracion genesis de la cadena (Clique PoA, chain ID 8888, periodo de bloque de 5s)
|
||||||
|
- `init-geth.sh` -- Script de inicializacion que importa la cuenta admin y arranca Geth
|
||||||
|
- `Dockerfile` -- Imagen basada en `ethereum/client-go:v1.13.15`
|
||||||
|
|
||||||
|
**Wallet de administrador:** Actua como sellador (sealer) de bloques y propietario (owner) del contrato inteligente. Es la unica autoridad en la cadena PoA.
|
||||||
|
|
||||||
|
### 2. Contrato AfterCoin (blockchain/contracts/AfterCoin.sol)
|
||||||
|
|
||||||
|
Contrato inteligente ERC-20 autocontenido (sin dependencias de OpenZeppelin).
|
||||||
|
|
||||||
|
**Caracteristicas principales:**
|
||||||
|
|
||||||
|
- `decimals()` retorna `0` -- los tokens son enteros, 1 token = 1 diamante
|
||||||
|
- Funciones restringidas al owner (wallet admin):
|
||||||
|
- `mint(address to, uint256 amount)` -- Crea nuevos tokens y los asigna a una direccion
|
||||||
|
- `burnFrom(address from, uint256 amount)` -- Quema tokens de una direccion especifica
|
||||||
|
- `bridgeTransfer(address from, address to, uint256 amount)` -- Transfiere tokens entre wallets via el bridge
|
||||||
|
- Cumple con el estandar ERC-20 completo (transfer, approve, transferFrom, allowance, etc.)
|
||||||
|
|
||||||
|
**Compilacion:**
|
||||||
|
|
||||||
|
- Compilado con `solcjs` version 0.8.34
|
||||||
|
- **Target: Paris EVM** -- No usa el opcode `PUSH0` (introducido en Shanghai EVM), ya que Geth v1.13 no lo soporta en cadenas privadas
|
||||||
|
- El contrato se despliega automaticamente por el Bridge API en el primer arranque
|
||||||
|
|
||||||
|
### 3. Bridge API (services/afc-bridge/)
|
||||||
|
|
||||||
|
Servicio backend que actua como puente entre el mundo de Minecraft y la blockchain.
|
||||||
|
|
||||||
|
**Stack tecnologico:** Node.js + Express + ethers.js v6 + better-sqlite3
|
||||||
|
|
||||||
|
**Funcionalidades clave:**
|
||||||
|
|
||||||
|
- **Auto-despliegue del contrato:** En el primer arranque, si no existe un contrato desplegado, lo despliega automaticamente y guarda la direccion en la base de datos SQLite
|
||||||
|
- **Cola de nonces:** Sistema de cola para transacciones que previene colisiones cuando multiples operaciones ocurren simultaneamente
|
||||||
|
|
||||||
|
**Endpoints:**
|
||||||
|
|
||||||
|
| Metodo | Ruta | Descripcion | Autenticacion |
|
||||||
|
|---|---|---|---|
|
||||||
|
| POST | `/api/register` | Crea una wallet custodial para un jugador | `x-bridge-secret` |
|
||||||
|
| POST | `/api/deposit` | Acuna (mint) AFC a la wallet del jugador | `x-bridge-secret` |
|
||||||
|
| POST | `/api/withdraw` | Quema (burn) AFC de la wallet del jugador | `x-bridge-secret` |
|
||||||
|
| GET | `/api/balance/:diskId` | Lee el saldo on-chain del jugador | Ninguna |
|
||||||
|
| GET | `/api/wallet/:diskId` | Retorna direccion de wallet + clave privada para importar en MetaMask | Ninguna |
|
||||||
|
|
||||||
|
**Seguridad:**
|
||||||
|
|
||||||
|
- Los endpoints POST requieren el header `x-bridge-secret` con el secreto configurado en las variables de entorno
|
||||||
|
- Los endpoints GET son publicos para facilitar la consulta de saldos y datos de wallet
|
||||||
|
|
||||||
|
**Archivo estatico:**
|
||||||
|
|
||||||
|
- Sirve el icono del token en `/afc-icon.svg` para que MetaMask pueda mostrar el logo del token
|
||||||
|
|
||||||
|
### 4. Proxy SSL para RPC (docker/nginx/rpc-ssl.conf)
|
||||||
|
|
||||||
|
Proxy inverso Nginx que proporciona acceso HTTPS al nodo Geth.
|
||||||
|
|
||||||
|
| Parametro | Valor |
|
||||||
|
|---|---|
|
||||||
|
| Puerto externo | 8443 (HTTPS) |
|
||||||
|
| Puerto interno | 8545 (HTTP hacia Geth) |
|
||||||
|
| Certificado | Let's Encrypt via Cloudflare DNS challenge |
|
||||||
|
|
||||||
|
**Por que es necesario:** MetaMask en dispositivos moviles **rechaza conexiones HTTP** para endpoints RPC. El proxy SSL permite que los jugadores conecten sus wallets desde el telefono usando HTTPS.
|
||||||
|
|
||||||
|
### 5. Mainframe Lua (Computer 7)
|
||||||
|
|
||||||
|
El mainframe es el computador central del casino dentro de Minecraft (CC:Tweaked). Coordina todos los juegos y gestiona los saldos de los jugadores.
|
||||||
|
|
||||||
|
**Funciones principales:**
|
||||||
|
|
||||||
|
| Funcion | Descripcion |
|
||||||
|
|---|---|
|
||||||
|
| `addPlayer(diskId, name)` | Registra una nueva wallet en el bridge para el jugador |
|
||||||
|
| `getPlayerBalance(diskId)` | Sincroniza el saldo desde la blockchain (detecta transferencias hechas desde MetaMask) |
|
||||||
|
| `setPlayerBalance(diskId, amount)` | Calcula la diferencia con el saldo actual y ejecuta mint o burn segun corresponda |
|
||||||
|
|
||||||
|
**Caracteristicas tecnicas:**
|
||||||
|
|
||||||
|
- Helpers HTTP con `pcall` como fallback para manejar errores de red
|
||||||
|
- **Bucle de sincronizacion periodica** cada 30 segundos usando `parallel.waitForAll`
|
||||||
|
- Los juegos del casino (slots, blackjack, poker) **no necesitan modificaciones** -- se comunican con el mainframe via rednet y este se encarga de toda la logica blockchain
|
||||||
|
|
||||||
|
### 6. Generador de Tarjetas Lua (Computer 4)
|
||||||
|
|
||||||
|
Computador auxiliar que genera las tarjetas de jugador del casino.
|
||||||
|
|
||||||
|
Despues de crear una tarjeta, muestra en pantalla:
|
||||||
|
|
||||||
|
- La **direccion de wallet** del jugador
|
||||||
|
- Instrucciones para **conectar MetaMask** y ver el saldo de AFC
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Servicios Docker
|
||||||
|
|
||||||
|
| Servicio | Puertos | depends_on | Volumenes | Limite de Memoria |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `geth` | 8545:8545, 8546:8546 | -- | `geth_data:/root/.ethereum` | 1 GB |
|
||||||
|
| `afc-bridge` | 3001:3001 | `geth` | `afc_bridge_data:/app/data` | -- |
|
||||||
|
| `rpc-ssl` | 8443:8443 | `geth` | `certbot_etc:/etc/letsencrypt:ro` | -- |
|
||||||
|
|
||||||
|
Todos los servicios forman parte de la red Docker compartida con el servidor de Minecraft, permitiendo comunicacion interna por nombre de servicio (ej: `http://afc-bridge:3001`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Variables de Entorno
|
||||||
|
|
||||||
|
Todas las variables relacionadas con AfterCoin usan el prefijo `AFC_` y se definen en el archivo `.env` del directorio `docker/`.
|
||||||
|
|
||||||
|
| Variable | Descripcion | Ejemplo |
|
||||||
|
|---|---|---|
|
||||||
|
| `AFC_ADMIN_PRIVATE_KEY` | Clave privada de la wallet administradora (sellador + owner del contrato) | `0xabc123...` |
|
||||||
|
| `AFC_ADMIN_ADDRESS` | Direccion publica de la wallet administradora | `0x742d35Cc...` |
|
||||||
|
| `AFC_BRIDGE_SECRET` | Secreto compartido entre el mainframe y el Bridge API | `mi-secreto-seguro` |
|
||||||
|
| `AFC_CHAIN_ID` | ID de la cadena (debe coincidir con genesis.json) | `8888` |
|
||||||
|
| `AFC_RPC_URL` | URL interna del nodo Geth (dentro de Docker) | `http://geth:8545` |
|
||||||
|
| `AFC_CONTRACT_ADDRESS` | Direccion del contrato desplegado (se genera automaticamente en el primer arranque) | `0x5458...918C` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Guia de Conexion con MetaMask
|
||||||
|
|
||||||
|
### Escritorio (Extension)
|
||||||
|
|
||||||
|
1. Abrir MetaMask en el navegador
|
||||||
|
2. Ir a **Settings** (Configuracion) > **Networks** (Redes) > **Add Network** (Agregar red)
|
||||||
|
3. Rellenar los campos:
|
||||||
|
|
||||||
|
| Campo | Valor |
|
||||||
|
|---|---|
|
||||||
|
| Network Name | `AfterLife` |
|
||||||
|
| RPC URL | `http://play.consultoria-as.com:8545` |
|
||||||
|
| Chain ID | `8888` |
|
||||||
|
| Currency Symbol | `ETH` |
|
||||||
|
|
||||||
|
4. MetaMask mostrara una advertencia sobre el chain ID desconocido -- esto es **normal para cadenas privadas**, proceder de todas formas
|
||||||
|
5. **Importar la wallet del juego:**
|
||||||
|
- Menu de cuentas > **Import Account** (Importar cuenta)
|
||||||
|
- Pegar la clave privada obtenida del endpoint `/api/wallet/:diskId` o de la terminal del generador de tarjetas (Computer 4)
|
||||||
|
6. **Agregar el token AFC:**
|
||||||
|
- Click en **Import Tokens** (Importar tokens) > **Custom Token** (Token personalizado)
|
||||||
|
- Pegar la direccion del contrato: `0x54583A08C29556d16BA626cbA66101816D79918C`
|
||||||
|
- Simbolo: `AFC`
|
||||||
|
- Decimales: `0`
|
||||||
|
|
||||||
|
### Movil (App)
|
||||||
|
|
||||||
|
1. Abrir la app de MetaMask
|
||||||
|
2. Ir al **menu hamburguesa** > **Settings** > **Networks** > **Add Network**
|
||||||
|
3. Rellenar los campos:
|
||||||
|
|
||||||
|
| Campo | Valor |
|
||||||
|
|---|---|
|
||||||
|
| Network Name | `AfterLife` |
|
||||||
|
| RPC URL | `https://play.consultoria-as.com:8443` |
|
||||||
|
| Chain ID | `8888` |
|
||||||
|
| Currency Symbol | `ETH` |
|
||||||
|
|
||||||
|
> **IMPORTANTE:** En movil se DEBE usar la URL HTTPS (puerto 8443), no HTTP. MetaMask movil rechaza conexiones HTTP para endpoints RPC.
|
||||||
|
|
||||||
|
4. **Importar la wallet del juego:**
|
||||||
|
- Icono de cuenta > **Add account or hardware wallet** > **Import account**
|
||||||
|
- Pegar la clave privada
|
||||||
|
5. **Agregar el token AFC:**
|
||||||
|
- En la pantalla principal, hacer scroll hacia abajo
|
||||||
|
- **Import Tokens** > **Custom Token**
|
||||||
|
- Pegar la direccion del contrato: `0x54583A08C29556d16BA626cbA66101816D79918C`
|
||||||
|
|
||||||
|
### Vincular una Wallet Personal
|
||||||
|
|
||||||
|
Por defecto, el bridge crea **wallets custodiales** para cada jugador (el bridge genera y almacena las claves privadas). Si un jugador quiere vincular su propia wallet de MetaMask:
|
||||||
|
|
||||||
|
1. Actualizar la direccion en la base de datos del bridge:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec docker-afc-bridge-1 node -e "
|
||||||
|
const db = require('./src/db');
|
||||||
|
db.db.prepare('UPDATE wallets SET address = ? WHERE disk_id = ?').run('0xDIRECCION_DEL_JUGADOR', 'DISK_ID');
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Acunar (mint) el saldo actual del jugador a la nueva direccion para sincronizar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s localhost:3001/api/deposit \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-bridge-secret: TU_SECRETO" \
|
||||||
|
-d '{"diskId":"DISK_ID","amount":SALDO_ACTUAL}'
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Nota:** Al vincular una wallet personal, el jugador tendra control total sobre sus tokens y podra transferirlos libremente. Esto puede tener implicaciones en la economia del casino.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Administracion
|
||||||
|
|
||||||
|
### Comandos de Verificacion
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verificar el chain ID (debe retornar 0x22b8 = 8888)
|
||||||
|
curl -s -X POST localhost:8545 \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}'
|
||||||
|
|
||||||
|
# Registrar un jugador de prueba
|
||||||
|
curl -s localhost:3001/api/register \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-bridge-secret: SECRET" \
|
||||||
|
-d '{"diskId":"99","name":"Test"}'
|
||||||
|
|
||||||
|
# Depositar 50 AFC (= 50 diamantes) al jugador
|
||||||
|
curl -s localhost:3001/api/deposit \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-bridge-secret: SECRET" \
|
||||||
|
-d '{"diskId":"99","amount":50}'
|
||||||
|
|
||||||
|
# Consultar saldo de un jugador
|
||||||
|
curl -s localhost:3001/api/balance/99
|
||||||
|
|
||||||
|
# Obtener informacion de wallet (direccion + clave privada)
|
||||||
|
curl -s localhost:3001/api/wallet/99
|
||||||
|
|
||||||
|
# Listar todas las wallets registradas
|
||||||
|
docker exec docker-afc-bridge-1 node -e \
|
||||||
|
"const db=require('./src/db');db.getAllWallets().forEach(w=>console.log(w.disk_id,w.name,w.address))"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Operaciones Directas con Tokens
|
||||||
|
|
||||||
|
Para operaciones que requieren interaccion directa con el contrato inteligente (sin pasar por el Bridge API):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Acunar tokens directamente a cualquier direccion
|
||||||
|
cd /tmp && node -e "
|
||||||
|
const {ethers}=require('ethers');
|
||||||
|
const artifact=require('/home/AfterlifeProject/services/afc-bridge/contracts/AfterCoin.json');
|
||||||
|
async function main(){
|
||||||
|
const provider=new ethers.JsonRpcProvider('http://localhost:8545');
|
||||||
|
const admin=new ethers.Wallet('ADMIN_PRIVATE_KEY',provider);
|
||||||
|
const contract=new ethers.Contract('CONTRACT_ADDRESS',artifact.abi,admin);
|
||||||
|
await (await contract.mint('DIRECCION_DESTINO',CANTIDAD)).wait();
|
||||||
|
console.log('Saldo:',Number(await contract.balanceOf('DIRECCION_DESTINO')));
|
||||||
|
}
|
||||||
|
main();
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Nota:** Reemplazar `ADMIN_PRIVATE_KEY`, `CONTRACT_ADDRESS`, `DIRECCION_DESTINO` y `CANTIDAD` con los valores reales.
|
||||||
|
|
||||||
|
### Renovacion de Certificado SSL
|
||||||
|
|
||||||
|
El certificado de Let's Encrypt expira cada 90 dias. Para renovarlo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Renovar el certificado
|
||||||
|
docker run --rm \
|
||||||
|
-v docker_certbot_etc:/etc/letsencrypt \
|
||||||
|
-v /tmp/certbot/cloudflare.ini:/run/secrets/cloudflare.ini:ro \
|
||||||
|
certbot/dns-cloudflare:latest renew
|
||||||
|
|
||||||
|
# Reiniciar el proxy SSL para cargar el nuevo certificado
|
||||||
|
docker compose -f docker-compose.dev.yml restart rpc-ssl
|
||||||
|
```
|
||||||
|
|
||||||
|
### Whitelist HTTP de CC:Tweaked
|
||||||
|
|
||||||
|
La regla de whitelist en `computercraft-server.toml` (dentro del volumen de datos de Minecraft) permite que el mainframe se comunique con el Bridge API. Si Minecraft se reinstala, la regla se pierde y debe recrearse:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Agregar la regla de whitelist para afc-bridge
|
||||||
|
docker exec minecraft-ftb sed -i '/\$private/i \\t[[http.rules]]\n\t\t host = "afc-bridge"\n\t\t action = "allow"\n' /data/config/computercraft-server.toml
|
||||||
|
|
||||||
|
# Reiniciar el servidor de Minecraft para aplicar cambios
|
||||||
|
docker restart minecraft-ftb
|
||||||
|
```
|
||||||
|
|
||||||
|
> **IMPORTANTE:** La regla `allow` para `afc-bridge` debe aparecer **ANTES** de la regla `deny` para `$private` en el archivo de configuracion. De lo contrario, la conexion sera bloqueada por la regla de denegacion general.
|
||||||
|
|
||||||
|
### Despliegue de Scripts Lua
|
||||||
|
|
||||||
|
Para actualizar los scripts de los computadores CC:Tweaked dentro de Minecraft:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copiar el script del mainframe (Computer 7)
|
||||||
|
docker cp /tmp/mainframe_startup.lua minecraft-ftb:/data/world/computercraft/computer/7/startup.lua
|
||||||
|
|
||||||
|
# Copiar el script del generador de tarjetas (Computer 4)
|
||||||
|
docker cp /tmp/cardgen_startup.lua minecraft-ftb:/data/world/computercraft/computer/4/startup.lua
|
||||||
|
```
|
||||||
|
|
||||||
|
Luego, reiniciar los computadores dentro del juego presionando **Ctrl+R** en cada terminal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detalles del Contrato
|
||||||
|
|
||||||
|
| Campo | Valor |
|
||||||
|
|---|---|
|
||||||
|
| Direccion | `0x54583A08C29556d16BA626cbA66101816D79918C` |
|
||||||
|
| ABI | `services/afc-bridge/contracts/AfterCoin.json` |
|
||||||
|
| Codigo fuente | `blockchain/contracts/AfterCoin.sol` |
|
||||||
|
| Compilador | solcjs 0.8.34 |
|
||||||
|
| Target EVM | Paris (sin opcode PUSH0) |
|
||||||
|
| Desplegado en | Primer arranque del Bridge API |
|
||||||
|
| Almacenamiento de direccion | Base de datos SQLite del Bridge API |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solucion de Problemas
|
||||||
|
|
||||||
|
### "invalid opcode: PUSH0"
|
||||||
|
|
||||||
|
**Causa:** El contrato fue compilado para Shanghai EVM pero la cadena ejecuta Paris EVM. El opcode `PUSH0` fue introducido en Shanghai y no esta disponible en Geth v1.13 para cadenas privadas.
|
||||||
|
|
||||||
|
**Solucion:** Recompilar el contrato con la opcion `--evm-version paris`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
solcjs --bin --abi --evm-version paris AfterCoin.sol
|
||||||
|
```
|
||||||
|
|
||||||
|
### Geth termina por OOM (Out of Memory)
|
||||||
|
|
||||||
|
**Causa:** El nodo Geth supera el limite de memoria asignado (actualmente 1 GB).
|
||||||
|
|
||||||
|
**Solucion:** Incrementar el limite de memoria en el docker-compose. La flag `--lightkdf` ya esta habilitada para reducir el uso de memoria durante la importacion de claves.
|
||||||
|
|
||||||
|
### Bridge no puede conectar con Geth
|
||||||
|
|
||||||
|
**Causa:** El nodo Geth aun no ha terminado de inicializar cuando el bridge intenta conectarse.
|
||||||
|
|
||||||
|
**Solucion:** Verificar que `depends_on` esta configurado correctamente en docker-compose. El bridge incluye logica de reintentos (`waitForGeth()`) que espera a que Geth este disponible antes de continuar.
|
||||||
|
|
||||||
|
### CC:Tweaked bloquea peticiones HTTP
|
||||||
|
|
||||||
|
**Causa:** El archivo `computercraft-server.toml` no tiene la regla de whitelist para `afc-bridge`, o la regla esta ubicada despues de la regla de denegacion `$private`.
|
||||||
|
|
||||||
|
**Solucion:** Verificar que la regla `allow` para `afc-bridge` existe y esta posicionada **antes** de la regla `deny` para `$private`. Ver la seccion [Whitelist HTTP de CC:Tweaked](#whitelist-http-de-cctweaked) para los comandos de correccion.
|
||||||
|
|
||||||
|
### MetaMask movil no puede conectar
|
||||||
|
|
||||||
|
**Causa:** Se esta usando la URL HTTP (puerto 8545) en lugar de HTTPS (puerto 8443). MetaMask en dispositivos moviles requiere conexiones HTTPS para endpoints RPC.
|
||||||
|
|
||||||
|
**Solucion:** Cambiar la URL de la red en MetaMask a `https://play.consultoria-as.com:8443`.
|
||||||
|
|
||||||
|
### Transacciones fallan con "nonce too low"
|
||||||
|
|
||||||
|
**Causa:** Multiples transacciones se enviaron simultaneamente y los nonces colisionaron.
|
||||||
|
|
||||||
|
**Solucion:** El Bridge API incluye una cola de nonces que deberia prevenir esto. Si ocurre, reiniciar el servicio del bridge:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.dev.yml restart afc-bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
### El saldo en MetaMask no coincide con el casino
|
||||||
|
|
||||||
|
**Causa:** El jugador realizo una transferencia desde MetaMask que aun no ha sido sincronizada por el mainframe.
|
||||||
|
|
||||||
|
**Solucion:** El mainframe ejecuta un bucle de sincronizacion cada 30 segundos. Esperar a que se complete el siguiente ciclo, o forzar la sincronizacion reiniciando el Computer 7 con Ctrl+R en el juego.
|
||||||
96
servers/minecraft/lua-scripts/cardgen_startup.lua
Normal file
96
servers/minecraft/lua-scripts/cardgen_startup.lua
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
MAINFRAME_ID = 7
|
||||||
|
|
||||||
|
-- AfterCoin Bridge config
|
||||||
|
local BRIDGE_URL = "http://afc-bridge:3001"
|
||||||
|
|
||||||
|
function addPlayer(player, name)
|
||||||
|
rednet.send(MAINFRAME_ID, {type="addPlayer", player=player, name=name}, "otto")
|
||||||
|
rednet.receive("otto")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Get wallet info from bridge API
|
||||||
|
function getWalletInfo(diskId)
|
||||||
|
local ok, result = pcall(function()
|
||||||
|
local response = http.get(BRIDGE_URL .. "/api/wallet/" .. tostring(diskId))
|
||||||
|
if not response then return nil end
|
||||||
|
local data = textutils.unserialiseJSON(response.readAll())
|
||||||
|
response.close()
|
||||||
|
return data
|
||||||
|
end)
|
||||||
|
if ok and result and result.success then
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local drive = peripheral.wrap("top")
|
||||||
|
rednet.open("left")
|
||||||
|
|
||||||
|
while true do
|
||||||
|
term.clear()
|
||||||
|
term.setCursorPos(1,1)
|
||||||
|
term.setTextColor(colors.yellow)
|
||||||
|
print("=== Card Generator ===")
|
||||||
|
print("")
|
||||||
|
print("Insert a floppy disk")
|
||||||
|
print("in the drive above.")
|
||||||
|
|
||||||
|
os.pullEvent("disk")
|
||||||
|
os.sleep(0.5)
|
||||||
|
|
||||||
|
local player = drive.getDiskID()
|
||||||
|
if player then
|
||||||
|
term.setTextColor(colors.white)
|
||||||
|
print("")
|
||||||
|
term.write("Username: ")
|
||||||
|
local name = read()
|
||||||
|
term.setTextColor(colors.yellow)
|
||||||
|
print("Generating card for "..name.."...")
|
||||||
|
|
||||||
|
addPlayer(player, name)
|
||||||
|
drive.setDiskLabel(name.."'s L'Otto Card - $0")
|
||||||
|
local mountPath = drive.getMountPath()
|
||||||
|
if mountPath then
|
||||||
|
local filePath = fs.combine(mountPath, "bal")
|
||||||
|
local file = fs.open(filePath, "w")
|
||||||
|
if file then
|
||||||
|
file.write("0")
|
||||||
|
file.close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Display wallet info for MetaMask
|
||||||
|
term.setTextColor(colors.lime)
|
||||||
|
print("Card created!")
|
||||||
|
print("")
|
||||||
|
local wallet = getWalletInfo(player)
|
||||||
|
if wallet then
|
||||||
|
term.setTextColor(colors.cyan)
|
||||||
|
print("== AfterCoin Wallet ==")
|
||||||
|
term.setTextColor(colors.white)
|
||||||
|
print("Address:")
|
||||||
|
print(wallet.address)
|
||||||
|
print("")
|
||||||
|
print("To view in MetaMask:")
|
||||||
|
print("Network: AfterLife")
|
||||||
|
print("RPC: play.consultoria-as.com:8545")
|
||||||
|
print("Chain ID: 8888")
|
||||||
|
print("")
|
||||||
|
print("Import wallet key at:")
|
||||||
|
print("/api/wallet/" .. tostring(player))
|
||||||
|
print("")
|
||||||
|
term.setTextColor(colors.yellow)
|
||||||
|
print("Press any key to eject...")
|
||||||
|
os.pullEvent("key")
|
||||||
|
end
|
||||||
|
|
||||||
|
drive.ejectDisk()
|
||||||
|
term.setTextColor(colors.lime)
|
||||||
|
print("Ejected.")
|
||||||
|
else
|
||||||
|
term.setTextColor(colors.red)
|
||||||
|
print("ERROR: Could not read disk.")
|
||||||
|
os.sleep(3)
|
||||||
|
end
|
||||||
|
end
|
||||||
164
servers/minecraft/lua-scripts/mainframe_startup.lua
Normal file
164
servers/minecraft/lua-scripts/mainframe_startup.lua
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
rednet.open("left")
|
||||||
|
local databasePath = "players"
|
||||||
|
local database
|
||||||
|
|
||||||
|
-- AfterCoin Bridge config
|
||||||
|
local BRIDGE_URL = "http://afc-bridge:3001"
|
||||||
|
local BRIDGE_SECRET = "afterlife_bridge_dev_2024"
|
||||||
|
local SYNC_INTERVAL = 30 -- seconds between chain sync polls
|
||||||
|
|
||||||
|
-- HTTP helper: POST to bridge API
|
||||||
|
local function bridgePost(endpoint, body)
|
||||||
|
local url = BRIDGE_URL .. endpoint
|
||||||
|
local jsonBody = textutils.serialiseJSON(body)
|
||||||
|
local ok, result = pcall(function()
|
||||||
|
local response, failReason = http.post(url, jsonBody, {
|
||||||
|
["Content-Type"] = "application/json",
|
||||||
|
["x-bridge-secret"] = BRIDGE_SECRET
|
||||||
|
})
|
||||||
|
if not response then
|
||||||
|
print("[Bridge] POST failed: " .. tostring(failReason))
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local data = textutils.unserialiseJSON(response.readAll())
|
||||||
|
response.close()
|
||||||
|
return data
|
||||||
|
end)
|
||||||
|
if not ok then
|
||||||
|
print("[Bridge] POST error: " .. tostring(result))
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- HTTP helper: GET from bridge API
|
||||||
|
local function bridgeGet(endpoint)
|
||||||
|
local url = BRIDGE_URL .. endpoint
|
||||||
|
local ok, result = pcall(function()
|
||||||
|
local response, failReason = http.get(url)
|
||||||
|
if not response then
|
||||||
|
print("[Bridge] GET failed: " .. tostring(failReason))
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local data = textutils.unserialiseJSON(response.readAll())
|
||||||
|
response.close()
|
||||||
|
return data
|
||||||
|
end)
|
||||||
|
if not ok then
|
||||||
|
print("[Bridge] GET error: " .. tostring(result))
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Sync on-chain balance for a player (returns on-chain balance or nil)
|
||||||
|
local function syncFromChain(diskId)
|
||||||
|
local resp = bridgeGet("/api/balance/" .. tostring(diskId))
|
||||||
|
if resp and resp.success then
|
||||||
|
return resp.balance
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Save database to disk
|
||||||
|
local function saveDatabase()
|
||||||
|
local file = fs.open(databasePath, "w")
|
||||||
|
file.write(textutils.serialise(database))
|
||||||
|
file.close()
|
||||||
|
end
|
||||||
|
|
||||||
|
if not fs.exists(databasePath) then
|
||||||
|
database = {}
|
||||||
|
local file, err = fs.open(databasePath, "w")
|
||||||
|
if not file then
|
||||||
|
print("ERROR creating db: "..tostring(err))
|
||||||
|
print("Trying alternate path...")
|
||||||
|
databasePath = "/players.txt"
|
||||||
|
file, err = fs.open(databasePath, "w")
|
||||||
|
if not file then
|
||||||
|
print("FATAL: "..tostring(err))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
file.write("{}")
|
||||||
|
file.close()
|
||||||
|
else
|
||||||
|
local file = fs.open(databasePath, "r")
|
||||||
|
database = textutils.unserialise(file.readAll())
|
||||||
|
file.close()
|
||||||
|
end
|
||||||
|
|
||||||
|
print("Database loaded.")
|
||||||
|
print("AfterCoin bridge: " .. BRIDGE_URL)
|
||||||
|
|
||||||
|
-- Rednet message handler
|
||||||
|
local function messageLoop()
|
||||||
|
while true do
|
||||||
|
local id, data = rednet.receive("otto")
|
||||||
|
print(textutils.serialise(data))
|
||||||
|
if data.type == "getPlayerBalance" then
|
||||||
|
print("Fetching balance for ", data.player)
|
||||||
|
local chainBalance = syncFromChain(data.player)
|
||||||
|
if chainBalance and database[data.player] then
|
||||||
|
database[data.player].balance = chainBalance
|
||||||
|
end
|
||||||
|
rednet.send(id, database[data.player], "otto")
|
||||||
|
elseif data.type == "setPlayerBalance" then
|
||||||
|
print("Setting balance for ", data.player, " to ", data.balance)
|
||||||
|
local oldBalance = database[data.player].balance
|
||||||
|
local diff = data.balance - oldBalance
|
||||||
|
database[data.player].balance = data.balance
|
||||||
|
saveDatabase()
|
||||||
|
if diff > 0 then
|
||||||
|
print("[Bridge] Minting " .. diff .. " AFC")
|
||||||
|
bridgePost("/api/deposit", {diskId=tostring(data.player), amount=diff})
|
||||||
|
elseif diff < 0 then
|
||||||
|
print("[Bridge] Burning " .. math.abs(diff) .. " AFC")
|
||||||
|
bridgePost("/api/withdraw", {diskId=tostring(data.player), amount=math.abs(diff)})
|
||||||
|
end
|
||||||
|
rednet.send(id, nil, "otto")
|
||||||
|
elseif data.type == "addPlayer" then
|
||||||
|
print("Adding player: #"..data.player, data.name)
|
||||||
|
database[data.player] = {
|
||||||
|
name=data.name,
|
||||||
|
balance=0
|
||||||
|
}
|
||||||
|
saveDatabase()
|
||||||
|
print("[Bridge] Registering wallet for " .. data.name)
|
||||||
|
bridgePost("/api/register", {diskId=tostring(data.player), name=data.name})
|
||||||
|
rednet.send(id, nil, "otto")
|
||||||
|
elseif data.type == "getLeaderboard" then
|
||||||
|
print("Sending leaderboard")
|
||||||
|
local leaderboard = {}
|
||||||
|
for pid, pdata in pairs(database) do
|
||||||
|
table.insert(leaderboard, {name=pdata.name, balance=pdata.balance})
|
||||||
|
end
|
||||||
|
table.sort(leaderboard, function(a, b) return a.balance > b.balance end)
|
||||||
|
rednet.send(id, leaderboard, "otto")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Periodic chain sync loop
|
||||||
|
local function syncLoop()
|
||||||
|
while true do
|
||||||
|
os.sleep(SYNC_INTERVAL)
|
||||||
|
local changed = false
|
||||||
|
for pid, pdata in pairs(database) do
|
||||||
|
local chainBalance = syncFromChain(pid)
|
||||||
|
if chainBalance and chainBalance ~= pdata.balance then
|
||||||
|
print("[Sync] " .. pdata.name .. ": " .. pdata.balance .. " -> " .. chainBalance .. " AFC")
|
||||||
|
database[pid].balance = chainBalance
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if changed then
|
||||||
|
saveDatabase()
|
||||||
|
print("[Sync] Database updated from chain.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Run both loops in parallel
|
||||||
|
print("Starting message handler + chain sync (every " .. SYNC_INTERVAL .. "s)...")
|
||||||
|
parallel.waitForAll(messageLoop, syncLoop)
|
||||||
Reference in New Issue
Block a user