"""Enter an Orbio agent's trading wallet in Orblivion's prize league, then show the entry and the standings. python enter_league.py AGENT_ID the trading wallet is the agent's owner: it signs python enter_league.py AGENT_ID --owner-key PATH the agent's owner is another wallet: its key file signs python enter_league.py --status this wallet's entries and the standings (nothing is signed) --wallet-key PATH the trading wallet's key file (default ~/.orblivion-bot/wallet.json, the starter bot's wallet) --api-url URL Orblivion's API (default: ORBLIVION_API_URL if set, else https://api.orblivion.com) The trading wallet is the one whose trades count: it signs in. The agent's owner, as Orbio's vault records it, signs the entry statement, which carries the owner's eligibility confirmation. Before anything is signed, the statement is shown, checked against the text the SDK builds itself, and signed only after you type SIGN. Signing sends no transaction and costs no gas. Key files ({"private_key": "0x..."}, the starter bot's format) stay outside this folder and are never printed. """ import argparse import json import os import sys import time try: from orblivion import ELIGIBILITY_CONFIRMATION, OrblivionClient, OrblivionError, league_statement_text except ImportError: # pragma: no cover sys.exit('The Orblivion SDK is missing: pip install "orblivion>=0.1.2"') HERE = os.path.dirname(os.path.abspath(__file__)) WALLET_KEY = "~/.orblivion-bot/wallet.json" LEAGUE_PAGE = "https://orblivion.com/league.html" TOP = 10 # What to do after some of Orblivion's refusals. HINTS = { "NOT_OWNER": "the owner Orbio's vault records signs: give its key file with --owner-key, or use the League page", "AGENT_ENTERED": "this agent already has a prize entry: the wallet that entered it, or the owner, can leave first", "ALREADY_ENTERED": "this wallet already plays for another agent: leave first (client.league.leave())", "NOT_AN_AGENT": "check the agent's number (#...) on the Orbio launchpad: it isn't its ticker", "STATEMENT_EXPIRED": "a statement works once, for 10 minutes: run the script again", "SANCTIONED": "no entry is possible for an address on the sanctions list", } def clean(s, n=40): """Other people's words (names, symbols, reasons) as plain text: no control characters reach the terminal.""" return "".join(ch if ch.isprintable() else "?" for ch in str(s if s is not None else ""))[:n] def utc(ts): g = time.gmtime(ts) return f"{time.strftime('%a', g)} {g.tm_mday} {time.strftime('%b %Y %H:%M', g)} UTC" # --- keys (outside this folder, never printed) ------------------------------------------------------------------------ def load_key(path, what): """An eth_account account from a key file. The key itself is never printed or logged.""" from eth_account import Account path = os.path.abspath(os.path.expanduser(path)) if os.path.commonpath([os.path.realpath(path), os.path.realpath(HERE)]) == os.path.realpath(HERE): sys.exit(f"Keep the {what}'s key file outside this folder, so it is never shared or committed with the code.") if not os.path.exists(path): sys.exit(f"No {what} key file at {path}.") try: with open(path, encoding="utf-8") as f: return Account.from_key(json.load(f)["private_key"]) except (ValueError, KeyError, TypeError): sys.exit(f'{path} isn\'t a key file: it holds {{"private_key": "0x..."}}.') # --- the statement ---------------------------------------------------------------------------------------------------- def check_statement(st, agent_id, wallet): """The SDK already refused any statement unlike its own text (LEAGUE_STATEMENT). Checked here again, in the open: the same text, rebuilt from the agent, this wallet, the nonce and the expiry, carrying the confirmation word for word, and not expired.""" want = league_statement_text(agent_id, f"prize league: trading wallet {wallet.lower()}", wallet, st["nonce"], st["expires_at"]) if st["message"] != want or ELIGIBILITY_CONFIRMATION not in st["message"].split("\n"): sys.exit("The statement isn't the text the SDK builds for this agent and wallet: nothing was signed.") if st["expires_at"] <= time.time(): sys.exit("The statement has expired: run the script again.") def sign(client, owner, message): """The owner's personal_sign of the checked statement: the trading wallet's own (the SDK signs only its own prize statement), or the owner's key file's.""" if owner is None: return client.league.sign_statement(message) from eth_account.messages import encode_defunct return "0x" + bytes(owner.sign_message(encode_defunct(text=message)).signature).hex() def enter(client, agent_id, owner): wallet = client.address.lower() signer = (owner.address if owner else wallet).lower() lg = client.league s = lg.summary() nxt = s["next_season"] mine = [e for e in lg.me()["entries"] if e["board"] == "prize" and e["status"] == "active"] if mine and mine[0]["agent_id"] != agent_id: sys.exit(f"This wallet already plays the prize league for agent #{mine[0]['agent_id']}: one entry per wallet. " "Leave first (client.league.leave()), then enter again.") if mine and mine[0]["confirmed"]: print(f"Already entered for agent #{agent_id}, and the owner's confirmation is on file.\n") return status(client) confirming = bool(mine) # entered without the owner's confirmation (by signing in): confirm it now st = lg.statement(agent_id) # signs in as the trading wallet; refused unless it is the SDK's own text check_statement(st, agent_id, wallet) vault_owner = (st.get("owner") or "").lower() or None print(f"Agent #{agent_id}") print(f"Trading {wallet} (its trades count)") print(f"Owner {vault_owner or 'not named by this server'} (the Orbio vault's record: it signs)") if not confirming: print(f"Plays from Season {nxt['number']}: {utc(nxt['starts_at'])} to {utc(nxt['ends_at'])}") if vault_owner and vault_owner != signer: sys.exit(f"\nThe Orbio vault records {vault_owner} as agent #{agent_id}'s owner, not {signer}: nothing was " "signed.\n" + ("Pass the owner's key file with --owner-key, " if owner is None else "Use the owner's key " "file, ") + f"or enter on the League page ({LEAGUE_PAGE}), where the owner signs in a browser wallet.") print("\nThe statement the owner signs (identical to the SDK's own text; it expires " + time.strftime("%H:%M:%S UTC", time.gmtime(st["expires_at"])) + "):\n") for line in st["message"].split("\n"): print(" | " + line) print("\nSigning confirms the line that starts \"I am 18 or older\" in the owner's name: 18 or older, not a US " "person, not in a sanctioned country or region or on a sanctions list, eligible under the laws that apply, " "and the Prize League Rules accepted. Under the rules the owner confirms it for itself and for whoever " "controls the trading wallet. Sign only if it is true.") if input("Type SIGN to sign it as the agent's owner: ").strip() != "SIGN": sys.exit("Not signed. Nothing was entered.") signature = sign(client, owner, st["message"]) if confirming: e = lg.confirm(agent_id, signature=signature, nonce=st["nonce"]) print(f"\nConfirmed entry {e['entry_id']}: the owner's eligibility confirmation is on file.") else: e = lg.enter(agent_id, signature=signature, nonce=st["nonce"]) print(f"\nEntered entry {e['entry_id']}: agent #{e['agent_id']}, trading wallet {e['wallet']}, from Season " f"{e['from_season']}") print(f"Confirmed {'yes' if e['confirmed'] else 'no'} (proof: {e['proof']})") print() return status(client) # --- the entry and the standings -------------------------------------------------------------------------------------- def status(client): lg = client.league entries = lg.me()["entries"] print(f"Your entries ({client.address.lower()})") for e in entries: conf = "" if e["confirmed"] is None else ", confirmed" if e["confirmed"] else ", NOT confirmed: no prize" print(f" {e['board']:<8} agent #{e['agent_id']:<6} entry {e['entry_id']:<4} {e['status']:<6} from Season " f"{e['from_season']}{conf}") if not entries: print(" none") sd = lg.standings() season, rows = sd["season"], sd["rows"] print(f"\nPrize league, Season {season['number']} ({sd['status']}): {utc(season['starts_at'])} to " f"{utc(season['ends_at'])}") if not rows: print(" No standings yet: entrants appear once the season starts." if sd["status"] == "upcoming" else " No entrants.") return 0 print(f" {'rank':>4} {'agent':<8} {'symbol':<10} {'profit USD':>12} {'trades':>6} eligibility") me = client.address.lower() for r in rows[:TOP] + [r for r in rows[TOP:] if r["wallet"] == me]: el = r["eligibility"] or {} tag = " <- you" if r["wallet"] == me else "" print(f" {r['rank'] or '-':>4} #{r['agent_id']:<7} {clean(r['symbol'], 10):<10} {float(r['profit_usd']):>12,.2f} " f"{r['trades']:>6} {el.get('status', '-')}{tag}") for r in rows: if r["wallet"] == me: print(f"\nYour entry's eligibility, rule by rule (agent #{r['agent_id']}):") for rule in (r["eligibility"] or {}).get("rules", []): print(f" {rule['rule']:<13} {rule['status']:<8} {clean(rule['reason'], 110)}") return 0 def main(argv=None): ap = argparse.ArgumentParser(description="Enter an Orbio agent's trading wallet in Orblivion's prize league.") ap.add_argument("agent_id", nargs="?", type=int, help="the Orbio agent's number") ap.add_argument("--owner-key", help="the agent owner's key file, when the owner isn't the trading wallet") ap.add_argument("--wallet-key", default=WALLET_KEY, help=f"the trading wallet's key file (default {WALLET_KEY})") ap.add_argument("--api-url", help="Orblivion's API (default: ORBLIVION_API_URL, else https://api.orblivion.com)") ap.add_argument("--status", action="store_true", help="show this wallet's entries and the standings") a = ap.parse_args(argv) if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8", errors="replace") if a.agent_id is None and not a.status: ap.error("give the Orbio agent's number, or --status") if a.agent_id is not None and a.agent_id < 1: ap.error("the agent's number is a whole number from 1") wallet = load_key(a.wallet_key, "trading wallet") owner = load_key(a.owner_key, "owner") if a.owner_key else None if owner is not None and owner.address.lower() == wallet.address.lower(): owner = None # the same wallet: it signs for itself client = OrblivionClient(base_url=a.api_url, account=wallet) print(f"Orblivion {client.http.base_url}\n") try: return status(client) if a.status else enter(client, a.agent_id, owner) except OrblivionError as e: hint = HINTS.get(e.code) sys.exit(f"\nRefused: {e.code}: {e.message}" + (f"\n ({hint})" if hint else "")) except KeyboardInterrupt: sys.exit("\nStopped. Nothing more was sent.") if __name__ == "__main__": sys.exit(main())