"""US-032: devnet custodial treasury — wallet binding + deposit watcher. The deposit watcher polls the treasury token account and credits the sending wallet's bound API key exactly once per transaction signature. The watcher is exercised against a fake treasury (hermetic CI, ADR-0007); the real solana-test-validator flow runs only when the binary is installed. """ import json import shutil import time import urllib.error import urllib.request import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from meshnet_tracker.billing import BillingLedger from meshnet_tracker.server import TrackerServer from meshnet_tracker.wallet_proof import binding_message def _keypair(): """Deterministic-enough local ed25519 keypair + base58 address for tests.""" priv = Ed25519PrivateKey.generate() from meshnet_node.wallet import _b58encode public = priv.public_key().public_bytes_raw() return priv, _b58encode(public) def _sign(priv: Ed25519PrivateKey, api_key: str, wallet: str) -> str: return priv.sign(binding_message(api_key, wallet)).hex() class _FakeDeposit: def __init__(self, signature, sender_wallet, amount_usdt): self.signature = signature self.sender_wallet = sender_wallet self.amount_usdt = amount_usdt class _FakeTreasury: """Stands in for SolanaCustodialTreasury: a queue of confirmed deposits.""" def __init__(self): self.deposits: list[_FakeDeposit] = [] def list_new_deposits(self, is_seen, *, limit=200): return [d for d in self.deposits if not is_seen(d.signature)] def _post_json(url: str, payload: dict, headers: dict | None = None) -> dict: req = urllib.request.Request( url, data=json.dumps(payload).encode(), headers={"Content-Type": "application/json", **(headers or {})}, method="POST", ) with urllib.request.urlopen(req) as r: return json.loads(r.read()) @pytest.fixture def watched_tracker(): ledger = BillingLedger(starting_credit=0.0, default_price_per_1k=0.02) treasury = _FakeTreasury() tracker = TrackerServer( billing=ledger, treasury=treasury, deposit_poll_interval=0.1, ) port = tracker.start() yield f"http://127.0.0.1:{port}", ledger, treasury tracker.stop() def _wait_for(predicate, timeout=3.0): deadline = time.monotonic() + timeout while time.monotonic() < deadline: if predicate(): return True time.sleep(0.05) return False def test_wallet_register_requires_auth(watched_tracker): tracker_url, _, _ = watched_tracker with pytest.raises(urllib.error.HTTPError) as exc_info: _post_json(f"{tracker_url}/v1/wallet/register", {"wallet": "So1anaWa11et111"}) assert exc_info.value.code == 401 def test_deposit_credits_bound_api_key_exactly_once(watched_tracker): tracker_url, ledger, treasury = watched_tracker priv, wallet = _keypair() reply = _post_json( f"{tracker_url}/v1/wallet/register", {"wallet": wallet, "signature": _sign(priv, "client-key-1", wallet)}, headers={"Authorization": "Bearer client-key-1"}, ) assert reply["bound"] is True treasury.deposits.append(_FakeDeposit("sig-1", wallet, 25.0)) assert _wait_for(lambda: ledger.get_client_balance("client-key-1") > 0) assert ledger.get_client_balance("client-key-1") == pytest.approx(25.0) # the same signature keeps being reported by the RPC — credited once only time.sleep(0.4) assert ledger.get_client_balance("client-key-1") == pytest.approx(25.0) # replay through the ledger API directly is also a no-op assert ledger.credit_deposit("client-key-1", 25.0, "sig-1") is None def test_unbound_wallet_deposit_is_not_credited(watched_tracker): _, ledger, treasury = watched_tracker treasury.deposits.append(_FakeDeposit("sig-2", "UnknownWallet999", 10.0)) time.sleep(0.4) assert ledger.snapshot()["clients"] == {} def test_binding_replicates_via_events(): a = BillingLedger(starting_credit=0.0) b = BillingLedger(starting_credit=0.0) a.bind_wallet("key-9", "WalletNine") a.credit_deposit("key-9", 3.0, "sig-9") events, _ = a.events_since(0) b.apply_events(events) assert b.api_key_for_wallet("WalletNine") == "key-9" assert b.get_client_balance("key-9") == pytest.approx(3.0) # deposit event id embeds the signature → replicated replay is a no-op assert b.apply_events(events) == 0 def test_solana_adapter_derives_treasury_accounts(): """Adapter smoke test without any RPC round-trip.""" pytest.importorskip("solders") token_instructions = pytest.importorskip("spl.token.instructions") if not hasattr(token_instructions, "InitializeMintParams"): pytest.skip("installed spl.token lacks InitializeMintParams") from solders.keypair import Keypair from meshnet_contracts.solana_adapter import SolanaCustodialTreasury keypair = Keypair() treasury = SolanaCustodialTreasury( "http://127.0.0.1:8899", "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", # real USDT mint address shape keypair, ) assert treasury.treasury_wallet == str(keypair.pubkey()) assert treasury.treasury_token_account # deterministic ATA derivation @pytest.mark.skipif( shutil.which("solana-test-validator") is None, reason="solana-test-validator not installed", ) def test_mint_deposit_credit_flow_against_local_validator(tmp_path): """Full mint → deposit → credit flow on a local validator (US-032).""" import subprocess from solders.keypair import Keypair from meshnet_contracts.solana_adapter import RpcClient, SolanaCustodialTreasury rpc_url = "http://127.0.0.1:8899" ledger_dir = tmp_path / "ledger" validator = subprocess.Popen( ["solana-test-validator", "--ledger", str(ledger_dir), "--quiet", "--rpc-port", "8899", "--reset"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) try: assert _wait_for(lambda: _rpc_alive(RpcClient(rpc_url)), timeout=60) treasury_kp, client_kp = Keypair(), Keypair() bootstrap = SolanaCustodialTreasury( rpc_url, "11111111111111111111111111111111", treasury_kp, ) client_bootstrap = SolanaCustodialTreasury( rpc_url, "11111111111111111111111111111111", client_kp, ) bootstrap.request_airdrop(2.0) client_bootstrap.request_airdrop(2.0) assert _wait_for(lambda: bootstrap.get_sol_balance() > 0, timeout=30) assert _wait_for(lambda: client_bootstrap.get_sol_balance() > 0, timeout=30) treasury, mint = bootstrap.create_mock_usdt_mint() assert _wait_for( lambda: treasury._account_exists( # noqa: SLF001 — test-only poke __import__("solders.pubkey", fromlist=["Pubkey"]).Pubkey.from_string(mint) ), timeout=30, ) treasury.ensure_token_account(treasury.treasury_wallet) treasury.mint_mock_usdt(str(client_kp.pubkey()), 50.0) # the client depositing into the treasury is just a transfer the # client signs — reuse send_payouts from an adapter bound to their key client_side = SolanaCustodialTreasury(rpc_url, mint, client_kp) assert _wait_for( lambda: _try_send(client_side, treasury.treasury_wallet, 25.0), timeout=30, ) ledger = BillingLedger(starting_credit=0.0) ledger.bind_wallet("api-key-e2e", str(client_kp.pubkey())) def _credited(): for dep in treasury.list_new_deposits( lambda sig: ledger.has_event(f"deposit-{sig}") ): key = ledger.api_key_for_wallet(dep.sender_wallet) if key: ledger.credit_deposit(key, dep.amount_usdt, dep.signature) return ledger.get_client_balance("api-key-e2e") > 0 assert _wait_for(_credited, timeout=60) assert ledger.get_client_balance("api-key-e2e") == pytest.approx(25.0) finally: validator.terminate() validator.wait(timeout=10) def _try_send(adapter, wallet: str, amount: float) -> bool: try: adapter.send_payouts([(wallet, amount)]) return True except Exception: return False def _rpc_alive(rpc) -> bool: try: return rpc.call("getHealth") == "ok" except Exception: return False