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.
@@ -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
+
@@ -0,0 +1,15 @@
1
+ # Basileus CLI
2
+
3
+ CLI for deploying Basileus autonomous prediction market agents on Base.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install basileus
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ basileus deploy
15
+ ```
File without changes
@@ -0,0 +1,3 @@
1
+ from .main import app
2
+
3
+ app(prog_name="basileus")
@@ -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