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.
Files changed (95) hide show
  1. traia_iatp/__init__.py +105 -8
  2. traia_iatp/cli/main.py +85 -1
  3. traia_iatp/client/__init__.py +28 -3
  4. traia_iatp/client/crewai_a2a_tools.py +32 -12
  5. traia_iatp/client/d402_a2a_client.py +348 -0
  6. traia_iatp/contracts/__init__.py +11 -0
  7. traia_iatp/contracts/data/abis/contract-abis-localhost.json +4091 -0
  8. traia_iatp/contracts/data/abis/contract-abis-sepolia.json +4890 -0
  9. traia_iatp/contracts/data/addresses/contract-addresses.json +17 -0
  10. traia_iatp/contracts/data/addresses/contract-proxies.json +12 -0
  11. traia_iatp/contracts/iatp_contracts_config.py +263 -0
  12. traia_iatp/contracts/wallet_creator.py +369 -0
  13. traia_iatp/core/models.py +17 -3
  14. traia_iatp/d402/MIDDLEWARE_ARCHITECTURE.md +205 -0
  15. traia_iatp/d402/PRICE_BUILDER_USAGE.md +249 -0
  16. traia_iatp/d402/README.md +489 -0
  17. traia_iatp/d402/__init__.py +54 -0
  18. traia_iatp/d402/asgi_wrapper.py +469 -0
  19. traia_iatp/d402/chains.py +102 -0
  20. traia_iatp/d402/client.py +150 -0
  21. traia_iatp/d402/clients/__init__.py +7 -0
  22. traia_iatp/d402/clients/base.py +218 -0
  23. traia_iatp/d402/clients/httpx.py +266 -0
  24. traia_iatp/d402/common.py +114 -0
  25. traia_iatp/d402/encoding.py +28 -0
  26. traia_iatp/d402/examples/client_example.py +197 -0
  27. traia_iatp/d402/examples/server_example.py +171 -0
  28. traia_iatp/d402/facilitator.py +481 -0
  29. traia_iatp/d402/mcp_middleware.py +296 -0
  30. traia_iatp/d402/models.py +116 -0
  31. traia_iatp/d402/networks.py +98 -0
  32. traia_iatp/d402/path.py +43 -0
  33. traia_iatp/d402/payment_introspection.py +126 -0
  34. traia_iatp/d402/payment_signing.py +183 -0
  35. traia_iatp/d402/price_builder.py +164 -0
  36. traia_iatp/d402/servers/__init__.py +61 -0
  37. traia_iatp/d402/servers/base.py +139 -0
  38. traia_iatp/d402/servers/example_general_server.py +140 -0
  39. traia_iatp/d402/servers/fastapi.py +253 -0
  40. traia_iatp/d402/servers/mcp.py +304 -0
  41. traia_iatp/d402/servers/starlette.py +878 -0
  42. traia_iatp/d402/starlette_middleware.py +529 -0
  43. traia_iatp/d402/types.py +300 -0
  44. traia_iatp/mcp/D402_MCP_ADAPTER_FLOW.md +357 -0
  45. traia_iatp/mcp/__init__.py +3 -0
  46. traia_iatp/mcp/d402_mcp_tool_adapter.py +526 -0
  47. traia_iatp/mcp/mcp_agent_template.py +78 -13
  48. traia_iatp/mcp/templates/Dockerfile.j2 +27 -4
  49. traia_iatp/mcp/templates/README.md.j2 +104 -8
  50. traia_iatp/mcp/templates/cursor-rules.md.j2 +194 -0
  51. traia_iatp/mcp/templates/deployment_params.json.j2 +1 -2
  52. traia_iatp/mcp/templates/docker-compose.yml.j2 +13 -3
  53. traia_iatp/mcp/templates/env.example.j2 +60 -0
  54. traia_iatp/mcp/templates/mcp_health_check.py.j2 +2 -2
  55. traia_iatp/mcp/templates/pyproject.toml.j2 +11 -5
  56. traia_iatp/mcp/templates/pyrightconfig.json.j2 +22 -0
  57. traia_iatp/mcp/templates/run_local_docker.sh.j2 +320 -10
  58. traia_iatp/mcp/templates/server.py.j2 +174 -197
  59. traia_iatp/mcp/traia_mcp_adapter.py +182 -20
  60. traia_iatp/registry/__init__.py +47 -12
  61. traia_iatp/registry/atlas_search_indexes.json +108 -54
  62. traia_iatp/registry/iatp_search_api.py +169 -39
  63. traia_iatp/registry/mongodb_registry.py +241 -69
  64. traia_iatp/registry/readmes/EMBEDDINGS_SETUP.md +1 -1
  65. traia_iatp/registry/readmes/IATP_SEARCH_API_GUIDE.md +8 -8
  66. traia_iatp/registry/readmes/MONGODB_X509_AUTH.md +1 -1
  67. traia_iatp/registry/readmes/README.md +3 -3
  68. traia_iatp/registry/readmes/REFACTORING_SUMMARY.md +6 -6
  69. traia_iatp/scripts/__init__.py +2 -0
  70. traia_iatp/scripts/create_wallet.py +244 -0
  71. traia_iatp/server/a2a_server.py +22 -7
  72. traia_iatp/server/iatp_server_template_generator.py +23 -0
  73. traia_iatp/server/templates/.dockerignore.j2 +48 -0
  74. traia_iatp/server/templates/Dockerfile.j2 +23 -1
  75. traia_iatp/server/templates/README.md +2 -2
  76. traia_iatp/server/templates/README.md.j2 +5 -5
  77. traia_iatp/server/templates/__main__.py.j2 +374 -66
  78. traia_iatp/server/templates/agent.py.j2 +12 -11
  79. traia_iatp/server/templates/agent_config.json.j2 +3 -3
  80. traia_iatp/server/templates/agent_executor.py.j2 +45 -27
  81. traia_iatp/server/templates/env.example.j2 +32 -4
  82. traia_iatp/server/templates/gitignore.j2 +7 -0
  83. traia_iatp/server/templates/pyproject.toml.j2 +13 -12
  84. traia_iatp/server/templates/run_local_docker.sh.j2 +143 -11
  85. traia_iatp/server/templates/server.py.j2 +197 -10
  86. traia_iatp/special_agencies/registry_search_agency.py +1 -1
  87. traia_iatp/utils/iatp_utils.py +6 -6
  88. traia_iatp-0.1.67.dist-info/METADATA +320 -0
  89. traia_iatp-0.1.67.dist-info/RECORD +117 -0
  90. traia_iatp-0.1.2.dist-info/METADATA +0 -414
  91. traia_iatp-0.1.2.dist-info/RECORD +0 -72
  92. {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/WHEEL +0 -0
  93. {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/entry_points.txt +0 -0
  94. {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/licenses/LICENSE +0 -0
  95. {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
+