t402 1.7.1__py3-none-any.whl → 1.9.1__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 (102) hide show
  1. t402/__init__.py +2 -1
  2. t402/bridge/client.py +13 -5
  3. t402/bridge/constants.py +3 -1
  4. t402/bridge/router.py +1 -1
  5. t402/bridge/scan.py +3 -1
  6. t402/chains.py +268 -1
  7. t402/cli.py +31 -9
  8. t402/common.py +2 -0
  9. t402/cosmos_paywall_template.py +2 -0
  10. t402/encoding.py +9 -3
  11. t402/erc4337/accounts.py +56 -51
  12. t402/erc4337/bundlers.py +105 -99
  13. t402/erc4337/paymasters.py +100 -109
  14. t402/erc4337/types.py +39 -26
  15. t402/evm_paywall_template.py +1 -1
  16. t402/fastapi/middleware.py +1 -3
  17. t402/mcp/server.py +79 -46
  18. t402/near_paywall_template.py +2 -0
  19. t402/networks.py +34 -1
  20. t402/paywall.py +1 -3
  21. t402/schemes/__init__.py +164 -1
  22. t402/schemes/aptos/__init__.py +70 -0
  23. t402/schemes/aptos/constants.py +349 -0
  24. t402/schemes/aptos/exact_direct/__init__.py +44 -0
  25. t402/schemes/aptos/exact_direct/client.py +202 -0
  26. t402/schemes/aptos/exact_direct/facilitator.py +426 -0
  27. t402/schemes/aptos/exact_direct/server.py +272 -0
  28. t402/schemes/aptos/types.py +237 -0
  29. t402/schemes/evm/__init__.py +67 -1
  30. t402/schemes/evm/exact/__init__.py +11 -0
  31. t402/schemes/evm/exact/client.py +3 -1
  32. t402/schemes/evm/exact/facilitator.py +894 -0
  33. t402/schemes/evm/exact/server.py +1 -1
  34. t402/schemes/evm/exact_legacy/__init__.py +38 -0
  35. t402/schemes/evm/exact_legacy/client.py +291 -0
  36. t402/schemes/evm/exact_legacy/facilitator.py +777 -0
  37. t402/schemes/evm/exact_legacy/server.py +231 -0
  38. t402/schemes/evm/upto/__init__.py +70 -0
  39. t402/schemes/evm/upto/client.py +244 -0
  40. t402/schemes/evm/upto/facilitator.py +625 -0
  41. t402/schemes/evm/upto/server.py +243 -0
  42. t402/schemes/evm/upto/types.py +307 -0
  43. t402/schemes/interfaces.py +6 -2
  44. t402/schemes/near/__init__.py +112 -0
  45. t402/schemes/near/constants.py +189 -0
  46. t402/schemes/near/exact_direct/__init__.py +21 -0
  47. t402/schemes/near/exact_direct/client.py +204 -0
  48. t402/schemes/near/exact_direct/facilitator.py +455 -0
  49. t402/schemes/near/exact_direct/server.py +303 -0
  50. t402/schemes/near/types.py +419 -0
  51. t402/schemes/polkadot/__init__.py +72 -0
  52. t402/schemes/polkadot/constants.py +155 -0
  53. t402/schemes/polkadot/exact_direct/__init__.py +43 -0
  54. t402/schemes/polkadot/exact_direct/client.py +235 -0
  55. t402/schemes/polkadot/exact_direct/facilitator.py +428 -0
  56. t402/schemes/polkadot/exact_direct/server.py +292 -0
  57. t402/schemes/polkadot/types.py +385 -0
  58. t402/schemes/registry.py +6 -2
  59. t402/schemes/stacks/__init__.py +68 -0
  60. t402/schemes/stacks/constants.py +122 -0
  61. t402/schemes/stacks/exact_direct/__init__.py +43 -0
  62. t402/schemes/stacks/exact_direct/client.py +222 -0
  63. t402/schemes/stacks/exact_direct/facilitator.py +424 -0
  64. t402/schemes/stacks/exact_direct/server.py +292 -0
  65. t402/schemes/stacks/types.py +380 -0
  66. t402/schemes/svm/__init__.py +29 -0
  67. t402/schemes/svm/exact/__init__.py +35 -0
  68. t402/schemes/svm/exact/client.py +23 -0
  69. t402/schemes/svm/exact/facilitator.py +24 -0
  70. t402/schemes/svm/exact/server.py +20 -0
  71. t402/schemes/tezos/__init__.py +84 -0
  72. t402/schemes/tezos/constants.py +372 -0
  73. t402/schemes/tezos/exact_direct/__init__.py +22 -0
  74. t402/schemes/tezos/exact_direct/client.py +226 -0
  75. t402/schemes/tezos/exact_direct/facilitator.py +491 -0
  76. t402/schemes/tezos/exact_direct/server.py +277 -0
  77. t402/schemes/tezos/types.py +220 -0
  78. t402/schemes/ton/__init__.py +9 -2
  79. t402/schemes/ton/exact/__init__.py +7 -0
  80. t402/schemes/ton/exact/facilitator.py +730 -0
  81. t402/schemes/ton/exact/server.py +1 -1
  82. t402/schemes/tron/__init__.py +11 -2
  83. t402/schemes/tron/exact/__init__.py +9 -0
  84. t402/schemes/tron/exact/facilitator.py +673 -0
  85. t402/schemes/tron/exact/server.py +1 -1
  86. t402/schemes/upto/__init__.py +80 -0
  87. t402/schemes/upto/types.py +376 -0
  88. t402/stacks_paywall_template.py +2 -0
  89. t402/svm.py +45 -11
  90. t402/svm_paywall_template.py +1 -1
  91. t402/ton.py +5 -1
  92. t402/ton_paywall_template.py +1 -192
  93. t402/tron.py +2 -0
  94. t402/tron_paywall_template.py +2 -0
  95. t402/types.py +4 -2
  96. t402/wdk/errors.py +15 -5
  97. t402/wdk/signer.py +11 -2
  98. {t402-1.7.1.dist-info → t402-1.9.1.dist-info}/METADATA +42 -1
  99. t402-1.9.1.dist-info/RECORD +125 -0
  100. t402-1.7.1.dist-info/RECORD +0 -67
  101. {t402-1.7.1.dist-info → t402-1.9.1.dist-info}/WHEEL +0 -0
  102. {t402-1.7.1.dist-info → t402-1.9.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,44 @@
1
+ """Aptos Exact-Direct Payment Scheme.
2
+
3
+ This package provides the exact-direct payment scheme implementation for Aptos
4
+ using Fungible Asset (FA) transfers via ``0x1::primary_fungible_store::transfer``.
5
+
6
+ The exact-direct scheme works as follows:
7
+ 1. Client executes the FA transfer on-chain directly.
8
+ 2. Client returns the transaction hash as proof of payment.
9
+ 3. Facilitator queries the Aptos REST API to verify the transaction details.
10
+
11
+ This is a "push" payment model where the client performs the transfer first,
12
+ unlike permit-based models where the facilitator executes settlement.
13
+ """
14
+
15
+ from t402.schemes.aptos.exact_direct.client import (
16
+ ExactDirectAptosClientScheme,
17
+ )
18
+ from t402.schemes.aptos.exact_direct.server import (
19
+ ExactDirectAptosServerScheme,
20
+ )
21
+ from t402.schemes.aptos.exact_direct.facilitator import (
22
+ ExactDirectAptosFacilitatorScheme,
23
+ )
24
+ from t402.schemes.aptos.constants import SCHEME_EXACT_DIRECT
25
+ from t402.schemes.aptos.types import (
26
+ ClientAptosSigner,
27
+ FacilitatorAptosSigner,
28
+ ExactDirectPayload,
29
+ )
30
+
31
+ __all__ = [
32
+ # Client
33
+ "ExactDirectAptosClientScheme",
34
+ "ClientAptosSigner",
35
+ # Server
36
+ "ExactDirectAptosServerScheme",
37
+ # Facilitator
38
+ "ExactDirectAptosFacilitatorScheme",
39
+ "FacilitatorAptosSigner",
40
+ # Types
41
+ "ExactDirectPayload",
42
+ # Constants
43
+ "SCHEME_EXACT_DIRECT",
44
+ ]
@@ -0,0 +1,202 @@
1
+ """Aptos Exact-Direct Scheme - Client Implementation.
2
+
3
+ This module provides the client-side implementation of the exact-direct payment
4
+ scheme for Aptos using Fungible Asset transfers.
5
+
6
+ The client executes ``0x1::primary_fungible_store::transfer`` on-chain and returns
7
+ the transaction hash as proof of payment.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from typing import Any, Dict, Union
14
+
15
+ from t402.types import (
16
+ PaymentRequirementsV2,
17
+ T402_VERSION_V1,
18
+ T402_VERSION_V2,
19
+ )
20
+ from t402.schemes.aptos.constants import (
21
+ SCHEME_EXACT_DIRECT,
22
+ CAIP_FAMILY,
23
+ FA_TRANSFER_FUNCTION,
24
+ is_valid_address,
25
+ is_valid_network,
26
+ is_valid_tx_hash,
27
+ )
28
+ from t402.schemes.aptos.types import (
29
+ ClientAptosSigner,
30
+ ExactDirectPayload,
31
+ )
32
+
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ class ExactDirectAptosClientScheme:
38
+ """Client scheme for Aptos exact-direct payments using FA transfers.
39
+
40
+ Executes a fungible asset transfer on-chain and returns the transaction
41
+ hash as proof of payment. The facilitator then verifies the transaction
42
+ details match the payment requirements.
43
+
44
+ Example:
45
+ ```python
46
+ scheme = ExactDirectAptosClientScheme(signer=my_aptos_signer)
47
+
48
+ payload = await scheme.create_payment_payload(
49
+ t402_version=2,
50
+ requirements={
51
+ "scheme": "exact-direct",
52
+ "network": "aptos:1",
53
+ "asset": "0xf73e887a8754f540ee6e1a93bdc6dde2af69fc7ca5de32013e89dd44244473cb",
54
+ "amount": "1000000",
55
+ "payTo": "0x1234...abcd",
56
+ },
57
+ )
58
+ ```
59
+
60
+ Attributes:
61
+ scheme: The scheme identifier ("exact-direct").
62
+ caip_family: The CAIP-2 family pattern ("aptos:*").
63
+ """
64
+
65
+ scheme = SCHEME_EXACT_DIRECT
66
+ caip_family = CAIP_FAMILY
67
+
68
+ def __init__(self, signer: ClientAptosSigner) -> None:
69
+ """Initialize the Aptos exact-direct client scheme.
70
+
71
+ Args:
72
+ signer: An implementation of ClientAptosSigner that can sign
73
+ and submit transactions to the Aptos network.
74
+ """
75
+ self._signer = signer
76
+
77
+ @property
78
+ def address(self) -> str:
79
+ """Return the signer's Aptos address."""
80
+ return self._signer.address()
81
+
82
+ async def create_payment_payload(
83
+ self,
84
+ t402_version: int,
85
+ requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
86
+ ) -> Dict[str, Any]:
87
+ """Create a payment payload by executing the FA transfer on-chain.
88
+
89
+ This method:
90
+ 1. Validates the payment requirements.
91
+ 2. Builds the FA transfer transaction payload.
92
+ 3. Signs and submits the transaction via the signer.
93
+ 4. Returns the transaction hash as proof of payment.
94
+
95
+ Args:
96
+ t402_version: Protocol version (1 or 2).
97
+ requirements: Payment requirements specifying amount, asset, and payTo.
98
+
99
+ Returns:
100
+ Payment payload dict containing the transaction hash and transfer details.
101
+
102
+ Raises:
103
+ ValueError: If requirements are invalid (bad address, network, amount, etc.).
104
+ Exception: If the signer fails to sign or submit the transaction.
105
+ """
106
+ # Convert to dict for easier access
107
+ if hasattr(requirements, "model_dump"):
108
+ req = requirements.model_dump(by_alias=True)
109
+ else:
110
+ req = dict(requirements)
111
+
112
+ # Extract and validate fields
113
+ network = req.get("network", "")
114
+ asset = req.get("asset", "")
115
+ amount = req.get("amount", "0")
116
+ pay_to = req.get("payTo", "")
117
+ scheme = req.get("scheme", "")
118
+
119
+ # Validate scheme
120
+ if scheme and scheme != SCHEME_EXACT_DIRECT:
121
+ raise ValueError(
122
+ f"Invalid scheme: expected {SCHEME_EXACT_DIRECT}, got {scheme}"
123
+ )
124
+
125
+ # Validate network
126
+ if not network.startswith("aptos:"):
127
+ raise ValueError(
128
+ f"Invalid network: {network} (expected aptos:* format)"
129
+ )
130
+ if not is_valid_network(network):
131
+ raise ValueError(f"Unsupported network: {network}")
132
+
133
+ # Validate payTo address
134
+ if not pay_to:
135
+ raise ValueError("PayTo address is required")
136
+ if not is_valid_address(pay_to):
137
+ raise ValueError(f"Invalid payTo address: {pay_to}")
138
+
139
+ # Validate asset (FA metadata address)
140
+ if not asset:
141
+ raise ValueError("Asset (FA metadata address) is required")
142
+ if not is_valid_address(asset):
143
+ raise ValueError(f"Invalid asset address: {asset}")
144
+
145
+ # Validate amount
146
+ if not amount:
147
+ raise ValueError("Amount is required")
148
+ try:
149
+ amount_int = int(amount)
150
+ except (ValueError, TypeError):
151
+ raise ValueError(f"Invalid amount: {amount}")
152
+ if amount_int <= 0:
153
+ raise ValueError(f"Amount must be positive, got: {amount}")
154
+
155
+ # Validate signer address
156
+ signer_address = self._signer.address()
157
+ if not is_valid_address(signer_address):
158
+ raise ValueError(f"Invalid signer address: {signer_address}")
159
+
160
+ # Build the FA transfer transaction payload
161
+ tx_payload: Dict[str, Any] = {
162
+ "type": "entry_function_payload",
163
+ "function": FA_TRANSFER_FUNCTION,
164
+ "type_arguments": [],
165
+ "arguments": [
166
+ asset, # FA metadata address
167
+ pay_to, # recipient address
168
+ amount, # amount (u64 as string)
169
+ ],
170
+ }
171
+
172
+ # Sign and submit the transaction
173
+ tx_hash = await self._signer.sign_and_submit(tx_payload, network)
174
+
175
+ # Validate returned transaction hash
176
+ if not is_valid_tx_hash(tx_hash):
177
+ raise ValueError(
178
+ f"Signer returned invalid transaction hash: {tx_hash}"
179
+ )
180
+
181
+ # Build the exact-direct payload
182
+ aptos_payload = ExactDirectPayload(
183
+ tx_hash=tx_hash,
184
+ from_address=signer_address,
185
+ to_address=pay_to,
186
+ amount=amount,
187
+ metadata_address=asset,
188
+ )
189
+
190
+ if t402_version == T402_VERSION_V1:
191
+ return {
192
+ "t402Version": T402_VERSION_V1,
193
+ "scheme": self.scheme,
194
+ "network": network,
195
+ "payload": aptos_payload.to_dict(),
196
+ }
197
+
198
+ # V2 format
199
+ return {
200
+ "t402Version": T402_VERSION_V2,
201
+ "payload": aptos_payload.to_dict(),
202
+ }
@@ -0,0 +1,426 @@
1
+ """Aptos Exact-Direct Scheme - Facilitator Implementation.
2
+
3
+ This module provides the facilitator-side implementation of the exact-direct
4
+ payment scheme for Aptos.
5
+
6
+ The facilitator:
7
+ 1. Verifies that the transaction hash in the payload corresponds to a successful
8
+ FA transfer on the Aptos network.
9
+ 2. Validates that sender, recipient, amount, and asset match the requirements.
10
+ 3. For settlement, the transfer is already complete (client executed it directly).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import time
16
+ import logging
17
+ import threading
18
+ from typing import Any, Dict, List, Optional, Union
19
+
20
+ from t402.types import (
21
+ PaymentRequirementsV2,
22
+ PaymentPayloadV2,
23
+ VerifyResponse,
24
+ SettleResponse,
25
+ Network,
26
+ )
27
+ from t402.schemes.aptos.constants import (
28
+ SCHEME_EXACT_DIRECT,
29
+ CAIP_FAMILY,
30
+ get_network_config,
31
+ is_valid_tx_hash,
32
+ compare_addresses,
33
+ )
34
+ from t402.schemes.aptos.types import (
35
+ FacilitatorAptosSigner,
36
+ ExactDirectPayload,
37
+ extract_transfer_details,
38
+ )
39
+
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+ # Default configuration values
44
+ DEFAULT_MAX_TRANSACTION_AGE = 3600 # 1 hour in seconds
45
+ DEFAULT_USED_TX_CACHE_DURATION = 86400 # 24 hours in seconds
46
+
47
+
48
+ class ExactDirectAptosFacilitatorScheme:
49
+ """Facilitator scheme for Aptos exact-direct payments.
50
+
51
+ Verifies FA transfer transactions on-chain and confirms that the payment
52
+ details (sender, recipient, amount, asset) match the requirements.
53
+
54
+ For exact-direct, settlement is a no-op since the client already executed
55
+ the transfer. The facilitator simply verifies and returns the transaction hash.
56
+
57
+ Example:
58
+ ```python
59
+ facilitator = ExactDirectAptosFacilitatorScheme(
60
+ signer=my_aptos_querier,
61
+ max_transaction_age=3600,
62
+ )
63
+
64
+ # Verify a payment
65
+ result = await facilitator.verify(payload, requirements)
66
+ if result.is_valid:
67
+ print(f"Payment verified from {result.payer}")
68
+
69
+ # Settle (returns existing tx hash since transfer is complete)
70
+ settlement = await facilitator.settle(payload, requirements)
71
+ print(f"Tx: {settlement.transaction}")
72
+ ```
73
+
74
+ Attributes:
75
+ scheme: The scheme identifier ("exact-direct").
76
+ caip_family: The CAIP-2 family pattern ("aptos:*").
77
+ """
78
+
79
+ scheme = SCHEME_EXACT_DIRECT
80
+ caip_family = CAIP_FAMILY
81
+
82
+ def __init__(
83
+ self,
84
+ signer: FacilitatorAptosSigner,
85
+ max_transaction_age: int = DEFAULT_MAX_TRANSACTION_AGE,
86
+ used_tx_cache_duration: int = DEFAULT_USED_TX_CACHE_DURATION,
87
+ ) -> None:
88
+ """Initialize the Aptos exact-direct facilitator scheme.
89
+
90
+ Args:
91
+ signer: An implementation of FacilitatorAptosSigner for querying
92
+ transactions from the Aptos network.
93
+ max_transaction_age: Maximum age of a transaction to accept, in seconds.
94
+ Default: 3600 (1 hour).
95
+ used_tx_cache_duration: How long to cache used transaction hashes
96
+ for replay protection, in seconds. Default: 86400 (24 hours).
97
+ """
98
+ self._signer = signer
99
+ self._max_transaction_age = max_transaction_age
100
+ self._used_tx_cache_duration = used_tx_cache_duration
101
+
102
+ # Used transaction cache for replay protection
103
+ self._used_txs: Dict[str, float] = {}
104
+ self._used_txs_lock = threading.Lock()
105
+
106
+ def get_extra(self, network: Network) -> Optional[Dict[str, Any]]:
107
+ """Get mechanism-specific extra data for supported kinds.
108
+
109
+ Returns the default token symbol and decimals for the network,
110
+ which clients use when building payment requirements.
111
+
112
+ Args:
113
+ network: The CAIP-2 network identifier.
114
+
115
+ Returns:
116
+ Dict with assetSymbol and assetDecimals, or None if network
117
+ is not supported.
118
+ """
119
+ config = get_network_config(str(network))
120
+ if not config:
121
+ return None
122
+ return {
123
+ "assetSymbol": config.default_token.symbol,
124
+ "assetDecimals": config.default_token.decimals,
125
+ }
126
+
127
+ def get_signers(self, network: Network) -> List[str]:
128
+ """Get signer addresses for this facilitator.
129
+
130
+ Args:
131
+ network: The CAIP-2 network identifier.
132
+
133
+ Returns:
134
+ List of facilitator addresses for the given network.
135
+ """
136
+ return self._signer.get_addresses(str(network))
137
+
138
+ async def verify(
139
+ self,
140
+ payload: Union[PaymentPayloadV2, Dict[str, Any]],
141
+ requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
142
+ ) -> VerifyResponse:
143
+ """Verify a payment payload by checking the on-chain transaction.
144
+
145
+ Validates:
146
+ 1. Payload has the correct structure with a valid transaction hash.
147
+ 2. Transaction exists on-chain and was successful.
148
+ 3. Transaction is not too old.
149
+ 4. Transaction has not been used before (replay protection).
150
+ 5. Recipient matches the payTo in requirements.
151
+ 6. Amount is greater than or equal to the required amount.
152
+
153
+ Args:
154
+ payload: The payment payload containing the transaction hash.
155
+ requirements: The payment requirements to verify against.
156
+
157
+ Returns:
158
+ VerifyResponse indicating validity and payer address.
159
+ """
160
+ try:
161
+ # Extract payload and requirements data
162
+ payload_data = self._extract_payload(payload)
163
+ req_data = self._extract_requirements(requirements)
164
+
165
+ network = req_data.get("network", "")
166
+
167
+ # Parse the exact-direct payload
168
+ aptos_payload = ExactDirectPayload.from_dict(payload_data)
169
+
170
+ # Validate transaction hash format
171
+ if not is_valid_tx_hash(aptos_payload.tx_hash):
172
+ return VerifyResponse(
173
+ is_valid=False,
174
+ invalid_reason="Invalid transaction hash format",
175
+ payer=None,
176
+ )
177
+
178
+ # Validate from address
179
+ if not aptos_payload.from_address:
180
+ return VerifyResponse(
181
+ is_valid=False,
182
+ invalid_reason="Missing 'from' address in payload",
183
+ payer=None,
184
+ )
185
+
186
+ # Check for replay attack
187
+ if self._is_tx_used(aptos_payload.tx_hash):
188
+ return VerifyResponse(
189
+ is_valid=False,
190
+ invalid_reason="Transaction has already been used",
191
+ payer=aptos_payload.from_address,
192
+ )
193
+
194
+ # Query the transaction from on-chain
195
+ try:
196
+ tx = await self._signer.get_transaction(
197
+ aptos_payload.tx_hash, network
198
+ )
199
+ except Exception as e:
200
+ logger.error(f"Failed to query transaction: {e}")
201
+ return VerifyResponse(
202
+ is_valid=False,
203
+ invalid_reason=f"Transaction not found: {str(e)}",
204
+ payer=aptos_payload.from_address,
205
+ )
206
+
207
+ # Verify transaction succeeded
208
+ if not tx.get("success"):
209
+ vm_status = tx.get("vm_status", "unknown")
210
+ return VerifyResponse(
211
+ is_valid=False,
212
+ invalid_reason=f"Transaction failed: vm_status={vm_status}",
213
+ payer=aptos_payload.from_address,
214
+ )
215
+
216
+ # Check transaction age
217
+ if self._max_transaction_age > 0:
218
+ timestamp_str = tx.get("timestamp", "")
219
+ if timestamp_str:
220
+ try:
221
+ # Aptos timestamps are in microseconds
222
+ tx_timestamp_sec = int(timestamp_str) / 1_000_000
223
+ age = time.time() - tx_timestamp_sec
224
+ if age > self._max_transaction_age:
225
+ return VerifyResponse(
226
+ is_valid=False,
227
+ invalid_reason=(
228
+ f"Transaction too old: {int(age)} seconds "
229
+ f"(max {self._max_transaction_age})"
230
+ ),
231
+ payer=aptos_payload.from_address,
232
+ )
233
+ except (ValueError, TypeError):
234
+ pass # Skip age check if timestamp parsing fails
235
+
236
+ # Extract transfer details from transaction
237
+ transfer = extract_transfer_details(tx)
238
+ if not transfer:
239
+ return VerifyResponse(
240
+ is_valid=False,
241
+ invalid_reason="Could not extract transfer details from transaction",
242
+ payer=aptos_payload.from_address,
243
+ )
244
+
245
+ # Verify recipient matches payTo
246
+ pay_to = req_data.get("payTo", "")
247
+ if not compare_addresses(transfer["to"], pay_to):
248
+ return VerifyResponse(
249
+ is_valid=False,
250
+ invalid_reason=(
251
+ f"Recipient mismatch: expected {pay_to}, "
252
+ f"got {transfer['to']}"
253
+ ),
254
+ payer=aptos_payload.from_address,
255
+ )
256
+
257
+ # Verify amount
258
+ try:
259
+ tx_amount = int(transfer["amount"])
260
+ except (ValueError, TypeError):
261
+ return VerifyResponse(
262
+ is_valid=False,
263
+ invalid_reason=f"Invalid transaction amount: {transfer['amount']}",
264
+ payer=aptos_payload.from_address,
265
+ )
266
+
267
+ required_amount_str = req_data.get("amount", "0")
268
+ try:
269
+ required_amount = int(required_amount_str)
270
+ except (ValueError, TypeError):
271
+ return VerifyResponse(
272
+ is_valid=False,
273
+ invalid_reason=f"Invalid required amount: {required_amount_str}",
274
+ payer=aptos_payload.from_address,
275
+ )
276
+
277
+ if tx_amount < required_amount:
278
+ return VerifyResponse(
279
+ is_valid=False,
280
+ invalid_reason=(
281
+ f"Insufficient amount: got {tx_amount}, "
282
+ f"required {required_amount}"
283
+ ),
284
+ payer=aptos_payload.from_address,
285
+ )
286
+
287
+ # Mark transaction as used
288
+ self._mark_tx_used(aptos_payload.tx_hash)
289
+
290
+ return VerifyResponse(
291
+ is_valid=True,
292
+ invalid_reason=None,
293
+ payer=aptos_payload.from_address,
294
+ )
295
+
296
+ except Exception as e:
297
+ logger.error(f"Aptos exact-direct verification failed: {e}")
298
+ return VerifyResponse(
299
+ is_valid=False,
300
+ invalid_reason=f"Verification error: {str(e)}",
301
+ payer=None,
302
+ )
303
+
304
+ async def settle(
305
+ self,
306
+ payload: Union[PaymentPayloadV2, Dict[str, Any]],
307
+ requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
308
+ ) -> SettleResponse:
309
+ """Settle a verified payment.
310
+
311
+ For exact-direct, the transfer is already complete since the client
312
+ executed it directly on-chain. Settlement simply verifies the transaction
313
+ and returns the existing transaction hash.
314
+
315
+ Args:
316
+ payload: The verified payment payload.
317
+ requirements: The payment requirements.
318
+
319
+ Returns:
320
+ SettleResponse with the transaction hash and status.
321
+ """
322
+ req_data = self._extract_requirements(requirements)
323
+ network = req_data.get("network", "")
324
+
325
+ # Verify first
326
+ verify_result = await self.verify(payload, requirements)
327
+
328
+ if not verify_result.is_valid:
329
+ return SettleResponse(
330
+ success=False,
331
+ error_reason=verify_result.invalid_reason or "Verification failed",
332
+ transaction=None,
333
+ network=network,
334
+ payer=verify_result.payer,
335
+ )
336
+
337
+ # Extract tx hash from payload
338
+ payload_data = self._extract_payload(payload)
339
+ aptos_payload = ExactDirectPayload.from_dict(payload_data)
340
+
341
+ # For exact-direct, settlement is already complete
342
+ return SettleResponse(
343
+ success=True,
344
+ error_reason=None,
345
+ transaction=aptos_payload.tx_hash,
346
+ network=network,
347
+ payer=verify_result.payer,
348
+ )
349
+
350
+ def cleanup_used_txs(self) -> int:
351
+ """Clean up expired entries from the used transaction cache.
352
+
353
+ Removes entries older than ``used_tx_cache_duration``.
354
+
355
+ Returns:
356
+ Number of entries removed.
357
+ """
358
+ cutoff = time.time() - self._used_tx_cache_duration
359
+ removed = 0
360
+ with self._used_txs_lock:
361
+ expired = [
362
+ tx_hash
363
+ for tx_hash, used_at in self._used_txs.items()
364
+ if used_at < cutoff
365
+ ]
366
+ for tx_hash in expired:
367
+ del self._used_txs[tx_hash]
368
+ removed += 1
369
+ return removed
370
+
371
+ def _is_tx_used(self, tx_hash: str) -> bool:
372
+ """Check if a transaction has been used.
373
+
374
+ Args:
375
+ tx_hash: Transaction hash to check.
376
+
377
+ Returns:
378
+ True if the transaction has been seen before.
379
+ """
380
+ with self._used_txs_lock:
381
+ return tx_hash in self._used_txs
382
+
383
+ def _mark_tx_used(self, tx_hash: str) -> None:
384
+ """Mark a transaction as used.
385
+
386
+ Args:
387
+ tx_hash: Transaction hash to mark.
388
+ """
389
+ with self._used_txs_lock:
390
+ self._used_txs[tx_hash] = time.time()
391
+
392
+ def _extract_payload(
393
+ self, payload: Union[PaymentPayloadV2, Dict[str, Any]]
394
+ ) -> Dict[str, Any]:
395
+ """Extract payload data as a dict.
396
+
397
+ Handles both PaymentPayloadV2 models and plain dicts. For models,
398
+ extracts the inner 'payload' field.
399
+
400
+ Args:
401
+ payload: Payment payload (model or dict).
402
+
403
+ Returns:
404
+ Dict containing the inner payload data.
405
+ """
406
+ if hasattr(payload, "model_dump"):
407
+ data = payload.model_dump(by_alias=True)
408
+ return data.get("payload", data)
409
+ elif isinstance(payload, dict):
410
+ return payload.get("payload", payload)
411
+ return dict(payload)
412
+
413
+ def _extract_requirements(
414
+ self, requirements: Union[PaymentRequirementsV2, Dict[str, Any]]
415
+ ) -> Dict[str, Any]:
416
+ """Extract requirements data as a dict.
417
+
418
+ Args:
419
+ requirements: Payment requirements (model or dict).
420
+
421
+ Returns:
422
+ Dict containing requirement fields.
423
+ """
424
+ if hasattr(requirements, "model_dump"):
425
+ return requirements.model_dump(by_alias=True)
426
+ return dict(requirements)