basileus 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- basileus-0.1.0/PKG-INFO +35 -0
- basileus-0.1.0/README.md +15 -0
- basileus-0.1.0/basileus/__init__.py +0 -0
- basileus-0.1.0/basileus/__main__.py +3 -0
- basileus-0.1.0/basileus/async_typer.py +22 -0
- basileus-0.1.0/basileus/chain/__init__.py +0 -0
- basileus-0.1.0/basileus/chain/balance.py +34 -0
- basileus-0.1.0/basileus/chain/builder_code.py +7 -0
- basileus-0.1.0/basileus/chain/constants.py +202 -0
- basileus-0.1.0/basileus/chain/ens.py +125 -0
- basileus-0.1.0/basileus/chain/erc8004.py +127 -0
- basileus-0.1.0/basileus/chain/superfluid.py +91 -0
- basileus-0.1.0/basileus/chain/swap.py +128 -0
- basileus-0.1.0/basileus/chain/wallet.py +24 -0
- basileus-0.1.0/basileus/commands/__init__.py +0 -0
- basileus-0.1.0/basileus/commands/deploy.py +504 -0
- basileus-0.1.0/basileus/commands/register.py +87 -0
- basileus-0.1.0/basileus/commands/set_content_hash.py +80 -0
- basileus-0.1.0/basileus/commands/stop.py +76 -0
- basileus-0.1.0/basileus/infra/__init__.py +0 -0
- basileus-0.1.0/basileus/infra/aleph.py +302 -0
- basileus-0.1.0/basileus/infra/scripts.py +47 -0
- basileus-0.1.0/basileus/infra/ssh.py +142 -0
- basileus-0.1.0/basileus/main.py +15 -0
- basileus-0.1.0/basileus/ui.py +33 -0
- basileus-0.1.0/pyproject.toml +29 -0
basileus-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: basileus
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for deploying Basileus autonomous prediction market agents
|
|
5
|
+
Author: Basileus Team
|
|
6
|
+
Requires-Python: >=3.11,<3.14
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Requires-Dist: aleph-sdk-python (>=2.3.0,<3.0.0)
|
|
12
|
+
Requires-Dist: eth-account (>=0.13.0,<0.14.0)
|
|
13
|
+
Requires-Dist: paramiko (>=3.5.1,<4.0.0)
|
|
14
|
+
Requires-Dist: pathspec (>=0.12.1,<0.13.0)
|
|
15
|
+
Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
|
|
16
|
+
Requires-Dist: typer[all] (>=0.15.0,<0.16.0)
|
|
17
|
+
Requires-Dist: web3 (>=7.0.0,<8.0.0)
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# Basileus CLI
|
|
21
|
+
|
|
22
|
+
CLI for deploying Basileus autonomous prediction market agents on Base.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install basileus
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
basileus deploy
|
|
34
|
+
```
|
|
35
|
+
|
basileus-0.1.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AsyncTyper(typer.Typer):
|
|
9
|
+
"""Typer subclass that supports async command functions."""
|
|
10
|
+
|
|
11
|
+
def command(self, *args: Any, **kwargs: Any) -> Any:
|
|
12
|
+
decorator = super().command(*args, **kwargs)
|
|
13
|
+
|
|
14
|
+
def wrapper(fn: Any) -> Any:
|
|
15
|
+
@wraps(fn)
|
|
16
|
+
def runner(*a: Any, **kw: Any) -> Any:
|
|
17
|
+
return asyncio.run(fn(*a, **kw))
|
|
18
|
+
|
|
19
|
+
decorator(runner)
|
|
20
|
+
return fn
|
|
21
|
+
|
|
22
|
+
return wrapper
|
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.live import Live
|
|
5
|
+
from rich.spinner import Spinner
|
|
6
|
+
from web3 import Web3
|
|
7
|
+
|
|
8
|
+
from basileus.chain.constants import BASE_RPC_URL, MIN_ETH_FUNDING
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_eth_balance(w3: Web3, address: str) -> float:
|
|
14
|
+
"""Get native ETH balance for address. Returns human-readable float."""
|
|
15
|
+
raw = w3.eth.get_balance(Web3.to_checksum_address(address))
|
|
16
|
+
return raw / (10**18)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def wait_for_eth_funding(
|
|
20
|
+
address: str, min_amount: float = MIN_ETH_FUNDING, poll_interval: int = 5
|
|
21
|
+
) -> float:
|
|
22
|
+
"""Poll RPC until ETH balance >= min_amount. Returns final balance."""
|
|
23
|
+
w3 = Web3(Web3.HTTPProvider(BASE_RPC_URL))
|
|
24
|
+
|
|
25
|
+
with Live(
|
|
26
|
+
Spinner("dots", text=f"Waiting for ETH deposit to {address}..."),
|
|
27
|
+
console=console,
|
|
28
|
+
transient=True,
|
|
29
|
+
):
|
|
30
|
+
while True:
|
|
31
|
+
balance = get_eth_balance(w3, address)
|
|
32
|
+
if balance >= min_amount:
|
|
33
|
+
return balance
|
|
34
|
+
time.sleep(poll_interval)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
_ERC_SUFFIX = bytes.fromhex("80218021802180218021802180218021")
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def builder_code_suffix(code: str) -> bytes:
|
|
5
|
+
"""ERC-8021 Schema 0: codes_ascii ∥ codesLength (1 byte) ∥ 0x00 ∥ ercSuffix (16 bytes)"""
|
|
6
|
+
codes_bytes = code.encode("ascii")
|
|
7
|
+
return codes_bytes + len(codes_bytes).to_bytes(1, "big") + b"\x00" + _ERC_SUFFIX
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# ERC-8021 builder code (Base)
|
|
2
|
+
BUILDER_CODE = "bc_kj26kx76"
|
|
3
|
+
|
|
4
|
+
# Base mainnet
|
|
5
|
+
BASE_RPC_URL = "https://mainnet.base.org"
|
|
6
|
+
BASE_CHAIN_ID = 8453
|
|
7
|
+
|
|
8
|
+
# USDC on Base
|
|
9
|
+
USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
10
|
+
USDC_DECIMALS = 6
|
|
11
|
+
|
|
12
|
+
# WETH on Base
|
|
13
|
+
WETH_ADDRESS = "0x4200000000000000000000000000000000000006"
|
|
14
|
+
|
|
15
|
+
# ALEPH on Base
|
|
16
|
+
ALEPH_ADDRESS = "0xc0Fbc4967259786C743361a5885ef49380473dCF"
|
|
17
|
+
ALEPH_DECIMALS = 18
|
|
18
|
+
|
|
19
|
+
# Uniswap V3 SwapRouter on Base
|
|
20
|
+
UNISWAP_ROUTER = "0x2626664c2603336E57B271c5C0b26F421741e481"
|
|
21
|
+
|
|
22
|
+
# Uniswap V3 ALEPH/WETH pool on Base
|
|
23
|
+
UNISWAP_ALEPH_POOL = "0xe11C66b25F0e9a9eBEf1616B43424CC6E2168FC8"
|
|
24
|
+
|
|
25
|
+
# Fee tiers
|
|
26
|
+
UNISWAP_FEE_ALEPH = 10000 # 1% for WETH/ALEPH
|
|
27
|
+
UNISWAP_FEE_USDC = 500 # 0.05% for WETH/USDC
|
|
28
|
+
|
|
29
|
+
# Funding flow
|
|
30
|
+
MIN_ETH_FUNDING = 0.01
|
|
31
|
+
MIN_ETH_RESERVE = 0.001
|
|
32
|
+
TARGET_ALEPH_TOKENS = 10
|
|
33
|
+
|
|
34
|
+
# Minimal ERC20 ABI for balanceOf
|
|
35
|
+
ERC20_BALANCE_ABI = [
|
|
36
|
+
{
|
|
37
|
+
"inputs": [{"name": "account", "type": "address"}],
|
|
38
|
+
"name": "balanceOf",
|
|
39
|
+
"outputs": [{"name": "", "type": "uint256"}],
|
|
40
|
+
"stateMutability": "view",
|
|
41
|
+
"type": "function",
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
# L2Registrar on Base (ENS subnames for basileus-agent.eth)
|
|
46
|
+
L2_REGISTRAR_ADDRESS = "0xBb3699a3018A8a82A94be194eCfe65512AD8E995"
|
|
47
|
+
|
|
48
|
+
L2_REGISTRAR_ABI = [
|
|
49
|
+
{
|
|
50
|
+
"inputs": [{"name": "owner", "type": "address"}],
|
|
51
|
+
"name": "reverseNames",
|
|
52
|
+
"outputs": [{"name": "", "type": "string"}],
|
|
53
|
+
"stateMutability": "view",
|
|
54
|
+
"type": "function",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"inputs": [{"name": "label", "type": "string"}],
|
|
58
|
+
"name": "available",
|
|
59
|
+
"outputs": [{"name": "", "type": "bool"}],
|
|
60
|
+
"stateMutability": "view",
|
|
61
|
+
"type": "function",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"inputs": [
|
|
65
|
+
{"name": "label", "type": "string"},
|
|
66
|
+
{"name": "owner", "type": "address"},
|
|
67
|
+
],
|
|
68
|
+
"name": "register",
|
|
69
|
+
"outputs": [],
|
|
70
|
+
"stateMutability": "nonpayable",
|
|
71
|
+
"type": "function",
|
|
72
|
+
},
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
# IPFS content hash (EIP-1577 encoded) — output of `npm run deploy:ipfs` in frontend/
|
|
76
|
+
FRONTEND_CONTENT_HASH = (
|
|
77
|
+
"0xe30101701220166670f07c9a6e42f990a9326a8ee4224ef863b89fd17ff21f82a5cd43470125"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# L2Registry on Base (ENS resolver for subnames)
|
|
81
|
+
L2_REGISTRY_ADDRESS = "0x2e84f843299a132103e110c948c5e4739682c961"
|
|
82
|
+
|
|
83
|
+
L2_REGISTRY_ABI = [
|
|
84
|
+
{
|
|
85
|
+
"inputs": [],
|
|
86
|
+
"name": "baseNode",
|
|
87
|
+
"outputs": [{"name": "", "type": "bytes32"}],
|
|
88
|
+
"stateMutability": "view",
|
|
89
|
+
"type": "function",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"inputs": [
|
|
93
|
+
{"name": "parentNode", "type": "bytes32"},
|
|
94
|
+
{"name": "label", "type": "string"},
|
|
95
|
+
],
|
|
96
|
+
"name": "makeNode",
|
|
97
|
+
"outputs": [{"name": "", "type": "bytes32"}],
|
|
98
|
+
"stateMutability": "pure",
|
|
99
|
+
"type": "function",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"inputs": [
|
|
103
|
+
{"name": "node", "type": "bytes32"},
|
|
104
|
+
{"name": "hash", "type": "bytes"},
|
|
105
|
+
],
|
|
106
|
+
"name": "setContenthash",
|
|
107
|
+
"outputs": [],
|
|
108
|
+
"stateMutability": "nonpayable",
|
|
109
|
+
"type": "function",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"inputs": [{"name": "node", "type": "bytes32"}],
|
|
113
|
+
"name": "contenthash",
|
|
114
|
+
"outputs": [{"name": "", "type": "bytes"}],
|
|
115
|
+
"stateMutability": "view",
|
|
116
|
+
"type": "function",
|
|
117
|
+
},
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
# ERC-8004 IdentityRegistry on Base
|
|
121
|
+
ERC8004_IDENTITY_REGISTRY = "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432"
|
|
122
|
+
|
|
123
|
+
ERC8004_IDENTITY_REGISTRY_ABI = [
|
|
124
|
+
{
|
|
125
|
+
"inputs": [
|
|
126
|
+
{"name": "agentURI", "type": "string"},
|
|
127
|
+
{
|
|
128
|
+
"name": "metadata",
|
|
129
|
+
"type": "tuple[]",
|
|
130
|
+
"components": [
|
|
131
|
+
{"name": "key", "type": "string"},
|
|
132
|
+
{"name": "value", "type": "bytes"},
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
"name": "register",
|
|
137
|
+
"outputs": [{"name": "agentId", "type": "uint256"}],
|
|
138
|
+
"stateMutability": "nonpayable",
|
|
139
|
+
"type": "function",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"inputs": [{"name": "owner", "type": "address"}],
|
|
143
|
+
"name": "balanceOf",
|
|
144
|
+
"outputs": [{"name": "", "type": "uint256"}],
|
|
145
|
+
"stateMutability": "view",
|
|
146
|
+
"type": "function",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"anonymous": False,
|
|
150
|
+
"inputs": [
|
|
151
|
+
{"indexed": True, "name": "agentId", "type": "uint256"},
|
|
152
|
+
{"indexed": False, "name": "agentURI", "type": "string"},
|
|
153
|
+
{"indexed": True, "name": "owner", "type": "address"},
|
|
154
|
+
],
|
|
155
|
+
"name": "Registered",
|
|
156
|
+
"type": "event",
|
|
157
|
+
},
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
# Uniswap V3 pool ABI (slot0 for price reading)
|
|
161
|
+
UNISWAP_POOL_ABI = [
|
|
162
|
+
{
|
|
163
|
+
"inputs": [],
|
|
164
|
+
"name": "slot0",
|
|
165
|
+
"outputs": [
|
|
166
|
+
{"name": "sqrtPriceX96", "type": "uint160"},
|
|
167
|
+
{"name": "tick", "type": "int24"},
|
|
168
|
+
{"name": "observationIndex", "type": "uint16"},
|
|
169
|
+
{"name": "observationCardinality", "type": "uint16"},
|
|
170
|
+
{"name": "observationCardinalityNext", "type": "uint16"},
|
|
171
|
+
{"name": "feeProtocol", "type": "uint8"},
|
|
172
|
+
{"name": "unlocked", "type": "bool"},
|
|
173
|
+
],
|
|
174
|
+
"stateMutability": "view",
|
|
175
|
+
"type": "function",
|
|
176
|
+
}
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
# Uniswap V3 SwapRouter ABI (exactInputSingle)
|
|
180
|
+
UNISWAP_ROUTER_ABI = [
|
|
181
|
+
{
|
|
182
|
+
"inputs": [
|
|
183
|
+
{
|
|
184
|
+
"components": [
|
|
185
|
+
{"name": "tokenIn", "type": "address"},
|
|
186
|
+
{"name": "tokenOut", "type": "address"},
|
|
187
|
+
{"name": "fee", "type": "uint24"},
|
|
188
|
+
{"name": "recipient", "type": "address"},
|
|
189
|
+
{"name": "amountIn", "type": "uint256"},
|
|
190
|
+
{"name": "amountOutMinimum", "type": "uint256"},
|
|
191
|
+
{"name": "sqrtPriceLimitX96", "type": "uint160"},
|
|
192
|
+
],
|
|
193
|
+
"name": "params",
|
|
194
|
+
"type": "tuple",
|
|
195
|
+
}
|
|
196
|
+
],
|
|
197
|
+
"name": "exactInputSingle",
|
|
198
|
+
"outputs": [{"name": "amountOut", "type": "uint256"}],
|
|
199
|
+
"stateMutability": "payable",
|
|
200
|
+
"type": "function",
|
|
201
|
+
}
|
|
202
|
+
]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from web3 import Web3
|
|
2
|
+
from eth_account import Account
|
|
3
|
+
|
|
4
|
+
from basileus.chain.constants import (
|
|
5
|
+
BUILDER_CODE,
|
|
6
|
+
L2_REGISTRAR_ADDRESS,
|
|
7
|
+
L2_REGISTRAR_ABI,
|
|
8
|
+
L2_REGISTRY_ADDRESS,
|
|
9
|
+
L2_REGISTRY_ABI,
|
|
10
|
+
)
|
|
11
|
+
from basileus.chain.builder_code import builder_code_suffix
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_registrar(w3: Web3):
|
|
15
|
+
return w3.eth.contract(
|
|
16
|
+
address=Web3.to_checksum_address(L2_REGISTRAR_ADDRESS),
|
|
17
|
+
abi=L2_REGISTRAR_ABI,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def check_existing_subname(w3: Web3, address: str) -> str | None:
|
|
22
|
+
"""Call reverseNames(address). Returns label or None if no subname."""
|
|
23
|
+
try:
|
|
24
|
+
contract = _get_registrar(w3)
|
|
25
|
+
label = contract.functions.reverseNames(
|
|
26
|
+
Web3.to_checksum_address(address)
|
|
27
|
+
).call()
|
|
28
|
+
return label if label else None
|
|
29
|
+
except Exception:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def check_label_available(w3: Web3, label: str) -> bool:
|
|
34
|
+
"""Call available(label). Returns True if label can be registered."""
|
|
35
|
+
contract = _get_registrar(w3)
|
|
36
|
+
return contract.functions.available(label).call()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def register_subname(w3: Web3, private_key: str, label: str, owner: str) -> str:
|
|
40
|
+
"""Call register(label, owner). Signs and sends tx. Returns tx hash hex.
|
|
41
|
+
Raises on failure."""
|
|
42
|
+
contract = _get_registrar(w3)
|
|
43
|
+
account = Account.from_key(private_key)
|
|
44
|
+
owner_checksummed = Web3.to_checksum_address(owner)
|
|
45
|
+
|
|
46
|
+
call = contract.functions.register(label, owner_checksummed)
|
|
47
|
+
tx = call.build_transaction(
|
|
48
|
+
{
|
|
49
|
+
"from": account.address,
|
|
50
|
+
"nonce": w3.eth.get_transaction_count(account.address, "pending"),
|
|
51
|
+
"gas": call.estimate_gas({"from": account.address}),
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if BUILDER_CODE:
|
|
56
|
+
suffix = builder_code_suffix(BUILDER_CODE)
|
|
57
|
+
tx["data"] += suffix.hex()
|
|
58
|
+
tx["gas"] += len(suffix) * 16
|
|
59
|
+
|
|
60
|
+
signed = account.sign_transaction(tx)
|
|
61
|
+
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
62
|
+
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=60)
|
|
63
|
+
|
|
64
|
+
if receipt["status"] != 1:
|
|
65
|
+
raise RuntimeError(f"Transaction reverted: 0x{tx_hash.hex()}")
|
|
66
|
+
|
|
67
|
+
return f"0x{tx_hash.hex()}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _get_registry(w3: Web3):
|
|
71
|
+
return w3.eth.contract(
|
|
72
|
+
address=Web3.to_checksum_address(L2_REGISTRY_ADDRESS),
|
|
73
|
+
abi=L2_REGISTRY_ABI,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_content_hash(w3: Web3, label: str) -> str | None:
|
|
78
|
+
"""Read current contentHash from L2Registry for a subname. Returns hex string or None."""
|
|
79
|
+
registry = _get_registry(w3)
|
|
80
|
+
base_node = registry.functions.baseNode().call()
|
|
81
|
+
node = registry.functions.makeNode(base_node, label).call()
|
|
82
|
+
raw = registry.functions.contenthash(node).call()
|
|
83
|
+
if not raw:
|
|
84
|
+
return None
|
|
85
|
+
return f"0x{raw.hex()}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def set_content_hash(
|
|
89
|
+
w3: Web3, private_key: str, label: str, content_hash_hex: str
|
|
90
|
+
) -> str:
|
|
91
|
+
"""Set contentHash on L2Registry for a subname.
|
|
92
|
+
|
|
93
|
+
content_hash_hex: EIP-1577 encoded hex from the frontend deploy script (0x...).
|
|
94
|
+
Returns tx hash hex.
|
|
95
|
+
"""
|
|
96
|
+
registry = _get_registry(w3)
|
|
97
|
+
account = Account.from_key(private_key)
|
|
98
|
+
|
|
99
|
+
base_node = registry.functions.baseNode().call()
|
|
100
|
+
node = registry.functions.makeNode(base_node, label).call()
|
|
101
|
+
|
|
102
|
+
content_hash = bytes.fromhex(content_hash_hex.removeprefix("0x"))
|
|
103
|
+
|
|
104
|
+
call = registry.functions.setContenthash(node, content_hash)
|
|
105
|
+
tx = call.build_transaction(
|
|
106
|
+
{
|
|
107
|
+
"from": account.address,
|
|
108
|
+
"nonce": w3.eth.get_transaction_count(account.address, "pending"),
|
|
109
|
+
"gas": call.estimate_gas({"from": account.address}),
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if BUILDER_CODE:
|
|
114
|
+
suffix = builder_code_suffix(BUILDER_CODE)
|
|
115
|
+
tx["data"] += suffix.hex()
|
|
116
|
+
tx["gas"] += len(suffix) * 16
|
|
117
|
+
|
|
118
|
+
signed = account.sign_transaction(tx)
|
|
119
|
+
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
120
|
+
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=60)
|
|
121
|
+
|
|
122
|
+
if receipt["status"] != 1:
|
|
123
|
+
raise RuntimeError(f"Transaction reverted: 0x{tx_hash.hex()}")
|
|
124
|
+
|
|
125
|
+
return f"0x{tx_hash.hex()}"
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""ERC-8004 IdentityRegistry interactions for registering Basileus agents on Base."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
import eth_abi
|
|
7
|
+
from aleph.sdk.chains.ethereum import ETHAccount
|
|
8
|
+
from aleph.sdk.client.authenticated_http import AuthenticatedAlephHttpClient
|
|
9
|
+
from aleph.sdk.types import StorageEnum
|
|
10
|
+
from eth_account import Account
|
|
11
|
+
from web3 import Web3
|
|
12
|
+
|
|
13
|
+
from basileus.chain.builder_code import builder_code_suffix
|
|
14
|
+
from basileus.chain.constants import (
|
|
15
|
+
BUILDER_CODE,
|
|
16
|
+
ERC8004_IDENTITY_REGISTRY,
|
|
17
|
+
ERC8004_IDENTITY_REGISTRY_ABI,
|
|
18
|
+
)
|
|
19
|
+
from basileus.infra.aleph import ALEPH_API_URL, ALEPH_CHANNEL
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _get_registry(w3: Web3):
|
|
23
|
+
return w3.eth.contract(
|
|
24
|
+
address=Web3.to_checksum_address(ERC8004_IDENTITY_REGISTRY),
|
|
25
|
+
abi=ERC8004_IDENTITY_REGISTRY_ABI,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_agent_metadata(label: str) -> dict:
|
|
30
|
+
"""Build ERC-8004 registration JSON for a Basileus agent."""
|
|
31
|
+
return {
|
|
32
|
+
"type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1",
|
|
33
|
+
"name": f"{label}.basileus-agent.eth",
|
|
34
|
+
"description": "Autonomous prediction market trading agent on Base",
|
|
35
|
+
"image": "",
|
|
36
|
+
"services": [],
|
|
37
|
+
"x402Support": True,
|
|
38
|
+
"active": True,
|
|
39
|
+
"registrations": [],
|
|
40
|
+
"supportedTrust": [],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def upload_metadata_to_ipfs(
|
|
45
|
+
aleph_account: ETHAccount, metadata: dict, max_retries: int = 3
|
|
46
|
+
) -> str:
|
|
47
|
+
"""Upload agent metadata JSON to IPFS via Aleph. Returns ipfs:// URI."""
|
|
48
|
+
import asyncio
|
|
49
|
+
|
|
50
|
+
content_bytes = json.dumps(metadata, indent=2).encode("utf-8")
|
|
51
|
+
|
|
52
|
+
for attempt in range(max_retries):
|
|
53
|
+
try:
|
|
54
|
+
async with AuthenticatedAlephHttpClient(
|
|
55
|
+
account=aleph_account, api_server=ALEPH_API_URL
|
|
56
|
+
) as client:
|
|
57
|
+
result, _status = await asyncio.wait_for(
|
|
58
|
+
client.create_store(
|
|
59
|
+
file_content=content_bytes,
|
|
60
|
+
storage_engine=StorageEnum.ipfs,
|
|
61
|
+
channel=ALEPH_CHANNEL,
|
|
62
|
+
guess_mime_type=True,
|
|
63
|
+
),
|
|
64
|
+
timeout=120,
|
|
65
|
+
)
|
|
66
|
+
cid = result.content.item_hash
|
|
67
|
+
return f"ipfs://{cid}"
|
|
68
|
+
except Exception:
|
|
69
|
+
if attempt >= max_retries - 1:
|
|
70
|
+
raise
|
|
71
|
+
await asyncio.sleep(3)
|
|
72
|
+
raise RuntimeError("IPFS upload failed after retries")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def check_existing_registration(w3: Web3, address: str) -> bool:
|
|
76
|
+
"""Check if address already owns an ERC-8004 identity NFT."""
|
|
77
|
+
contract = _get_registry(w3)
|
|
78
|
+
checksummed = Web3.to_checksum_address(address)
|
|
79
|
+
balance = contract.functions.balanceOf(checksummed).call()
|
|
80
|
+
return balance > 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def register_agent(
|
|
84
|
+
w3: Web3, private_key: str, agent_uri: str, ens_name: str
|
|
85
|
+
) -> tuple[int, str]:
|
|
86
|
+
"""Register agent on-chain via ERC-8004 IdentityRegistry.
|
|
87
|
+
|
|
88
|
+
Returns (agentId, tx_hash).
|
|
89
|
+
"""
|
|
90
|
+
contract = _get_registry(w3)
|
|
91
|
+
account = Account.from_key(private_key)
|
|
92
|
+
|
|
93
|
+
# Encode ENS name as metadata entry: (key, abi-encoded value)
|
|
94
|
+
ens_value = eth_abi.encode(["string"], [ens_name])
|
|
95
|
+
metadata_entries = [("ens", ens_value)]
|
|
96
|
+
|
|
97
|
+
call = contract.functions.register(agent_uri, metadata_entries)
|
|
98
|
+
tx = call.build_transaction(
|
|
99
|
+
{
|
|
100
|
+
"from": account.address,
|
|
101
|
+
"nonce": w3.eth.get_transaction_count(account.address, "pending"),
|
|
102
|
+
"gas": call.estimate_gas({"from": account.address}),
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if BUILDER_CODE:
|
|
107
|
+
suffix = builder_code_suffix(BUILDER_CODE)
|
|
108
|
+
tx["data"] += suffix.hex()
|
|
109
|
+
tx["gas"] += len(suffix) * 16
|
|
110
|
+
|
|
111
|
+
signed = account.sign_transaction(tx)
|
|
112
|
+
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
113
|
+
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=60)
|
|
114
|
+
|
|
115
|
+
if receipt["status"] != 1:
|
|
116
|
+
raise RuntimeError(f"Transaction reverted: 0x{tx_hash.hex()}")
|
|
117
|
+
|
|
118
|
+
# Extract agentId from Registered event (suppress MismatchedABI warnings
|
|
119
|
+
# from unrelated logs like ERC-721 Transfer/Approval)
|
|
120
|
+
with warnings.catch_warnings():
|
|
121
|
+
warnings.filterwarnings("ignore", message=".*MismatchedABI.*")
|
|
122
|
+
registered_events = contract.events.Registered().process_receipt(receipt)
|
|
123
|
+
if not registered_events:
|
|
124
|
+
raise RuntimeError(f"No Registered event found in tx 0x{tx_hash.hex()}")
|
|
125
|
+
agent_id = registered_events[0]["args"]["agentId"]
|
|
126
|
+
|
|
127
|
+
return (agent_id, f"0x{tx_hash.hex()}")
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
from aleph.sdk.chains.ethereum import ETHAccount
|
|
6
|
+
from aleph.sdk.client.authenticated_http import AuthenticatedAlephHttpClient
|
|
7
|
+
from aleph.sdk.evm_utils import FlowUpdate
|
|
8
|
+
from aleph_message.models import InstanceMessage
|
|
9
|
+
|
|
10
|
+
from basileus.infra.aleph import ALEPH_API_URL, COMMUNITY_RECEIVER, CRNInfo
|
|
11
|
+
|
|
12
|
+
COMMUNITY_FLOW_PERCENTAGE = Decimal("0.2")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class FlowRates:
|
|
17
|
+
"""Computed flow rates for operator and community."""
|
|
18
|
+
|
|
19
|
+
operator: Decimal
|
|
20
|
+
community: Decimal
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def compute_flow_rates(
|
|
24
|
+
account: ETHAccount,
|
|
25
|
+
instance_hash: str,
|
|
26
|
+
) -> FlowRates:
|
|
27
|
+
"""Compute required Superfluid flow rates from instance pricing."""
|
|
28
|
+
async with AuthenticatedAlephHttpClient(
|
|
29
|
+
account=account, api_server=ALEPH_API_URL
|
|
30
|
+
) as client:
|
|
31
|
+
instance_msg = await client.get_message(instance_hash, with_status=False)
|
|
32
|
+
if not isinstance(instance_msg, InstanceMessage):
|
|
33
|
+
raise ValueError(f"{instance_hash} is not an instance")
|
|
34
|
+
|
|
35
|
+
estimated = await client.get_estimated_price(content=instance_msg.content)
|
|
36
|
+
total_flow = Decimal(estimated.required_tokens)
|
|
37
|
+
return FlowRates(
|
|
38
|
+
operator=total_flow * (1 - COMMUNITY_FLOW_PERCENTAGE),
|
|
39
|
+
community=total_flow * COMMUNITY_FLOW_PERCENTAGE,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _check_tx(account: ETHAccount, tx_hash: str | None, label: str) -> None:
|
|
44
|
+
"""Check that a tx succeeded on-chain. Raises if reverted."""
|
|
45
|
+
if tx_hash is None:
|
|
46
|
+
raise ValueError(f"{label}: no tx hash returned")
|
|
47
|
+
from hexbytes import HexBytes
|
|
48
|
+
|
|
49
|
+
receipt = account._provider.eth.wait_for_transaction_receipt( # type: ignore[union-attr]
|
|
50
|
+
HexBytes(tx_hash), timeout=60
|
|
51
|
+
)
|
|
52
|
+
if receipt["status"] != 1:
|
|
53
|
+
raise ValueError(f"{label}: tx {tx_hash} reverted on-chain")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def create_operator_flow(
|
|
57
|
+
account: ETHAccount,
|
|
58
|
+
crn: CRNInfo,
|
|
59
|
+
flow_rate: Decimal,
|
|
60
|
+
) -> str | None:
|
|
61
|
+
"""Create operator Superfluid flow. Checks tx receipt. Returns tx hash."""
|
|
62
|
+
existing = await account.get_flow(crn.receiver_address)
|
|
63
|
+
existing_rate = Decimal(existing["flowRate"] or 0)
|
|
64
|
+
if existing_rate < flow_rate:
|
|
65
|
+
tx_hash = await account.manage_flow(
|
|
66
|
+
receiver=crn.receiver_address,
|
|
67
|
+
flow=flow_rate - existing_rate,
|
|
68
|
+
update_type=FlowUpdate.INCREASE,
|
|
69
|
+
)
|
|
70
|
+
_check_tx(account, tx_hash, "Operator flow")
|
|
71
|
+
return tx_hash
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def create_community_flow(
|
|
76
|
+
account: ETHAccount,
|
|
77
|
+
flow_rate: Decimal,
|
|
78
|
+
) -> str | None:
|
|
79
|
+
"""Create community Superfluid flow. Checks tx receipt. Returns tx hash."""
|
|
80
|
+
await asyncio.sleep(5)
|
|
81
|
+
existing = await account.get_flow(COMMUNITY_RECEIVER)
|
|
82
|
+
existing_rate = Decimal(existing["flowRate"] or 0)
|
|
83
|
+
if existing_rate < flow_rate:
|
|
84
|
+
tx_hash = await account.manage_flow(
|
|
85
|
+
receiver=COMMUNITY_RECEIVER,
|
|
86
|
+
flow=flow_rate - existing_rate,
|
|
87
|
+
update_type=FlowUpdate.INCREASE,
|
|
88
|
+
)
|
|
89
|
+
_check_tx(account, tx_hash, "Community flow")
|
|
90
|
+
return tx_hash
|
|
91
|
+
return None
|