Spaces:
Running
Running
| """Deploy the **v2** ReputationRegistry contract to Arc testnet (W14-CONTRACT-PREP). | |
| The v2 contract fixes two issues in the deployed v1 (`REPUTATION_REGISTRY_ADDRESS`): | |
| * β: ``_fillSignal`` now rescales 6-decimal USDC base units to 1e18 fixed-point | |
| before the ln() input, so ``fillSignal`` actually spans [0.5, 2.0] instead of | |
| collapsing to ``FILL_MIN=0.5`` for any realistic fee. | |
| * α: initial score on first touch is ``HALF=0.5e18`` instead of ``ONE=1e18`` so | |
| the first ``_recompute`` does not strictly decrease the score from a maxed-out | |
| prior. | |
| Idempotency | |
| ----------- | |
| By default the script **does not deploy** — it only prints what *would* happen. | |
| Pass ``--confirm`` to actually broadcast the deployment transaction. | |
| If ``REPUTATION_REGISTRY_V2_ADDRESS`` is set in the environment and the | |
| contract exists at that address (non-empty bytecode), the script exits early — | |
| the v2 contract is considered already deployed and the script is a no-op. | |
| Authorization | |
| ------------- | |
| After deployment, the script calls ``setAuthorized(operator, true)`` for the | |
| operator EOA so the orchestrator can immediately push state. To wire the | |
| downstream contracts (``TranslationAuction`` / ``BuilderFeeRouter`` / | |
| ``JudgePanel``), pass their addresses via the matching env vars (or CLI flags) | |
| and the script will authorize each in turn. | |
| Environment | |
| ----------- | |
| * ``HACKATHON_WALLET_PRIVATE_KEY`` — operator private key (required) | |
| * ``ARC_TESTNET_RPC`` — RPC URL (default https://rpc.testnet.arc.network) | |
| * ``ARC_CHAIN_ID`` — chain id (default 5042002) | |
| * ``REPUTATION_REGISTRY_V2_ADDRESS`` — set to skip deploy (idempotency) | |
| * ``TRANSLATION_AUCTION_ADDRESS`` — optional: authorize after deploy | |
| * ``BUILDER_FEE_ROUTER_ADDRESS`` — optional: authorize after deploy | |
| * ``JUDGE_PANEL_ADDRESS`` — optional: authorize after deploy | |
| Usage:: | |
| # dry-run (default) | |
| .venv/bin/python scripts/deploy_reputation_registry_v2.py | |
| # actually deploy | |
| .venv/bin/python scripts/deploy_reputation_registry_v2.py --confirm | |
| # deploy + explicit downstream authorizations | |
| .venv/bin/python scripts/deploy_reputation_registry_v2.py --confirm \\ | |
| --authorize-auction 0x... --authorize-router 0x... --authorize-judge 0x... | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import sys | |
| import time | |
| from pathlib import Path | |
| from eth_account import Account | |
| from web3 import Web3 | |
| REPO_ROOT = Path(__file__).resolve().parents[1] | |
| FOUNDRY_OUT = REPO_ROOT / "contracts" / "out" | |
| ARTIFACT_NAME = "ReputationRegistry" | |
| def load_artifact(name: str) -> dict: | |
| """Load a Foundry artifact (ABI + bytecode) from contracts/out/.""" | |
| path = FOUNDRY_OUT / f"{name}.sol" / f"{name}.json" | |
| if not path.exists(): | |
| raise FileNotFoundError( | |
| f"Foundry artifact not found at {path}. Run `cd contracts && forge build` first." | |
| ) | |
| with path.open("r", encoding="utf-8") as fh: | |
| art = json.load(fh) | |
| return {"abi": art["abi"], "bytecode": art["bytecode"]["object"]} | |
| def wait_receipt(w3: Web3, tx_hash: bytes, timeout: int = 180) -> dict: | |
| receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=timeout) | |
| if receipt.status != 1: | |
| raise RuntimeError(f"tx failed: {tx_hash.hex()}") | |
| return receipt | |
| def send_signed(w3: Web3, txn: dict, account) -> dict: | |
| signed = w3.eth.account.sign_transaction(txn, account.key) | |
| raw = getattr(signed, "raw_transaction", None) or signed.rawTransaction | |
| tx_hash = w3.eth.send_raw_transaction(raw) | |
| return wait_receipt(w3, tx_hash) | |
| def deploy_registry(w3: Web3, account, chain_id: int) -> tuple[str, int]: | |
| art = load_artifact(ARTIFACT_NAME) | |
| contract = w3.eth.contract(abi=art["abi"], bytecode=art["bytecode"]) | |
| nonce = w3.eth.get_transaction_count(account.address) | |
| txn = contract.constructor().build_transaction( | |
| { | |
| "from": account.address, | |
| "nonce": nonce, | |
| "chainId": chain_id, | |
| "gasPrice": w3.eth.gas_price, | |
| "gas": 4_500_000, | |
| } | |
| ) | |
| print(f" -> deploying {ARTIFACT_NAME} v2 ...") | |
| receipt = send_signed(w3, txn, account) | |
| addr = receipt.contractAddress | |
| print(f" deployed at {addr} (gas {receipt.gasUsed})") | |
| return addr, receipt.gasUsed | |
| def authorize( | |
| w3: Web3, account, chain_id: int, rep_address: str, rep_abi: list, who: str, label: str | |
| ) -> int: | |
| contract = w3.eth.contract(address=Web3.to_checksum_address(rep_address), abi=rep_abi) | |
| nonce = w3.eth.get_transaction_count(account.address) | |
| txn = contract.functions.setAuthorized( | |
| Web3.to_checksum_address(who), True | |
| ).build_transaction( | |
| { | |
| "from": account.address, | |
| "nonce": nonce, | |
| "chainId": chain_id, | |
| "gasPrice": w3.eth.gas_price, | |
| "gas": 120_000, | |
| } | |
| ) | |
| print(f" -> setAuthorized({label} = {who}, true) ...") | |
| receipt = send_signed(w3, txn, account) | |
| print(f" authorized (gas {receipt.gasUsed})") | |
| return receipt.gasUsed | |
| def already_deployed(w3: Web3, address: str | None) -> bool: | |
| """Return True if `address` is non-empty and contains contract code.""" | |
| if not address: | |
| return False | |
| try: | |
| addr = Web3.to_checksum_address(address) | |
| except (ValueError, TypeError): | |
| return False | |
| code = w3.eth.get_code(addr) | |
| return code is not None and len(code) > 0 and code != b"\x00" | |
| def main() -> int: | |
| parser = argparse.ArgumentParser( | |
| description="Deploy v2 ReputationRegistry (β + α fix). Dry-run unless --confirm." | |
| ) | |
| parser.add_argument( | |
| "--confirm", | |
| action="store_true", | |
| help="Actually broadcast deployment + authorization transactions. " | |
| "Without this flag the script is a dry-run.", | |
| ) | |
| parser.add_argument( | |
| "--authorize-auction", | |
| default=os.environ.get("TRANSLATION_AUCTION_ADDRESS"), | |
| help="Address of TranslationAuction to authorize post-deploy.", | |
| ) | |
| parser.add_argument( | |
| "--authorize-router", | |
| default=os.environ.get("BUILDER_FEE_ROUTER_ADDRESS"), | |
| help="Address of BuilderFeeRouter to authorize post-deploy.", | |
| ) | |
| parser.add_argument( | |
| "--authorize-judge", | |
| default=os.environ.get("JUDGE_PANEL_ADDRESS"), | |
| help="Address of JudgePanel to authorize post-deploy.", | |
| ) | |
| args = parser.parse_args() | |
| pk = os.environ.get("HACKATHON_WALLET_PRIVATE_KEY") | |
| if not pk: | |
| print("ERROR: HACKATHON_WALLET_PRIVATE_KEY is required", file=sys.stderr) | |
| return 2 | |
| rpc_url = os.environ.get("ARC_TESTNET_RPC", "https://rpc.testnet.arc.network") | |
| chain_id = int(os.environ.get("ARC_CHAIN_ID", "5042002")) | |
| existing_v2 = os.environ.get("REPUTATION_REGISTRY_V2_ADDRESS") | |
| w3 = Web3(Web3.HTTPProvider(rpc_url)) | |
| if not w3.is_connected(): | |
| print(f"ERROR: cannot reach Arc RPC at {rpc_url}", file=sys.stderr) | |
| return 3 | |
| account = Account.from_key(pk) | |
| print(f"deployer: {account.address}") | |
| print(f"chain id: {chain_id}") | |
| print(f"RPC: {rpc_url}") | |
| print(f"existing v2 env: {existing_v2 or '(unset)'}") | |
| print( | |
| f"balance: {w3.from_wei(w3.eth.get_balance(account.address), 'ether')} ETH-equivalent" | |
| ) | |
| print() | |
| # Idempotency check | |
| if already_deployed(w3, existing_v2): | |
| print(f"v2 already deployed at {existing_v2} — no action taken (idempotent).") | |
| return 0 | |
| if not args.confirm: | |
| print("DRY-RUN — would do the following (re-run with --confirm to broadcast):") | |
| print(f" 1. deploy ReputationRegistry (constructor takes no args)") | |
| print(f" 2. setAuthorized({account.address}, true) # operator EOA") | |
| for label, addr in ( | |
| ("TranslationAuction", args.authorize_auction), | |
| ("BuilderFeeRouter", args.authorize_router), | |
| ("JudgePanel", args.authorize_judge), | |
| ): | |
| if addr: | |
| print(f" 3. setAuthorized({addr}, true) # {label}") | |
| return 0 | |
| # 1. Deploy | |
| rep_addr, deploy_gas = deploy_registry(w3, account, chain_id) | |
| # 2. Authorize operator (constructor already does this — deployer is auto-authorized, | |
| # but we re-issue defensively in case the wallet differs from msg.sender). | |
| rep_art = load_artifact(ARTIFACT_NAME) | |
| auth_gas = {} | |
| # The deployer is the operator EOA and is already authorized by the constructor, | |
| # so we skip a redundant setAuthorized(operator) call. But we still authorize | |
| # any downstream contracts the user passed in. | |
| for label, addr in ( | |
| ("TranslationAuction", args.authorize_auction), | |
| ("BuilderFeeRouter", args.authorize_router), | |
| ("JudgePanel", args.authorize_judge), | |
| ): | |
| if addr: | |
| auth_gas[f"auth({label})"] = authorize( | |
| w3, account, chain_id, rep_addr, rep_art["abi"], addr, label | |
| ) | |
| final_balance = w3.from_wei(w3.eth.get_balance(account.address), "ether") | |
| print() | |
| print("=== Deployment summary ===") | |
| print(f" ReputationRegistry (v2): {rep_addr} (deploy gas {deploy_gas})") | |
| for label, g in auth_gas.items(): | |
| print(f" {label}: {g}") | |
| print(f" final balance: {final_balance} ETH-equivalent") | |
| print() | |
| print("=== Suggested .env update ===") | |
| print(f"REPUTATION_REGISTRY_V2_ADDRESS={rep_addr}") | |
| print( | |
| " # Once verified, update REPUTATION_REGISTRY_ADDRESS to point at this " | |
| "address and remove the V2 suffix." | |
| ) | |
| # Dump a JSON artifact alongside the existing deployment_v2.json | |
| out_path = REPO_ROOT / "outputs" / "deployment_reputation_v2.json" | |
| out_path.parent.mkdir(parents=True, exist_ok=True) | |
| out_path.write_text( | |
| json.dumps( | |
| { | |
| "chain_id": chain_id, | |
| "deployer": account.address, | |
| "deployed_at": int(time.time()), | |
| "address": rep_addr, | |
| "deploy_gas": deploy_gas, | |
| "auth_gas": auth_gas, | |
| "notes": "W14-CONTRACT-PREP β + α fix", | |
| }, | |
| indent=2, | |
| ) | |
| ) | |
| print(f"wrote {out_path}") | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |