traia-iatp 0.1.2__py3-none-any.whl → 0.1.67__py3-none-any.whl
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.
- traia_iatp/__init__.py +105 -8
- traia_iatp/cli/main.py +85 -1
- traia_iatp/client/__init__.py +28 -3
- traia_iatp/client/crewai_a2a_tools.py +32 -12
- traia_iatp/client/d402_a2a_client.py +348 -0
- traia_iatp/contracts/__init__.py +11 -0
- traia_iatp/contracts/data/abis/contract-abis-localhost.json +4091 -0
- traia_iatp/contracts/data/abis/contract-abis-sepolia.json +4890 -0
- traia_iatp/contracts/data/addresses/contract-addresses.json +17 -0
- traia_iatp/contracts/data/addresses/contract-proxies.json +12 -0
- traia_iatp/contracts/iatp_contracts_config.py +263 -0
- traia_iatp/contracts/wallet_creator.py +369 -0
- traia_iatp/core/models.py +17 -3
- traia_iatp/d402/MIDDLEWARE_ARCHITECTURE.md +205 -0
- traia_iatp/d402/PRICE_BUILDER_USAGE.md +249 -0
- traia_iatp/d402/README.md +489 -0
- traia_iatp/d402/__init__.py +54 -0
- traia_iatp/d402/asgi_wrapper.py +469 -0
- traia_iatp/d402/chains.py +102 -0
- traia_iatp/d402/client.py +150 -0
- traia_iatp/d402/clients/__init__.py +7 -0
- traia_iatp/d402/clients/base.py +218 -0
- traia_iatp/d402/clients/httpx.py +266 -0
- traia_iatp/d402/common.py +114 -0
- traia_iatp/d402/encoding.py +28 -0
- traia_iatp/d402/examples/client_example.py +197 -0
- traia_iatp/d402/examples/server_example.py +171 -0
- traia_iatp/d402/facilitator.py +481 -0
- traia_iatp/d402/mcp_middleware.py +296 -0
- traia_iatp/d402/models.py +116 -0
- traia_iatp/d402/networks.py +98 -0
- traia_iatp/d402/path.py +43 -0
- traia_iatp/d402/payment_introspection.py +126 -0
- traia_iatp/d402/payment_signing.py +183 -0
- traia_iatp/d402/price_builder.py +164 -0
- traia_iatp/d402/servers/__init__.py +61 -0
- traia_iatp/d402/servers/base.py +139 -0
- traia_iatp/d402/servers/example_general_server.py +140 -0
- traia_iatp/d402/servers/fastapi.py +253 -0
- traia_iatp/d402/servers/mcp.py +304 -0
- traia_iatp/d402/servers/starlette.py +878 -0
- traia_iatp/d402/starlette_middleware.py +529 -0
- traia_iatp/d402/types.py +300 -0
- traia_iatp/mcp/D402_MCP_ADAPTER_FLOW.md +357 -0
- traia_iatp/mcp/__init__.py +3 -0
- traia_iatp/mcp/d402_mcp_tool_adapter.py +526 -0
- traia_iatp/mcp/mcp_agent_template.py +78 -13
- traia_iatp/mcp/templates/Dockerfile.j2 +27 -4
- traia_iatp/mcp/templates/README.md.j2 +104 -8
- traia_iatp/mcp/templates/cursor-rules.md.j2 +194 -0
- traia_iatp/mcp/templates/deployment_params.json.j2 +1 -2
- traia_iatp/mcp/templates/docker-compose.yml.j2 +13 -3
- traia_iatp/mcp/templates/env.example.j2 +60 -0
- traia_iatp/mcp/templates/mcp_health_check.py.j2 +2 -2
- traia_iatp/mcp/templates/pyproject.toml.j2 +11 -5
- traia_iatp/mcp/templates/pyrightconfig.json.j2 +22 -0
- traia_iatp/mcp/templates/run_local_docker.sh.j2 +320 -10
- traia_iatp/mcp/templates/server.py.j2 +174 -197
- traia_iatp/mcp/traia_mcp_adapter.py +182 -20
- traia_iatp/registry/__init__.py +47 -12
- traia_iatp/registry/atlas_search_indexes.json +108 -54
- traia_iatp/registry/iatp_search_api.py +169 -39
- traia_iatp/registry/mongodb_registry.py +241 -69
- traia_iatp/registry/readmes/EMBEDDINGS_SETUP.md +1 -1
- traia_iatp/registry/readmes/IATP_SEARCH_API_GUIDE.md +8 -8
- traia_iatp/registry/readmes/MONGODB_X509_AUTH.md +1 -1
- traia_iatp/registry/readmes/README.md +3 -3
- traia_iatp/registry/readmes/REFACTORING_SUMMARY.md +6 -6
- traia_iatp/scripts/__init__.py +2 -0
- traia_iatp/scripts/create_wallet.py +244 -0
- traia_iatp/server/a2a_server.py +22 -7
- traia_iatp/server/iatp_server_template_generator.py +23 -0
- traia_iatp/server/templates/.dockerignore.j2 +48 -0
- traia_iatp/server/templates/Dockerfile.j2 +23 -1
- traia_iatp/server/templates/README.md +2 -2
- traia_iatp/server/templates/README.md.j2 +5 -5
- traia_iatp/server/templates/__main__.py.j2 +374 -66
- traia_iatp/server/templates/agent.py.j2 +12 -11
- traia_iatp/server/templates/agent_config.json.j2 +3 -3
- traia_iatp/server/templates/agent_executor.py.j2 +45 -27
- traia_iatp/server/templates/env.example.j2 +32 -4
- traia_iatp/server/templates/gitignore.j2 +7 -0
- traia_iatp/server/templates/pyproject.toml.j2 +13 -12
- traia_iatp/server/templates/run_local_docker.sh.j2 +143 -11
- traia_iatp/server/templates/server.py.j2 +197 -10
- traia_iatp/special_agencies/registry_search_agency.py +1 -1
- traia_iatp/utils/iatp_utils.py +6 -6
- traia_iatp-0.1.67.dist-info/METADATA +320 -0
- traia_iatp-0.1.67.dist-info/RECORD +117 -0
- traia_iatp-0.1.2.dist-info/METADATA +0 -414
- traia_iatp-0.1.2.dist-info/RECORD +0 -72
- {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/WHEEL +0 -0
- {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/entry_points.txt +0 -0
- {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/licenses/LICENSE +0 -0
- {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"localhost": {
|
|
3
|
+
"IATPWalletImplementation": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707",
|
|
4
|
+
"RoleManagerImplementation": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
|
|
5
|
+
"Congress": "0x94Fc9eddBd1779542b78eb92F0569762603876e2",
|
|
6
|
+
"IATPSettlementLayerImplementation": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
|
|
7
|
+
"IATPWalletFactoryImplementation": "0x0165878A594ca255338adfa4d48449f69242Eb8F"
|
|
8
|
+
},
|
|
9
|
+
"sepolia": {
|
|
10
|
+
"Congress": "0x94Fc9eddBd1779542b78eb92F0569762603876e2",
|
|
11
|
+
"TraiaCongressMembersRegistry": "0x3B685403b195f16D103b42FCf56F848A278d6049",
|
|
12
|
+
"IATPWalletImplementation": "0xCdc251C242Cc9d2289d9D20355a02425A040952b",
|
|
13
|
+
"RoleManagerImplementation": "0x585AD85FCFBec3B1503E50b46407bF65d4006560",
|
|
14
|
+
"IATPSettlementLayerImplementation": "0xfe28b67C848D479858C8279dcEB50D4b894420Da",
|
|
15
|
+
"IATPWalletFactoryImplementation": "0xe423001656e6055094f131C2d86AA6D64032C14D"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"localhost": {
|
|
3
|
+
"RoleManager": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
|
|
4
|
+
"IATPSettlementLayer": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9",
|
|
5
|
+
"IATPWalletFactory": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853"
|
|
6
|
+
},
|
|
7
|
+
"sepolia": {
|
|
8
|
+
"RoleManager": "0x71d388142EA9194e5b51Eee3FEfe3B87D494dd38",
|
|
9
|
+
"IATPSettlementLayer": "0x0205ea98258eda5f6C94116a44f9811d8089f110",
|
|
10
|
+
"IATPWalletFactory": "0x54083Cc03c5D9D408D71D36170Ef01310E0C78fE"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Contract configuration and utilities for IATP.
|
|
2
|
+
|
|
3
|
+
This module provides centralized contract configuration management for all IATP packages.
|
|
4
|
+
It loads contract addresses and ABIs from the iatp-contracts deployment artifacts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from web3 import Web3
|
|
14
|
+
|
|
15
|
+
# Contract types
|
|
16
|
+
class ContractName(str, Enum):
|
|
17
|
+
"""Supported contract names."""
|
|
18
|
+
IATP_WALLET = "IATPWallet"
|
|
19
|
+
IATP_WALLET_FACTORY = "IATPWalletFactory"
|
|
20
|
+
IATP_SETTLEMENT_LAYER = "IATPSettlementLayer"
|
|
21
|
+
ROLE_MANAGER = "RoleManager"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Supported networks
|
|
25
|
+
SUPPORTED_NETWORKS = ["sepolia", "base-sepolia", "arbitrum-sepolia", "localhost"]
|
|
26
|
+
|
|
27
|
+
# Default RPC URLs
|
|
28
|
+
DEFAULT_RPC_URLS = {
|
|
29
|
+
"sepolia": "https://ethereum-sepolia-rpc.publicnode.com",
|
|
30
|
+
"base-sepolia": "https://sepolia.base.org",
|
|
31
|
+
"arbitrum-sepolia": "https://sepolia-rollup.arbitrum.io/rpc",
|
|
32
|
+
"localhost": "http://127.0.0.1:8545"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Cache for loaded data
|
|
36
|
+
_contract_addresses_cache: Dict[str, Dict] = {}
|
|
37
|
+
_contract_abis_cache: Dict[str, Dict] = {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _get_contracts_dir() -> Path:
|
|
41
|
+
"""Get the contracts directory path.
|
|
42
|
+
|
|
43
|
+
Uses the package's data directory where ABIs and addresses are copied.
|
|
44
|
+
This directory should be updated manually whenever contracts are redeployed.
|
|
45
|
+
|
|
46
|
+
Location: IATP/src/traia_iatp/contracts/data/
|
|
47
|
+
"""
|
|
48
|
+
# Use the data directory in the package
|
|
49
|
+
data_dir = Path(__file__).parent / "data"
|
|
50
|
+
|
|
51
|
+
# Verify it exists and has required subdirectories
|
|
52
|
+
abis_dir = data_dir / "abis"
|
|
53
|
+
addresses_dir = data_dir / "addresses"
|
|
54
|
+
|
|
55
|
+
if not data_dir.exists():
|
|
56
|
+
raise FileNotFoundError(
|
|
57
|
+
f"Contracts data directory not found: {data_dir}\n"
|
|
58
|
+
"Please ensure contract ABIs and addresses are copied to this location."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if not abis_dir.exists() or not addresses_dir.exists():
|
|
62
|
+
raise FileNotFoundError(
|
|
63
|
+
f"Contract ABIs or addresses directory missing in {data_dir}\n"
|
|
64
|
+
"Expected structure:\n"
|
|
65
|
+
" data/abis/contract-abis-*.json\n"
|
|
66
|
+
" data/addresses/contract-*.json"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Verify files exist
|
|
70
|
+
if not list(abis_dir.glob("*.json")):
|
|
71
|
+
raise FileNotFoundError(f"No ABI files found in {abis_dir}")
|
|
72
|
+
|
|
73
|
+
if not list(addresses_dir.glob("*.json")):
|
|
74
|
+
raise FileNotFoundError(f"No address files found in {addresses_dir}")
|
|
75
|
+
|
|
76
|
+
return data_dir
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_contract_address(contract_name: str, network: str = "sepolia") -> Optional[str]:
|
|
80
|
+
"""Get contract address for a given network.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
contract_name: Name of the contract (e.g., "IATPWallet")
|
|
84
|
+
network: Network name (default: "sepolia")
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Contract address as hex string, or None if not found
|
|
88
|
+
"""
|
|
89
|
+
if network not in SUPPORTED_NETWORKS:
|
|
90
|
+
raise ValueError(f"Unsupported network: {network}. Supported: {SUPPORTED_NETWORKS}")
|
|
91
|
+
|
|
92
|
+
# Check cache
|
|
93
|
+
cache_key = f"{network}:addresses"
|
|
94
|
+
if cache_key not in _contract_addresses_cache:
|
|
95
|
+
# Load addresses file
|
|
96
|
+
contracts_dir = _get_contracts_dir()
|
|
97
|
+
|
|
98
|
+
# Get addresses directory (no symlinks - direct files)
|
|
99
|
+
addresses_dir = contracts_dir / "addresses"
|
|
100
|
+
|
|
101
|
+
if network == "localhost":
|
|
102
|
+
addresses_file = addresses_dir / "contract-addresses.json"
|
|
103
|
+
else:
|
|
104
|
+
addresses_file = addresses_dir / f"contract-proxies-{network}.json"
|
|
105
|
+
|
|
106
|
+
if not addresses_file.exists():
|
|
107
|
+
# Try without network suffix
|
|
108
|
+
addresses_file = addresses_dir / "contract-proxies.json"
|
|
109
|
+
|
|
110
|
+
if not addresses_file.exists():
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
with open(addresses_file, 'r') as f:
|
|
114
|
+
data = json.load(f)
|
|
115
|
+
|
|
116
|
+
# Handle nested structure: {network: {contract: address}}
|
|
117
|
+
if network in data:
|
|
118
|
+
_contract_addresses_cache[cache_key] = data[network]
|
|
119
|
+
else:
|
|
120
|
+
# Flat structure: {contract: address}
|
|
121
|
+
_contract_addresses_cache[cache_key] = data
|
|
122
|
+
|
|
123
|
+
addresses = _contract_addresses_cache[cache_key]
|
|
124
|
+
return addresses.get(contract_name)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_contract_abi(contract_name: str, network: str = "sepolia") -> Optional[List[dict]]:
|
|
128
|
+
"""Get contract ABI for a given network.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
contract_name: Name of the contract (e.g., "IATPWallet")
|
|
132
|
+
network: Network name (default: "sepolia")
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Contract ABI as list of dicts, or None if not found
|
|
136
|
+
"""
|
|
137
|
+
if network not in SUPPORTED_NETWORKS:
|
|
138
|
+
raise ValueError(f"Unsupported network: {network}. Supported: {SUPPORTED_NETWORKS}")
|
|
139
|
+
|
|
140
|
+
# Check cache
|
|
141
|
+
cache_key = f"{network}:abis"
|
|
142
|
+
if cache_key not in _contract_abis_cache:
|
|
143
|
+
# Load ABIs file
|
|
144
|
+
contracts_dir = _get_contracts_dir()
|
|
145
|
+
|
|
146
|
+
# Get ABIs directory (no symlinks - direct files)
|
|
147
|
+
abis_dir = contracts_dir / "abis"
|
|
148
|
+
|
|
149
|
+
abis_file = abis_dir / f"contract-abis-{network}.json"
|
|
150
|
+
|
|
151
|
+
if not abis_file.exists():
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
with open(abis_file, 'r') as f:
|
|
155
|
+
data = json.load(f)
|
|
156
|
+
|
|
157
|
+
# Handle nested structure: {network: {contract: [abi]}}
|
|
158
|
+
if network in data:
|
|
159
|
+
# Data is nested by network
|
|
160
|
+
_contract_abis_cache[cache_key] = data[network]
|
|
161
|
+
else:
|
|
162
|
+
# Flat structure: {contract: {abi: [...]}} or {contract: [abi]}
|
|
163
|
+
_contract_abis_cache[cache_key] = data
|
|
164
|
+
|
|
165
|
+
abis = _contract_abis_cache[cache_key]
|
|
166
|
+
|
|
167
|
+
# Handle different ABI structures
|
|
168
|
+
contract_data = abis.get(contract_name)
|
|
169
|
+
if isinstance(contract_data, list):
|
|
170
|
+
# Direct ABI list: {contract: [abi]}
|
|
171
|
+
return contract_data
|
|
172
|
+
elif isinstance(contract_data, dict) and "abi" in contract_data:
|
|
173
|
+
# Wrapped ABI: {contract: {abi: [...]}}
|
|
174
|
+
return contract_data.get("abi")
|
|
175
|
+
else:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def get_rpc_url(network: str = "sepolia") -> str:
|
|
180
|
+
"""Get RPC URL for a network.
|
|
181
|
+
|
|
182
|
+
First checks environment variables, then falls back to defaults.
|
|
183
|
+
|
|
184
|
+
Environment variables:
|
|
185
|
+
- SEPOLIA_RPC_URL
|
|
186
|
+
- BASE_SEPOLIA_RPC_URL
|
|
187
|
+
- ARBITRUM_SEPOLIA_RPC_URL
|
|
188
|
+
- LOCALHOST_RPC_URL
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
network: Network name (default: "sepolia")
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
RPC URL as string
|
|
195
|
+
"""
|
|
196
|
+
if network not in SUPPORTED_NETWORKS:
|
|
197
|
+
raise ValueError(f"Unsupported network: {network}. Supported: {SUPPORTED_NETWORKS}")
|
|
198
|
+
|
|
199
|
+
# Check environment variable
|
|
200
|
+
env_var = f"{network.upper().replace('-', '_')}_RPC_URL"
|
|
201
|
+
rpc_url = os.getenv(env_var)
|
|
202
|
+
|
|
203
|
+
if rpc_url:
|
|
204
|
+
return rpc_url
|
|
205
|
+
|
|
206
|
+
# Fall back to default
|
|
207
|
+
return DEFAULT_RPC_URLS.get(network, DEFAULT_RPC_URLS["sepolia"])
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def get_web3_provider(network: str = "sepolia") -> Web3:
|
|
211
|
+
"""Get Web3 provider for a network.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
network: Network name (default: "sepolia")
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Web3 instance connected to the network
|
|
218
|
+
"""
|
|
219
|
+
rpc_url = get_rpc_url(network)
|
|
220
|
+
w3 = Web3(Web3.HTTPProvider(rpc_url))
|
|
221
|
+
|
|
222
|
+
if not w3.is_connected():
|
|
223
|
+
raise ConnectionError(f"Failed to connect to {network} at {rpc_url}")
|
|
224
|
+
|
|
225
|
+
return w3
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def load_contract(
|
|
229
|
+
contract_name: str,
|
|
230
|
+
network: str = "sepolia",
|
|
231
|
+
address: Optional[str] = None
|
|
232
|
+
):
|
|
233
|
+
"""Load a contract instance with Web3.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
contract_name: Name of the contract
|
|
237
|
+
network: Network name (default: "sepolia")
|
|
238
|
+
address: Optional contract address (uses deployed address if not provided)
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Web3 contract instance
|
|
242
|
+
"""
|
|
243
|
+
w3 = get_web3_provider(network)
|
|
244
|
+
|
|
245
|
+
# Get ABI
|
|
246
|
+
abi = get_contract_abi(contract_name, network)
|
|
247
|
+
if not abi:
|
|
248
|
+
raise ValueError(f"ABI not found for {contract_name} on {network}")
|
|
249
|
+
|
|
250
|
+
# Get address
|
|
251
|
+
if not address:
|
|
252
|
+
address = get_contract_address(contract_name, network)
|
|
253
|
+
if not address:
|
|
254
|
+
raise ValueError(f"Address not found for {contract_name} on {network}")
|
|
255
|
+
|
|
256
|
+
# Create contract instance
|
|
257
|
+
contract = w3.eth.contract(
|
|
258
|
+
address=Web3.to_checksum_address(address),
|
|
259
|
+
abi=abi
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return contract, w3
|
|
263
|
+
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""IATPWallet creation script using IATPWalletFactory.
|
|
2
|
+
|
|
3
|
+
This script creates a new IATPWallet by calling the IATPWalletFactory contract.
|
|
4
|
+
It generates an operator keypair and deploys a wallet for a given owner.
|
|
5
|
+
|
|
6
|
+
Uses centralized contract configuration from traia_iatp.contracts.config.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import json
|
|
11
|
+
from typing import Dict, Optional, Tuple
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from eth_account import Account
|
|
14
|
+
from web3 import Web3
|
|
15
|
+
from web3.contract import Contract
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_contract_config(network: str = "sepolia") -> Dict:
|
|
19
|
+
"""Load contract addresses and ABIs for a network.
|
|
20
|
+
|
|
21
|
+
Uses the centralized config module from traia_iatp.contracts.config.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
network: Network name (sepolia, base-sepolia, etc.)
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Dict with addresses and ABIs
|
|
28
|
+
"""
|
|
29
|
+
from traia_iatp.contracts.iatp_contracts_config import get_contract_address, get_contract_abi
|
|
30
|
+
|
|
31
|
+
# Get all contract addresses and ABIs
|
|
32
|
+
contract_names = ["IATPWalletFactory", "IATPSettlementLayer", "IATPWallet", "RoleManager"]
|
|
33
|
+
addresses = {}
|
|
34
|
+
abis = {}
|
|
35
|
+
|
|
36
|
+
for name in contract_names:
|
|
37
|
+
try:
|
|
38
|
+
address = get_contract_address(name, network)
|
|
39
|
+
if address:
|
|
40
|
+
addresses[name] = address
|
|
41
|
+
|
|
42
|
+
abi = get_contract_abi(name, network)
|
|
43
|
+
if abi:
|
|
44
|
+
abis[name] = {"abi": abi}
|
|
45
|
+
except Exception as e:
|
|
46
|
+
# Some contracts may not be on all networks
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
"addresses": addresses,
|
|
51
|
+
"abis": abis
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def create_iatp_wallet(
|
|
56
|
+
owner_private_key: str,
|
|
57
|
+
operator_address: Optional[str] = None,
|
|
58
|
+
create_operator: bool = False,
|
|
59
|
+
wallet_name: str = "",
|
|
60
|
+
wallet_type: str = "MCP_SERVER",
|
|
61
|
+
wallet_description: str = "",
|
|
62
|
+
network: str = "sepolia",
|
|
63
|
+
rpc_url: Optional[str] = None,
|
|
64
|
+
maintainer_private_key: Optional[str] = None
|
|
65
|
+
) -> Dict[str, str]:
|
|
66
|
+
"""Create a new IATPWallet using IATPWalletFactory.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
owner_private_key: Private key of the wallet owner (REQUIRED INPUT)
|
|
70
|
+
operator_address: Operator address (REQUIRED unless create_operator=True)
|
|
71
|
+
create_operator: If True, generates new operator keypair
|
|
72
|
+
wallet_name: Name for the wallet (e.g., "My MCP Server")
|
|
73
|
+
wallet_type: Type of wallet - one of: CLIENT, HUMAN, MCP_SERVER, WEB_SERVER, AGENT
|
|
74
|
+
wallet_description: Description of the wallet/service
|
|
75
|
+
network: Network name (default: sepolia)
|
|
76
|
+
rpc_url: Optional RPC URL (uses default if not provided)
|
|
77
|
+
maintainer_private_key: Optional maintainer key for createWalletFor (uses env/SSM if not provided)
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Dictionary with:
|
|
81
|
+
- wallet_address: Deployed IATPWallet contract address
|
|
82
|
+
- owner_address: Owner address (from owner_private_key)
|
|
83
|
+
- operator_address: Operator address
|
|
84
|
+
- operator_private_key: Operator private key (only if create_operator=True)
|
|
85
|
+
- network: Network name
|
|
86
|
+
- transaction_hash: Deployment transaction
|
|
87
|
+
"""
|
|
88
|
+
# Setup Web3
|
|
89
|
+
if not rpc_url:
|
|
90
|
+
rpc_urls = {
|
|
91
|
+
"sepolia": os.getenv("SEPOLIA_RPC_URL", "https://ethereum-sepolia-rpc.publicnode.com"),
|
|
92
|
+
"base-sepolia": os.getenv("BASE_SEPOLIA_RPC_URL", "https://sepolia.base.org"),
|
|
93
|
+
"arbitrum-sepolia": os.getenv("ARBITRUM_SEPOLIA_RPC_URL", "https://sepolia-rollup.arbitrum.io/rpc"),
|
|
94
|
+
}
|
|
95
|
+
rpc_url = rpc_urls.get(network)
|
|
96
|
+
|
|
97
|
+
w3 = Web3(Web3.HTTPProvider(rpc_url))
|
|
98
|
+
|
|
99
|
+
if not w3.is_connected():
|
|
100
|
+
raise ConnectionError(f"Could not connect to {network} at {rpc_url}")
|
|
101
|
+
|
|
102
|
+
print(f"✅ Connected to {network}")
|
|
103
|
+
print(f" RPC: {rpc_url}")
|
|
104
|
+
print(f" Chain ID: {w3.eth.chain_id}")
|
|
105
|
+
|
|
106
|
+
# Load contract config
|
|
107
|
+
config = get_contract_config(network)
|
|
108
|
+
factory_address = config["addresses"].get("IATPWalletFactory")
|
|
109
|
+
factory_abi = config["abis"].get("IATPWalletFactory", {}).get("abi")
|
|
110
|
+
|
|
111
|
+
if not factory_address:
|
|
112
|
+
raise ValueError(f"IATPWalletFactory address not found for {network}")
|
|
113
|
+
|
|
114
|
+
if not factory_abi:
|
|
115
|
+
raise ValueError(f"IATPWalletFactory ABI not found for {network}")
|
|
116
|
+
|
|
117
|
+
print(f"\n📝 Contract Configuration:")
|
|
118
|
+
print(f" Factory: {factory_address}")
|
|
119
|
+
|
|
120
|
+
# Create owner account
|
|
121
|
+
if owner_private_key.startswith("0x"):
|
|
122
|
+
owner_private_key_clean = owner_private_key[2:]
|
|
123
|
+
else:
|
|
124
|
+
owner_private_key_clean = owner_private_key
|
|
125
|
+
owner_account = Account.from_key(owner_private_key_clean)
|
|
126
|
+
print(f"\n👤 Owner Account: {owner_account.address}")
|
|
127
|
+
|
|
128
|
+
# Handle operator
|
|
129
|
+
operator_private_key_output = None
|
|
130
|
+
if create_operator:
|
|
131
|
+
# Generate new operator keypair
|
|
132
|
+
operator_account = Account.create()
|
|
133
|
+
operator_address = operator_account.address
|
|
134
|
+
operator_private_key_output = operator_account.key.hex()
|
|
135
|
+
|
|
136
|
+
print(f"\n🔑 Generated Operator:")
|
|
137
|
+
print(f" Address: {operator_address}")
|
|
138
|
+
print(f" Private Key: {operator_private_key_output}")
|
|
139
|
+
elif not operator_address:
|
|
140
|
+
raise ValueError("operator_address required (or use --create-operator)")
|
|
141
|
+
else:
|
|
142
|
+
print(f"\n🔑 Using Provided Operator:")
|
|
143
|
+
print(f" Address: {operator_address}")
|
|
144
|
+
|
|
145
|
+
# Map wallet type string to enum value
|
|
146
|
+
wallet_type_map = {
|
|
147
|
+
"CLIENT": 0,
|
|
148
|
+
"HUMAN": 1,
|
|
149
|
+
"MCP_SERVER": 2,
|
|
150
|
+
"WEB_SERVER": 3,
|
|
151
|
+
"AGENT": 4
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if isinstance(wallet_type, str):
|
|
155
|
+
wallet_type_int = wallet_type_map.get(wallet_type.upper())
|
|
156
|
+
if wallet_type_int is None:
|
|
157
|
+
raise ValueError(f"Invalid wallet_type: {wallet_type}. Must be one of: {list(wallet_type_map.keys())}")
|
|
158
|
+
else:
|
|
159
|
+
wallet_type_int = int(wallet_type)
|
|
160
|
+
|
|
161
|
+
print(f"\n📝 Wallet Metadata:")
|
|
162
|
+
print(f" Name: {wallet_name or '(none)'}")
|
|
163
|
+
print(f" Type: {list(wallet_type_map.keys())[wallet_type_int]} ({wallet_type_int})")
|
|
164
|
+
print(f" Description: {wallet_description[:50] + '...' if len(wallet_description) > 50 else wallet_description or '(none)'}")
|
|
165
|
+
|
|
166
|
+
# Create factory contract instance
|
|
167
|
+
factory_contract = w3.eth.contract(
|
|
168
|
+
address=Web3.to_checksum_address(factory_address),
|
|
169
|
+
abi=factory_abi
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Determine which method to call
|
|
173
|
+
if maintainer_private_key:
|
|
174
|
+
# Use createWalletFor (maintainer creates for someone else)
|
|
175
|
+
# This is used by backend deployment scripts
|
|
176
|
+
if maintainer_private_key.startswith("0x"):
|
|
177
|
+
maintainer_private_key = maintainer_private_key[2:]
|
|
178
|
+
caller_account = Account.from_key(maintainer_private_key)
|
|
179
|
+
|
|
180
|
+
print(f"\n📞 Calling createWalletFor() as maintainer")
|
|
181
|
+
print(f" Maintainer: {caller_account.address}")
|
|
182
|
+
|
|
183
|
+
# Check maintainer balance
|
|
184
|
+
maintainer_balance = w3.eth.get_balance(caller_account.address)
|
|
185
|
+
print(f" Balance: {w3.from_wei(maintainer_balance, 'ether')} ETH")
|
|
186
|
+
|
|
187
|
+
if maintainer_balance < w3.to_wei(0.001, 'ether'):
|
|
188
|
+
raise ValueError(f"Insufficient maintainer balance: {w3.from_wei(maintainer_balance, 'ether')} ETH (need >= 0.001 ETH)")
|
|
189
|
+
|
|
190
|
+
# Build transaction with metadata
|
|
191
|
+
tx = factory_contract.functions.createWalletFor(
|
|
192
|
+
Web3.to_checksum_address(owner_account.address),
|
|
193
|
+
Web3.to_checksum_address(operator_address),
|
|
194
|
+
wallet_type_int, # WalletType enum
|
|
195
|
+
wallet_name, # Name
|
|
196
|
+
wallet_description # Description
|
|
197
|
+
).build_transaction({
|
|
198
|
+
'from': caller_account.address,
|
|
199
|
+
'nonce': w3.eth.get_transaction_count(caller_account.address),
|
|
200
|
+
'gas': 3000000,
|
|
201
|
+
'maxFeePerGas': w3.to_wei('2', 'gwei'),
|
|
202
|
+
'maxPriorityFeePerGas': w3.to_wei('0.1', 'gwei'),
|
|
203
|
+
'chainId': w3.eth.chain_id
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
# Estimate gas
|
|
207
|
+
try:
|
|
208
|
+
estimated_gas = w3.eth.estimate_gas(tx)
|
|
209
|
+
tx['gas'] = int(estimated_gas * 1.2)
|
|
210
|
+
print(f"\n⛽ Estimated gas: {estimated_gas}")
|
|
211
|
+
except Exception as e:
|
|
212
|
+
print(f"\n⚠️ Gas estimation failed: {e}, using default")
|
|
213
|
+
|
|
214
|
+
# Sign and send
|
|
215
|
+
signed_tx = w3.eth.account.sign_transaction(tx, caller_account.key)
|
|
216
|
+
|
|
217
|
+
else:
|
|
218
|
+
# Use createWallet (owner creates their own wallet)
|
|
219
|
+
# This is the normal path for developers using the CLI
|
|
220
|
+
print(f"\n📞 Calling createWallet() as owner")
|
|
221
|
+
print(f" Owner: {owner_account.address}")
|
|
222
|
+
|
|
223
|
+
# Check owner balance
|
|
224
|
+
owner_balance = w3.eth.get_balance(owner_account.address)
|
|
225
|
+
print(f" Balance: {w3.from_wei(owner_balance, 'ether')} ETH")
|
|
226
|
+
|
|
227
|
+
if owner_balance < w3.to_wei(0.001, 'ether'):
|
|
228
|
+
raise ValueError(f"Insufficient owner balance: {w3.from_wei(owner_balance, 'ether')} ETH (need >= 0.001 ETH for gas)")
|
|
229
|
+
|
|
230
|
+
# Build transaction
|
|
231
|
+
tx = factory_contract.functions.createWallet(
|
|
232
|
+
Web3.to_checksum_address(operator_address),
|
|
233
|
+
wallet_type_int, # WalletType enum
|
|
234
|
+
wallet_name, # Name
|
|
235
|
+
wallet_description # Description
|
|
236
|
+
).build_transaction({
|
|
237
|
+
'from': owner_account.address,
|
|
238
|
+
'nonce': w3.eth.get_transaction_count(owner_account.address),
|
|
239
|
+
'gas': 3000000,
|
|
240
|
+
'maxFeePerGas': w3.to_wei('2', 'gwei'),
|
|
241
|
+
'maxPriorityFeePerGas': w3.to_wei('0.1', 'gwei'),
|
|
242
|
+
'chainId': w3.eth.chain_id
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
# Estimate gas
|
|
246
|
+
try:
|
|
247
|
+
estimated_gas = w3.eth.estimate_gas(tx)
|
|
248
|
+
tx['gas'] = int(estimated_gas * 1.2)
|
|
249
|
+
print(f"\n⛽ Estimated gas: {estimated_gas}")
|
|
250
|
+
except Exception as e:
|
|
251
|
+
print(f"\n⚠️ Gas estimation failed: {e}, using default")
|
|
252
|
+
|
|
253
|
+
# Sign and send with owner key
|
|
254
|
+
signed_tx = w3.eth.account.sign_transaction(tx, owner_account.key)
|
|
255
|
+
|
|
256
|
+
print(f"\n🚀 Sending transaction...")
|
|
257
|
+
raw_tx = signed_tx.raw_transaction if hasattr(signed_tx, 'raw_transaction') else signed_tx.rawTransaction
|
|
258
|
+
tx_hash = w3.eth.send_raw_transaction(raw_tx)
|
|
259
|
+
print(f" TX Hash: {tx_hash.hex()}")
|
|
260
|
+
|
|
261
|
+
print(f"\n⏳ Waiting for confirmation...")
|
|
262
|
+
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)
|
|
263
|
+
|
|
264
|
+
if tx_receipt['status'] == 1:
|
|
265
|
+
print(f"✅ Transaction confirmed!")
|
|
266
|
+
print(f" Block: {tx_receipt['blockNumber']}")
|
|
267
|
+
print(f" Gas Used: {tx_receipt['gasUsed']}")
|
|
268
|
+
|
|
269
|
+
# Parse logs to get wallet address
|
|
270
|
+
wallet_address = None
|
|
271
|
+
try:
|
|
272
|
+
wallet_created_event = factory_contract.events.WalletCreated()
|
|
273
|
+
logs = wallet_created_event.process_receipt(tx_receipt)
|
|
274
|
+
|
|
275
|
+
if logs:
|
|
276
|
+
wallet_address = logs[0]['args']['wallet']
|
|
277
|
+
print(f"\n🎉 IATPWallet Deployed!")
|
|
278
|
+
print(f" Wallet Address: {wallet_address}")
|
|
279
|
+
print(f" Owner: {logs[0]['args']['owner']}")
|
|
280
|
+
print(f" Operator: {logs[0]['args']['operatorAddress']}")
|
|
281
|
+
except Exception as e:
|
|
282
|
+
print(f"⚠️ Could not parse event logs: {e}")
|
|
283
|
+
|
|
284
|
+
# Fallback: Get wallet using getWalletForOwner
|
|
285
|
+
if not wallet_address:
|
|
286
|
+
try:
|
|
287
|
+
wallet_address = factory_contract.functions.getWalletForOwner(
|
|
288
|
+
Web3.to_checksum_address(owner_account.address)
|
|
289
|
+
).call()
|
|
290
|
+
print(f"\n🎉 IATPWallet Deployed!")
|
|
291
|
+
print(f" Wallet Address: {wallet_address}")
|
|
292
|
+
except Exception as e:
|
|
293
|
+
raise Exception(f"Could not retrieve wallet address: {e}")
|
|
294
|
+
|
|
295
|
+
# Build result dictionary
|
|
296
|
+
result = {
|
|
297
|
+
"wallet_address": wallet_address,
|
|
298
|
+
"owner_address": owner_account.address,
|
|
299
|
+
"operator_address": operator_address,
|
|
300
|
+
"network": network,
|
|
301
|
+
"transaction_hash": tx_hash.hex(),
|
|
302
|
+
"gas_used": tx_receipt['gasUsed'],
|
|
303
|
+
"block_number": tx_receipt['blockNumber']
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
# Include operator private key if we generated it
|
|
307
|
+
if operator_private_key_output:
|
|
308
|
+
result["operator_private_key"] = operator_private_key_output
|
|
309
|
+
|
|
310
|
+
return result
|
|
311
|
+
else:
|
|
312
|
+
raise Exception(f"Transaction failed: {tx_receipt}")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def main():
|
|
316
|
+
"""CLI entry point for IATP wallet creation."""
|
|
317
|
+
import argparse
|
|
318
|
+
|
|
319
|
+
parser = argparse.ArgumentParser(description="Create IATPWallet using IATPWalletFactory")
|
|
320
|
+
parser.add_argument("--owner-key", required=True, help="Owner's private key (REQUIRED INPUT)")
|
|
321
|
+
parser.add_argument("--operator-address", help="Operator address (required unless --create-operator)")
|
|
322
|
+
parser.add_argument("--create-operator", action="store_true", help="Generate new operator keypair")
|
|
323
|
+
parser.add_argument("--wallet-name", default="", help="Name for the wallet")
|
|
324
|
+
parser.add_argument("--wallet-type", default="MCP_SERVER",
|
|
325
|
+
help="Wallet type: CLIENT, HUMAN, MCP_SERVER, WEB_SERVER, AGENT (default: MCP_SERVER)")
|
|
326
|
+
parser.add_argument("--wallet-description", default="", help="Description of the wallet/service")
|
|
327
|
+
parser.add_argument("--network", default="sepolia", help="Network name (default: sepolia)")
|
|
328
|
+
parser.add_argument("--rpc-url", help="Custom RPC URL")
|
|
329
|
+
parser.add_argument("--maintainer-key", help="Maintainer key (or use MAINTAINER_PRIVATE_KEY env var)")
|
|
330
|
+
parser.add_argument("--output", help="Output file for wallet info (JSON)")
|
|
331
|
+
|
|
332
|
+
args = parser.parse_args()
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
result = create_iatp_wallet(
|
|
336
|
+
owner_private_key=args.owner_key,
|
|
337
|
+
operator_address=args.operator_address,
|
|
338
|
+
create_operator=args.create_operator,
|
|
339
|
+
wallet_name=args.wallet_name,
|
|
340
|
+
wallet_type=args.wallet_type,
|
|
341
|
+
wallet_description=args.wallet_description,
|
|
342
|
+
network=args.network,
|
|
343
|
+
rpc_url=args.rpc_url,
|
|
344
|
+
maintainer_private_key=args.maintainer_key
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
if args.output:
|
|
348
|
+
with open(args.output, 'w') as f:
|
|
349
|
+
json.dump(result, f, indent=2)
|
|
350
|
+
print(f"\n💾 Wallet info saved to: {args.output}")
|
|
351
|
+
|
|
352
|
+
print(f"\n{'='*80}")
|
|
353
|
+
print("IATPWallet Created Successfully!")
|
|
354
|
+
print(f"{'='*80}")
|
|
355
|
+
print(json.dumps(result, indent=2))
|
|
356
|
+
|
|
357
|
+
except Exception as e:
|
|
358
|
+
print(f"\n❌ Error: {e}")
|
|
359
|
+
import traceback
|
|
360
|
+
traceback.print_exc()
|
|
361
|
+
return 1
|
|
362
|
+
|
|
363
|
+
return 0
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
if __name__ == "__main__":
|
|
367
|
+
import sys
|
|
368
|
+
sys.exit(main())
|
|
369
|
+
|