Spaces:
Running
Running
| """Idempotent faucet for the four translator agent wallets. | |
| Reads ``outputs/agent_wallets.json`` for the agent public addresses and | |
| tops each one up using the operator wallet | |
| (``HACKATHON_WALLET_PRIVATE_KEY``) until two minimum balances are met: | |
| * Arc native (ETH-shaped) >= ``ETH_TARGET`` (default 0.05). | |
| * MockUSDC >= ``USDC_TARGET`` (default 20.0). | |
| Safe to re-run: if a wallet is already above the target the script | |
| silently skips its top-up. The final summary prints the post-faucet | |
| balances per agent. | |
| Usage:: | |
| .venv/bin/python scripts/faucet_agents.py | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import os | |
| import sys | |
| import time | |
| from pathlib import Path | |
| from typing import Any | |
| from eth_account import Account | |
| from eth_account.signers.local import LocalAccount | |
| from web3 import Web3 | |
| # Make the repo root importable without ``pip install -e .``. | |
| _REPO_ROOT = Path(__file__).resolve().parents[1] | |
| if str(_REPO_ROOT) not in sys.path: | |
| sys.path.insert(0, str(_REPO_ROOT)) | |
| import polyglot_alpha # noqa: E402 — runs the .env loader | |
| from polyglot_alpha.agents.wallets import derive_all_wallets # noqa: E402 | |
| from polyglot_alpha.onchain import ( # noqa: E402 | |
| OnChainClient, | |
| USDC_DECIMALS, | |
| usdc_to_units, | |
| ) | |
| ETH_TARGET = float(os.environ.get("AGENT_ETH_TARGET", "0.05")) | |
| USDC_TARGET = float(os.environ.get("AGENT_USDC_TARGET", "20.0")) | |
| # Gas overhead the operator needs to keep for itself (sanity check). | |
| OPERATOR_RESERVE_ETH = 0.01 | |
| ETH_GAS_LIMIT = 21_000 | |
| USDC_TRANSFER_GAS = 80_000 | |
| USDC_MINT_GAS = 120_000 | |
| def _operator_account() -> LocalAccount: | |
| pk = os.environ.get("HACKATHON_WALLET_PRIVATE_KEY") | |
| if not pk: | |
| sys.exit("HACKATHON_WALLET_PRIVATE_KEY not set; aborting") | |
| return Account.from_key(pk) | |
| def _send(w3: Web3, txn: dict[str, Any], account: LocalAccount) -> str: | |
| signed = w3.eth.account.sign_transaction(txn, account.key) | |
| raw_tx = getattr(signed, "raw_transaction", None) or signed.rawTransaction | |
| tx_hash = w3.eth.send_raw_transaction(raw_tx).hex() | |
| if not tx_hash.startswith("0x"): | |
| tx_hash = "0x" + tx_hash | |
| receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120) | |
| if receipt.status != 1: | |
| raise RuntimeError(f"tx reverted: {tx_hash}") | |
| return tx_hash | |
| def _top_up_eth( | |
| w3: Web3, | |
| operator: LocalAccount, | |
| target_address: str, | |
| current_balance_wei: int, | |
| target_eth: float, | |
| ) -> tuple[bool, str]: | |
| target_wei = int(target_eth * 1e18) | |
| if current_balance_wei >= target_wei: | |
| return False, "" | |
| delta_wei = target_wei - current_balance_wei | |
| nonce = w3.eth.get_transaction_count(operator.address) | |
| txn = { | |
| "from": operator.address, | |
| "to": Web3.to_checksum_address(target_address), | |
| "value": delta_wei, | |
| "nonce": nonce, | |
| "gas": ETH_GAS_LIMIT, | |
| "gasPrice": w3.eth.gas_price, | |
| "chainId": w3.eth.chain_id, | |
| } | |
| tx_hash = _send(w3, txn, operator) | |
| return True, tx_hash | |
| def _top_up_usdc( | |
| client: OnChainClient, | |
| operator: LocalAccount, | |
| target_address: str, | |
| current_units: int, | |
| target_usdc: float, | |
| ) -> tuple[bool, str]: | |
| target_units = usdc_to_units(target_usdc) | |
| if current_units >= target_units: | |
| return False, "" | |
| delta_units = target_units - current_units | |
| # Prefer ``transfer`` from the operator. If the operator's USDC | |
| # balance is short, fall back to ``mint`` (the deployed token is a | |
| # MockUSDC with permissionless mint). | |
| op_bal = int( | |
| client.usdc.functions.balanceOf(operator.address).call() | |
| ) | |
| nonce = client.w3.eth.get_transaction_count(operator.address) | |
| if op_bal >= delta_units: | |
| txn = client.usdc.functions.transfer( | |
| Web3.to_checksum_address(target_address), delta_units | |
| ).build_transaction( | |
| { | |
| "from": operator.address, | |
| "nonce": nonce, | |
| "gas": USDC_TRANSFER_GAS, | |
| "gasPrice": client.w3.eth.gas_price, | |
| "chainId": client.config.chain_id, | |
| } | |
| ) | |
| tx_hash = _send(client.w3, txn, operator) | |
| return True, tx_hash | |
| # Fallback: mint directly to the agent. | |
| try: | |
| txn = client.usdc.functions.mint( | |
| Web3.to_checksum_address(target_address), delta_units | |
| ).build_transaction( | |
| { | |
| "from": operator.address, | |
| "nonce": nonce, | |
| "gas": USDC_MINT_GAS, | |
| "gasPrice": client.w3.eth.gas_price, | |
| "chainId": client.config.chain_id, | |
| } | |
| ) | |
| tx_hash = _send(client.w3, txn, operator) | |
| return True, tx_hash | |
| except Exception as exc: # pragma: no cover - real-RPC dependent | |
| return False, f"mint failed: {exc!s}" | |
| def _parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser( | |
| description=( | |
| "Top up the four agent wallets with native ETH + MockUSDC. " | |
| "Idempotent: skips any wallet already at or above the target." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--dry-run", | |
| action="store_true", | |
| help=( | |
| "Inspect balances and report the deltas that would be sent " | |
| "without broadcasting any transaction." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--eth-target", | |
| type=float, | |
| default=ETH_TARGET, | |
| help=f"Minimum native balance per agent (default {ETH_TARGET}).", | |
| ) | |
| parser.add_argument( | |
| "--usdc-target", | |
| type=float, | |
| default=USDC_TARGET, | |
| help=f"Minimum MockUSDC balance per agent (default {USDC_TARGET}).", | |
| ) | |
| return parser.parse_args() | |
| def main() -> int: | |
| args = _parse_args() | |
| eth_target = float(args.eth_target) | |
| usdc_target = float(args.usdc_target) | |
| dry_run = bool(args.dry_run) | |
| print( | |
| f"[faucet] target eth={eth_target} usdc={usdc_target}" | |
| f"{' (DRY-RUN)' if dry_run else ''}" | |
| ) | |
| operator = _operator_account() | |
| client = OnChainClient() | |
| w3 = client.w3 | |
| op_eth_wei = w3.eth.get_balance(operator.address) | |
| op_eth = op_eth_wei / 1e18 | |
| op_usdc_units = int(client.usdc.functions.balanceOf(operator.address).call()) | |
| op_usdc = op_usdc_units / (10 ** USDC_DECIMALS) | |
| print( | |
| f"[faucet] operator={operator.address} " | |
| f"eth={op_eth:.6f} usdc={op_usdc:.4f}" | |
| ) | |
| if op_eth < OPERATOR_RESERVE_ETH: | |
| print( | |
| f"[faucet] WARNING: operator eth {op_eth:.6f} below reserve " | |
| f"{OPERATOR_RESERVE_ETH:.4f}; top-ups may fail" | |
| ) | |
| wallets = derive_all_wallets() | |
| summary_rows: list[tuple[str, str, float, float, str]] = [] | |
| for name, wallet in wallets.items(): | |
| addr = wallet.address | |
| eth_bal = w3.eth.get_balance(addr) | |
| usdc_bal = int(client.usdc.functions.balanceOf(addr).call()) | |
| pre_eth = eth_bal / 1e18 | |
| pre_usdc = usdc_bal / (10 ** USDC_DECIMALS) | |
| if dry_run: | |
| eth_delta_wei = max(int(eth_target * 1e18) - eth_bal, 0) | |
| usdc_delta_units = max(usdc_to_units(usdc_target) - usdc_bal, 0) | |
| notes_parts: list[str] = [] | |
| if eth_delta_wei > 0: | |
| notes_parts.append(f"would_send_eth={eth_delta_wei / 1e18:.6f}") | |
| if usdc_delta_units > 0: | |
| notes_parts.append( | |
| f"would_send_usdc={usdc_delta_units / (10 ** USDC_DECIMALS):.4f}" | |
| ) | |
| notes = ", ".join(notes_parts) or "no top-up needed" | |
| summary_rows.append((name, addr, pre_eth, pre_usdc, notes)) | |
| print( | |
| f"[faucet] {name:9s} {addr} pre eth={pre_eth:.6f} " | |
| f"usdc={pre_usdc:.4f} ({notes})" | |
| ) | |
| continue | |
| eth_changed, eth_tx = _top_up_eth( | |
| w3, operator, addr, eth_bal, eth_target | |
| ) | |
| # Re-read post top-up so the summary is accurate. | |
| if eth_changed: | |
| time.sleep(1) | |
| eth_bal = w3.eth.get_balance(addr) | |
| usdc_changed, usdc_tx = _top_up_usdc( | |
| client, operator, addr, usdc_bal, usdc_target | |
| ) | |
| if usdc_changed: | |
| time.sleep(1) | |
| usdc_bal = int(client.usdc.functions.balanceOf(addr).call()) | |
| eth_float = eth_bal / 1e18 | |
| usdc_float = usdc_bal / (10 ** USDC_DECIMALS) | |
| notes_parts = [] | |
| if eth_changed: | |
| notes_parts.append(f"eth_tx={eth_tx}") | |
| if usdc_changed: | |
| notes_parts.append(f"usdc_tx={usdc_tx}") | |
| notes = ", ".join(notes_parts) or "no top-up needed" | |
| summary_rows.append((name, addr, eth_float, usdc_float, notes)) | |
| print( | |
| f"[faucet] {name:9s} {addr} pre eth={pre_eth:.6f} " | |
| f"usdc={pre_usdc:.4f} -> post eth={eth_float:.6f} " | |
| f"usdc={usdc_float:.4f} ({notes})" | |
| ) | |
| print("\n[faucet] summary:") | |
| for name, addr, eth, usdc, notes in summary_rows: | |
| print(f" {name:9s} {addr} eth={eth:.6f} usdc={usdc:.4f} {notes}") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |