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,296 @@
1
+ """D402 payment helpers for MCP servers with official SDK.
2
+
3
+ This module provides decorators and helper functions for d402 payment protocol.
4
+ Works with official MCP SDK (mcp.server.fastmcp).
5
+
6
+ FastMCP Middleware classes are in fastmcp_middleware.py (deprecated).
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
+
@@ -0,0 +1,116 @@
1
+ """D402 payment models for IATP protocol."""
2
+
3
+ from enum import Enum
4
+ from typing import Optional, Dict, Any
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class PaymentScheme(str, Enum):
9
+ """Payment schemes supported by d402."""
10
+ EXACT = "exact" # EIP-3009 exact payment
11
+
12
+
13
+ class D402ServicePrice(BaseModel):
14
+ """Pricing configuration for an IATP service.
15
+
16
+ Supports any ERC20 token payment with full token details.
17
+ """
18
+
19
+ # Token details
20
+ token_address: str = Field(..., description="Token contract address (e.g., USDC, TRAIA, DAI)")
21
+ token_symbol: str = Field(..., description="Token symbol for display")
22
+ token_decimals: int = Field(..., description="Token decimals (6 for USDC, 18 for most)")
23
+
24
+ # Price (stored in multiple formats for convenience)
25
+ price_wei: str = Field(..., description="Price in wei/atomic units")
26
+ price_float: float = Field(..., description="Price in token units (human-readable)")
27
+
28
+ # Network configuration
29
+ network: str = Field(..., description="Network (sepolia, base-sepolia, etc.)")
30
+ chain_id: int = Field(..., description="Chain ID")
31
+
32
+ # Optional USD equivalent (for display only)
33
+ usd_amount: Optional[float] = Field(None, description="Approximate USD value")
34
+
35
+ # Maximum timeout for payment completion
36
+ max_timeout_seconds: int = Field(default=300, description="Max time to complete payment")
37
+
38
+
39
+ class D402Config(BaseModel):
40
+ """Configuration for d402 payment integration in IATP.
41
+
42
+ Supports per-path pricing with different tokens.
43
+ """
44
+
45
+ # Enable/disable d402 payments
46
+ enabled: bool = Field(default=False, description="Enable d402 payments")
47
+
48
+ # Payment address (utility agent contract address)
49
+ pay_to_address: str = Field(..., description="Ethereum address to receive payments")
50
+
51
+ # Pricing configuration
52
+ # Can be per-path (e.g., {"/analyze": D402ServicePrice(...), "/extract": D402ServicePrice(...)})
53
+ # or default for all paths
54
+ path_prices: Dict[str, D402ServicePrice] = Field(
55
+ default_factory=dict,
56
+ description="Price configuration per path"
57
+ )
58
+
59
+ # Default price (used if path not in path_prices)
60
+ default_price: Optional[D402ServicePrice] = Field(None, description="Default price for all paths")
61
+
62
+ # Legacy: Pricing per service/skill (deprecated, use path_prices)
63
+ skill_prices: Dict[str, D402ServicePrice] = Field(
64
+ default_factory=dict,
65
+ description="Custom prices per skill ID (deprecated)"
66
+ )
67
+
68
+ # Facilitator configuration
69
+ facilitator_url: str = Field(
70
+ default="https://api.traia.io/d402/facilitator",
71
+ description="URL of the d402 facilitator service"
72
+ )
73
+
74
+ # Custom facilitator authentication (if needed)
75
+ facilitator_api_key: Optional[str] = Field(None, description="API key for facilitator")
76
+
77
+ # Paths to gate with payments (* for all)
78
+ protected_paths: list[str] = Field(
79
+ default_factory=lambda: ["*"],
80
+ description="Paths that require payment"
81
+ )
82
+
83
+ # Service description for payment prompt
84
+ service_description: str = Field(..., description="Description shown in payment UI")
85
+
86
+ # Metadata
87
+ metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
88
+
89
+
90
+ class D402PaymentInfo(BaseModel):
91
+ """Payment information for agent card discovery."""
92
+
93
+ enabled: bool = Field(..., description="Whether d402 is enabled")
94
+ payment_schemes: list[PaymentScheme] = Field(
95
+ default_factory=lambda: [PaymentScheme.EXACT],
96
+ description="Supported payment schemes"
97
+ )
98
+ networks: list[str] = Field(..., description="Supported blockchain networks")
99
+ default_price: D402ServicePrice = Field(..., description="Default pricing")
100
+ facilitator_url: str = Field(..., description="Facilitator service URL")
101
+
102
+ class Config:
103
+ use_enum_values = True
104
+
105
+
106
+ class IATPSettlementRequest(BaseModel):
107
+ """Request to settle a payment through IATP settlement layer."""
108
+
109
+ consumer: str = Field(..., description="Consumer address (client agent)")
110
+ provider: str = Field(..., description="Provider address (utility agent)")
111
+ amount: str = Field(..., description="Amount in atomic units")
112
+ timestamp: int = Field(..., description="Request timestamp")
113
+ service_description: str = Field(..., description="Description of service")
114
+ consumer_signature: str = Field(..., description="Consumer's EIP-712 signature")
115
+ provider_signature: str = Field(..., description="Provider's attestation signature")
116
+
@@ -0,0 +1,98 @@
1
+ """Network configuration for d402 payments.
2
+
3
+ This module defines supported networks and their token configurations.
4
+ Customized for IATP - uses database-driven network configuration.
5
+ """
6
+
7
+ from typing import Literal, Dict, Any
8
+ from typing_extensions import TypedDict
9
+
10
+
11
+ # Network type definition
12
+ SupportedNetworks = Literal[
13
+ "sepolia",
14
+ "base-sepolia",
15
+ "arbitrum-sepolia",
16
+ "base-mainnet",
17
+ "arbitrum-mainnet",
18
+ ]
19
+
20
+
21
+ class NetworkConfig(TypedDict):
22
+ """Network configuration."""
23
+ chain_id: int
24
+ name: str
25
+ rpc_url: str
26
+ explorer_url: str
27
+ usdc_address: str
28
+
29
+
30
+ # Network configurations
31
+ NETWORKS: Dict[str, NetworkConfig] = {
32
+ "sepolia": {
33
+ "chain_id": 11155111,
34
+ "name": "Ethereum Sepolia",
35
+ "rpc_url": "https://ethereum-sepolia-rpc.publicnode.com",
36
+ "explorer_url": "https://sepolia.etherscan.io",
37
+ "usdc_address": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"
38
+ },
39
+ "base-sepolia": {
40
+ "chain_id": 84532,
41
+ "name": "Base Sepolia",
42
+ "rpc_url": "https://sepolia.base.org",
43
+ "explorer_url": "https://sepolia.basescan.org",
44
+ "usdc_address": "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
45
+ },
46
+ "arbitrum-sepolia": {
47
+ "chain_id": 421614,
48
+ "name": "Arbitrum Sepolia",
49
+ "rpc_url": "https://arbitrum-sepolia-rpc.publicnode.com",
50
+ "explorer_url": "https://sepolia.arbiscan.io",
51
+ "usdc_address": "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d"
52
+ },
53
+ "base-mainnet": {
54
+ "chain_id": 8453,
55
+ "name": "Base",
56
+ "rpc_url": "https://mainnet.base.org",
57
+ "explorer_url": "https://basescan.org",
58
+ "usdc_address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
59
+ },
60
+ "arbitrum-mainnet": {
61
+ "chain_id": 42161,
62
+ "name": "Arbitrum One",
63
+ "rpc_url": "https://arbitrum-one-rpc.publicnode.com",
64
+ "explorer_url": "https://arbiscan.io",
65
+ "usdc_address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
66
+ }
67
+ }
68
+
69
+
70
+ def get_network_config(network: str) -> NetworkConfig:
71
+ """Get configuration for a network."""
72
+ if network not in NETWORKS:
73
+ raise ValueError(f"Unsupported network: {network}")
74
+ return NETWORKS[network]
75
+
76
+
77
+ def get_usdc_address(network: str) -> str:
78
+ """Get USDC address for a network."""
79
+ return get_network_config(network)["usdc_address"]
80
+
81
+
82
+ def get_chain_id(network: str) -> int:
83
+ """Get chain ID for a network."""
84
+ return get_network_config(network)["chain_id"]
85
+
86
+
87
+ # TODO: Load from database network table
88
+ async def load_networks_from_db() -> Dict[str, NetworkConfig]:
89
+ """Load network configurations from database.
90
+
91
+ This will query the Network table and build NETWORKS dict dynamically.
92
+ For now, returns static config.
93
+ """
94
+ # from db.dal.models import Network
95
+ # networks = await db.query(Network).filter(Network.active == True).all()
96
+ # return {net.shortname: {...} for net in networks}
97
+ return NETWORKS
98
+
@@ -0,0 +1,43 @@
1
+ import fnmatch
2
+ import re
3
+ from typing import Union
4
+
5
+
6
+ def path_is_match(path: Union[str, list[str]], request_path: str) -> bool:
7
+ """
8
+ Check if request path matches the specified path pattern(s).
9
+
10
+ Supports:
11
+ - Exact matching: "/api/users"
12
+ - Glob patterns: "/api/users/*", "/api/*/profile"
13
+ - Regex patterns (prefix with 'regex:'): "regex:^/api/users/\\d+$"
14
+ - List of any of the above
15
+
16
+ Args:
17
+ path: Path pattern(s) to match against. Can be a string or list of strings.
18
+ request_path: The actual request path to check.
19
+
20
+ Returns:
21
+ bool: True if the request path matches any of the patterns, False otherwise.
22
+ """
23
+
24
+ def single_path_match(pattern: str) -> bool:
25
+ # Regex pattern
26
+ if pattern.startswith("regex:"):
27
+ regex_pattern = pattern[6:] # Remove 'regex:' prefix
28
+ return bool(re.match(regex_pattern, request_path))
29
+
30
+ # Glob pattern (contains * or ?)
31
+ elif "*" in pattern or "?" in pattern:
32
+ return fnmatch.fnmatch(request_path, pattern)
33
+
34
+ # Exact match
35
+ else:
36
+ return pattern == request_path
37
+
38
+ if isinstance(path, str):
39
+ return single_path_match(path)
40
+ elif isinstance(path, list):
41
+ return any(single_path_match(p) for p in path)
42
+
43
+ return False
@@ -0,0 +1,126 @@
1
+ """
2
+ Helper to extract payment configurations from @require_payment_for_tool decorators.
3
+
4
+ This allows us to have a single source of truth - payment config is declared
5
+ in the decorator, and we introspect it to build TOOL_PAYMENT_CONFIGS.
6
+ """
7
+
8
+ import logging
9
+ from typing import Dict, Any, Optional
10
+
11
+ from .types import TokenAmount
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def extract_payment_configs_from_mcp(mcp_server, server_address: str) -> Dict[str, Dict[str, Any]]:
17
+ """
18
+ Extract payment configurations from tools decorated with @require_payment_for_tool.
19
+
20
+ This introspects the decorator closures to extract TokenAmount objects,
21
+ eliminating the need to duplicate payment configuration.
22
+
23
+ Args:
24
+ mcp_server: FastMCP server instance
25
+ server_address: Server's payment address
26
+
27
+ Returns:
28
+ Dict mapping tool names to payment configurations
29
+ Format: {"tool_name": {"price_wei": "1000", "token_address": "0x...", ...}}
30
+
31
+ Usage:
32
+ mcp = FastMCP("Server")
33
+
34
+ # Add tools with @require_payment_for_tool decorators
35
+ @mcp.tool()
36
+ @require_payment_for_tool(price=TokenAmount(...))
37
+ async def my_tool(context): ...
38
+
39
+ # Extract configs dynamically
40
+ TOOL_PAYMENT_CONFIGS = extract_payment_configs_from_mcp(mcp, SERVER_ADDRESS)
41
+
42
+ # Add middleware with extracted configs
43
+ app.add_middleware(D402PaymentMiddleware, tool_payment_configs=TOOL_PAYMENT_CONFIGS, ...)
44
+ """
45
+ tool_payment_configs = {}
46
+
47
+ try:
48
+ # Get registered tools from FastMCP
49
+ tools = mcp_server._tool_manager.list_tools()
50
+
51
+ for tool in tools:
52
+ if not hasattr(tool, 'fn'):
53
+ continue
54
+
55
+ fn = tool.fn
56
+ tool_name = tool.name
57
+
58
+ # Check if function has payment metadata (from @require_payment_for_tool decorator)
59
+ # New approach: metadata stored as attribute (no closure needed)
60
+ if hasattr(fn, '_d402_payment_config'):
61
+ payment_config = fn._d402_payment_config
62
+ price = payment_config.get('price')
63
+ description = payment_config.get('description', tool_name)
64
+
65
+ if isinstance(price, TokenAmount):
66
+ token_amount = price
67
+ else:
68
+ logger.debug(f"Tool {tool_name}: price is not TokenAmount")
69
+ continue
70
+ # Fallback: Check closure for backwards compatibility
71
+ elif hasattr(fn, '__closure__') and fn.__closure__:
72
+ token_amount = None
73
+ description = None
74
+ for cell in fn.__closure__:
75
+ try:
76
+ val = cell.cell_contents
77
+ if isinstance(val, TokenAmount):
78
+ token_amount = val
79
+ elif isinstance(val, str):
80
+ description = val
81
+ except:
82
+ pass
83
+
84
+ if not token_amount:
85
+ logger.debug(f"Tool {tool_name}: No TokenAmount in closure")
86
+ continue
87
+
88
+ description = description or tool.description or tool_name
89
+ else:
90
+ logger.debug(f"Tool {tool_name}: No payment metadata found")
91
+ continue
92
+
93
+ if token_amount:
94
+ # Extract payment config from TokenAmount (including EIP712 domain)
95
+ # Use description from metadata if available, otherwise from tool
96
+ final_description = description if 'description' in locals() else (tool.description or tool_name)
97
+
98
+ config = {
99
+ "price_wei": token_amount.amount,
100
+ "token_address": token_amount.asset.address,
101
+ "network": token_amount.asset.network,
102
+ "server_address": server_address,
103
+ "description": final_description,
104
+ "eip712_domain": {
105
+ "name": token_amount.asset.eip712.name if token_amount.asset.eip712 else "USD Coin",
106
+ "version": token_amount.asset.eip712.version if token_amount.asset.eip712 else "2"
107
+ }
108
+ }
109
+
110
+ tool_payment_configs[tool_name] = config
111
+ logger.info(f"✅ Extracted payment config for {tool_name}: {config['price_wei']} wei on {config['network']}")
112
+ else:
113
+ logger.debug(f"Tool {tool_name}: No TokenAmount found (free tool)")
114
+
115
+ logger.info(f"📊 Extracted {len(tool_payment_configs)} payment configs from decorators")
116
+
117
+ except Exception as e:
118
+ logger.error(f"Error extracting payment configs: {e}")
119
+ import traceback
120
+ logger.error(traceback.format_exc())
121
+
122
+ return tool_payment_configs
123
+
124
+
125
+ __all__ = ["extract_payment_configs_from_mcp"]
126
+