diff --git a/docs/aftercoin.md b/docs/aftercoin.md new file mode 100644 index 0000000..f18e4bf --- /dev/null +++ b/docs/aftercoin.md @@ -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. diff --git a/servers/minecraft/lua-scripts/cardgen_startup.lua b/servers/minecraft/lua-scripts/cardgen_startup.lua new file mode 100644 index 0000000..2b7cc39 --- /dev/null +++ b/servers/minecraft/lua-scripts/cardgen_startup.lua @@ -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 diff --git a/servers/minecraft/lua-scripts/mainframe_startup.lua b/servers/minecraft/lua-scripts/mainframe_startup.lua new file mode 100644 index 0000000..7dcb396 --- /dev/null +++ b/servers/minecraft/lua-scripts/mainframe_startup.lua @@ -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)