|
| 1 | +"""Shared USYC chain configs, ABIs, and helpers. |
| 2 | +
|
| 3 | +Single source of truth for all USYC-related scripts. |
| 4 | +To add a new chain: add an entry to CHAINS and import where needed. |
| 5 | +""" |
| 6 | + |
| 7 | +import os |
| 8 | +from dataclasses import dataclass |
| 9 | +from pathlib import Path |
| 10 | +from web3 import Web3 |
| 11 | + |
| 12 | +# ── .env loader (idempotent) ──────────────────────────────────────── |
| 13 | +_env_loaded = False |
| 14 | + |
| 15 | + |
| 16 | +def load_env() -> None: |
| 17 | + """Load .env from project root (once per process).""" |
| 18 | + global _env_loaded |
| 19 | + if _env_loaded: |
| 20 | + return |
| 21 | + env_path = Path(__file__).resolve().parent.parent / ".env" |
| 22 | + if env_path.exists(): |
| 23 | + for line in env_path.read_text().splitlines(): |
| 24 | + line = line.strip() |
| 25 | + if line and not line.startswith("#") and "=" in line: |
| 26 | + k, v = line.split("=", 1) |
| 27 | + os.environ.setdefault(k.strip(), v.strip()) |
| 28 | + _env_loaded = True |
| 29 | + |
| 30 | + |
| 31 | +# ── Chain configs ──────────────────────────────────────────────────── |
| 32 | +@dataclass(frozen=True) |
| 33 | +class ChainConfig: |
| 34 | + name: str |
| 35 | + chain_id: int |
| 36 | + rpc_url: str |
| 37 | + explorer_tx: str # URL template: {tx_hash} |
| 38 | + usdc: str |
| 39 | + usyc: str |
| 40 | + teller: str |
| 41 | + entitlements: str |
| 42 | + private_key_env: str # env var name for the deployer key |
| 43 | + |
| 44 | + |
| 45 | +CHAINS: dict[str, ChainConfig] = { |
| 46 | + "arc": ChainConfig( |
| 47 | + name="Arc Testnet", |
| 48 | + chain_id=5042002, |
| 49 | + rpc_url="https://rpc.testnet.arc.network", |
| 50 | + explorer_tx="https://testnet.arcscan.app/tx/{tx_hash}", |
| 51 | + usdc="0x3600000000000000000000000000000000000000", |
| 52 | + usyc="0xe9185F0c5F296Ed1797AaE4238D26CCaBEadb86C", |
| 53 | + teller="0x9fdF14c5B14173D74C08Af27AebFf39240dC105A", |
| 54 | + entitlements="0xcc205224862c7641930c87679e98999d23c26113", |
| 55 | + private_key_env="ARC_DEPLOYER_PRIVATE_KEY", |
| 56 | + ), |
| 57 | + "sepolia": ChainConfig( |
| 58 | + name="Ethereum Sepolia", |
| 59 | + chain_id=11155111, |
| 60 | + rpc_url=os.getenv("SEPOLIA_RPC_URL", "https://ethereum-sepolia-rpc.publicnode.com"), |
| 61 | + explorer_tx="https://sepolia.etherscan.io/tx/{tx_hash}", |
| 62 | + usdc="0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", |
| 63 | + usyc="0x38D3A3f8717F4DB1CcB4Ad7D8C755919440848A3", |
| 64 | + teller="0x96424C885951ceb4B79fecb934eD857999e6f82B", |
| 65 | + entitlements="0x7f44c5F5551ed651F7298367Ee51f1CBa608571b", |
| 66 | + private_key_env="SEPOLIA_DEPLOYER_PRIVATE_KEY", |
| 67 | + ), |
| 68 | +} |
| 69 | + |
| 70 | +DEFAULT_CHAIN = "arc" |
| 71 | + |
| 72 | + |
| 73 | +def get_chain(name: str | None = None) -> ChainConfig: |
| 74 | + """Return chain config by name, or default.""" |
| 75 | + load_env() |
| 76 | + key = name or os.getenv("USYC_CHAIN", DEFAULT_CHAIN) |
| 77 | + if key not in CHAINS: |
| 78 | + raise ValueError(f"Unknown chain '{key}'. Available: {list(CHAINS)}") |
| 79 | + return CHAINS[key] |
| 80 | + |
| 81 | + |
| 82 | +# ── Shared ABIs ────────────────────────────────────────────────────── |
| 83 | +ERC20_ABI = [ |
| 84 | + { |
| 85 | + "name": "approve", |
| 86 | + "type": "function", |
| 87 | + "stateMutability": "nonpayable", |
| 88 | + "inputs": [ |
| 89 | + {"name": "spender", "type": "address"}, |
| 90 | + {"name": "amount", "type": "uint256"}, |
| 91 | + ], |
| 92 | + "outputs": [{"name": "", "type": "bool"}], |
| 93 | + }, |
| 94 | + { |
| 95 | + "name": "allowance", |
| 96 | + "type": "function", |
| 97 | + "stateMutability": "view", |
| 98 | + "inputs": [ |
| 99 | + {"name": "owner", "type": "address"}, |
| 100 | + {"name": "spender", "type": "address"}, |
| 101 | + ], |
| 102 | + "outputs": [{"name": "", "type": "uint256"}], |
| 103 | + }, |
| 104 | + { |
| 105 | + "name": "balanceOf", |
| 106 | + "type": "function", |
| 107 | + "stateMutability": "view", |
| 108 | + "inputs": [{"name": "account", "type": "address"}], |
| 109 | + "outputs": [{"name": "", "type": "uint256"}], |
| 110 | + }, |
| 111 | + { |
| 112 | + "name": "decimals", |
| 113 | + "type": "function", |
| 114 | + "stateMutability": "view", |
| 115 | + "inputs": [], |
| 116 | + "outputs": [{"name": "", "type": "uint8"}], |
| 117 | + }, |
| 118 | + { |
| 119 | + "name": "totalSupply", |
| 120 | + "type": "function", |
| 121 | + "stateMutability": "view", |
| 122 | + "inputs": [], |
| 123 | + "outputs": [{"name": "", "type": "uint256"}], |
| 124 | + }, |
| 125 | + { |
| 126 | + "name": "name", |
| 127 | + "type": "function", |
| 128 | + "stateMutability": "view", |
| 129 | + "inputs": [], |
| 130 | + "outputs": [{"name": "", "type": "string"}], |
| 131 | + }, |
| 132 | +] |
| 133 | + |
| 134 | +TELLER_ABI = [ |
| 135 | + { |
| 136 | + "name": "deposit", |
| 137 | + "type": "function", |
| 138 | + "stateMutability": "nonpayable", |
| 139 | + "inputs": [ |
| 140 | + {"name": "_assets", "type": "uint256"}, |
| 141 | + {"name": "_receiver", "type": "address"}, |
| 142 | + ], |
| 143 | + "outputs": [{"name": "", "type": "uint256"}], |
| 144 | + }, |
| 145 | + { |
| 146 | + "name": "redeem", |
| 147 | + "type": "function", |
| 148 | + "stateMutability": "nonpayable", |
| 149 | + "inputs": [ |
| 150 | + {"name": "_shares", "type": "uint256"}, |
| 151 | + {"name": "_receiver", "type": "address"}, |
| 152 | + {"name": "_account", "type": "address"}, |
| 153 | + ], |
| 154 | + "outputs": [{"name": "", "type": "uint256"}], |
| 155 | + }, |
| 156 | +] |
| 157 | + |
| 158 | +ENTITLEMENTS_ABI = [ |
| 159 | + { |
| 160 | + "name": "isAllowed", |
| 161 | + "type": "function", |
| 162 | + "stateMutability": "view", |
| 163 | + "inputs": [{"name": "account", "type": "address"}], |
| 164 | + "outputs": [{"name": "", "type": "bool"}], |
| 165 | + }, |
| 166 | + { |
| 167 | + "name": "isEntitled", |
| 168 | + "type": "function", |
| 169 | + "stateMutability": "view", |
| 170 | + "inputs": [{"name": "account", "type": "address"}], |
| 171 | + "outputs": [{"name": "", "type": "bool"}], |
| 172 | + }, |
| 173 | + { |
| 174 | + "name": "allowlist", |
| 175 | + "type": "function", |
| 176 | + "stateMutability": "view", |
| 177 | + "inputs": [{"name": "account", "type": "address"}], |
| 178 | + "outputs": [{"name": "", "type": "bool"}], |
| 179 | + }, |
| 180 | + { |
| 181 | + "name": "hasRole", |
| 182 | + "type": "function", |
| 183 | + "stateMutability": "view", |
| 184 | + "inputs": [ |
| 185 | + {"name": "role", "type": "bytes32"}, |
| 186 | + {"name": "account", "type": "address"}, |
| 187 | + ], |
| 188 | + "outputs": [{"name": "", "type": "bool"}], |
| 189 | + }, |
| 190 | +] |
| 191 | + |
| 192 | + |
| 193 | +# ── Helpers ────────────────────────────────────────────────────────── |
| 194 | +def get_w3(chain: ChainConfig | None = None) -> Web3: |
| 195 | + """Return a Web3 instance for the given chain.""" |
| 196 | + cfg = chain or get_chain() |
| 197 | + return Web3(Web3.HTTPProvider(cfg.rpc_url)) |
| 198 | + |
| 199 | + |
| 200 | +def get_wallet(chain: ChainConfig | None = None) -> tuple[Web3, str]: |
| 201 | + """Return (w3, wallet_address) from the chain's private key env var.""" |
| 202 | + cfg = chain or get_chain() |
| 203 | + load_env() |
| 204 | + pk = os.getenv(cfg.private_key_env) |
| 205 | + if not pk: |
| 206 | + raise RuntimeError( |
| 207 | + f"Set {cfg.private_key_env} in .env for {cfg.name}" |
| 208 | + ) |
| 209 | + w3 = get_w3(cfg) |
| 210 | + return w3, w3.eth.account.from_key(pk).address |
| 211 | + |
| 212 | + |
| 213 | +def tx_url(chain: ChainConfig, tx_hash: str | bytes) -> str: |
| 214 | + """Build an explorer URL for a tx hash.""" |
| 215 | + if isinstance(tx_hash, bytes): |
| 216 | + tx_hash = "0x" + tx_hash.hex() |
| 217 | + return chain.explorer_tx.format(tx_hash=tx_hash) |
| 218 | + |
| 219 | + |
| 220 | +def check_balances(w3: Web3, wallet: str, chain: ChainConfig) -> dict: |
| 221 | + """Return {usdc, usyc} balances as raw ints.""" |
| 222 | + usdc = w3.eth.contract( |
| 223 | + address=Web3.to_checksum_address(chain.usdc), abi=ERC20_ABI |
| 224 | + ) |
| 225 | + usyc = w3.eth.contract( |
| 226 | + address=Web3.to_checksum_address(chain.usyc), abi=ERC20_ABI |
| 227 | + ) |
| 228 | + return { |
| 229 | + "usdc": usdc.functions.balanceOf(wallet).call(), |
| 230 | + "usyc": usyc.functions.balanceOf(wallet).call(), |
| 231 | + } |
0 commit comments