traia-iatp 0.1.29__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.
Potentially problematic release.
This version of traia-iatp might be problematic. Click here for more details.
- traia_iatp/README.md +368 -0
- traia_iatp/__init__.py +54 -0
- traia_iatp/cli/__init__.py +5 -0
- traia_iatp/cli/main.py +483 -0
- traia_iatp/client/__init__.py +10 -0
- traia_iatp/client/a2a_client.py +274 -0
- traia_iatp/client/crewai_a2a_tools.py +335 -0
- traia_iatp/client/d402_a2a_client.py +293 -0
- traia_iatp/client/grpc_a2a_tools.py +349 -0
- traia_iatp/client/root_path_a2a_client.py +1 -0
- traia_iatp/contracts/__init__.py +12 -0
- traia_iatp/contracts/iatp_contracts_config.py +263 -0
- traia_iatp/contracts/wallet_creator.py +255 -0
- traia_iatp/core/__init__.py +43 -0
- traia_iatp/core/models.py +172 -0
- traia_iatp/d402/__init__.py +55 -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 +219 -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 +453 -0
- traia_iatp/d402/fastapi_middleware/__init__.py +6 -0
- traia_iatp/d402/fastapi_middleware/middleware.py +225 -0
- traia_iatp/d402/fastmcp_middleware.py +147 -0
- traia_iatp/d402/mcp_middleware.py +434 -0
- traia_iatp/d402/middleware.py +193 -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 +104 -0
- traia_iatp/d402/payment_signing.py +178 -0
- traia_iatp/d402/paywall.py +119 -0
- traia_iatp/d402/starlette_middleware.py +326 -0
- traia_iatp/d402/template.py +1 -0
- traia_iatp/d402/types.py +300 -0
- traia_iatp/mcp/__init__.py +18 -0
- traia_iatp/mcp/client.py +201 -0
- traia_iatp/mcp/d402_mcp_tool_adapter.py +361 -0
- traia_iatp/mcp/mcp_agent_template.py +481 -0
- traia_iatp/mcp/templates/Dockerfile.j2 +80 -0
- traia_iatp/mcp/templates/README.md.j2 +310 -0
- traia_iatp/mcp/templates/cursor-rules.md.j2 +520 -0
- traia_iatp/mcp/templates/deployment_params.json.j2 +20 -0
- traia_iatp/mcp/templates/docker-compose.yml.j2 +32 -0
- traia_iatp/mcp/templates/dockerignore.j2 +47 -0
- traia_iatp/mcp/templates/env.example.j2 +57 -0
- traia_iatp/mcp/templates/gitignore.j2 +77 -0
- traia_iatp/mcp/templates/mcp_health_check.py.j2 +150 -0
- traia_iatp/mcp/templates/pyproject.toml.j2 +32 -0
- traia_iatp/mcp/templates/pyrightconfig.json.j2 +22 -0
- traia_iatp/mcp/templates/run_local_docker.sh.j2 +390 -0
- traia_iatp/mcp/templates/server.py.j2 +175 -0
- traia_iatp/mcp/traia_mcp_adapter.py +543 -0
- traia_iatp/preview_diagrams.html +181 -0
- traia_iatp/registry/__init__.py +26 -0
- traia_iatp/registry/atlas_search_indexes.json +280 -0
- traia_iatp/registry/embeddings.py +298 -0
- traia_iatp/registry/iatp_search_api.py +846 -0
- traia_iatp/registry/mongodb_registry.py +771 -0
- traia_iatp/registry/readmes/ATLAS_SEARCH_INDEXES.md +252 -0
- traia_iatp/registry/readmes/ATLAS_SEARCH_SETUP.md +134 -0
- traia_iatp/registry/readmes/AUTHENTICATION_UPDATE.md +124 -0
- traia_iatp/registry/readmes/EMBEDDINGS_SETUP.md +172 -0
- traia_iatp/registry/readmes/IATP_SEARCH_API_GUIDE.md +257 -0
- traia_iatp/registry/readmes/MONGODB_X509_AUTH.md +208 -0
- traia_iatp/registry/readmes/README.md +251 -0
- traia_iatp/registry/readmes/REFACTORING_SUMMARY.md +191 -0
- traia_iatp/scripts/__init__.py +2 -0
- traia_iatp/scripts/create_wallet.py +244 -0
- traia_iatp/server/__init__.py +15 -0
- traia_iatp/server/a2a_server.py +219 -0
- traia_iatp/server/example_template_usage.py +72 -0
- traia_iatp/server/iatp_server_agent_generator.py +237 -0
- traia_iatp/server/iatp_server_template_generator.py +235 -0
- traia_iatp/server/templates/.dockerignore.j2 +48 -0
- traia_iatp/server/templates/Dockerfile.j2 +49 -0
- traia_iatp/server/templates/README.md +137 -0
- traia_iatp/server/templates/README.md.j2 +425 -0
- traia_iatp/server/templates/__init__.py +1 -0
- traia_iatp/server/templates/__main__.py.j2 +565 -0
- traia_iatp/server/templates/agent.py.j2 +94 -0
- traia_iatp/server/templates/agent_config.json.j2 +22 -0
- traia_iatp/server/templates/agent_executor.py.j2 +279 -0
- traia_iatp/server/templates/docker-compose.yml.j2 +23 -0
- traia_iatp/server/templates/env.example.j2 +84 -0
- traia_iatp/server/templates/gitignore.j2 +78 -0
- traia_iatp/server/templates/grpc_server.py.j2 +218 -0
- traia_iatp/server/templates/pyproject.toml.j2 +78 -0
- traia_iatp/server/templates/run_local_docker.sh.j2 +103 -0
- traia_iatp/server/templates/server.py.j2 +243 -0
- traia_iatp/special_agencies/__init__.py +4 -0
- traia_iatp/special_agencies/registry_search_agency.py +392 -0
- traia_iatp/utils/__init__.py +10 -0
- traia_iatp/utils/docker_utils.py +251 -0
- traia_iatp/utils/general.py +64 -0
- traia_iatp/utils/iatp_utils.py +126 -0
- traia_iatp-0.1.29.dist-info/METADATA +423 -0
- traia_iatp-0.1.29.dist-info/RECORD +107 -0
- traia_iatp-0.1.29.dist-info/WHEEL +5 -0
- traia_iatp-0.1.29.dist-info/entry_points.txt +2 -0
- traia_iatp-0.1.29.dist-info/licenses/LICENSE +21 -0
- traia_iatp-0.1.29.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""D402 middleware for IATP FastAPI servers (utility agents)."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Callable, Optional
|
|
5
|
+
from fastapi import Request
|
|
6
|
+
from fastapi.responses import JSONResponse, HTMLResponse
|
|
7
|
+
|
|
8
|
+
from .fastapi_middleware.middleware import require_payment as d402_require_payment
|
|
9
|
+
from .types import Price, PaywallConfig, HTTPInputSchema
|
|
10
|
+
from typing import Callable, Dict
|
|
11
|
+
from typing_extensions import TypedDict
|
|
12
|
+
|
|
13
|
+
from .models import D402Config, PaymentScheme
|
|
14
|
+
|
|
15
|
+
# FacilitatorConfig (copied from d402)
|
|
16
|
+
class FacilitatorConfig(TypedDict, total=False):
|
|
17
|
+
url: str
|
|
18
|
+
create_headers: Callable[[], dict[str, dict[str, str]]]
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class D402IATPMiddleware:
|
|
24
|
+
"""Middleware that integrates d402 payments into IATP servers.
|
|
25
|
+
|
|
26
|
+
This middleware wraps the Coinbase d402 middleware and adapts it for
|
|
27
|
+
IATP utility agents, connecting to the custom IATP Settlement Layer.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, config: D402Config):
|
|
31
|
+
"""Initialize the d402 IATP middleware.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
config: D402 configuration including pricing and facilitator settings
|
|
35
|
+
"""
|
|
36
|
+
self.config = config
|
|
37
|
+
self.facilitator_config: FacilitatorConfig = {
|
|
38
|
+
"url": config.facilitator_url,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Add authentication headers if API key is provided
|
|
42
|
+
if config.facilitator_api_key:
|
|
43
|
+
async def create_headers():
|
|
44
|
+
return {
|
|
45
|
+
"verify": {"Authorization": f"Bearer {config.facilitator_api_key}"},
|
|
46
|
+
"settle": {"Authorization": f"Bearer {config.facilitator_api_key}"}
|
|
47
|
+
}
|
|
48
|
+
self.facilitator_config["create_headers"] = create_headers
|
|
49
|
+
|
|
50
|
+
def create_middleware(
|
|
51
|
+
self,
|
|
52
|
+
skill_id: Optional[str] = None,
|
|
53
|
+
custom_price: Optional[Price] = None,
|
|
54
|
+
custom_description: Optional[str] = None,
|
|
55
|
+
) -> Callable:
|
|
56
|
+
"""Create a FastAPI middleware function for a specific skill or endpoint.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
skill_id: Optional skill ID to use custom pricing
|
|
60
|
+
custom_price: Optional override price
|
|
61
|
+
custom_description: Optional override description
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
FastAPI middleware function
|
|
65
|
+
"""
|
|
66
|
+
if not self.config.enabled:
|
|
67
|
+
# Return passthrough middleware if payments not enabled
|
|
68
|
+
async def passthrough(request: Request, call_next: Callable):
|
|
69
|
+
return await call_next(request)
|
|
70
|
+
return passthrough
|
|
71
|
+
|
|
72
|
+
# Determine the price to use
|
|
73
|
+
if custom_price:
|
|
74
|
+
price = custom_price
|
|
75
|
+
elif skill_id and skill_id in self.config.skill_prices:
|
|
76
|
+
price_config = self.config.skill_prices[skill_id]
|
|
77
|
+
price = price_config.usd_amount
|
|
78
|
+
else:
|
|
79
|
+
price = self.config.default_price.usd_amount
|
|
80
|
+
|
|
81
|
+
# Determine the description
|
|
82
|
+
description = custom_description or self.config.service_description
|
|
83
|
+
|
|
84
|
+
# Get the pricing configuration
|
|
85
|
+
price_config = (
|
|
86
|
+
self.config.skill_prices.get(skill_id)
|
|
87
|
+
if skill_id and skill_id in self.config.skill_prices
|
|
88
|
+
else self.config.default_price
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Create the d402 middleware using Coinbase's implementation
|
|
92
|
+
return d402_require_payment(
|
|
93
|
+
price=price,
|
|
94
|
+
pay_to_address=self.config.pay_to_address,
|
|
95
|
+
path=self.config.protected_paths,
|
|
96
|
+
description=description,
|
|
97
|
+
mime_type="application/json",
|
|
98
|
+
max_deadline_seconds=price_config.max_timeout_seconds,
|
|
99
|
+
input_schema=HTTPInputSchema(
|
|
100
|
+
query_params=None,
|
|
101
|
+
body_type="json",
|
|
102
|
+
body_fields=None,
|
|
103
|
+
header_fields=None
|
|
104
|
+
),
|
|
105
|
+
output_schema=None,
|
|
106
|
+
discoverable=True,
|
|
107
|
+
facilitator_config=self.facilitator_config,
|
|
108
|
+
network=price_config.network,
|
|
109
|
+
resource=None, # Will be determined from request URL
|
|
110
|
+
paywall_config=None, # Could add custom paywall UI here
|
|
111
|
+
custom_paywall_html=None
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def require_iatp_payment(
|
|
116
|
+
config: D402Config,
|
|
117
|
+
skill_id: Optional[str] = None,
|
|
118
|
+
custom_price: Optional[Price] = None,
|
|
119
|
+
custom_description: Optional[str] = None,
|
|
120
|
+
) -> Callable:
|
|
121
|
+
"""Convenience function to create d402 middleware for IATP.
|
|
122
|
+
|
|
123
|
+
Usage:
|
|
124
|
+
@app.middleware("http")
|
|
125
|
+
async def payment_middleware(request: Request, call_next):
|
|
126
|
+
middleware = require_iatp_payment(d402_config)
|
|
127
|
+
return await middleware(request, call_next)
|
|
128
|
+
|
|
129
|
+
Or for specific routes:
|
|
130
|
+
@app.post("/process")
|
|
131
|
+
async def process_request(request: Request):
|
|
132
|
+
# Will require payment
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
app.middleware("http")(
|
|
136
|
+
require_iatp_payment(d402_config, skill_id="process_request")
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
config: D402 configuration
|
|
141
|
+
skill_id: Optional skill ID for custom pricing
|
|
142
|
+
custom_price: Optional price override
|
|
143
|
+
custom_description: Optional description override
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
FastAPI middleware function
|
|
147
|
+
"""
|
|
148
|
+
middleware = D402IATPMiddleware(config)
|
|
149
|
+
return middleware.create_middleware(skill_id, custom_price, custom_description)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
async def add_d402_info_to_agent_card(agent_card: dict, config: D402Config) -> dict:
|
|
153
|
+
"""Add d402 payment information to an agent card.
|
|
154
|
+
|
|
155
|
+
This adds payment capabilities to the agent card for client discovery.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
agent_card: The agent card dictionary
|
|
159
|
+
config: D402 configuration
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Updated agent card with payment information
|
|
163
|
+
"""
|
|
164
|
+
if not config.enabled:
|
|
165
|
+
return agent_card
|
|
166
|
+
|
|
167
|
+
# Add d402 payment information to metadata
|
|
168
|
+
agent_card.setdefault("metadata", {})
|
|
169
|
+
agent_card["metadata"]["d402"] = {
|
|
170
|
+
"enabled": True,
|
|
171
|
+
"paymentSchemes": [PaymentScheme.EXACT.value],
|
|
172
|
+
"networks": [config.default_price.network],
|
|
173
|
+
"defaultPrice": {
|
|
174
|
+
"usdAmount": config.default_price.usd_amount,
|
|
175
|
+
"network": config.default_price.network,
|
|
176
|
+
"asset": config.default_price.asset_address,
|
|
177
|
+
"maxTimeoutSeconds": config.default_price.max_timeout_seconds
|
|
178
|
+
},
|
|
179
|
+
"payToAddress": config.pay_to_address,
|
|
180
|
+
"facilitatorUrl": config.facilitator_url,
|
|
181
|
+
"skillPrices": {
|
|
182
|
+
skill_id: {
|
|
183
|
+
"usdAmount": price.usd_amount,
|
|
184
|
+
"network": price.network,
|
|
185
|
+
"asset": price.asset_address,
|
|
186
|
+
"maxTimeoutSeconds": price.max_timeout_seconds
|
|
187
|
+
}
|
|
188
|
+
for skill_id, price in config.skill_prices.items()
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return agent_card
|
|
193
|
+
|
|
@@ -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
|
+
|
traia_iatp/d402/path.py
ADDED
|
@@ -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,104 @@
|
|
|
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 a closure (from decorators)
|
|
59
|
+
if not hasattr(fn, '__closure__') or not fn.__closure__:
|
|
60
|
+
logger.debug(f"Tool {tool_name}: No closure (no payment decorator)")
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
# Look for TokenAmount in closure
|
|
64
|
+
token_amount = None
|
|
65
|
+
for cell in fn.__closure__:
|
|
66
|
+
try:
|
|
67
|
+
val = cell.cell_contents
|
|
68
|
+
if isinstance(val, TokenAmount):
|
|
69
|
+
token_amount = val
|
|
70
|
+
break
|
|
71
|
+
except:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
if token_amount:
|
|
75
|
+
# Extract payment config from TokenAmount (including EIP712 domain)
|
|
76
|
+
config = {
|
|
77
|
+
"price_wei": token_amount.amount,
|
|
78
|
+
"token_address": token_amount.asset.address,
|
|
79
|
+
"network": token_amount.asset.network,
|
|
80
|
+
"server_address": server_address,
|
|
81
|
+
"description": tool.description or tool_name,
|
|
82
|
+
"eip712_domain": {
|
|
83
|
+
"name": token_amount.asset.eip712.name if token_amount.asset.eip712 else "USD Coin",
|
|
84
|
+
"version": token_amount.asset.eip712.version if token_amount.asset.eip712 else "2"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
tool_payment_configs[tool_name] = config
|
|
89
|
+
logger.info(f"✅ Extracted payment config for {tool_name}: {config['price_wei']} wei on {config['network']}")
|
|
90
|
+
else:
|
|
91
|
+
logger.debug(f"Tool {tool_name}: No TokenAmount found (free tool)")
|
|
92
|
+
|
|
93
|
+
logger.info(f"📊 Extracted {len(tool_payment_configs)} payment configs from decorators")
|
|
94
|
+
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.error(f"Error extracting payment configs: {e}")
|
|
97
|
+
import traceback
|
|
98
|
+
logger.error(traceback.format_exc())
|
|
99
|
+
|
|
100
|
+
return tool_payment_configs
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
__all__ = ["extract_payment_configs_from_mcp"]
|
|
104
|
+
|