#!/usr/bin/env python3
"""
CRC miner — proof-of-work miner for MineableCRC.

Rate-limit-aware:  the contract enforces a 5-minute cooldown per address.
This miner reads `cooldownRemaining(wallet)` and sleeps until it's zero
before hashing again, so it never wastes CPU racing a cooldown it can't beat.

Usage:
    export CRC_RPC=https://rpc.mainnet.chain.robinhood.com
    export CRC_CONTRACT=0xdd1547eeB4965257206F3529DCBA0921348F902D
    export CRC_PRIVATE_KEY=0x…
    python3 miner.py
"""
import os
import sys
import time
import json
import secrets

try:
    from eth_hash.auto import keccak
    from web3 import Web3
    from eth_account import Account
except ImportError:
    print("install deps: pip install web3 eth-hash eth-account")
    sys.exit(1)

RPC        = os.environ.get("CRC_RPC", "https://rpc.mainnet.chain.robinhood.com")
CONTRACT   = os.environ.get("CRC_CONTRACT", "0xdd1547eeB4965257206F3529DCBA0921348F902D")
PRIVKEY    = os.environ["CRC_PRIVATE_KEY"]
POLL_SECS  = float(os.environ.get("POLL_SECS", "8"))
MAX_TX_GAS = int(os.environ.get("MAX_TX_GAS", "300000"))

ABI = json.loads(r"""[
  {"type":"function","name":"mint","stateMutability":"nonpayable","inputs":[{"name":"nonce","type":"uint256"}],"outputs":[{"type":"bool"}]},
  {"type":"function","name":"miningInfo","stateMutability":"view","inputs":[],"outputs":[
    {"name":"challenge","type":"bytes32"},{"name":"target","type":"uint256"},
    {"name":"solves","type":"uint256"},{"name":"minted","type":"uint256"},
    {"name":"maxSupply","type":"uint256"},{"name":"reward","type":"uint256"},
    {"name":"epochStartTs","type":"uint256"},{"name":"epochSolves","type":"uint256"}]},
  {"type":"function","name":"cooldownRemaining","stateMutability":"view","inputs":[{"name":"who","type":"address"}],"outputs":[{"type":"uint256"}]},
  {"type":"function","name":"difficulty","stateMutability":"view","inputs":[],"outputs":[{"type":"uint256"}]},
  {"type":"function","name":"balanceOf","stateMutability":"view","inputs":[{"name":"","type":"address"}],"outputs":[{"type":"uint256"}]}
]""")

w3 = Web3(Web3.HTTPProvider(RPC))
acct = Account.from_key(PRIVKEY)
contract = w3.eth.contract(address=Web3.to_checksum_address(CONTRACT), abi=ABI)
MINER_ADDR = bytes.fromhex(acct.address[2:].lower())


def wait_cooldown():
    """Poll cooldown until it's clear."""
    rem = contract.functions.cooldownRemaining(acct.address).call()
    while rem > 0:
        m = rem // 60
        s = rem % 60
        print(f"  ⏳ cooldown {m}m {s}s (per-wallet 5-min rate limit)")
        time.sleep(min(rem + 1, 30))
        rem = contract.functions.cooldownRemaining(acct.address).call()


def print_state():
    info = contract.functions.miningInfo().call()
    bal = contract.functions.balanceOf(acct.address).call()
    challenge, target, solves, minted, max_sup, reward, epoch_ts, epoch_solves = info
    difficulty = (1 << 256) // target if target > 0 else 0
    print(f"\n═══ CRC mining ═══")
    print(f"  wallet:      {acct.address}")
    print(f"  balance:     {bal/1e18:.4f} CRC")
    print(f"  solves:      {solves}")
    print(f"  reward:      {reward/1e18} CRC (flat, per solve)")
    print(f"  minted:      {minted/1e18:.4f} / {max_sup/1e18:.0f}")
    print(f"  target:      0x{target:064x}")
    print(f"  difficulty:  {difficulty:,}")
    return challenge, target


def mine_one(challenge, target):
    """Hash-loop until we find a valid nonce or the challenge rolls."""
    challenge_bytes = challenge if isinstance(challenge, (bytes, bytearray)) else \
        bytes.fromhex(challenge[2:] if isinstance(challenge, str) else "")
    nonce = secrets.randbits(64)
    t0 = time.time()
    hashes = 0
    last_report = t0
    while True:
        digest = keccak(challenge_bytes + MINER_ADDR + nonce.to_bytes(32, "big"))
        if int.from_bytes(digest, "big") < target:
            return nonce, digest, hashes, time.time() - t0
        nonce += 1
        hashes += 1
        if hashes % 250_000 == 0:
            now = time.time()
            elapsed = now - t0
            hps = hashes / elapsed if elapsed else 0
            print(f"    {hashes/1e6:.2f}M hashes | {hps/1000:.1f} kH/s | {elapsed:.1f}s", end="\r", flush=True)
            if now - last_report > POLL_SECS:
                info = contract.functions.miningInfo().call()
                if info[0] != challenge:
                    print("\n  challenge rolled — restart")
                    return None, None, hashes, elapsed
                last_report = now


def submit(nonce):
    tx = contract.functions.mint(nonce).build_transaction({
        "from":     acct.address,
        "nonce":    w3.eth.get_transaction_count(acct.address),
        "gas":      MAX_TX_GAS,
        "gasPrice": w3.eth.gas_price,
    })
    signed = acct.sign_transaction(tx)
    sig = w3.eth.send_raw_transaction(signed.raw_transaction)
    print(f"  submitted: 0x{sig.hex()}")
    try:
        rec = w3.eth.wait_for_transaction_receipt(sig, timeout=60)
        if rec.status == 1:
            print(f"  ✓ MINED  block {rec.blockNumber}  gas {rec.gasUsed}  +50 CRC")
            return True
        print(f"  ✗ reverted (probably lost race)")
        return False
    except Exception as e:
        print(f"  wait err: {e}")
        return False


def main():
    print(f"CRC miner v2 (cooldown-aware)")
    print(f"  RPC:      {RPC}")
    print(f"  contract: {CONTRACT}")
    print(f"  wallet:   {acct.address}")
    print(f"  ETH bal:  {w3.eth.get_balance(acct.address)/1e18:.6f}")
    while True:
        wait_cooldown()
        challenge, target = print_state()
        nonce, digest, hashes, elapsed = mine_one(challenge, target)
        if nonce is None:
            continue
        hps = hashes / elapsed if elapsed else 0
        print(f"\n★ SOLVE  nonce={nonce}  ({hashes:,} hashes in {elapsed:.1f}s — {hps/1000:.1f} kH/s)")
        print(f"  digest: 0x{digest.hex()}")
        submit(nonce)


if __name__ == "__main__":
    main()
