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
|
||||
Reference in New Issue
Block a user