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