feat: add AfterCoin (AFC) private blockchain for Minecraft casino
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
Private Ethereum chain (Clique PoA, chain ID 8888) with ERC-20 token (0 decimals, 1 AFC = 1 diamond) bridging casino balances on-chain so players can view tokens in MetaMask. - Geth v1.13.15 node with 5s block time, zero gas cost - AfterCoin ERC-20 contract with owner-gated mint/burn/bridgeTransfer - Bridge API (Express + ethers.js + SQLite) with register, deposit, withdraw, balance, and wallet endpoints - Nonce queue for serial transaction safety - Auto-deploys contract on first boot - Updated mainframe Lua with diff-based on-chain sync (pcall fallback) - Updated card generator Lua with wallet info display Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
12
blockchain/Dockerfile
Normal file
12
blockchain/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
FROM ethereum/client-go:v1.13.15
|
||||||
|
|
||||||
|
COPY genesis.json /app/genesis.json
|
||||||
|
COPY init-geth.sh /app/init-geth.sh
|
||||||
|
|
||||||
|
RUN chmod +x /app/init-geth.sh
|
||||||
|
|
||||||
|
EXPOSE 8545 8546
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/init-geth.sh"]
|
||||||
183
blockchain/contracts/AfterCoin.sol
Normal file
183
blockchain/contracts/AfterCoin.sol
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.8.20;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @title AfterCoin (AFC)
|
||||||
|
* @notice ERC-20 token for the Afterlife Project game preservation platform.
|
||||||
|
* 1 AFC = 1 diamond. Zero decimals — integer-only balances.
|
||||||
|
* @dev Self-contained implementation (no OpenZeppelin). Owner-gated mint,
|
||||||
|
* burn-from, and bridge-transfer helpers for the off-chain bridge service.
|
||||||
|
*/
|
||||||
|
contract AfterCoin {
|
||||||
|
|
||||||
|
// ──────────────────────────── ERC-20 metadata ────────────────────────────
|
||||||
|
|
||||||
|
string private constant _name = "AfterCoin";
|
||||||
|
string private constant _symbol = "AFC";
|
||||||
|
uint8 private constant _decimals = 0; // 1 token = 1 diamond
|
||||||
|
|
||||||
|
// ──────────────────────────── State ──────────────────────────────────────
|
||||||
|
|
||||||
|
uint256 private _totalSupply;
|
||||||
|
|
||||||
|
mapping(address => uint256) private _balances;
|
||||||
|
mapping(address => mapping(address => uint256)) private _allowances;
|
||||||
|
|
||||||
|
address public owner;
|
||||||
|
|
||||||
|
// ──────────────────────────── Events (ERC-20) ────────────────────────────
|
||||||
|
|
||||||
|
event Transfer(address indexed from, address indexed to, uint256 value);
|
||||||
|
event Approval(address indexed owner, address indexed spender, uint256 value);
|
||||||
|
|
||||||
|
// ──────────────────────────── Errors ─────────────────────────────────────
|
||||||
|
|
||||||
|
error NotOwner();
|
||||||
|
error ZeroAddress();
|
||||||
|
error InsufficientBalance(address account, uint256 required, uint256 available);
|
||||||
|
error InsufficientAllowance(address spender, uint256 required, uint256 available);
|
||||||
|
|
||||||
|
// ──────────────────────────── Modifier ───────────────────────────────────
|
||||||
|
|
||||||
|
modifier onlyOwner() {
|
||||||
|
if (msg.sender != owner) revert NotOwner();
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────── Constructor ────────────────────────────────
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
owner = msg.sender;
|
||||||
|
// Initial supply is 0 — tokens are minted on demand by the bridge.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────── ERC-20 view functions ──────────────────────
|
||||||
|
|
||||||
|
function name() external pure returns (string memory) {
|
||||||
|
return _name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function symbol() external pure returns (string memory) {
|
||||||
|
return _symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decimals() external pure returns (uint8) {
|
||||||
|
return _decimals;
|
||||||
|
}
|
||||||
|
|
||||||
|
function totalSupply() external view returns (uint256) {
|
||||||
|
return _totalSupply;
|
||||||
|
}
|
||||||
|
|
||||||
|
function balanceOf(address account) external view returns (uint256) {
|
||||||
|
return _balances[account];
|
||||||
|
}
|
||||||
|
|
||||||
|
function allowance(address tokenOwner, address spender) external view returns (uint256) {
|
||||||
|
return _allowances[tokenOwner][spender];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────── ERC-20 mutative functions ──────────────────
|
||||||
|
|
||||||
|
function transfer(address to, uint256 amount) external returns (bool) {
|
||||||
|
_transfer(msg.sender, to, amount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function approve(address spender, uint256 amount) external returns (bool) {
|
||||||
|
_approve(msg.sender, spender, amount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transferFrom(
|
||||||
|
address from,
|
||||||
|
address to,
|
||||||
|
uint256 amount
|
||||||
|
) external returns (bool) {
|
||||||
|
uint256 currentAllowance = _allowances[from][msg.sender];
|
||||||
|
if (currentAllowance != type(uint256).max) {
|
||||||
|
if (currentAllowance < amount) {
|
||||||
|
revert InsufficientAllowance(msg.sender, amount, currentAllowance);
|
||||||
|
}
|
||||||
|
unchecked {
|
||||||
|
_approve(from, msg.sender, currentAllowance - amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_transfer(from, to, amount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────── Owner-only functions ───────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Mint new tokens to `to`. Only callable by the contract owner.
|
||||||
|
* @param to Recipient address.
|
||||||
|
* @param amount Number of tokens to create.
|
||||||
|
*/
|
||||||
|
function mint(address to, uint256 amount) external onlyOwner {
|
||||||
|
if (to == address(0)) revert ZeroAddress();
|
||||||
|
_totalSupply += amount;
|
||||||
|
_balances[to] += amount;
|
||||||
|
emit Transfer(address(0), to, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Burn tokens from `from`. Only callable by the contract owner.
|
||||||
|
* Does NOT require an allowance — the owner is the bridge operator.
|
||||||
|
* @param from Address whose tokens are burned.
|
||||||
|
* @param amount Number of tokens to destroy.
|
||||||
|
*/
|
||||||
|
function burnFrom(address from, uint256 amount) external onlyOwner {
|
||||||
|
if (from == address(0)) revert ZeroAddress();
|
||||||
|
uint256 bal = _balances[from];
|
||||||
|
if (bal < amount) {
|
||||||
|
revert InsufficientBalance(from, amount, bal);
|
||||||
|
}
|
||||||
|
unchecked {
|
||||||
|
_balances[from] = bal - amount;
|
||||||
|
}
|
||||||
|
_totalSupply -= amount;
|
||||||
|
emit Transfer(from, address(0), amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Transfer tokens between two addresses on behalf of the bridge.
|
||||||
|
* Only callable by the contract owner.
|
||||||
|
* @param from Source address.
|
||||||
|
* @param to Destination address.
|
||||||
|
* @param amount Number of tokens to move.
|
||||||
|
*/
|
||||||
|
function bridgeTransfer(
|
||||||
|
address from,
|
||||||
|
address to,
|
||||||
|
uint256 amount
|
||||||
|
) external onlyOwner {
|
||||||
|
_transfer(from, to, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────── Internal helpers ───────────────────────────
|
||||||
|
|
||||||
|
function _transfer(address from, address to, uint256 amount) internal {
|
||||||
|
if (from == address(0)) revert ZeroAddress();
|
||||||
|
if (to == address(0)) revert ZeroAddress();
|
||||||
|
|
||||||
|
uint256 fromBal = _balances[from];
|
||||||
|
if (fromBal < amount) {
|
||||||
|
revert InsufficientBalance(from, amount, fromBal);
|
||||||
|
}
|
||||||
|
unchecked {
|
||||||
|
_balances[from] = fromBal - amount;
|
||||||
|
}
|
||||||
|
_balances[to] += amount;
|
||||||
|
|
||||||
|
emit Transfer(from, to, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _approve(address tokenOwner, address spender, uint256 amount) internal {
|
||||||
|
if (tokenOwner == address(0)) revert ZeroAddress();
|
||||||
|
if (spender == address(0)) revert ZeroAddress();
|
||||||
|
|
||||||
|
_allowances[tokenOwner][spender] = amount;
|
||||||
|
emit Approval(tokenOwner, spender, amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
blockchain/genesis.json
Normal file
27
blockchain/genesis.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"chainId": 8888,
|
||||||
|
"homesteadBlock": 0,
|
||||||
|
"eip150Block": 0,
|
||||||
|
"eip155Block": 0,
|
||||||
|
"eip158Block": 0,
|
||||||
|
"byzantiumBlock": 0,
|
||||||
|
"constantinopleBlock": 0,
|
||||||
|
"petersburgBlock": 0,
|
||||||
|
"istanbulBlock": 0,
|
||||||
|
"berlinBlock": 0,
|
||||||
|
"londonBlock": 0,
|
||||||
|
"clique": {
|
||||||
|
"period": 5,
|
||||||
|
"epoch": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"difficulty": "0x1",
|
||||||
|
"gasLimit": "0x1C9C380",
|
||||||
|
"extradata": "0x0000000000000000000000000000000000000000000000000000000000000000751c6F0Efd9B97A004969cfF9ACfA32230bdC4c40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
"alloc": {
|
||||||
|
"0x751c6F0Efd9B97A004969cfF9ACfA32230bdC4c4": {
|
||||||
|
"balance": "0xffffffffffffffff"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
blockchain/init-geth.sh
Executable file
50
blockchain/init-geth.sh
Executable file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
GENESIS_FILE="/app/genesis.json"
|
||||||
|
DATADIR="/data"
|
||||||
|
|
||||||
|
# Initialize geth datadir if not already done
|
||||||
|
if [ ! -d "$DATADIR/geth/chaindata" ]; then
|
||||||
|
echo "Initializing geth datadir with genesis block..."
|
||||||
|
geth init --datadir "$DATADIR" "$GENESIS_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Import admin private key if provided and no accounts exist yet
|
||||||
|
if [ -n "$ADMIN_PRIVATE_KEY" ]; then
|
||||||
|
EXISTING_ACCOUNTS=$(geth account list --datadir "$DATADIR" 2>/dev/null || true)
|
||||||
|
if [ -z "$EXISTING_ACCOUNTS" ]; then
|
||||||
|
echo "Importing admin private key..."
|
||||||
|
TMPKEY=$(mktemp)
|
||||||
|
echo "$ADMIN_PRIVATE_KEY" > "$TMPKEY"
|
||||||
|
geth account import --datadir "$DATADIR" --password /dev/null --lightkdf "$TMPKEY"
|
||||||
|
rm -f "$TMPKEY"
|
||||||
|
else
|
||||||
|
echo "Account(s) already exist, skipping import."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting geth node..."
|
||||||
|
exec geth \
|
||||||
|
--datadir "$DATADIR" \
|
||||||
|
--networkid 8888 \
|
||||||
|
--http \
|
||||||
|
--http.addr 0.0.0.0 \
|
||||||
|
--http.port 8545 \
|
||||||
|
--http.api eth,net,web3,personal,txpool \
|
||||||
|
--http.corsdomain "*" \
|
||||||
|
--http.vhosts "*" \
|
||||||
|
--ws \
|
||||||
|
--ws.addr 0.0.0.0 \
|
||||||
|
--ws.port 8546 \
|
||||||
|
--ws.api eth,net,web3 \
|
||||||
|
--ws.origins "*" \
|
||||||
|
--mine \
|
||||||
|
--miner.etherbase "$ADMIN_ADDRESS" \
|
||||||
|
--unlock "$ADMIN_ADDRESS" \
|
||||||
|
--password /dev/null \
|
||||||
|
--allow-insecure-unlock \
|
||||||
|
--nodiscover \
|
||||||
|
--maxpeers 0 \
|
||||||
|
--syncmode full \
|
||||||
|
--gcmode archive
|
||||||
@@ -24,3 +24,9 @@ PUBLIC_HOST=play.yourdomain.com
|
|||||||
# Cloudflare API Token (create at https://dash.cloudflare.com/profile/api-tokens)
|
# Cloudflare API Token (create at https://dash.cloudflare.com/profile/api-tokens)
|
||||||
# Permissions needed: Zone > DNS > Edit
|
# Permissions needed: Zone > DNS > Edit
|
||||||
CF_API_TOKEN=your_cloudflare_api_token
|
CF_API_TOKEN=your_cloudflare_api_token
|
||||||
|
|
||||||
|
# AfterCoin Blockchain (private Ethereum chain for casino tokens)
|
||||||
|
# Generate with: node -e "const {ethers}=require('ethers');const w=ethers.Wallet.createRandom();console.log(w.address,w.privateKey)"
|
||||||
|
AFC_ADMIN_ADDRESS=0xYOUR_ADMIN_ADDRESS
|
||||||
|
AFC_ADMIN_PRIVATE_KEY=your_private_key_without_0x_prefix
|
||||||
|
AFC_BRIDGE_SECRET=change_me_in_production
|
||||||
|
|||||||
@@ -116,8 +116,47 @@ services:
|
|||||||
limits:
|
limits:
|
||||||
memory: 8G
|
memory: 8G
|
||||||
|
|
||||||
|
geth:
|
||||||
|
build:
|
||||||
|
context: ../blockchain
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
ADMIN_PRIVATE_KEY: ${AFC_ADMIN_PRIVATE_KEY}
|
||||||
|
ADMIN_ADDRESS: ${AFC_ADMIN_ADDRESS}
|
||||||
|
ports:
|
||||||
|
- "8545:8545"
|
||||||
|
- "8546:8546"
|
||||||
|
volumes:
|
||||||
|
- geth_data:/data
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 1G
|
||||||
|
|
||||||
|
afc-bridge:
|
||||||
|
build:
|
||||||
|
context: ../services/afc-bridge
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- geth
|
||||||
|
environment:
|
||||||
|
GETH_RPC_URL: http://geth:8545
|
||||||
|
ADMIN_PRIVATE_KEY: ${AFC_ADMIN_PRIVATE_KEY}
|
||||||
|
BRIDGE_SECRET: ${AFC_BRIDGE_SECRET}
|
||||||
|
PORT: 3001
|
||||||
|
DB_PATH: /data/bridge.db
|
||||||
|
GAS_FUND_AMOUNT: "0.01"
|
||||||
|
ports:
|
||||||
|
- "3001:3001"
|
||||||
|
volumes:
|
||||||
|
- afc_bridge_data:/data
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
minio_data:
|
minio_data:
|
||||||
openfusion_data:
|
openfusion_data:
|
||||||
minecraft_ftb_data:
|
minecraft_ftb_data:
|
||||||
|
geth_data:
|
||||||
|
afc_bridge_data:
|
||||||
|
|||||||
13
services/afc-bridge/Dockerfile
Normal file
13
services/afc-bridge/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm install --production
|
||||||
|
|
||||||
|
COPY contracts/ ./contracts/
|
||||||
|
COPY src/ ./src/
|
||||||
|
|
||||||
|
EXPOSE 3001
|
||||||
|
|
||||||
|
CMD ["node", "src/index.js"]
|
||||||
357
services/afc-bridge/contracts/AfterCoin.json
Normal file
357
services/afc-bridge/contracts/AfterCoin.json
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
{
|
||||||
|
"contractName": "AfterCoin",
|
||||||
|
"abi": [
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "constructor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "spender",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "required",
|
||||||
|
"type": "uint256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "available",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "InsufficientAllowance",
|
||||||
|
"type": "error"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "account",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "required",
|
||||||
|
"type": "uint256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "available",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "InsufficientBalance",
|
||||||
|
"type": "error"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"name": "NotOwner",
|
||||||
|
"type": "error"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"name": "ZeroAddress",
|
||||||
|
"type": "error"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"indexed": true,
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "owner",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": true,
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "spender",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": false,
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "value",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "Approval",
|
||||||
|
"type": "event"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"indexed": true,
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "from",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": true,
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "to",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": false,
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "value",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "Transfer",
|
||||||
|
"type": "event"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "tokenOwner",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "spender",
|
||||||
|
"type": "address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "allowance",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "spender",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "amount",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "approve",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "bool",
|
||||||
|
"name": "",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "account",
|
||||||
|
"type": "address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "balanceOf",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "from",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "to",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "amount",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "bridgeTransfer",
|
||||||
|
"outputs": [],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "from",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "amount",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "burnFrom",
|
||||||
|
"outputs": [],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"name": "decimals",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "uint8",
|
||||||
|
"name": "",
|
||||||
|
"type": "uint8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "pure",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "to",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "amount",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "mint",
|
||||||
|
"outputs": [],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"name": "name",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "string",
|
||||||
|
"name": "",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "pure",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"name": "owner",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "",
|
||||||
|
"type": "address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"name": "symbol",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "string",
|
||||||
|
"name": "",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "pure",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"name": "totalSupply",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "to",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "amount",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "transfer",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "bool",
|
||||||
|
"name": "",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "from",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "to",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "amount",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "transferFrom",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "bool",
|
||||||
|
"name": "",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bytecode": "0x6080604052348015600f57600080fd5b50600380546001600160a01b031916331790556108ac806100316000396000f3fe608060405234801561001057600080fd5b50600436106100cf5760003560e01c8063671afbce1161008c5780638da5cb5b116100665780638da5cb5b146101c657806395d89b41146101f1578063a9059cbb14610210578063dd62ed3e1461022357600080fd5b8063671afbce1461017757806370a082311461018a57806379cc6790146101b357600080fd5b806306fdde03146100d4578063095ea7b31461010b57806318160ddd1461012e57806323b872dd14610140578063313ce5671461015357806340c10f1914610162575b600080fd5b60408051808201909152600981526820b33a32b921b7b4b760b91b60208201525b6040516101029190610714565b60405180910390f35b61011e61011936600461077e565b61025c565b6040519015158152602001610102565b6000545b604051908152602001610102565b61011e61014e3660046107a8565b610273565b60405160008152602001610102565b61017561017036600461077e565b6102f9565b005b6101756101853660046107a8565b6103d3565b6101326101983660046107e5565b6001600160a01b031660009081526001602052604090205490565b6101756101c136600461077e565b61040e565b6003546101d9906001600160a01b031681565b6040516001600160a01b039091168152602001610102565b60408051808201909152600381526241464360e81b60208201526100f5565b61011e61021e36600461077e565b61052e565b610132610231366004610807565b6001600160a01b03918216600090815260026020908152604080832093909416825291909152205490565b600061026933848461053b565b5060015b92915050565b6001600160a01b038316600090815260026020908152604080832033845290915281205460001981146102e357828110156102d657604051630c95cf2760e11b815233600482015260248101849052604481018290526064015b60405180910390fd5b6102e3853385840361053b565b6102ee8585856105e2565b506001949350505050565b6003546001600160a01b03163314610324576040516330cd747160e01b815260040160405180910390fd5b6001600160a01b03821661034b5760405163d92e233d60e01b815260040160405180910390fd5b8060008082825461035c9190610850565b90915550506001600160a01b03821660009081526001602052604081208054839290610389908490610850565b90915550506040518181526001600160a01b038316906000907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9060200160405180910390a35050565b6003546001600160a01b031633146103fe576040516330cd747160e01b815260040160405180910390fd5b6104098383836105e2565b505050565b6003546001600160a01b03163314610439576040516330cd747160e01b815260040160405180910390fd5b6001600160a01b0382166104605760405163d92e233d60e01b815260040160405180910390fd5b6001600160a01b038216600090815260016020526040902054818110156104b35760405163db42144d60e01b81526001600160a01b038416600482015260248101839052604481018290526064016102cd565b6001600160a01b03831660009081526001602052604081208383039055805483919081906104e2908490610863565b90915550506040518281526000906001600160a01b038516907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef906020015b60405180910390a3505050565b60006102693384846105e2565b6001600160a01b0383166105625760405163d92e233d60e01b815260040160405180910390fd5b6001600160a01b0382166105895760405163d92e233d60e01b815260040160405180910390fd5b6001600160a01b0383811660008181526002602090815260408083209487168084529482529182902085905590518481527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259101610521565b6001600160a01b0383166106095760405163d92e233d60e01b815260040160405180910390fd5b6001600160a01b0382166106305760405163d92e233d60e01b815260040160405180910390fd5b6001600160a01b038316600090815260016020526040902054818110156106835760405163db42144d60e01b81526001600160a01b038516600482015260248101839052604481018290526064016102cd565b6001600160a01b038085166000908152600160205260408082208585039055918516815290812080548492906106ba908490610850565b92505081905550826001600160a01b0316846001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8460405161070691815260200190565b60405180910390a350505050565b602081526000825180602084015260005b818110156107425760208186018101516040868401015201610725565b506000604082850101526040601f19601f83011684010191505092915050565b80356001600160a01b038116811461077957600080fd5b919050565b6000806040838503121561079157600080fd5b61079a83610762565b946020939093013593505050565b6000806000606084860312156107bd57600080fd5b6107c684610762565b92506107d460208501610762565b929592945050506040919091013590565b6000602082840312156107f757600080fd5b61080082610762565b9392505050565b6000806040838503121561081a57600080fd5b61082383610762565b915061083160208401610762565b90509250929050565b634e487b7160e01b600052601160045260246000fd5b8082018082111561026d5761026d61083a565b8181038181111561026d5761026d61083a56fea2646970667358221220385d4e7aa6f29433bdf52f6b52498c4501151f35e2ecf304acc5bb0007e5327264736f6c63430008220033"
|
||||||
|
}
|
||||||
1358
services/afc-bridge/package-lock.json
generated
Normal file
1358
services/afc-bridge/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
services/afc-bridge/package.json
Normal file
15
services/afc-bridge/package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "afc-bridge",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "AfterCoin bridge API - syncs Minecraft casino balances to private Ethereum chain",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node src/index.js",
|
||||||
|
"deploy": "node src/deploy.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.21.0",
|
||||||
|
"ethers": "^6.13.0",
|
||||||
|
"better-sqlite3": "^11.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
23
services/afc-bridge/src/config.js
Normal file
23
services/afc-bridge/src/config.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
if (!process.env.ADMIN_PRIVATE_KEY) {
|
||||||
|
throw new Error("ADMIN_PRIVATE_KEY environment variable is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.env.BRIDGE_SECRET) {
|
||||||
|
throw new Error("BRIDGE_SECRET environment variable is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize private key to have 0x prefix for ethers.js
|
||||||
|
const rawKey = process.env.ADMIN_PRIVATE_KEY;
|
||||||
|
const normalizedKey = rawKey.startsWith("0x") ? rawKey : "0x" + rawKey;
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
GETH_RPC_URL: process.env.GETH_RPC_URL || "http://geth:8545",
|
||||||
|
ADMIN_PRIVATE_KEY: normalizedKey,
|
||||||
|
AFC_CONTRACT_ADDRESS: process.env.AFC_CONTRACT_ADDRESS || "",
|
||||||
|
BRIDGE_SECRET: process.env.BRIDGE_SECRET,
|
||||||
|
PORT: parseInt(process.env.PORT) || 3001,
|
||||||
|
DB_PATH: process.env.DB_PATH || "/data/bridge.db",
|
||||||
|
GAS_FUND_AMOUNT: process.env.GAS_FUND_AMOUNT || "0.01",
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
64
services/afc-bridge/src/db.js
Normal file
64
services/afc-bridge/src/db.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
const Database = require("better-sqlite3");
|
||||||
|
const config = require("./config");
|
||||||
|
|
||||||
|
const db = new Database(config.DB_PATH);
|
||||||
|
|
||||||
|
// Enable WAL mode for better concurrent read performance
|
||||||
|
db.pragma("journal_mode = WAL");
|
||||||
|
|
||||||
|
// Create tables if they don't exist
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS wallets (
|
||||||
|
disk_id TEXT PRIMARY KEY,
|
||||||
|
address TEXT NOT NULL,
|
||||||
|
private_key TEXT NOT NULL,
|
||||||
|
name TEXT,
|
||||||
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS contract_state (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
function getWallet(diskId) {
|
||||||
|
const stmt = db.prepare("SELECT * FROM wallets WHERE disk_id = ?");
|
||||||
|
return stmt.get(diskId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWallet(diskId, address, privateKey, name) {
|
||||||
|
const stmt = db.prepare(
|
||||||
|
"INSERT INTO wallets (disk_id, address, private_key, name) VALUES (?, ?, ?, ?)"
|
||||||
|
);
|
||||||
|
return stmt.run(diskId, address, privateKey, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContractAddress() {
|
||||||
|
const stmt = db.prepare(
|
||||||
|
"SELECT value FROM contract_state WHERE key = 'contract_address'"
|
||||||
|
);
|
||||||
|
const row = stmt.get();
|
||||||
|
return row ? row.value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setContractAddress(address) {
|
||||||
|
const stmt = db.prepare(
|
||||||
|
"INSERT OR REPLACE INTO contract_state (key, value) VALUES ('contract_address', ?)"
|
||||||
|
);
|
||||||
|
return stmt.run(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllWallets() {
|
||||||
|
const stmt = db.prepare("SELECT * FROM wallets");
|
||||||
|
return stmt.all();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
db,
|
||||||
|
getWallet,
|
||||||
|
createWallet,
|
||||||
|
getContractAddress,
|
||||||
|
setContractAddress,
|
||||||
|
getAllWallets,
|
||||||
|
};
|
||||||
33
services/afc-bridge/src/deploy.js
Normal file
33
services/afc-bridge/src/deploy.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const config = require("./config");
|
||||||
|
const db = require("./db");
|
||||||
|
const { waitForGeth, deployContract } = require("./ethereum");
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log("=== AfterCoin Contract Deployment ===");
|
||||||
|
|
||||||
|
// Wait for the Geth node to be reachable
|
||||||
|
await waitForGeth();
|
||||||
|
|
||||||
|
// Check if already deployed
|
||||||
|
const existing = db.getContractAddress();
|
||||||
|
if (existing) {
|
||||||
|
console.log("Contract already deployed at:", existing);
|
||||||
|
console.log(
|
||||||
|
"To redeploy, clear the contract_state table in the database."
|
||||||
|
);
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deploy the contract
|
||||||
|
const address = await deployContract();
|
||||||
|
console.log("=== Deployment complete ===");
|
||||||
|
console.log("Contract address:", address);
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.then(() => process.exit(0))
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Deployment failed:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
64
services/afc-bridge/src/ethereum.js
Normal file
64
services/afc-bridge/src/ethereum.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
const { ethers } = require("ethers");
|
||||||
|
const path = require("path");
|
||||||
|
const config = require("./config");
|
||||||
|
const db = require("./db");
|
||||||
|
|
||||||
|
const abiPath = path.join(__dirname, "..", "contracts", "AfterCoin.json");
|
||||||
|
const artifact = require(abiPath);
|
||||||
|
|
||||||
|
const provider = new ethers.JsonRpcProvider(config.GETH_RPC_URL);
|
||||||
|
const adminWallet = new ethers.Wallet(config.ADMIN_PRIVATE_KEY, provider);
|
||||||
|
|
||||||
|
function getContract() {
|
||||||
|
const address = db.getContractAddress() || config.AFC_CONTRACT_ADDRESS;
|
||||||
|
if (!address) {
|
||||||
|
throw new Error(
|
||||||
|
"AfterCoin contract not deployed yet. Run 'npm run deploy' first."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new ethers.Contract(address, artifact.abi, adminWallet);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deployContract() {
|
||||||
|
console.log("Deploying AfterCoin contract...");
|
||||||
|
console.log("Admin address:", adminWallet.address);
|
||||||
|
|
||||||
|
const factory = new ethers.ContractFactory(
|
||||||
|
artifact.abi,
|
||||||
|
artifact.bytecode,
|
||||||
|
adminWallet
|
||||||
|
);
|
||||||
|
|
||||||
|
const contract = await factory.deploy();
|
||||||
|
await contract.waitForDeployment();
|
||||||
|
|
||||||
|
const address = await contract.getAddress();
|
||||||
|
db.setContractAddress(address);
|
||||||
|
|
||||||
|
console.log("AfterCoin deployed at:", address);
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForGeth(retryInterval = 5000, maxRetries = 60) {
|
||||||
|
for (let i = 0; i < maxRetries; i++) {
|
||||||
|
try {
|
||||||
|
const blockNumber = await provider.getBlockNumber();
|
||||||
|
console.log(`Geth is ready (block #${blockNumber})`);
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(
|
||||||
|
`Waiting for Geth... attempt ${i + 1}/${maxRetries} (${err.message})`
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, retryInterval));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`Geth did not respond after ${maxRetries} retries`);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
provider,
|
||||||
|
adminWallet,
|
||||||
|
getContract,
|
||||||
|
deployContract,
|
||||||
|
waitForGeth,
|
||||||
|
};
|
||||||
79
services/afc-bridge/src/index.js
Normal file
79
services/afc-bridge/src/index.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const config = require("./config");
|
||||||
|
const db = require("./db");
|
||||||
|
const { waitForGeth, deployContract, getContract } = require("./ethereum");
|
||||||
|
|
||||||
|
const registerRouter = require("./routes/register");
|
||||||
|
const depositRouter = require("./routes/deposit");
|
||||||
|
const withdrawRouter = require("./routes/withdraw");
|
||||||
|
const balanceRouter = require("./routes/balance");
|
||||||
|
const walletRouter = require("./routes/wallet");
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Parse JSON request bodies
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Auth middleware — only applies to state-changing (POST) routes
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const secret = req.headers["x-bridge-secret"];
|
||||||
|
if (secret !== config.BRIDGE_SECRET) {
|
||||||
|
return res
|
||||||
|
.status(401)
|
||||||
|
.json({ success: false, error: "Unauthorized: invalid bridge secret" });
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mount routes
|
||||||
|
app.use(registerRouter);
|
||||||
|
app.use(depositRouter);
|
||||||
|
app.use(withdrawRouter);
|
||||||
|
app.use(balanceRouter);
|
||||||
|
app.use(walletRouter);
|
||||||
|
|
||||||
|
// Global error handler
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error("Unhandled error:", err);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: err.message || "Internal server error",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Startup sequence
|
||||||
|
async function start() {
|
||||||
|
// 1. Wait for Geth node to be reachable
|
||||||
|
await waitForGeth();
|
||||||
|
|
||||||
|
// 2. Deploy contract if not already deployed
|
||||||
|
const existing = db.getContractAddress();
|
||||||
|
if (!existing) {
|
||||||
|
console.log("No contract found in database, deploying...");
|
||||||
|
await deployContract();
|
||||||
|
} else {
|
||||||
|
console.log("Contract already deployed at:", existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the contract is accessible
|
||||||
|
const contract = getContract();
|
||||||
|
const contractAddress = await contract.getAddress();
|
||||||
|
|
||||||
|
// 3. Start listening
|
||||||
|
app.listen(config.PORT, () => {
|
||||||
|
console.log("=== AFC Bridge API ===");
|
||||||
|
console.log(`Listening on port ${config.PORT}`);
|
||||||
|
console.log(`AfterCoin contract: ${contractAddress}`);
|
||||||
|
console.log(`Geth RPC: ${config.GETH_RPC_URL}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
start().catch((err) => {
|
||||||
|
console.error("Failed to start AFC Bridge:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
31
services/afc-bridge/src/routes/balance.js
Normal file
31
services/afc-bridge/src/routes/balance.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
const { Router } = require("express");
|
||||||
|
const db = require("../db");
|
||||||
|
const { getContract } = require("../ethereum");
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get("/api/balance/:diskId", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { diskId } = req.params;
|
||||||
|
|
||||||
|
const wallet = db.getWallet(diskId);
|
||||||
|
if (!wallet) {
|
||||||
|
return res
|
||||||
|
.status(404)
|
||||||
|
.json({ success: false, error: "Wallet not found for this diskId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const contract = getContract();
|
||||||
|
const balance = await contract.balanceOf(wallet.address);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
diskId,
|
||||||
|
balance: Number(balance),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
54
services/afc-bridge/src/routes/deposit.js
Normal file
54
services/afc-bridge/src/routes/deposit.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
const { Router } = require("express");
|
||||||
|
const db = require("../db");
|
||||||
|
const { getContract } = require("../ethereum");
|
||||||
|
const txQueue = require("../txQueue");
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.post("/api/deposit", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { diskId, amount } = req.body;
|
||||||
|
|
||||||
|
if (!diskId || amount === undefined) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ success: false, error: "diskId and amount are required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(amount) || amount <= 0) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ success: false, error: "amount must be a positive integer" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const wallet = db.getWallet(diskId);
|
||||||
|
if (!wallet) {
|
||||||
|
return res
|
||||||
|
.status(404)
|
||||||
|
.json({ success: false, error: "Wallet not found for this diskId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const contract = getContract();
|
||||||
|
|
||||||
|
// Mint tokens to the player's wallet via the transaction queue
|
||||||
|
const receipt = await txQueue.enqueue(async () => {
|
||||||
|
const tx = await contract.mint(wallet.address, amount);
|
||||||
|
return tx.wait();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read the new on-chain balance
|
||||||
|
const balance = await contract.balanceOf(wallet.address);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
diskId,
|
||||||
|
amount,
|
||||||
|
balance: Number(balance),
|
||||||
|
txHash: receipt.hash,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
60
services/afc-bridge/src/routes/register.js
Normal file
60
services/afc-bridge/src/routes/register.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
const { Router } = require("express");
|
||||||
|
const { ethers } = require("ethers");
|
||||||
|
const config = require("../config");
|
||||||
|
const db = require("../db");
|
||||||
|
const { adminWallet, provider } = require("../ethereum");
|
||||||
|
const txQueue = require("../txQueue");
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.post("/api/register", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { diskId, name } = req.body;
|
||||||
|
|
||||||
|
if (!diskId || !name) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ success: false, error: "diskId and name are required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if wallet already exists
|
||||||
|
const existing = db.getWallet(diskId);
|
||||||
|
if (existing) {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
diskId: existing.disk_id,
|
||||||
|
address: existing.address,
|
||||||
|
name: existing.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new random wallet
|
||||||
|
const wallet = ethers.Wallet.createRandom();
|
||||||
|
|
||||||
|
// Save to database
|
||||||
|
db.createWallet(diskId, wallet.address, wallet.privateKey, name);
|
||||||
|
|
||||||
|
// Fund the new wallet with gas ETH from the admin wallet
|
||||||
|
await txQueue.enqueue(async () => {
|
||||||
|
const tx = await adminWallet.sendTransaction({
|
||||||
|
to: wallet.address,
|
||||||
|
value: ethers.parseEther(config.GAS_FUND_AMOUNT),
|
||||||
|
});
|
||||||
|
await tx.wait();
|
||||||
|
console.log(
|
||||||
|
`Funded ${wallet.address} with ${config.GAS_FUND_AMOUNT} ETH (tx: ${tx.hash})`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
diskId,
|
||||||
|
address: wallet.address,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
29
services/afc-bridge/src/routes/wallet.js
Normal file
29
services/afc-bridge/src/routes/wallet.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const { Router } = require("express");
|
||||||
|
const db = require("../db");
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get("/api/wallet/:diskId", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { diskId } = req.params;
|
||||||
|
|
||||||
|
const wallet = db.getWallet(diskId);
|
||||||
|
if (!wallet) {
|
||||||
|
return res
|
||||||
|
.status(404)
|
||||||
|
.json({ success: false, error: "Wallet not found for this diskId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
diskId,
|
||||||
|
address: wallet.address,
|
||||||
|
privateKey: wallet.private_key,
|
||||||
|
name: wallet.name,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
63
services/afc-bridge/src/routes/withdraw.js
Normal file
63
services/afc-bridge/src/routes/withdraw.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
const { Router } = require("express");
|
||||||
|
const db = require("../db");
|
||||||
|
const { getContract } = require("../ethereum");
|
||||||
|
const txQueue = require("../txQueue");
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.post("/api/withdraw", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { diskId, amount } = req.body;
|
||||||
|
|
||||||
|
if (!diskId || amount === undefined) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ success: false, error: "diskId and amount are required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(amount) || amount <= 0) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ success: false, error: "amount must be a positive integer" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const wallet = db.getWallet(diskId);
|
||||||
|
if (!wallet) {
|
||||||
|
return res
|
||||||
|
.status(404)
|
||||||
|
.json({ success: false, error: "Wallet not found for this diskId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const contract = getContract();
|
||||||
|
|
||||||
|
// Check on-chain balance before burning
|
||||||
|
const balance = await contract.balanceOf(wallet.address);
|
||||||
|
if (Number(balance) < amount) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: `Insufficient balance: has ${Number(balance)}, requested ${amount}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Burn tokens from the player's wallet via the transaction queue
|
||||||
|
const receipt = await txQueue.enqueue(async () => {
|
||||||
|
const tx = await contract.burnFrom(wallet.address, amount);
|
||||||
|
return tx.wait();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read updated balance
|
||||||
|
const newBalance = await contract.balanceOf(wallet.address);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
diskId,
|
||||||
|
amount,
|
||||||
|
balance: Number(newBalance),
|
||||||
|
txHash: receipt.hash,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
9
services/afc-bridge/src/txQueue.js
Normal file
9
services/afc-bridge/src/txQueue.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
let queue = Promise.resolve();
|
||||||
|
|
||||||
|
function enqueue(fn) {
|
||||||
|
const task = queue.then(() => fn());
|
||||||
|
queue = task.catch(() => {}); // don't let rejections break the chain
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { enqueue };
|
||||||
Reference in New Issue
Block a user