#!/usr/bin/env python3 """CharmMine miner (Python, no third-party packages). Finds a (nonce, anchorBlock) pair that satisfies the on-chain proof of work, then prints the calldata for `mine(uint256 nonce, uint256 anchorBlock, uint8 minTier)` and the price to attach. minTier is set from --want so a front-run downgrade reverts (TierBelowMin) instead of minting a lower tier. It never signs or sends anything. Hand the output to your wallet tooling (e.g. Binance Agentic Wallet: `baw contract-call preview ...` then `execute`). Usage: python mine.py --contract 0x... --wallet 0x... [--rpc https://bsc-dataseed.bnbchain.org] [--want rare|epic|legendary] [--max-seconds 50] Notes: * The anchor block must be within the last `anchorMaxAge` blocks when the tx is mined (8000 blocks, about 60 minutes on BSC via the EIP-2935 history contract; 256 blocks on chains without it). The script reads that value from status() and reports `submitWithinSeconds`; still submit promptly. * Pure-Python keccak does roughly 20-60k hashes/s. The contract's easiest target needs ~262k hashes on average; when difficulty is higher, prefer mine.js (Node, ~10-30x faster). """ import argparse, json, sys, time, urllib.request # ----------------------------------------------------------------- keccak-256 # Pure Python runs ~1k hashes/s, which is far too slow for real mining (the easiest target needs ~262k). # If pycryptodome is installed (`pip install pycryptodome`) we use its C keccak (~1M hashes/s). # Node `mine.js` needs nothing extra and does ~500k hashes/s; prefer it for mining. try: # pragma: no cover from Crypto.Hash import keccak as _ck # type: ignore def keccak256(data: bytes) -> bytes: # noqa: F811 h = _ck.new(digest_bits=256) h.update(data) return h.digest() FAST_KECCAK = True except Exception: # pragma: no cover FAST_KECCAK = False # ----------------------------------------------------------------- keccak-256 (pure python fallback) _RC = [0x0000000000000001, 0x0000000000008082, 0x800000000000808A, 0x8000000080008000, 0x000000000000808B, 0x0000000080000001, 0x8000000080008081, 0x8000000000008009, 0x000000000000008A, 0x0000000000000088, 0x0000000080008009, 0x000000008000000A, 0x000000008000808B, 0x800000000000008B, 0x8000000000008089, 0x8000000000008003, 0x8000000000008002, 0x8000000000000080, 0x000000000000800A, 0x800000008000000A, 0x8000000080008081, 0x8000000000008080, 0x0000000080000001, 0x8000000080008008] _ROT = [[0, 36, 3, 41, 18], [1, 44, 10, 45, 2], [62, 6, 43, 15, 61], [28, 55, 25, 21, 56], [27, 20, 39, 8, 14]] _M = (1 << 64) - 1 def _f(A): for rnd in range(24): C = [A[x][0] ^ A[x][1] ^ A[x][2] ^ A[x][3] ^ A[x][4] for x in range(5)] D = [C[(x - 1) % 5] ^ (((C[(x + 1) % 5] << 1) | (C[(x + 1) % 5] >> 63)) & _M) for x in range(5)] A = [[A[x][y] ^ D[x] for y in range(5)] for x in range(5)] B = [[0] * 5 for _ in range(5)] for x in range(5): for y in range(5): n = _ROT[x][y] v = A[x][y] B[y][(2 * x + 3 * y) % 5] = ((v << n) | (v >> (64 - n))) & _M if n else v A = [[B[x][y] ^ ((~B[(x + 1) % 5][y]) & B[(x + 2) % 5][y]) for y in range(5)] for x in range(5)] A[0][0] ^= _RC[rnd] return A def _keccak256_py(data: bytes) -> bytes: rate = 136 buf = bytearray(data) + b"\x01" while len(buf) % rate: buf += b"\x00" buf[-1] |= 0x80 A = [[0] * 5 for _ in range(5)] for off in range(0, len(buf), rate): blk = buf[off:off + rate] for i in range(17): A[i % 5][i // 5] ^= int.from_bytes(blk[8 * i:8 * i + 8], "little") A = _f(A) out = b"" for i in range(4): out += A[i % 5][i // 5].to_bytes(8, "little") return out if not FAST_KECCAK: keccak256 = _keccak256_py # noqa: F811 # ----------------------------------------------------------------- rpc helpers def rpc(url, method, params): req = urllib.request.Request(url, data=json.dumps({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}).encode(), headers={"content-type": "application/json"}) r = json.load(urllib.request.urlopen(req, timeout=30)) if "error" in r: raise RuntimeError(r["error"]) return r["result"] def sel(sig): return keccak256(sig.encode())[:4] def call(url, to, data): return rpc(url, "eth_call", [{"to": to, "data": "0x" + data.hex()}, "latest"]) def pad_addr(a): return bytes(12) + bytes.fromhex(a[2:]) def u256(x): return x.to_bytes(32, "big") TIER = ["Common", "Rare", "Epic", "Legendary"] DIV = {"common": 1, "rare": 4, "epic": 20, "legendary": 200} def main(): ap = argparse.ArgumentParser() ap.add_argument("--contract", required=True) ap.add_argument("--wallet", required=True) ap.add_argument("--rpc", default="https://bsc-dataseed.bnbchain.org") ap.add_argument("--want", default="common", choices=list(DIV), help="keep mining until at least this tier") ap.add_argument("--max-seconds", type=float, default=0, help="give up after this many seconds (0 = never)") ap.add_argument("--chain-id", type=int, default=0, help="0 = read eth_chainId from the RPC (recommended)") a = ap.parse_args() C, W, url = a.contract, a.wallet, a.rpc rpc_chain = int(rpc(url, "eth_chainId", []), 16) if a.chain_id and a.chain_id != rpc_chain: sys.exit(f"--chain-id {a.chain_id} does not match the RPC chain id {rpc_chain}; the proof would be invalid") a.chain_id = rpc_chain if not FAST_KECCAK: print("WARNING: pure-Python keccak (~1k hashes/s). Install pycryptodome or use mine.js for real mining.", file=sys.stderr) # read status(wallet) raw = call(url, C, sel("status(address)") + pad_addr(W)) b = bytes.fromhex(raw[2:]) words = [int.from_bytes(b[i:i + 32], "big") for i in range(0, len(b), 32)] st = dict(zip(["totalMinted", "remaining", "currentPrice", "yourPrice", "epoch", "nextPriceAt", "target", "easiestTarget", "freeRemaining", "open", "startBlock", "hasAgentId", "mintedByYou", "anchorMaxAge"], words)) if len(words) != 14: sys.exit(f"unexpected status() layout ({len(words)} words); wrong contract or outdated script") print(f"minted {st['totalMinted']}/4096 price for you {st['yourPrice']/1e18:.4f} BNB " f"target 2^{st['target'].bit_length()-1} open={bool(st['open'])} agentId={bool(st['hasAgentId'])}", file=sys.stderr) if not st["hasAgentId"]: sys.exit("wallet has no Agent ID; mint would revert") if not st["open"]: sys.exit(f"mint not open (opens at block {st['startBlock']}) or sold out") seq = st["mintedByYou"] target = st["target"] want_div = DIV[a.want] # abi.encodePacked(uint256 chainid, address this, address miner, uint32 seq, bytes32 anchor, uint256 nonce) packed_head = u256(a.chain_id) + bytes.fromhex(C[2:]) + bytes.fromhex(W[2:]) + seq.to_bytes(4, "big") t0 = time.time() tries = 0 best = None while True: latest = int(rpc(url, "eth_blockNumber", []), 16) anchor_block = max(latest - 2, st["startBlock"]) # never older than START_BLOCK (such anchors are invalid) anchor = bytes.fromhex(rpc(url, "eth_getBlockByNumber", [hex(anchor_block), False])["hash"][2:]) head = packed_head + anchor anchored_at = time.time() window = min(st["anchorMaxAge"], 7200) * 0.45 # seconds the anchor stays valid (BSC 0.45 s blocks) per_anchor = 600 if st["anchorMaxAge"] >= 1000 else 30 deadline = anchored_at + per_anchor nonce = int.from_bytes(keccak256(head + t0.hex().encode())[:8], "big") while time.time() < deadline: for _ in range(2000): nonce += 1 work = int.from_bytes(keccak256(head + u256(nonce)), "big") tries += 1 if work < target: tier = 3 if work < target // 200 else 2 if work < target // 20 else 1 if work < target // 4 else 0 if best is None or work < best[0]: best = (work, nonce, anchor_block, tier) if work < target // want_div: _emit(a, st, best, tries, t0, anchored_at=anchored_at) return if a.max_seconds and time.time() - t0 > a.max_seconds: if best: _emit(a, st, best, tries, t0, note="time limit: best found so far", anchored_at=anchored_at) return sys.exit("time limit reached, nothing found") # anchor about to expire: if we hold a valid result below the wanted tier, emit it rather than lose it if best and best[2] == anchor_block: _emit(a, st, best, tries, t0, note="anchor expiring: emitting best found", anchored_at=anchored_at) return best = None print(f"re-anchoring after {tries} hashes ({tries/(time.time()-t0):.0f} h/s)", file=sys.stderr) def _emit(a, st, best, tries, t0, note="", anchored_at=None): window = min(st["anchorMaxAge"], 7200) * 0.45 margin = 60 if st["anchorMaxAge"] >= 1000 else 0 remaining = int(window) if anchored_at is None else max(5, int(window - (time.time() - anchored_at)) - margin) work, nonce, anchor_block, tier = best min_tier = {"common": 0, "rare": 1, "epic": 2, "legendary": 3}[a.want] data = sel("mine(uint256,uint256,uint8)") + u256(nonce) + u256(anchor_block) + u256(min_tier) out = { "contract": a.contract, "wallet": a.wallet, "nonce": nonce, "anchorBlock": anchor_block, "tier": TIER[tier], "minTier": min_tier, "work": "0x" + work.to_bytes(32, "big").hex(), "valueWei": str(st["yourPrice"]), "valueBNB": st["yourPrice"] / 1e18, "calldata": "0x" + data.hex(), "hashes": tries, "seconds": round(time.time() - t0, 1), "submitWithinSeconds": remaining, "note": note, "baw_preview": f"baw contract-call preview --binanceChainId {a.chain_id} --from {a.wallet} --to {a.contract} " f"--value {st['yourPrice']} --inputData 0x{data.hex()} --json", } print(json.dumps(out, indent=2)) if __name__ == "__main__": main()