File size: 13,032 Bytes
88d2f2a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
"""W16-B helper: fund + register the rotated v2 seeder wallets.

The legacy seeder slots (``gemini`` / ``deepseek`` / ``qwen``) carry
on-chain reputation below TranslationAuction's 0.7 gate, so every
``submitBid`` from those wallets reverts with ``"reputation gate"``.

Rotating to fresh agent names (``gemini-v2`` / ``deepseek-v2`` /
``qwen-v2``) produces wallets with a fresh ReputationRegistry score
(initial value 1.0) which sails through the gate. This script:

1. Derives the three v2 wallet addresses from
   ``HACKATHON_WALLET_PRIVATE_KEY`` (same deterministic SHA-256 derivation
   used by :mod:`polyglot_alpha.agents.wallets`).
2. Idempotently tops each one up with native ETH + MockUSDC from the
   operator wallet (skips if already above target).
3. Calls ``TranslationAuction.registerAgent()`` from each wallet
   (5 USDC stake), skipping wallets already marked ``registered``.

Safe to re-run. Prints a summary table with addresses, balances,
register tx hashes, and on-chain reputation post-register.

Usage::

    .venv/bin/python scripts/fund_seeder_wallets_v2.py
    .venv/bin/python scripts/fund_seeder_wallets_v2.py --dry-run
"""

from __future__ import annotations

import argparse
import asyncio
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_agent_wallet  # noqa: E402
from polyglot_alpha.chain.auction_client import AuctionClient  # noqa: E402
from polyglot_alpha.onchain import (  # noqa: E402
    OnChainClient,
    USDC_DECIMALS,
    usdc_to_units,
)

# v2 seeder slots (W16-B rotation). Mirrored in
# ``polyglot_alpha.agents.wallets.AGENT_NAMES`` and the orchestrator's
# ``agent_names`` tuple.
V2_AGENT_NAMES: tuple[str, ...] = ("gemini-v2", "deepseek-v2", "qwen-v2")

ETH_TARGET = float(os.environ.get("AGENT_ETH_TARGET_V2", "0.05"))
USDC_TARGET = float(os.environ.get("AGENT_USDC_TARGET_V2", "20.0"))
STAKE_USDC = float(os.environ.get("AGENT_STAKE_USDC", "5.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

    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
    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=(
            "Fund + register the W16-B v2 seeder wallets. "
            "Idempotent: skips any wallet already at/above target balance "
            "and already-registered agents."
        ),
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Inspect balances + registration state, do not send any tx.",
    )
    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}).",
    )
    parser.add_argument(
        "--stake-usdc", type=float, default=STAKE_USDC,
        help=f"Stake amount in USDC for registerAgent (default {STAKE_USDC}).",
    )
    parser.add_argument(
        "--skip-register",
        action="store_true",
        help="Only fund; do not call registerAgent (useful when only re-funding).",
    )
    return parser.parse_args()


async def _register_one(
    auction_client: AuctionClient,
    onchain: OnChainClient,
    name: str,
    pk: str,
    address: str,
    stake_usdc: float,
) -> tuple[str, str]:
    """Call registerAgent if not already registered. Returns (tx_or_status, note)."""

    if onchain.is_registered(address):
        return ("already_registered", "skipped")
    tx_hash = await auction_client.register_agent(agent_pk=pk, stake_usdc=stake_usdc)
    # Confirm receipt
    receipt = onchain.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
    status = int(getattr(receipt, "status", 0))
    if status != 1:
        return (tx_hash, "REVERTED")
    return (tx_hash, "ok")


async def _async_main(args: argparse.Namespace) -> int:
    eth_target = float(args.eth_target)
    usdc_target = float(args.usdc_target)
    stake_usdc = float(args.stake_usdc)
    dry_run = bool(args.dry_run)

    print(
        f"[fund-v2] target eth={eth_target} usdc={usdc_target} "
        f"stake={stake_usdc}{' (DRY-RUN)' if dry_run else ''}"
    )
    operator = _operator_account()
    op_pk = operator.key.hex()
    if not op_pk.startswith("0x"):
        op_pk = "0x" + op_pk
    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"[fund-v2] operator={operator.address} "
        f"eth={op_eth:.6f} usdc={op_usdc:.4f}"
    )
    if op_eth < OPERATOR_RESERVE_ETH:
        print(
            f"[fund-v2] WARNING: operator eth {op_eth:.6f} below reserve "
            f"{OPERATOR_RESERVE_ETH:.4f}; top-ups may fail"
        )

    # Pre-flight USDC sufficiency check for the stake legs.
    total_usdc_needed_for_topup = 0.0
    wallets: dict[str, Any] = {}
    for name in V2_AGENT_NAMES:
        w = derive_agent_wallet(op_pk, name)
        wallets[name] = w
        cur_usdc = int(client.usdc.functions.balanceOf(w.address).call())
        delta = max(usdc_to_units(usdc_target) - cur_usdc, 0)
        total_usdc_needed_for_topup += delta / (10 ** USDC_DECIMALS)
    print(
        f"[fund-v2] usdc needed for top-up across 3 agents: "
        f"{total_usdc_needed_for_topup:.4f} (op has {op_usdc:.4f})"
    )

    rows: list[dict[str, Any]] = []
    auction_client = AuctionClient(onchain=client)

    for name in V2_AGENT_NAMES:
        w = wallets[name]
        addr = w.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)
        pre_rep = client.get_reputation(addr)
        pre_reg = client.is_registered(addr)

        eth_tx: str = ""
        usdc_tx: str = ""
        reg_tx: str = ""
        reg_status: str = ""

        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)
            print(
                f"[fund-v2] {name:13s} {addr} eth={pre_eth:.6f} "
                f"usdc={pre_usdc:.4f} rep={pre_rep:.4f} registered={pre_reg}  "
                f"would_send_eth={eth_delta_wei/1e18:.6f} "
                f"would_send_usdc={usdc_delta_units/(10**USDC_DECIMALS):.4f} "
                f"would_register={not pre_reg and not args.skip_register}"
            )
            rows.append(
                {
                    "name": name, "address": addr,
                    "eth": pre_eth, "usdc": pre_usdc,
                    "reputation": pre_rep, "registered": pre_reg,
                    "eth_tx": "", "usdc_tx": "", "reg_tx": "", "reg_status": "dry-run",
                }
            )
            continue

        # ------ ETH top-up
        eth_changed, eth_tx = _top_up_eth(
            w3, operator, addr, eth_bal, eth_target
        )
        if eth_changed:
            time.sleep(1)
            eth_bal = w3.eth.get_balance(addr)

        # ------ USDC top-up
        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())

        # ------ Register on auction
        if not args.skip_register:
            try:
                reg_tx, reg_status = await _register_one(
                    auction_client, client, name, w.private_key, addr, stake_usdc
                )
            except Exception as exc:
                reg_tx = ""
                reg_status = f"FAILED: {exc!s}"
        else:
            reg_status = "skipped (--skip-register)"

        # Re-read post-register state
        post_rep = client.get_reputation(addr)
        post_reg = client.is_registered(addr)
        post_eth = w3.eth.get_balance(addr) / 1e18
        post_usdc = int(client.usdc.functions.balanceOf(addr).call()) / (10 ** USDC_DECIMALS)

        print(
            f"[fund-v2] {name:13s} {addr}"
            f"\n         pre  eth={pre_eth:.6f} usdc={pre_usdc:.4f} rep={pre_rep:.4f} registered={pre_reg}"
            f"\n         post eth={post_eth:.6f} usdc={post_usdc:.4f} rep={post_rep:.4f} registered={post_reg}"
            f"\n         eth_tx={eth_tx or '(skipped)'}"
            f"\n         usdc_tx={usdc_tx or '(skipped)'}"
            f"\n         reg_tx={reg_tx or '(none)'}  status={reg_status}"
        )

        rows.append(
            {
                "name": name, "address": addr,
                "eth": post_eth, "usdc": post_usdc,
                "reputation": post_rep, "registered": post_reg,
                "eth_tx": eth_tx, "usdc_tx": usdc_tx,
                "reg_tx": reg_tx, "reg_status": reg_status,
            }
        )

    print("\n[fund-v2] summary:")
    print(f"  {'name':13s} {'address':42s}  {'eth':>10s}  {'usdc':>8s}  {'rep':>6s}  {'reg':>5s}  reg_tx")
    for r in rows:
        print(
            f"  {r['name']:13s} {r['address']:42s}  "
            f"{r['eth']:10.6f}  {r['usdc']:8.4f}  "
            f"{r['reputation']:6.4f}  {str(r['registered']):>5s}  {r['reg_tx']}"
        )
    return 0


def main() -> int:
    args = _parse_args()
    return asyncio.run(_async_main(args))


if __name__ == "__main__":
    raise SystemExit(main())