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,481 @@
|
|
|
1
|
+
"""Custom d402 facilitator that interfaces with IATP Settlement Layer."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
import httpx
|
|
7
|
+
from eth_account import Account
|
|
8
|
+
from web3 import Web3
|
|
9
|
+
from eth_account.messages import encode_defunct
|
|
10
|
+
|
|
11
|
+
from .types import (
|
|
12
|
+
PaymentPayload,
|
|
13
|
+
PaymentRequirements,
|
|
14
|
+
VerifyResponse,
|
|
15
|
+
SettleResponse
|
|
16
|
+
)
|
|
17
|
+
from .models import IATPSettlementRequest
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_now_in_utc():
|
|
21
|
+
"""Get current time in UTC."""
|
|
22
|
+
return datetime.now(timezone.utc)
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class IATPSettlementFacilitator:
|
|
28
|
+
"""Custom d402 facilitator that settles payments through IATP Settlement Layer.
|
|
29
|
+
|
|
30
|
+
This facilitator verifies d402 payment headers and then submits settlement
|
|
31
|
+
requests to the facilitator service for batch on-chain settlement.
|
|
32
|
+
|
|
33
|
+
Flow:
|
|
34
|
+
1. MCP Server receives request with X-PAYMENT header
|
|
35
|
+
2. Facilitator /verify verifies the payment signature and authorization
|
|
36
|
+
3. MCP Server processes the request
|
|
37
|
+
4. Facilitator /settle accepts provider attestation and queues for settlement
|
|
38
|
+
5. Facilitator cron batches and submits to IATPSettlementLayer.sol on-chain
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
facilitator_url: str,
|
|
44
|
+
facilitator_api_key: Optional[str] = None,
|
|
45
|
+
provider_operator_key: Optional[str] = None,
|
|
46
|
+
web3_provider: Optional[str] = None,
|
|
47
|
+
server_name: Optional[str] = None,
|
|
48
|
+
server_url: Optional[str] = None
|
|
49
|
+
):
|
|
50
|
+
"""Initialize the IATP Settlement Facilitator.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
facilitator_url: URL of the facilitator service (handles both /verify and /settle)
|
|
54
|
+
facilitator_api_key: Optional API key for facilitator authentication
|
|
55
|
+
provider_operator_key: Operator private key for provider attestation signing
|
|
56
|
+
web3_provider: Optional Web3 provider URL for direct blockchain interaction
|
|
57
|
+
server_name: Optional MCP server name/ID (sent to facilitator for tracking)
|
|
58
|
+
server_url: Optional MCP server URL (sent to facilitator for tracking)
|
|
59
|
+
"""
|
|
60
|
+
self.facilitator_url = facilitator_url.rstrip("/")
|
|
61
|
+
self.facilitator_api_key = facilitator_api_key
|
|
62
|
+
self.provider_operator_key = provider_operator_key
|
|
63
|
+
self.server_name = server_name
|
|
64
|
+
self.server_url = server_url
|
|
65
|
+
|
|
66
|
+
# Initialize Web3 if provider is given
|
|
67
|
+
self.w3 = Web3(Web3.HTTPProvider(web3_provider)) if web3_provider else None
|
|
68
|
+
|
|
69
|
+
# Initialize operator account if key provided
|
|
70
|
+
self.operator_account = None
|
|
71
|
+
if provider_operator_key:
|
|
72
|
+
if provider_operator_key.startswith("0x"):
|
|
73
|
+
provider_operator_key = provider_operator_key[2:]
|
|
74
|
+
self.operator_account = Account.from_key(provider_operator_key)
|
|
75
|
+
|
|
76
|
+
async def verify(
|
|
77
|
+
self,
|
|
78
|
+
payment: PaymentPayload,
|
|
79
|
+
payment_requirements: PaymentRequirements
|
|
80
|
+
) -> VerifyResponse:
|
|
81
|
+
"""Verify a payment header is valid.
|
|
82
|
+
|
|
83
|
+
If facilitator_url is configured, calls external facilitator /verify endpoint
|
|
84
|
+
which returns a payment_uuid. Otherwise performs local verification.
|
|
85
|
+
|
|
86
|
+
This checks:
|
|
87
|
+
1. Signature is valid
|
|
88
|
+
2. Authorization is not expired
|
|
89
|
+
3. Amount matches requirements
|
|
90
|
+
4. From address has sufficient balance
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
payment: Payment payload from X-PAYMENT header
|
|
94
|
+
payment_requirements: Payment requirements from server
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
VerifyResponse with validation result and payment_uuid (if from external facilitator)
|
|
98
|
+
"""
|
|
99
|
+
# If external facilitator URL is configured, call it
|
|
100
|
+
if self.facilitator_url:
|
|
101
|
+
try:
|
|
102
|
+
headers = {"Content-Type": "application/json"}
|
|
103
|
+
if self.facilitator_api_key:
|
|
104
|
+
headers["X-API-Key"] = self.facilitator_api_key
|
|
105
|
+
|
|
106
|
+
verify_request = {
|
|
107
|
+
"paymentPayload": payment.model_dump(by_alias=True),
|
|
108
|
+
"paymentRequirements": payment_requirements.model_dump(by_alias=True)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Include server name and URL if available
|
|
112
|
+
if self.server_name:
|
|
113
|
+
verify_request["serverName"] = self.server_name
|
|
114
|
+
if self.server_url:
|
|
115
|
+
verify_request["serverUrl"] = self.server_url
|
|
116
|
+
|
|
117
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
118
|
+
response = await client.post(
|
|
119
|
+
f"{self.facilitator_url}/verify",
|
|
120
|
+
json=verify_request,
|
|
121
|
+
headers=headers
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if response.status_code == 200:
|
|
125
|
+
result = response.json()
|
|
126
|
+
# Parse response and extract payment_uuid
|
|
127
|
+
verify_response = VerifyResponse(**result)
|
|
128
|
+
if verify_response.payment_uuid:
|
|
129
|
+
logger.info(f"Payment verified via facilitator with payment_uuid: {verify_response.payment_uuid[:20]}...")
|
|
130
|
+
return verify_response
|
|
131
|
+
else:
|
|
132
|
+
error_msg = f"Facilitator verify error: {response.status_code} - {response.text}"
|
|
133
|
+
logger.error(error_msg)
|
|
134
|
+
return VerifyResponse(
|
|
135
|
+
is_valid=False,
|
|
136
|
+
invalid_reason=error_msg,
|
|
137
|
+
payer=None,
|
|
138
|
+
payment_uuid=None
|
|
139
|
+
)
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.error(f"Error calling external facilitator verify: {e}")
|
|
142
|
+
# Fall back to local verification
|
|
143
|
+
logger.warning("Falling back to local verification")
|
|
144
|
+
|
|
145
|
+
# Local verification (fallback or if no facilitator_url configured)
|
|
146
|
+
try:
|
|
147
|
+
# Extract payment details
|
|
148
|
+
if payment.scheme != "exact":
|
|
149
|
+
return VerifyResponse(
|
|
150
|
+
is_valid=False,
|
|
151
|
+
invalid_reason=f"Unsupported scheme: {payment.scheme}",
|
|
152
|
+
payer=None,
|
|
153
|
+
payment_uuid=None
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
payload = payment.payload
|
|
157
|
+
authorization = payload.authorization
|
|
158
|
+
signature = payload.signature
|
|
159
|
+
|
|
160
|
+
# Verify the signature matches the authorization
|
|
161
|
+
payer = authorization.from_
|
|
162
|
+
|
|
163
|
+
# Reconstruct the EIP-712 message and verify signature
|
|
164
|
+
# This would use the exact EIP-712 domain from payment_requirements.extra
|
|
165
|
+
eip712_domain = payment_requirements.extra or {}
|
|
166
|
+
|
|
167
|
+
# For now, perform basic validation
|
|
168
|
+
# In production, this should verify the EIP-3009 signature
|
|
169
|
+
if not payer or not signature:
|
|
170
|
+
return VerifyResponse(
|
|
171
|
+
is_valid=False,
|
|
172
|
+
invalid_reason="Missing payer or signature",
|
|
173
|
+
payer=None,
|
|
174
|
+
payment_uuid=None
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Verify amount matches requirements
|
|
178
|
+
if authorization.value != payment_requirements.max_amount_required:
|
|
179
|
+
return VerifyResponse(
|
|
180
|
+
is_valid=False,
|
|
181
|
+
invalid_reason=f"Amount mismatch: expected {payment_requirements.max_amount_required}, got {authorization.value}",
|
|
182
|
+
payer=payer,
|
|
183
|
+
payment_uuid=None
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Verify not expired
|
|
187
|
+
import time
|
|
188
|
+
current_time = int(time.time())
|
|
189
|
+
valid_after = int(authorization.valid_after)
|
|
190
|
+
valid_before = int(authorization.valid_before)
|
|
191
|
+
|
|
192
|
+
if current_time < valid_after or current_time > valid_before:
|
|
193
|
+
return VerifyResponse(
|
|
194
|
+
is_valid=False,
|
|
195
|
+
invalid_reason="Authorization expired or not yet valid",
|
|
196
|
+
payer=payer,
|
|
197
|
+
payment_uuid=None
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Verify to address matches pay_to
|
|
201
|
+
if authorization.to.lower() != payment_requirements.pay_to.lower():
|
|
202
|
+
return VerifyResponse(
|
|
203
|
+
is_valid=False,
|
|
204
|
+
invalid_reason=f"Pay-to address mismatch",
|
|
205
|
+
payer=payer,
|
|
206
|
+
payment_uuid=None
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# All checks passed (local verification - no payment_uuid)
|
|
210
|
+
return VerifyResponse(
|
|
211
|
+
is_valid=True,
|
|
212
|
+
invalid_reason=None,
|
|
213
|
+
payer=payer,
|
|
214
|
+
payment_uuid=None # Local verification doesn't provide payment_uuid
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.error(f"Error verifying payment: {e}")
|
|
219
|
+
return VerifyResponse(
|
|
220
|
+
is_valid=False,
|
|
221
|
+
invalid_reason=f"Verification error: {str(e)}",
|
|
222
|
+
payer=None,
|
|
223
|
+
payment_uuid=None
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
async def settle(
|
|
227
|
+
self,
|
|
228
|
+
payment: PaymentPayload,
|
|
229
|
+
payment_requirements: PaymentRequirements
|
|
230
|
+
) -> SettleResponse:
|
|
231
|
+
"""Settle a verified payment through the IATP Settlement Layer.
|
|
232
|
+
|
|
233
|
+
This submits the payment to the facilitator, which will:
|
|
234
|
+
1. Verify both consumer and provider signatures
|
|
235
|
+
2. Queue for batch settlement
|
|
236
|
+
3. Batch submit to IATPSettlementLayer.settleRequests() on-chain
|
|
237
|
+
4. Credit the provider's balance after confirmation
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
payment: Verified payment payload
|
|
241
|
+
payment_requirements: Payment requirements
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
SettleResponse with settlement result
|
|
245
|
+
"""
|
|
246
|
+
try:
|
|
247
|
+
payload = payment.payload
|
|
248
|
+
authorization = payload.authorization
|
|
249
|
+
consumer_signature = payload.signature
|
|
250
|
+
|
|
251
|
+
# Create the service request struct (matches Solidity ServiceRequest)
|
|
252
|
+
service_request = {
|
|
253
|
+
"consumer": authorization.from_,
|
|
254
|
+
"provider": payment_requirements.pay_to,
|
|
255
|
+
"amount": authorization.value,
|
|
256
|
+
"timestamp": int(authorization.valid_after),
|
|
257
|
+
"serviceDescription": Web3.keccak(
|
|
258
|
+
text=payment_requirements.description
|
|
259
|
+
).hex()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
# Create provider attestation if operator key is available
|
|
263
|
+
provider_signature = None
|
|
264
|
+
extra = payment_requirements.extra or {}
|
|
265
|
+
output_hash = extra.get("output_hash")
|
|
266
|
+
payment_uuid = extra.get("payment_uuid") # Primary payment identifier from facilitator verify
|
|
267
|
+
facilitator_fee_percent = extra.get("facilitator_fee_percent", 250) # Get fee from facilitator verify response
|
|
268
|
+
|
|
269
|
+
if not payment_uuid:
|
|
270
|
+
logger.warning("No payment_uuid in payment_requirements.extra - attestation may not be linkable")
|
|
271
|
+
|
|
272
|
+
if self.operator_account:
|
|
273
|
+
# Create EIP-712 ProviderAttestation signature matching IATPWallet.sol
|
|
274
|
+
# ProviderAttestation(bytes32 consumerSignature, bytes32 outputHash, uint256 timestamp, bytes32 serviceDescription, uint256 facilitatorFeePercent)
|
|
275
|
+
|
|
276
|
+
# Hash the consumer signature bytes
|
|
277
|
+
consumer_signature_hash = Web3.keccak(hexstr=consumer_signature)
|
|
278
|
+
|
|
279
|
+
# Prepare output hash for contract verification
|
|
280
|
+
# Python: output_hash = keccak256(output_json) ← First hash (line 161 in mcp_middleware.py)
|
|
281
|
+
# Contract: outputHashHash = keccak256(outputHash bytes) ← Second hash (line 245 in IATPWallet.sol)
|
|
282
|
+
# Provider signs over outputHashHash (the double-hashed value)
|
|
283
|
+
if output_hash:
|
|
284
|
+
# Output hash is hex string like "0xabcd..." (already hashed once)
|
|
285
|
+
output_hash_bytes = bytes.fromhex(output_hash[2:] if output_hash.startswith("0x") else output_hash)
|
|
286
|
+
# Hash it again to match what contract will compute: keccak256(outputHash)
|
|
287
|
+
output_hash_hash = Web3.keccak(output_hash_bytes)
|
|
288
|
+
logger.debug(f"Output hash (1st): {output_hash[:20]}...")
|
|
289
|
+
logger.debug(f"Output hash (2nd): {output_hash_hash.hex()[:20]}...")
|
|
290
|
+
else:
|
|
291
|
+
# Use zero hash if no output provided
|
|
292
|
+
output_hash_hash = Web3.keccak(b"")
|
|
293
|
+
|
|
294
|
+
# Get service description hash
|
|
295
|
+
service_description_hash = Web3.keccak(text=payment_requirements.description)
|
|
296
|
+
|
|
297
|
+
# Attestation timestamp = current time when service is executed
|
|
298
|
+
# This is when provider actually rendered the service
|
|
299
|
+
import time as time_module
|
|
300
|
+
attestation_timestamp = int(time_module.time())
|
|
301
|
+
|
|
302
|
+
# Build EIP-712 typed data for ProviderAttestation
|
|
303
|
+
# Domain should be the Provider's IATPWallet domain
|
|
304
|
+
from .chains import get_chain_id
|
|
305
|
+
|
|
306
|
+
typed_data = {
|
|
307
|
+
"types": {
|
|
308
|
+
"ProviderAttestation": [
|
|
309
|
+
{"name": "consumerSignature", "type": "bytes32"},
|
|
310
|
+
{"name": "outputHash", "type": "bytes32"},
|
|
311
|
+
{"name": "timestamp", "type": "uint256"},
|
|
312
|
+
{"name": "serviceDescription", "type": "bytes32"},
|
|
313
|
+
{"name": "facilitatorFeePercent", "type": "uint256"},
|
|
314
|
+
]
|
|
315
|
+
},
|
|
316
|
+
"primaryType": "ProviderAttestation",
|
|
317
|
+
"domain": {
|
|
318
|
+
"name": "IATPWallet",
|
|
319
|
+
"version": "1",
|
|
320
|
+
"chainId": int(get_chain_id(payment.network)),
|
|
321
|
+
"verifyingContract": payment_requirements.pay_to, # Provider's IATPWallet address
|
|
322
|
+
},
|
|
323
|
+
"message": {
|
|
324
|
+
"consumerSignature": consumer_signature_hash,
|
|
325
|
+
"outputHash": output_hash_hash,
|
|
326
|
+
"timestamp": attestation_timestamp,
|
|
327
|
+
"serviceDescription": service_description_hash,
|
|
328
|
+
"facilitatorFeePercent": facilitator_fee_percent,
|
|
329
|
+
},
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
# Sign with provider's operator key
|
|
333
|
+
logger.info(f"🔐 Signing provider attestation with EIP-712...")
|
|
334
|
+
logger.info(f" Domain: {typed_data['domain']}")
|
|
335
|
+
logger.info(f" Message fields:")
|
|
336
|
+
logger.info(f" consumerSignature (hash): {consumer_signature_hash.hex()[:40]}...")
|
|
337
|
+
logger.info(f" outputHash (double-hash): {output_hash_hash.hex()[:40]}...")
|
|
338
|
+
logger.info(f" timestamp: {attestation_timestamp}")
|
|
339
|
+
logger.info(f" serviceDescription (hash): {service_description_hash.hex()[:40]}...")
|
|
340
|
+
logger.info(f" facilitatorFeePercent: {facilitator_fee_percent}")
|
|
341
|
+
|
|
342
|
+
signed = self.operator_account.sign_typed_data(
|
|
343
|
+
domain_data=typed_data["domain"],
|
|
344
|
+
message_types=typed_data["types"],
|
|
345
|
+
message_data=typed_data["message"],
|
|
346
|
+
)
|
|
347
|
+
provider_signature = signed.signature.hex()
|
|
348
|
+
if not provider_signature.startswith("0x"):
|
|
349
|
+
provider_signature = f"0x{provider_signature}"
|
|
350
|
+
|
|
351
|
+
logger.info(f"✅ Provider attestation (EIP-712) created:")
|
|
352
|
+
logger.info(f" Signature: {provider_signature[:40]}...")
|
|
353
|
+
logger.info(f" Signer (operator): {self.operator_account.address}")
|
|
354
|
+
if output_hash:
|
|
355
|
+
logger.info(f" Output hash: {output_hash[:20]}...")
|
|
356
|
+
if payment_uuid:
|
|
357
|
+
logger.info(f" Payment UUID: {payment_uuid[:20]}...")
|
|
358
|
+
logger.info(f" Facilitator fee: {facilitator_fee_percent} basis points ({facilitator_fee_percent/100}%)")
|
|
359
|
+
|
|
360
|
+
# Prepare settlement request for facilitator
|
|
361
|
+
# Use camelCase field names to match facilitator's SettleRequest model
|
|
362
|
+
settlement_request = {
|
|
363
|
+
"paymentUuid": payment_uuid, # Required
|
|
364
|
+
"providerSignature": provider_signature or "0x", # Required
|
|
365
|
+
"outputHash": output_hash or "0x", # Optional
|
|
366
|
+
"serviceDescription": payment_requirements.description, # Required
|
|
367
|
+
"facilitatorFeePercent": facilitator_fee_percent, # Default: 250
|
|
368
|
+
"attestationTimestamp": attestation_timestamp # When provider executed service
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
# Submit to facilitator
|
|
372
|
+
headers = {"Content-Type": "application/json"}
|
|
373
|
+
if self.facilitator_api_key:
|
|
374
|
+
headers["X-API-Key"] = self.facilitator_api_key
|
|
375
|
+
|
|
376
|
+
logger.info(f"📤 Sending settle request to facilitator:")
|
|
377
|
+
logger.info(f" Payment UUID: {payment_uuid}")
|
|
378
|
+
logger.info(f" Output hash: {output_hash[:20] if output_hash else 'N/A'}...")
|
|
379
|
+
|
|
380
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
381
|
+
response = await client.post(
|
|
382
|
+
f"{self.facilitator_url}/settle",
|
|
383
|
+
json=settlement_request,
|
|
384
|
+
headers=headers
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
if response.status_code == 200:
|
|
388
|
+
result = response.json()
|
|
389
|
+
return SettleResponse(
|
|
390
|
+
success=True,
|
|
391
|
+
error_reason=None,
|
|
392
|
+
transaction=result.get("transactionHash"),
|
|
393
|
+
network=payment.network,
|
|
394
|
+
payer=authorization.from_
|
|
395
|
+
)
|
|
396
|
+
else:
|
|
397
|
+
error_msg = f"Facilitator settle error: {response.status_code} - {response.text}"
|
|
398
|
+
logger.error(error_msg)
|
|
399
|
+
return SettleResponse(
|
|
400
|
+
success=False,
|
|
401
|
+
error_reason=error_msg,
|
|
402
|
+
transaction=None,
|
|
403
|
+
network=payment.network,
|
|
404
|
+
payer=authorization.from_
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
except Exception as e:
|
|
408
|
+
logger.error(f"Error settling payment: {e}")
|
|
409
|
+
return SettleResponse(
|
|
410
|
+
success=False,
|
|
411
|
+
error_reason=f"Settlement error: {str(e)}",
|
|
412
|
+
transaction=None,
|
|
413
|
+
network=payment.network,
|
|
414
|
+
payer=None
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
async def list(self, request: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
418
|
+
"""List discoverable IATP services that accept d402 payments.
|
|
419
|
+
|
|
420
|
+
This queries the Traia registry for utility agents with d402 enabled.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
request: Optional filters for discovery
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
List of discoverable services
|
|
427
|
+
"""
|
|
428
|
+
# This would query the MongoDB registry
|
|
429
|
+
# For now, return empty list
|
|
430
|
+
return {
|
|
431
|
+
"d402Version": 1,
|
|
432
|
+
"items": [],
|
|
433
|
+
"pagination": {
|
|
434
|
+
"limit": 100,
|
|
435
|
+
"offset": 0,
|
|
436
|
+
"total": 0
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def create_iatp_facilitator(
|
|
442
|
+
facilitator_url: str = "https://facilitator.d402.net",
|
|
443
|
+
facilitator_api_key: Optional[str] = None,
|
|
444
|
+
provider_operator_key: Optional[str] = None,
|
|
445
|
+
web3_provider: Optional[str] = None
|
|
446
|
+
) -> IATPSettlementFacilitator:
|
|
447
|
+
"""Convenience function to create an IATP Settlement Facilitator.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
facilitator_url: URL of the facilitator service (handles /verify and /settle)
|
|
451
|
+
facilitator_api_key: Optional API key for facilitator
|
|
452
|
+
provider_operator_key: Provider's operator private key for attestation signing
|
|
453
|
+
web3_provider: Optional Web3 provider URL
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
Configured IATPSettlementFacilitator
|
|
457
|
+
|
|
458
|
+
Example:
|
|
459
|
+
facilitator = create_iatp_facilitator(
|
|
460
|
+
facilitator_url="http://localhost:8080",
|
|
461
|
+
facilitator_api_key=os.getenv("FACILITATOR_API_KEY"),
|
|
462
|
+
provider_operator_key=os.getenv("OPERATOR_PRIVATE_KEY")
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# Use in d402 middleware
|
|
466
|
+
from traia_iatp.d402 import D402Config, require_iatp_payment
|
|
467
|
+
|
|
468
|
+
config = D402Config(
|
|
469
|
+
enabled=True,
|
|
470
|
+
pay_to_address="0x...", # Utility agent contract address
|
|
471
|
+
default_price=D402ServicePrice(...),
|
|
472
|
+
facilitator_url="http://localhost:8080"
|
|
473
|
+
)
|
|
474
|
+
"""
|
|
475
|
+
return IATPSettlementFacilitator(
|
|
476
|
+
facilitator_url=facilitator_url,
|
|
477
|
+
facilitator_api_key=facilitator_api_key,
|
|
478
|
+
provider_operator_key=provider_operator_key,
|
|
479
|
+
web3_provider=web3_provider
|
|
480
|
+
)
|
|
481
|
+
|