polyglot-alpha / scripts /faucet_agents.py
licaomeng
deploy: main@8970ffb → HF Spaces (2026-05-27T05:19Z)
88d2f2a
"""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())