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,253 @@
|
|
|
1
|
+
"""FastAPI middleware for d402 payment protocol.
|
|
2
|
+
|
|
3
|
+
This module provides FastAPI-specific middleware for the d402 payment protocol.
|
|
4
|
+
Since FastAPI is built on Starlette, this middleware wraps the Starlette middleware
|
|
5
|
+
with FastAPI-friendly configuration and decorators.
|
|
6
|
+
|
|
7
|
+
Example usage:
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from fastapi import FastAPI
|
|
11
|
+
from traia_iatp.d402.servers.fastapi import D402FastAPIMiddleware, require_payment
|
|
12
|
+
|
|
13
|
+
app = FastAPI()
|
|
14
|
+
|
|
15
|
+
# Add payment middleware
|
|
16
|
+
middleware = D402FastAPIMiddleware(
|
|
17
|
+
server_address="0x...",
|
|
18
|
+
internal_api_key="your_api_key",
|
|
19
|
+
facilitator_url="https://facilitator.d402.net"
|
|
20
|
+
)
|
|
21
|
+
middleware.add_to_app(app)
|
|
22
|
+
|
|
23
|
+
# Protect specific endpoints with payment
|
|
24
|
+
@app.post("/api/analyze")
|
|
25
|
+
@require_payment(price_usd=0.01, description="Sentiment analysis")
|
|
26
|
+
async def analyze(request: Request):
|
|
27
|
+
return {"result": "analysis complete"}
|
|
28
|
+
```
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
import os
|
|
33
|
+
from typing import Dict, Any, Optional, Callable, List
|
|
34
|
+
from functools import wraps
|
|
35
|
+
|
|
36
|
+
from fastapi import FastAPI, Request
|
|
37
|
+
from fastapi.responses import JSONResponse
|
|
38
|
+
|
|
39
|
+
from .starlette import D402PaymentMiddleware
|
|
40
|
+
from ..types import TokenAmount, TokenAsset, EIP712Domain, PaymentRequirements
|
|
41
|
+
from ..common import process_price_to_atomic_amount
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class D402FastAPIMiddleware:
|
|
47
|
+
"""
|
|
48
|
+
FastAPI-specific wrapper for d402 payment middleware.
|
|
49
|
+
|
|
50
|
+
This class provides a FastAPI-friendly interface to the underlying
|
|
51
|
+
Starlette middleware, with additional utilities for route protection.
|
|
52
|
+
|
|
53
|
+
Usage:
|
|
54
|
+
middleware = D402FastAPIMiddleware(
|
|
55
|
+
server_address="0x...",
|
|
56
|
+
internal_api_key="your_key",
|
|
57
|
+
facilitator_url="https://facilitator.example.com"
|
|
58
|
+
)
|
|
59
|
+
middleware.add_to_app(app)
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
server_address: str,
|
|
65
|
+
requires_auth: bool = False,
|
|
66
|
+
internal_api_key: Optional[str] = None,
|
|
67
|
+
testing_mode: bool = False,
|
|
68
|
+
facilitator_url: Optional[str] = None,
|
|
69
|
+
facilitator_api_key: Optional[str] = None,
|
|
70
|
+
server_name: Optional[str] = None,
|
|
71
|
+
protected_paths: Optional[List[str]] = None
|
|
72
|
+
):
|
|
73
|
+
"""Initialize FastAPI middleware.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
server_address: Address where payments should be sent
|
|
77
|
+
requires_auth: Whether server accepts API keys for free access
|
|
78
|
+
internal_api_key: Server's internal API key (used when client pays)
|
|
79
|
+
testing_mode: If True, skip facilitator verification
|
|
80
|
+
facilitator_url: URL of the payment facilitator
|
|
81
|
+
facilitator_api_key: API key for facilitator authentication
|
|
82
|
+
server_name: Name/ID of this server for tracking
|
|
83
|
+
protected_paths: List of path patterns that require payment (e.g., ["/api/*"])
|
|
84
|
+
"""
|
|
85
|
+
self.server_address = server_address
|
|
86
|
+
self.requires_auth = requires_auth
|
|
87
|
+
self.internal_api_key = internal_api_key
|
|
88
|
+
self.testing_mode = testing_mode
|
|
89
|
+
self.facilitator_url = facilitator_url
|
|
90
|
+
self.facilitator_api_key = facilitator_api_key
|
|
91
|
+
self.server_name = server_name
|
|
92
|
+
self.protected_paths = protected_paths or []
|
|
93
|
+
|
|
94
|
+
# Tool payment configs will be populated by decorators
|
|
95
|
+
self.tool_payment_configs: Dict[str, Dict[str, Any]] = {}
|
|
96
|
+
|
|
97
|
+
def add_to_app(self, app: FastAPI):
|
|
98
|
+
"""Add the d402 payment middleware to a FastAPI app.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
app: FastAPI application instance
|
|
102
|
+
"""
|
|
103
|
+
app.add_middleware(
|
|
104
|
+
D402PaymentMiddleware,
|
|
105
|
+
tool_payment_configs=self.tool_payment_configs,
|
|
106
|
+
server_address=self.server_address,
|
|
107
|
+
requires_auth=self.requires_auth,
|
|
108
|
+
internal_api_key=self.internal_api_key,
|
|
109
|
+
testing_mode=self.testing_mode,
|
|
110
|
+
facilitator_url=self.facilitator_url,
|
|
111
|
+
facilitator_api_key=self.facilitator_api_key,
|
|
112
|
+
server_name=self.server_name
|
|
113
|
+
)
|
|
114
|
+
logger.info(f"✅ D402 payment middleware added to FastAPI app")
|
|
115
|
+
logger.info(f" Server address: {self.server_address}")
|
|
116
|
+
logger.info(f" Testing mode: {self.testing_mode}")
|
|
117
|
+
|
|
118
|
+
def register_endpoint(
|
|
119
|
+
self,
|
|
120
|
+
path: str,
|
|
121
|
+
price_wei: str,
|
|
122
|
+
token_address: str,
|
|
123
|
+
network: str,
|
|
124
|
+
description: str = ""
|
|
125
|
+
):
|
|
126
|
+
"""Register an endpoint that requires payment.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
path: Endpoint path (e.g., "/api/analyze")
|
|
130
|
+
price_wei: Price in wei (smallest unit of token)
|
|
131
|
+
token_address: Token contract address
|
|
132
|
+
network: Network name (e.g., "sepolia", "base-mainnet")
|
|
133
|
+
description: Description of the service
|
|
134
|
+
"""
|
|
135
|
+
self.tool_payment_configs[path] = {
|
|
136
|
+
"price_wei": price_wei,
|
|
137
|
+
"token_address": token_address,
|
|
138
|
+
"network": network,
|
|
139
|
+
"description": description,
|
|
140
|
+
"server_address": self.server_address
|
|
141
|
+
}
|
|
142
|
+
logger.info(f"📝 Registered payment endpoint: {path} (price: {price_wei} wei)")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def require_payment(
|
|
146
|
+
price_usd: Optional[float] = None,
|
|
147
|
+
price_wei: Optional[str] = None,
|
|
148
|
+
token_address: Optional[str] = None,
|
|
149
|
+
network: str = "sepolia",
|
|
150
|
+
description: str = ""
|
|
151
|
+
):
|
|
152
|
+
"""
|
|
153
|
+
Decorator to mark a FastAPI endpoint as requiring payment.
|
|
154
|
+
|
|
155
|
+
Can specify price in USD (will use default USDC) or in wei with custom token.
|
|
156
|
+
|
|
157
|
+
Usage with USD:
|
|
158
|
+
@app.post("/api/analyze")
|
|
159
|
+
@require_payment(price_usd=0.01, description="Sentiment analysis")
|
|
160
|
+
async def analyze():
|
|
161
|
+
return {"result": "done"}
|
|
162
|
+
|
|
163
|
+
Usage with custom token:
|
|
164
|
+
@app.post("/api/analyze")
|
|
165
|
+
@require_payment(
|
|
166
|
+
price_wei="1000",
|
|
167
|
+
token_address="0x...",
|
|
168
|
+
network="base-mainnet",
|
|
169
|
+
description="Sentiment analysis"
|
|
170
|
+
)
|
|
171
|
+
async def analyze():
|
|
172
|
+
return {"result": "done"}
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
price_usd: Price in USD (uses default USDC token)
|
|
176
|
+
price_wei: Price in wei (smallest unit of token)
|
|
177
|
+
token_address: Token contract address (required if price_wei is set)
|
|
178
|
+
network: Network name (default: "sepolia")
|
|
179
|
+
description: Description of the service
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Decorated function
|
|
183
|
+
"""
|
|
184
|
+
def decorator(func: Callable):
|
|
185
|
+
# Store payment metadata on function
|
|
186
|
+
if price_usd is not None:
|
|
187
|
+
# Convert USD to wei using default USDC
|
|
188
|
+
from ..common import process_price_to_atomic_amount
|
|
189
|
+
from ..types import Money
|
|
190
|
+
|
|
191
|
+
price = Money(usd=str(price_usd))
|
|
192
|
+
wei_amount, asset_addr, eip712_domain = process_price_to_atomic_amount(price, network)
|
|
193
|
+
|
|
194
|
+
func._d402_payment_config = {
|
|
195
|
+
"price_wei": wei_amount,
|
|
196
|
+
"token_address": asset_addr,
|
|
197
|
+
"network": network,
|
|
198
|
+
"description": description or f"API call: {func.__name__}"
|
|
199
|
+
}
|
|
200
|
+
elif price_wei is not None and token_address is not None:
|
|
201
|
+
# Use custom token and price
|
|
202
|
+
func._d402_payment_config = {
|
|
203
|
+
"price_wei": price_wei,
|
|
204
|
+
"token_address": token_address,
|
|
205
|
+
"network": network,
|
|
206
|
+
"description": description or f"API call: {func.__name__}"
|
|
207
|
+
}
|
|
208
|
+
else:
|
|
209
|
+
raise ValueError("Must specify either price_usd or (price_wei + token_address)")
|
|
210
|
+
|
|
211
|
+
@wraps(func)
|
|
212
|
+
async def wrapper(*args, **kwargs):
|
|
213
|
+
# Payment is handled by middleware
|
|
214
|
+
return await func(*args, **kwargs)
|
|
215
|
+
|
|
216
|
+
return wrapper
|
|
217
|
+
|
|
218
|
+
return decorator
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def get_api_key(request: Request) -> Optional[str]:
|
|
222
|
+
"""
|
|
223
|
+
Get the active API key for the current request.
|
|
224
|
+
|
|
225
|
+
This returns the API key that was resolved by the payment middleware:
|
|
226
|
+
- Client's API key if they provided one (Mode 1: Free)
|
|
227
|
+
- Server's API key if client paid (Mode 2: Paid)
|
|
228
|
+
|
|
229
|
+
Usage:
|
|
230
|
+
@app.post("/api/analyze")
|
|
231
|
+
async def analyze(request: Request):
|
|
232
|
+
api_key = get_api_key(request)
|
|
233
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
234
|
+
response = requests.get("https://api.example.com", headers=headers)
|
|
235
|
+
return response.json()
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
request: FastAPI request object
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
API key string if available, None otherwise
|
|
242
|
+
"""
|
|
243
|
+
if hasattr(request.state, 'api_key_to_use'):
|
|
244
|
+
return request.state.api_key_to_use
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
__all__ = [
|
|
249
|
+
"D402FastAPIMiddleware",
|
|
250
|
+
"require_payment",
|
|
251
|
+
"get_api_key",
|
|
252
|
+
]
|
|
253
|
+
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""D402 payment helpers for MCP servers with official SDK.
|
|
2
|
+
|
|
3
|
+
This module provides decorators and helper functions for d402 payment protocol
|
|
4
|
+
specifically for Model Context Protocol (MCP) servers using FastMCP.
|
|
5
|
+
|
|
6
|
+
Works with official MCP SDK (mcp.server.fastmcp).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
from typing import Optional, Dict, Any, Callable
|
|
12
|
+
from functools import wraps
|
|
13
|
+
|
|
14
|
+
# Import Context from official SDK
|
|
15
|
+
from mcp.server.fastmcp import Context
|
|
16
|
+
|
|
17
|
+
from starlette.requests import Request
|
|
18
|
+
from starlette.responses import JSONResponse
|
|
19
|
+
from web3 import Web3
|
|
20
|
+
|
|
21
|
+
from ..types import PaymentPayload, Price, TokenAmount, TokenAsset, EIP712Domain, PaymentRequirements, d402PaymentRequiredResponse
|
|
22
|
+
from ..encoding import safe_base64_decode
|
|
23
|
+
from ..common import process_price_to_atomic_amount, d402_VERSION
|
|
24
|
+
from ..facilitator import IATPSettlementFacilitator
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class D402PaymentRequiredException(Exception):
|
|
28
|
+
"""Exception raised when payment is required (HTTP 402)."""
|
|
29
|
+
def __init__(self, payment_response: Dict[str, Any]):
|
|
30
|
+
self.payment_response = payment_response
|
|
31
|
+
super().__init__("Payment required")
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EndpointPaymentInfo:
|
|
37
|
+
"""Payment information for a specific endpoint."""
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
settlement_token_address: str,
|
|
41
|
+
settlement_token_network: str,
|
|
42
|
+
payment_price_float: float,
|
|
43
|
+
payment_price_wei: str,
|
|
44
|
+
server_address: str
|
|
45
|
+
):
|
|
46
|
+
self.settlement_token_address = settlement_token_address
|
|
47
|
+
self.settlement_token_network = settlement_token_network
|
|
48
|
+
self.payment_price_float = payment_price_float
|
|
49
|
+
self.payment_price_wei = payment_price_wei
|
|
50
|
+
self.server_address = server_address
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_active_api_key(context: Any) -> Optional[str]:
|
|
54
|
+
"""
|
|
55
|
+
Get the API key to use for calling external APIs.
|
|
56
|
+
|
|
57
|
+
Returns api_key_to_use which was set by:
|
|
58
|
+
1. D402PaymentMiddleware (in request.state)
|
|
59
|
+
2. @require_payment_for_tool decorator (copied to context.state)
|
|
60
|
+
|
|
61
|
+
Priority:
|
|
62
|
+
1. context.state.api_key_to_use (set by decorator)
|
|
63
|
+
2. request.state.api_key_to_use (set by middleware)
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
context: MCP context object
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
API key string (client's OR server's) if authorized, None otherwise
|
|
70
|
+
|
|
71
|
+
Usage in tools:
|
|
72
|
+
api_key = get_active_api_key(context)
|
|
73
|
+
if api_key:
|
|
74
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
# Check request.state (set by middleware)
|
|
78
|
+
# Context is a Pydantic model - we can't set arbitrary fields on it
|
|
79
|
+
# So we read directly from request.state where middleware stored it
|
|
80
|
+
logger.debug(f"get_active_api_key: Checking context type={type(context).__name__}")
|
|
81
|
+
logger.debug(f" has request_context: {hasattr(context, 'request_context')}")
|
|
82
|
+
|
|
83
|
+
if hasattr(context, 'request_context') and context.request_context:
|
|
84
|
+
logger.debug(f" request_context exists")
|
|
85
|
+
if hasattr(context.request_context, 'request') and context.request_context.request:
|
|
86
|
+
request = context.request_context.request
|
|
87
|
+
logger.debug(f" request exists, has state: {hasattr(request, 'state')}")
|
|
88
|
+
if hasattr(request, 'state'):
|
|
89
|
+
api_key = getattr(request.state, 'api_key_to_use', None)
|
|
90
|
+
logger.debug(f" api_key_to_use: {api_key[:10] if api_key else None}")
|
|
91
|
+
if api_key:
|
|
92
|
+
return api_key
|
|
93
|
+
|
|
94
|
+
logger.warning(f"get_active_api_key: Could not find api_key_to_use in request.state")
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error(f"get_active_api_key error: {e}")
|
|
98
|
+
import traceback
|
|
99
|
+
logger.error(traceback.format_exc())
|
|
100
|
+
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def settle_payment(
|
|
105
|
+
context: Any,
|
|
106
|
+
endpoint_info: EndpointPaymentInfo,
|
|
107
|
+
output_data: Any,
|
|
108
|
+
middleware: Optional[Any] = None # Middleware instance (Starlette or FastMCP)
|
|
109
|
+
) -> bool:
|
|
110
|
+
"""
|
|
111
|
+
Settle a payment after successful API call with output hash attestation.
|
|
112
|
+
|
|
113
|
+
Complete 402 settlement flow:
|
|
114
|
+
1. Hash the output data (result returned to client)
|
|
115
|
+
2. Provider signs over output_hash + consumer_request
|
|
116
|
+
3. Submit to facilitator with proof of service completion
|
|
117
|
+
4. Facilitator submits to IATP Settlement Layer on-chain
|
|
118
|
+
|
|
119
|
+
This should be called AFTER the tool successfully processes the request
|
|
120
|
+
to submit the payment settlement to the facilitator/blockchain.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
context: MCP context (contains payment_payload)
|
|
124
|
+
endpoint_info: Endpoint payment requirements
|
|
125
|
+
output_data: The actual output/result being returned to client (will be hashed)
|
|
126
|
+
middleware: Optional D402MCPMiddleware instance (for facilitator access)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
bool: True if settlement submitted successfully
|
|
130
|
+
|
|
131
|
+
Usage in tools (for production settlement):
|
|
132
|
+
# Execute API call
|
|
133
|
+
response = requests.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
134
|
+
result = response.json()
|
|
135
|
+
|
|
136
|
+
# Settle payment with output hash
|
|
137
|
+
if context.state.payment_payload:
|
|
138
|
+
await settle_payment(context, endpoint_payment, output_data=result, middleware=...)
|
|
139
|
+
|
|
140
|
+
return result
|
|
141
|
+
"""
|
|
142
|
+
try:
|
|
143
|
+
payment_payload = getattr(context.state, 'payment_payload', None) if hasattr(context, 'state') else None
|
|
144
|
+
if not payment_payload:
|
|
145
|
+
logger.debug("No payment to settle (authenticated mode)")
|
|
146
|
+
return True # Not an error - client used their own API key
|
|
147
|
+
|
|
148
|
+
# Skip settlement in testing mode
|
|
149
|
+
if middleware and middleware.testing_mode:
|
|
150
|
+
logger.info("⚠️ Testing mode: Skipping payment settlement")
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
# Step 1: Hash the output data (proof of service completion)
|
|
154
|
+
import json
|
|
155
|
+
from web3 import Web3
|
|
156
|
+
|
|
157
|
+
logger.info("🔐 Starting payment settlement process...")
|
|
158
|
+
|
|
159
|
+
# Serialize output to JSON and hash it
|
|
160
|
+
output_json = json.dumps(output_data, sort_keys=True, separators=(',', ':'))
|
|
161
|
+
output_hash = Web3.keccak(text=output_json).hex()
|
|
162
|
+
logger.info(f"📊 Output data serialized: {len(output_json)} bytes")
|
|
163
|
+
logger.info(f"🔑 Output hash calculated: {output_hash}")
|
|
164
|
+
logger.info(f" First 1000 chars of output: {output_json[:1000]}")
|
|
165
|
+
|
|
166
|
+
# Step 2: Get payment_uuid from context (from facilitator verify response)
|
|
167
|
+
# The payment_uuid is the primary payment identifier from the facilitator
|
|
168
|
+
# It was set in verify_endpoint_payment() after facilitator.verify() returned it
|
|
169
|
+
payment_uuid = None
|
|
170
|
+
if hasattr(context, 'state') and hasattr(context.state, 'payment_uuid'):
|
|
171
|
+
payment_uuid = context.state.payment_uuid
|
|
172
|
+
|
|
173
|
+
if not payment_uuid:
|
|
174
|
+
logger.warning("No payment_uuid found in context - payment may not have been verified via facilitator")
|
|
175
|
+
|
|
176
|
+
# Step 3: Get facilitator fee from context (set by verify response)
|
|
177
|
+
facilitator_fee_percent = 250 # Default
|
|
178
|
+
if hasattr(context, 'state') and hasattr(context.state, 'facilitator_fee_percent'):
|
|
179
|
+
facilitator_fee_percent = context.state.facilitator_fee_percent
|
|
180
|
+
|
|
181
|
+
# Step 4: Create PaymentRequirements for this endpoint
|
|
182
|
+
# Include output_hash, payment_uuid, and facilitatorFeePercent in extra data
|
|
183
|
+
extra_data = {
|
|
184
|
+
"output_hash": output_hash,
|
|
185
|
+
"facilitator_fee_percent": facilitator_fee_percent
|
|
186
|
+
}
|
|
187
|
+
if payment_uuid:
|
|
188
|
+
extra_data["payment_uuid"] = payment_uuid
|
|
189
|
+
|
|
190
|
+
payment_requirements = PaymentRequirements(
|
|
191
|
+
scheme="exact",
|
|
192
|
+
network=endpoint_info.settlement_token_network,
|
|
193
|
+
pay_to=endpoint_info.server_address,
|
|
194
|
+
max_amount_required=endpoint_info.payment_price_wei,
|
|
195
|
+
max_timeout_seconds=300,
|
|
196
|
+
description=f"Service completed - output_hash: {output_hash}",
|
|
197
|
+
resource="",
|
|
198
|
+
mime_type="application/json",
|
|
199
|
+
asset=endpoint_info.settlement_token_address,
|
|
200
|
+
extra=extra_data # Include output hash, payment_uuid, and facilitatorFeePercent
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Step 4: Settle via facilitator
|
|
204
|
+
# The facilitator will:
|
|
205
|
+
# - Create provider attestation signing over the consumer's request + output_hash
|
|
206
|
+
# - Submit to relayer with proof of service completion
|
|
207
|
+
# - Relayer submits to IATPSettlementLayer on-chain
|
|
208
|
+
if middleware and middleware.facilitator:
|
|
209
|
+
try:
|
|
210
|
+
logger.info(f"📤 Submitting settlement to facilitator...")
|
|
211
|
+
logger.info(f" Payment UUID: {payment_uuid if payment_uuid else 'N/A'}")
|
|
212
|
+
logger.info(f" Output hash: {output_hash}")
|
|
213
|
+
logger.info(f" Amount: {endpoint_info.payment_price_wei} wei")
|
|
214
|
+
|
|
215
|
+
settle_result = await middleware.facilitator.settle(payment_payload, payment_requirements)
|
|
216
|
+
if settle_result.success:
|
|
217
|
+
logger.info(f"✅ Payment settlement request accepted by facilitator:")
|
|
218
|
+
logger.info(f" Status: PENDING_SETTLEMENT (queued for on-chain settlement)")
|
|
219
|
+
logger.info(f" Network: {settle_result.network}")
|
|
220
|
+
logger.info(f" Payer: {settle_result.payer}")
|
|
221
|
+
logger.info(f" Output Hash: {output_hash}")
|
|
222
|
+
logger.info(f" Note: Facilitator cron will batch-settle on-chain")
|
|
223
|
+
return True
|
|
224
|
+
else:
|
|
225
|
+
logger.error(f"❌ Payment settlement FAILED: {settle_result.error_reason}")
|
|
226
|
+
# TODO: Queue for retry
|
|
227
|
+
return False
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logger.error(f"Error settling payment via facilitator: {e}")
|
|
230
|
+
# Don't fail the request if settlement fails
|
|
231
|
+
# Settlement can be retried later
|
|
232
|
+
logger.warning("Settlement failed but request completed - will retry later")
|
|
233
|
+
# TODO: Queue settlement for retry
|
|
234
|
+
return False
|
|
235
|
+
else:
|
|
236
|
+
logger.warning("No facilitator available for settlement")
|
|
237
|
+
# TODO: Queue settlement for later retry
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
except Exception as e:
|
|
241
|
+
import traceback
|
|
242
|
+
logger.error(f"Error in settle_payment: {e}")
|
|
243
|
+
logger.error(f"Traceback:\n{traceback.format_exc()}")
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def require_payment_for_tool(
|
|
248
|
+
price: Price,
|
|
249
|
+
description: str = ""
|
|
250
|
+
):
|
|
251
|
+
"""
|
|
252
|
+
Decorator for MCP tools that require payment - METADATA ONLY.
|
|
253
|
+
|
|
254
|
+
This decorator ONLY stores payment configuration metadata on the function.
|
|
255
|
+
All payment processing (verify, settle) is handled by D402PaymentMiddleware.
|
|
256
|
+
|
|
257
|
+
Usage:
|
|
258
|
+
@mcp.tool()
|
|
259
|
+
@require_payment_for_tool(
|
|
260
|
+
price=TokenAmount(
|
|
261
|
+
amount="1000",
|
|
262
|
+
asset=TokenAsset(
|
|
263
|
+
address="0xUSDC...",
|
|
264
|
+
decimals=6,
|
|
265
|
+
network="base-sepolia",
|
|
266
|
+
eip712=EIP712Domain(name="IATPWallet", version="1")
|
|
267
|
+
)
|
|
268
|
+
),
|
|
269
|
+
description="Get cryptocurrency price data"
|
|
270
|
+
)
|
|
271
|
+
async def get_price(context: Context, coin_id: str) -> Dict[str, Any]:
|
|
272
|
+
# Payment already verified by middleware
|
|
273
|
+
api_key = get_active_api_key(context)
|
|
274
|
+
response = requests.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
275
|
+
return response.json()
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
price: Payment configuration (TokenAmount with network, token, etc)
|
|
279
|
+
description: Service description for settlement
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Decorator that attaches metadata to the function
|
|
283
|
+
"""
|
|
284
|
+
def decorator(func: Callable):
|
|
285
|
+
# Store payment metadata on function for middleware extraction
|
|
286
|
+
func._d402_payment_config = {
|
|
287
|
+
"price": price,
|
|
288
|
+
"description": description
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
# Return function unchanged - middleware handles all payment logic
|
|
292
|
+
return func
|
|
293
|
+
|
|
294
|
+
return decorator
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
__all__ = [
|
|
298
|
+
"D402PaymentRequiredException",
|
|
299
|
+
"EndpointPaymentInfo",
|
|
300
|
+
"get_active_api_key",
|
|
301
|
+
"settle_payment",
|
|
302
|
+
"require_payment_for_tool",
|
|
303
|
+
]
|
|
304
|
+
|