#!/usr/bin/env python3 """Devnet custodial treasury setup (ADR-0015, US-032). Creates the mock-USDT SPL mint (6 decimals, matching real USDT) and the treasury associated token account on Solana devnet, then prints/writes the .env.devnet values. Real USDT exists only on mainnet — the mint address is config, so mainnet cutover is a config change. Usage: python scripts/devnet_setup.py # full setup python scripts/devnet_setup.py --mint # reuse existing mint python scripts/devnet_setup.py --mint --mint-to --amount 25 """ import argparse import json import os import sys import time sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "packages", "contracts")) from solders.keypair import Keypair # noqa: E402 from meshnet_contracts.solana_adapter import SolanaCustodialTreasury # noqa: E402 DEFAULT_KEYPAIR = os.path.expanduser("~/.config/solana/meshnet-treasury.json") DEFAULT_RPC = "https://api.devnet.solana.com" # placeholder mint for bootstrapping the adapter before the real mint exists _SYSTEM_PROGRAM = "11111111111111111111111111111111" def _load_or_create_keypair(path: str) -> Keypair: if os.path.exists(path): with open(path, encoding="utf-8") as fh: return Keypair.from_bytes(bytes(json.load(fh))) keypair = Keypair() os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w", encoding="utf-8") as fh: json.dump(list(bytes(keypair)), fh) os.chmod(path, 0o600) print(f"generated treasury keypair: {path}") return keypair def _ensure_sol(treasury: SolanaCustodialTreasury, minimum_sol: float = 0.5) -> None: balance = treasury.get_sol_balance() if balance >= minimum_sol: print(f"treasury SOL balance: {balance:.4f}") return print(f"airdropping 2 SOL to {treasury.treasury_wallet} (balance {balance:.4f})…") signature = treasury.request_airdrop(2.0) for _ in range(30): time.sleep(2) if treasury.get_sol_balance() >= minimum_sol: print("airdrop confirmed") return sys.exit( f"airdrop {signature} not confirmed — devnet faucet may be rate-limited; retry later" ) def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--rpc-url", default=DEFAULT_RPC) parser.add_argument("--keypair", default=DEFAULT_KEYPAIR, help="Treasury keypair path") parser.add_argument("--mint", default=None, help="Reuse an existing mock-USDT mint address") parser.add_argument("--mint-to", default=None, metavar="WALLET", help="Mint mock USDT to a client wallet (creates their token account)") parser.add_argument("--amount", type=float, default=100.0, help="Amount for --mint-to") parser.add_argument("--env-out", default=".env.devnet", help="Env file to write") args = parser.parse_args() keypair = _load_or_create_keypair(args.keypair) bootstrap = SolanaCustodialTreasury(args.rpc_url, args.mint or _SYSTEM_PROGRAM, keypair) print(f"treasury wallet: {bootstrap.treasury_wallet}") _ensure_sol(bootstrap) if args.mint: treasury = SolanaCustodialTreasury(args.rpc_url, args.mint, keypair) mint_address = args.mint print(f"using existing mock-USDT mint: {mint_address}") else: treasury, mint_address = bootstrap.create_mock_usdt_mint() print(f"created mock-USDT mint (6 decimals): {mint_address}") time.sleep(2) # let the mint transaction confirm before creating ATAs treasury.ensure_token_account(treasury.treasury_wallet) print(f"treasury token account: {treasury.treasury_token_account}") if args.mint_to: signature = treasury.mint_mock_usdt(args.mint_to, args.amount) print(f"minted {args.amount} mock USDT to {args.mint_to} (tx {signature})") env = ( f"MESHNET_SOLANA_RPC_URL={args.rpc_url}\n" f"MESHNET_USDT_MINT={mint_address}\n" f"MESHNET_TREASURY_KEYPAIR={args.keypair}\n" f"MESHNET_TREASURY_WALLET={treasury.treasury_wallet}\n" f"MESHNET_TREASURY_TOKEN_ACCOUNT={treasury.treasury_token_account}\n" ) with open(args.env_out, "w", encoding="utf-8") as fh: fh.write(env) print(f"\nwrote {args.env_out}:\n{env}") if __name__ == "__main__": main()