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.

Files changed (107) hide show
  1. traia_iatp/README.md +368 -0
  2. traia_iatp/__init__.py +54 -0
  3. traia_iatp/cli/__init__.py +5 -0
  4. traia_iatp/cli/main.py +483 -0
  5. traia_iatp/client/__init__.py +10 -0
  6. traia_iatp/client/a2a_client.py +274 -0
  7. traia_iatp/client/crewai_a2a_tools.py +335 -0
  8. traia_iatp/client/d402_a2a_client.py +293 -0
  9. traia_iatp/client/grpc_a2a_tools.py +349 -0
  10. traia_iatp/client/root_path_a2a_client.py +1 -0
  11. traia_iatp/contracts/__init__.py +12 -0
  12. traia_iatp/contracts/iatp_contracts_config.py +263 -0
  13. traia_iatp/contracts/wallet_creator.py +255 -0
  14. traia_iatp/core/__init__.py +43 -0
  15. traia_iatp/core/models.py +172 -0
  16. traia_iatp/d402/__init__.py +55 -0
  17. traia_iatp/d402/chains.py +102 -0
  18. traia_iatp/d402/client.py +150 -0
  19. traia_iatp/d402/clients/__init__.py +7 -0
  20. traia_iatp/d402/clients/base.py +218 -0
  21. traia_iatp/d402/clients/httpx.py +219 -0
  22. traia_iatp/d402/common.py +114 -0
  23. traia_iatp/d402/encoding.py +28 -0
  24. traia_iatp/d402/examples/client_example.py +197 -0
  25. traia_iatp/d402/examples/server_example.py +171 -0
  26. traia_iatp/d402/facilitator.py +453 -0
  27. traia_iatp/d402/fastapi_middleware/__init__.py +6 -0
  28. traia_iatp/d402/fastapi_middleware/middleware.py +225 -0
  29. traia_iatp/d402/fastmcp_middleware.py +147 -0
  30. traia_iatp/d402/mcp_middleware.py +434 -0
  31. traia_iatp/d402/middleware.py +193 -0
  32. traia_iatp/d402/models.py +116 -0
  33. traia_iatp/d402/networks.py +98 -0
  34. traia_iatp/d402/path.py +43 -0
  35. traia_iatp/d402/payment_introspection.py +104 -0
  36. traia_iatp/d402/payment_signing.py +178 -0
  37. traia_iatp/d402/paywall.py +119 -0
  38. traia_iatp/d402/starlette_middleware.py +326 -0
  39. traia_iatp/d402/template.py +1 -0
  40. traia_iatp/d402/types.py +300 -0
  41. traia_iatp/mcp/__init__.py +18 -0
  42. traia_iatp/mcp/client.py +201 -0
  43. traia_iatp/mcp/d402_mcp_tool_adapter.py +361 -0
  44. traia_iatp/mcp/mcp_agent_template.py +481 -0
  45. traia_iatp/mcp/templates/Dockerfile.j2 +80 -0
  46. traia_iatp/mcp/templates/README.md.j2 +310 -0
  47. traia_iatp/mcp/templates/cursor-rules.md.j2 +520 -0
  48. traia_iatp/mcp/templates/deployment_params.json.j2 +20 -0
  49. traia_iatp/mcp/templates/docker-compose.yml.j2 +32 -0
  50. traia_iatp/mcp/templates/dockerignore.j2 +47 -0
  51. traia_iatp/mcp/templates/env.example.j2 +57 -0
  52. traia_iatp/mcp/templates/gitignore.j2 +77 -0
  53. traia_iatp/mcp/templates/mcp_health_check.py.j2 +150 -0
  54. traia_iatp/mcp/templates/pyproject.toml.j2 +32 -0
  55. traia_iatp/mcp/templates/pyrightconfig.json.j2 +22 -0
  56. traia_iatp/mcp/templates/run_local_docker.sh.j2 +390 -0
  57. traia_iatp/mcp/templates/server.py.j2 +175 -0
  58. traia_iatp/mcp/traia_mcp_adapter.py +543 -0
  59. traia_iatp/preview_diagrams.html +181 -0
  60. traia_iatp/registry/__init__.py +26 -0
  61. traia_iatp/registry/atlas_search_indexes.json +280 -0
  62. traia_iatp/registry/embeddings.py +298 -0
  63. traia_iatp/registry/iatp_search_api.py +846 -0
  64. traia_iatp/registry/mongodb_registry.py +771 -0
  65. traia_iatp/registry/readmes/ATLAS_SEARCH_INDEXES.md +252 -0
  66. traia_iatp/registry/readmes/ATLAS_SEARCH_SETUP.md +134 -0
  67. traia_iatp/registry/readmes/AUTHENTICATION_UPDATE.md +124 -0
  68. traia_iatp/registry/readmes/EMBEDDINGS_SETUP.md +172 -0
  69. traia_iatp/registry/readmes/IATP_SEARCH_API_GUIDE.md +257 -0
  70. traia_iatp/registry/readmes/MONGODB_X509_AUTH.md +208 -0
  71. traia_iatp/registry/readmes/README.md +251 -0
  72. traia_iatp/registry/readmes/REFACTORING_SUMMARY.md +191 -0
  73. traia_iatp/scripts/__init__.py +2 -0
  74. traia_iatp/scripts/create_wallet.py +244 -0
  75. traia_iatp/server/__init__.py +15 -0
  76. traia_iatp/server/a2a_server.py +219 -0
  77. traia_iatp/server/example_template_usage.py +72 -0
  78. traia_iatp/server/iatp_server_agent_generator.py +237 -0
  79. traia_iatp/server/iatp_server_template_generator.py +235 -0
  80. traia_iatp/server/templates/.dockerignore.j2 +48 -0
  81. traia_iatp/server/templates/Dockerfile.j2 +49 -0
  82. traia_iatp/server/templates/README.md +137 -0
  83. traia_iatp/server/templates/README.md.j2 +425 -0
  84. traia_iatp/server/templates/__init__.py +1 -0
  85. traia_iatp/server/templates/__main__.py.j2 +565 -0
  86. traia_iatp/server/templates/agent.py.j2 +94 -0
  87. traia_iatp/server/templates/agent_config.json.j2 +22 -0
  88. traia_iatp/server/templates/agent_executor.py.j2 +279 -0
  89. traia_iatp/server/templates/docker-compose.yml.j2 +23 -0
  90. traia_iatp/server/templates/env.example.j2 +84 -0
  91. traia_iatp/server/templates/gitignore.j2 +78 -0
  92. traia_iatp/server/templates/grpc_server.py.j2 +218 -0
  93. traia_iatp/server/templates/pyproject.toml.j2 +78 -0
  94. traia_iatp/server/templates/run_local_docker.sh.j2 +103 -0
  95. traia_iatp/server/templates/server.py.j2 +243 -0
  96. traia_iatp/special_agencies/__init__.py +4 -0
  97. traia_iatp/special_agencies/registry_search_agency.py +392 -0
  98. traia_iatp/utils/__init__.py +10 -0
  99. traia_iatp/utils/docker_utils.py +251 -0
  100. traia_iatp/utils/general.py +64 -0
  101. traia_iatp/utils/iatp_utils.py +126 -0
  102. traia_iatp-0.1.29.dist-info/METADATA +423 -0
  103. traia_iatp-0.1.29.dist-info/RECORD +107 -0
  104. traia_iatp-0.1.29.dist-info/WHEEL +5 -0
  105. traia_iatp-0.1.29.dist-info/entry_points.txt +2 -0
  106. traia_iatp-0.1.29.dist-info/licenses/LICENSE +21 -0
  107. traia_iatp-0.1.29.dist-info/top_level.txt +1 -0
@@ -0,0 +1,218 @@
1
+ import time
2
+ from typing import Optional, Callable, Dict, Any, List
3
+ from eth_account import Account
4
+ from ..payment_signing import sign_payment_header
5
+ from ..types import (
6
+ PaymentRequirements,
7
+ UnsupportedSchemeException,
8
+ )
9
+ from ..common import d402_VERSION
10
+ import secrets
11
+ from ..encoding import safe_base64_decode
12
+ import json
13
+
14
+ # Define type for the payment requirements selector
15
+ PaymentSelectorCallable = Callable[
16
+ [List[PaymentRequirements], Optional[str], Optional[str], Optional[int]],
17
+ PaymentRequirements,
18
+ ]
19
+
20
+
21
+ def decode_x_payment_response(header: str) -> Dict[str, Any]:
22
+ """Decode the X-PAYMENT-RESPONSE header.
23
+
24
+ Args:
25
+ header: The X-PAYMENT-RESPONSE header to decode
26
+
27
+ Returns:
28
+ The decoded payment response containing:
29
+ - success: bool
30
+ - transaction: str (hex)
31
+ - network: str
32
+ - payer: str (address)
33
+ """
34
+ decoded = safe_base64_decode(header)
35
+ result = json.loads(decoded)
36
+ return result
37
+
38
+
39
+ class PaymentError(Exception):
40
+ """Base class for payment-related errors."""
41
+
42
+ pass
43
+
44
+
45
+ class PaymentAmountExceededError(PaymentError):
46
+ """Raised when payment amount exceeds maximum allowed value."""
47
+
48
+ pass
49
+
50
+
51
+ class MissingRequestConfigError(PaymentError):
52
+ """Raised when request configuration is missing."""
53
+
54
+ pass
55
+
56
+
57
+ class PaymentAlreadyAttemptedError(PaymentError):
58
+ """Raised when payment has already been attempted."""
59
+
60
+ pass
61
+
62
+
63
+ class d402Client:
64
+ """Base client for handling d402 payments."""
65
+
66
+ def __init__(
67
+ self,
68
+ operator_account: Account,
69
+ wallet_address: str = None,
70
+ max_value: Optional[int] = None,
71
+ payment_requirements_selector: Optional[PaymentSelectorCallable] = None,
72
+ ):
73
+ """Initialize the d402 client.
74
+
75
+ Args:
76
+ operator_account: Operator account with private key for signing payments (EOA)
77
+ wallet_address: Consumer's IATPWallet contract address (if None, uses operator_account.address for testing)
78
+ max_value: Optional safety limit for maximum payment amount per request in base units.
79
+ This is a global safety check that prevents paying more than intended.
80
+ WARNING: This is a simple numeric comparison and does NOT account for:
81
+ - Different tokens (USDC vs TRAIA vs others) - amounts are compared directly
82
+ - Token decimals - ensure max_value uses the same decimals as expected tokens
83
+ - Exchange rates - this is not a USD limit, it's a token amount limit
84
+ Each endpoint can have different payment requirements (amount and token),
85
+ but this limit applies to all requests. Set it based on your most expensive
86
+ expected payment in the token's base units.
87
+ If None, no limit is enforced (not recommended for production).
88
+ payment_requirements_selector: Optional custom selector for payment requirements
89
+ """
90
+ self.operator_account = operator_account # Operator EOA for signing
91
+ self.wallet_address = wallet_address or operator_account.address # IATPWallet contract or EOA for testing
92
+ self.max_value = max_value
93
+ self._payment_requirements_selector = (
94
+ payment_requirements_selector or self.default_payment_requirements_selector
95
+ )
96
+
97
+ @staticmethod
98
+ def default_payment_requirements_selector(
99
+ accepts: List[PaymentRequirements],
100
+ network_filter: Optional[str] = None,
101
+ scheme_filter: Optional[str] = None,
102
+ max_value: Optional[int] = None,
103
+ ) -> PaymentRequirements:
104
+ """Select payment requirements from the list of accepted requirements.
105
+
106
+ Args:
107
+ accepts: List of accepted payment requirements
108
+ network_filter: Optional network to filter by
109
+ scheme_filter: Optional scheme to filter by
110
+ max_value: Optional maximum allowed payment amount
111
+
112
+ Returns:
113
+ Selected payment requirements (PaymentRequirements instance from ..types)
114
+
115
+ Raises:
116
+ UnsupportedSchemeException: If no supported scheme is found
117
+ PaymentAmountExceededError: If payment amount exceeds max_value
118
+ """
119
+ for paymentRequirements in accepts:
120
+ scheme = paymentRequirements.scheme
121
+ network = paymentRequirements.network
122
+
123
+ # Check scheme filter
124
+ if scheme_filter and scheme != scheme_filter:
125
+ continue
126
+
127
+ # Check network filter
128
+ if network_filter and network != network_filter:
129
+ continue
130
+
131
+ if scheme == "exact":
132
+ # Check max value if set
133
+ # NOTE: This is a simple numeric comparison. It does NOT account for:
134
+ # - Different tokens (USDC vs TRAIA vs others)
135
+ # - Token decimals differences
136
+ # - Exchange rates between tokens
137
+ # This is a safety limit to prevent accidentally paying too much.
138
+ # The comparison is done on the raw amount values in base units.
139
+ if max_value is not None:
140
+ max_amount = int(paymentRequirements.max_amount_required)
141
+ if max_amount > max_value:
142
+ raise PaymentAmountExceededError(
143
+ f"Payment amount {max_amount} (token: {paymentRequirements.asset}) "
144
+ f"exceeds maximum allowed value {max_value} base units. "
145
+ f"Note: This comparison does not account for token differences or decimals."
146
+ )
147
+
148
+ return paymentRequirements
149
+
150
+ raise UnsupportedSchemeException("No supported payment scheme found")
151
+
152
+ def select_payment_requirements(
153
+ self,
154
+ accepts: List[PaymentRequirements],
155
+ network_filter: Optional[str] = None,
156
+ scheme_filter: Optional[str] = None,
157
+ ) -> PaymentRequirements:
158
+ """Select payment requirements using the configured selector.
159
+
160
+ Args:
161
+ accepts: List of accepted payment requirements (PaymentRequirements models)
162
+ network_filter: Optional network to filter by
163
+ scheme_filter: Optional scheme to filter by
164
+
165
+ Returns:
166
+ Selected payment requirements (PaymentRequirements instance from ..types)
167
+
168
+ Raises:
169
+ UnsupportedSchemeException: If no supported scheme is found
170
+ PaymentAmountExceededError: If payment amount exceeds max_value
171
+ """
172
+ return self._payment_requirements_selector(
173
+ accepts, network_filter, scheme_filter, self.max_value
174
+ )
175
+
176
+
177
+ def create_payment_header(
178
+ self,
179
+ payment_requirements: PaymentRequirements,
180
+ d402_version: int = d402_VERSION,
181
+ request_path: str = None,
182
+ ) -> str:
183
+ """Create a payment header for the given requirements.
184
+
185
+ Args:
186
+ payment_requirements: Selected payment requirements
187
+ d402_version: d402 protocol version
188
+ request_path: Optional API request path (if None, uses payment_requirements.resource)
189
+
190
+ Returns:
191
+ Signed payment header with PullFundsForSettlement signature
192
+ """
193
+ unsigned_header = {
194
+ "d402Version": d402_version,
195
+ "scheme": payment_requirements.scheme,
196
+ "network": payment_requirements.network,
197
+ "payload": {
198
+ "signature": None,
199
+ "authorization": {
200
+ "from": self.wallet_address, # IATPWallet contract address
201
+ "to": payment_requirements.pay_to, # Provider's IATPWallet
202
+ "value": payment_requirements.max_amount_required,
203
+ "validAfter": str(int(time.time()) - 60), # 60 seconds before
204
+ "validBefore": str(
205
+ int(time.time()) + payment_requirements.max_timeout_seconds
206
+ ),
207
+ },
208
+ },
209
+ }
210
+
211
+ signed_header = sign_payment_header(
212
+ self.operator_account,
213
+ payment_requirements,
214
+ unsigned_header,
215
+ wallet_address=self.wallet_address,
216
+ request_path=request_path or payment_requirements.resource
217
+ )
218
+ return signed_header
@@ -0,0 +1,219 @@
1
+ """HTTPX client integration for d402 payments."""
2
+
3
+ import logging
4
+ from typing import Optional, Dict, List
5
+ from httpx import Request, Response, AsyncClient
6
+ from eth_account import Account
7
+ from functools import wraps
8
+
9
+ from .base import (
10
+ d402Client,
11
+ MissingRequestConfigError,
12
+ PaymentError,
13
+ PaymentSelectorCallable,
14
+ decode_x_payment_response,
15
+ )
16
+ from ..types import d402PaymentRequiredResponse
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class HttpxHooks:
22
+ """Event hooks for httpx client to handle d402 payments."""
23
+
24
+ def __init__(self, client: d402Client):
25
+ self.client = client
26
+ self._is_retry = False
27
+
28
+ async def on_request(self, request: Request):
29
+ """Handle request before it is sent."""
30
+ #TODO: (TBD) if mongodb had endpoints data for each mcp server then the client herre could apriori get the payment details and construct the payment payload.
31
+ pass
32
+
33
+ async def on_response(self, response: Response) -> Response:
34
+ """Handle response after it is received.
35
+
36
+ When a 402 Payment Required response is received:
37
+ 1. Parse payment requirements from the response
38
+ 2. Select appropriate payment option (token/network)
39
+ 3. Create EIP-3009 signed payment authorization
40
+ 4. Retry the original request with X-Payment header
41
+ """
42
+
43
+ # Log every response for debugging
44
+ logger.debug(f"d402 hook: on_response called - status={response.status_code}, url={response.url}")
45
+
46
+ # If this is not a 402, just return the response
47
+ if response.status_code != 402:
48
+ return response
49
+
50
+ # If this is a retry response, just return it (avoid infinite loop)
51
+ if self._is_retry:
52
+ logger.debug(f"d402 hook: This is a retry response, returning as-is")
53
+ return response
54
+
55
+ logger.info(f"🔔 d402 hook: Intercepted HTTP 402 - creating payment...")
56
+
57
+ try:
58
+ if not response.request:
59
+ raise MissingRequestConfigError("Missing request configuration")
60
+
61
+ # Read the response content before parsing
62
+ await response.aread()
63
+
64
+ # Parse payment requirements from 402 response
65
+ data = response.json()
66
+ payment_response = d402PaymentRequiredResponse(**data)
67
+
68
+ # Select payment requirements (matches token/network, checks max_value)
69
+ selected_requirements = self.client.select_payment_requirements(
70
+ payment_response.accepts
71
+ )
72
+
73
+ # The server sets the resource field in payment_requirements
74
+ # This is the authoritative requestPath for the signature
75
+ # The server knows what endpoint is being called and sets it correctly
76
+ print(f"📍 DEBUG: Request path from server resource field: '{selected_requirements.resource}'")
77
+
78
+ # Create signed payment header using CLIENT's account
79
+ # Use payment_requirements.resource (don't override)
80
+ payment_header = self.client.create_payment_header(
81
+ selected_requirements,
82
+ payment_response.d402_version
83
+ # No request_path parameter - uses payment_requirements.resource
84
+ )
85
+
86
+ # Mark as retry to avoid infinite loop
87
+ self._is_retry = True
88
+
89
+ # Get the original request and add payment header
90
+ request = response.request
91
+ request.headers["X-Payment"] = payment_header
92
+ request.headers["Access-Control-Expose-Headers"] = "X-Payment-Response"
93
+
94
+ # Retry the request using the same httpx client that made the original request
95
+ # This ensures the retry uses the same client configuration (base_url, timeout, etc.)
96
+ # and that any hooks are still applied (via monkey-patch)
97
+ original_client = response.request.extensions.get("client")
98
+ if original_client and isinstance(original_client, AsyncClient):
99
+ # Use the original client for retry
100
+ retry_response = await original_client.send(request)
101
+ else:
102
+ # Fallback: create new client (will have hooks if monkey-patch is active)
103
+ async with AsyncClient() as new_client:
104
+ retry_response = await new_client.send(request)
105
+
106
+ # Copy the retry response data to the original response object
107
+ response.status_code = retry_response.status_code
108
+ response.headers = retry_response.headers
109
+ response._content = retry_response._content
110
+ return response
111
+
112
+ except PaymentError as e:
113
+ self._is_retry = False
114
+ raise e
115
+ except Exception as e:
116
+ self._is_retry = False
117
+ raise PaymentError(f"Failed to handle payment: {str(e)}") from e
118
+
119
+
120
+ def d402_payment_hooks(
121
+ operator_account: Account,
122
+ wallet_address: str = None,
123
+ max_value: Optional[int] = None,
124
+ payment_requirements_selector: Optional[PaymentSelectorCallable] = None,
125
+ ) -> Dict[str, List]:
126
+ """Create httpx event hooks dictionary for handling 402 Payment Required responses.
127
+
128
+ Args:
129
+ operator_account: Operator account with private key for signing payments (EOA)
130
+ wallet_address: Consumer's IATPWallet contract address (if None, uses operator_account.address for testing)
131
+ max_value: Optional maximum allowed payment amount in base units
132
+ payment_requirements_selector: Optional custom selector for payment requirements.
133
+ Should be a callable that takes (accepts, network_filter, scheme_filter, max_value)
134
+ and returns a PaymentRequirements object.
135
+
136
+ Returns:
137
+ Dictionary of event hooks that can be directly assigned to client.event_hooks
138
+
139
+ Example:
140
+ ```python
141
+ from eth_account import Account
142
+ from traia_iatp.d402.clients.httpx import d402_payment_hooks
143
+ import httpx
144
+
145
+ # For testing (uses EOA as wallet)
146
+ operator_account = Account.from_key("0x...")
147
+ client.event_hooks = d402_payment_hooks(operator_account)
148
+
149
+ # For production (with IATPWallet contract)
150
+ operator_account = Account.from_key("0x...") # Operator key
151
+ wallet = "0x..." # IATPWallet contract address
152
+ client.event_hooks = d402_payment_hooks(operator_account, wallet_address=wallet)
153
+ ```
154
+ """
155
+ # Create d402Client
156
+ client = d402Client(
157
+ operator_account,
158
+ wallet_address=wallet_address,
159
+ max_value=max_value,
160
+ payment_requirements_selector=payment_requirements_selector,
161
+ )
162
+
163
+ # Create hooks
164
+ hooks = HttpxHooks(client)
165
+
166
+ # Return event hooks dictionary
167
+ return {
168
+ "request": [hooks.on_request],
169
+ "response": [hooks.on_response],
170
+ }
171
+
172
+
173
+ class d402HttpxClient(AsyncClient):
174
+ """AsyncClient with built-in d402 payment handling."""
175
+
176
+ def __init__(
177
+ self,
178
+ operator_account: Account,
179
+ wallet_address: str = None,
180
+ max_value: Optional[int] = None,
181
+ payment_requirements_selector: Optional[PaymentSelectorCallable] = None,
182
+ **kwargs,
183
+ ):
184
+ """Initialize an AsyncClient with d402 payment handling.
185
+
186
+ Args:
187
+ operator_account: Operator account with private key for signing payments (EOA)
188
+ wallet_address: Consumer's IATPWallet contract address (if None, uses operator_account.address for testing)
189
+ max_value: Optional maximum allowed payment amount in base units
190
+ payment_requirements_selector: Optional custom selector for payment requirements.
191
+ Should be a callable that takes (accepts, network_filter, scheme_filter, max_value)
192
+ and returns a PaymentRequirements object.
193
+ **kwargs: Additional arguments to pass to AsyncClient
194
+
195
+ Example:
196
+ ```python
197
+ from eth_account import Account
198
+ from traia_iatp.d402.clients.httpx import d402HttpxClient
199
+
200
+ # For testing (uses EOA as wallet)
201
+ operator_account = Account.from_key("0x...")
202
+ async with d402HttpxClient(operator_account, base_url="https://api.example.com") as client:
203
+ response = await client.get("/protected-endpoint")
204
+
205
+ # For production (with IATPWallet contract)
206
+ operator_account = Account.from_key("0x...") # Operator key
207
+ wallet = "0x..." # IATPWallet contract
208
+ async with d402HttpxClient(operator_account, wallet_address=wallet, base_url="https://api.example.com") as client:
209
+ response = await client.get("/protected-endpoint")
210
+ ```
211
+ """
212
+ super().__init__(**kwargs)
213
+ self.event_hooks = d402_payment_hooks(
214
+ operator_account, wallet_address, max_value, payment_requirements_selector
215
+ )
216
+
217
+
218
+ __all__ = ["d402_payment_hooks", "d402HttpxClient", "HttpxHooks"]
219
+
@@ -0,0 +1,114 @@
1
+ from decimal import Decimal
2
+ from typing import List, Optional
3
+
4
+ from .chains import (
5
+ get_chain_id,
6
+ get_token_decimals,
7
+ get_token_name,
8
+ get_token_version,
9
+ get_default_token_address,
10
+ )
11
+ from .types import Price, TokenAmount, PaymentRequirements, PaymentPayload
12
+
13
+
14
+ def parse_money(amount: str | int, address: str, network: str) -> int:
15
+ """Parse money string or int into int
16
+
17
+ Params:
18
+ amount: str | int - if int, should be the full amount including token specific decimals
19
+ """
20
+ if isinstance(amount, str):
21
+ if amount.startswith("$"):
22
+ amount = amount[1:]
23
+ decimal_amount = Decimal(amount)
24
+
25
+ chain_id = get_chain_id(network)
26
+ decimals = get_token_decimals(chain_id, address)
27
+ decimal_amount = decimal_amount * Decimal(10**decimals)
28
+ return int(decimal_amount)
29
+ return amount
30
+
31
+
32
+ def process_price_to_atomic_amount(
33
+ price: Price, network: str
34
+ ) -> tuple[str, str, dict[str, str]]:
35
+ """Process a Price into atomic amount, asset address, and EIP-712 domain info
36
+
37
+ Args:
38
+ price: Either Money (USD string/int) or TokenAmount
39
+ network: Network identifier
40
+
41
+ Returns:
42
+ Tuple of (max_amount_required, asset_address, eip712_domain)
43
+
44
+ Raises:
45
+ ValueError: If price format is invalid
46
+ """
47
+ if isinstance(price, (str, int)):
48
+ # Money type - convert USD to USDC atomic units
49
+ try:
50
+ if isinstance(price, str) and price.startswith("$"):
51
+ price = price[1:]
52
+ amount = Decimal(str(price))
53
+
54
+ # Get USDC address for the network
55
+ chain_id = get_chain_id(network)
56
+ asset_address = get_usdc_address(chain_id)
57
+ decimals = get_token_decimals(chain_id, asset_address)
58
+
59
+ # Convert to atomic units
60
+ atomic_amount = int(amount * Decimal(10**decimals))
61
+
62
+ # Get EIP-712 domain info
63
+ eip712_domain = {
64
+ "name": get_token_name(chain_id, asset_address),
65
+ "version": get_token_version(chain_id, asset_address),
66
+ }
67
+
68
+ return str(atomic_amount), asset_address, eip712_domain
69
+
70
+ except (ValueError, KeyError) as e:
71
+ raise ValueError(f"Invalid price format: {price}. Error: {e}")
72
+
73
+ elif isinstance(price, TokenAmount):
74
+ # TokenAmount type - already in atomic units with asset info
75
+ return (
76
+ price.amount,
77
+ price.asset.address,
78
+ {
79
+ "name": price.asset.eip712.name,
80
+ "version": price.asset.eip712.version,
81
+ },
82
+ )
83
+
84
+ else:
85
+ raise ValueError(f"Invalid price type: {type(price)}")
86
+
87
+
88
+ def get_usdc_address(chain_id: int | str) -> str:
89
+ """Get the USDC contract address for a given chain ID"""
90
+ chain_id_str = str(chain_id) # Convert to string for consistency
91
+ return get_default_token_address(chain_id_str, "usdc")
92
+
93
+
94
+ def find_matching_payment_requirements(
95
+ payment_requirements: List[PaymentRequirements],
96
+ payment: PaymentPayload,
97
+ ) -> Optional[PaymentRequirements]:
98
+ """
99
+ Finds the matching payment requirements for the given payment.
100
+
101
+ Args:
102
+ payment_requirements: The payment requirements to search through
103
+ payment: The payment to match against
104
+
105
+ Returns:
106
+ The matching payment requirements or None if no match is found
107
+ """
108
+ for req in payment_requirements:
109
+ if req.scheme == payment.scheme and req.network == payment.network:
110
+ return req
111
+ return None
112
+
113
+
114
+ d402_VERSION = 1
@@ -0,0 +1,28 @@
1
+ import base64
2
+ from typing import Union
3
+
4
+
5
+ def safe_base64_encode(data: Union[str, bytes]) -> str:
6
+ """Safely encode string or bytes to base64 string.
7
+
8
+ Args:
9
+ data: String or bytes to encode
10
+
11
+ Returns:
12
+ Base64 encoded string
13
+ """
14
+ if isinstance(data, str):
15
+ data = data.encode("utf-8")
16
+ return base64.b64encode(data).decode("utf-8")
17
+
18
+
19
+ def safe_base64_decode(data: str) -> str:
20
+ """Safely decode base64 string to bytes and then to utf-8 string.
21
+
22
+ Args:
23
+ data: Base64 encoded string
24
+
25
+ Returns:
26
+ Decoded utf-8 string
27
+ """
28
+ return base64.b64decode(data).decode("utf-8")