t402 1.9.0__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.
- t402/__init__.py +2 -1
- t402/bridge/client.py +13 -5
- t402/bridge/constants.py +3 -1
- t402/bridge/router.py +1 -1
- t402/bridge/scan.py +3 -1
- t402/chains.py +268 -1
- t402/cli.py +31 -9
- t402/common.py +2 -0
- t402/cosmos_paywall_template.py +2 -0
- t402/encoding.py +9 -3
- t402/erc4337/accounts.py +56 -51
- t402/erc4337/bundlers.py +105 -99
- t402/erc4337/paymasters.py +100 -109
- t402/erc4337/types.py +39 -26
- t402/evm_paywall_template.py +1 -1
- t402/fastapi/middleware.py +1 -3
- t402/mcp/server.py +79 -46
- t402/near_paywall_template.py +2 -0
- t402/networks.py +34 -1
- t402/paywall.py +1 -3
- t402/schemes/__init__.py +124 -0
- t402/schemes/aptos/__init__.py +70 -0
- t402/schemes/aptos/constants.py +349 -0
- t402/schemes/aptos/exact_direct/__init__.py +44 -0
- t402/schemes/aptos/exact_direct/client.py +202 -0
- t402/schemes/aptos/exact_direct/facilitator.py +426 -0
- t402/schemes/aptos/exact_direct/server.py +272 -0
- t402/schemes/aptos/types.py +237 -0
- t402/schemes/evm/__init__.py +46 -1
- t402/schemes/evm/exact/__init__.py +11 -0
- t402/schemes/evm/exact/client.py +3 -1
- t402/schemes/evm/exact/facilitator.py +894 -0
- t402/schemes/evm/exact/server.py +1 -1
- t402/schemes/evm/exact_legacy/__init__.py +38 -0
- t402/schemes/evm/exact_legacy/client.py +291 -0
- t402/schemes/evm/exact_legacy/facilitator.py +777 -0
- t402/schemes/evm/exact_legacy/server.py +231 -0
- t402/schemes/evm/upto/__init__.py +12 -0
- t402/schemes/evm/upto/client.py +6 -2
- t402/schemes/evm/upto/facilitator.py +625 -0
- t402/schemes/evm/upto/server.py +243 -0
- t402/schemes/evm/upto/types.py +3 -1
- t402/schemes/interfaces.py +6 -2
- t402/schemes/near/__init__.py +112 -0
- t402/schemes/near/constants.py +189 -0
- t402/schemes/near/exact_direct/__init__.py +21 -0
- t402/schemes/near/exact_direct/client.py +204 -0
- t402/schemes/near/exact_direct/facilitator.py +455 -0
- t402/schemes/near/exact_direct/server.py +303 -0
- t402/schemes/near/types.py +419 -0
- t402/schemes/polkadot/__init__.py +72 -0
- t402/schemes/polkadot/constants.py +155 -0
- t402/schemes/polkadot/exact_direct/__init__.py +43 -0
- t402/schemes/polkadot/exact_direct/client.py +235 -0
- t402/schemes/polkadot/exact_direct/facilitator.py +428 -0
- t402/schemes/polkadot/exact_direct/server.py +292 -0
- t402/schemes/polkadot/types.py +385 -0
- t402/schemes/registry.py +6 -2
- t402/schemes/stacks/__init__.py +68 -0
- t402/schemes/stacks/constants.py +122 -0
- t402/schemes/stacks/exact_direct/__init__.py +43 -0
- t402/schemes/stacks/exact_direct/client.py +222 -0
- t402/schemes/stacks/exact_direct/facilitator.py +424 -0
- t402/schemes/stacks/exact_direct/server.py +292 -0
- t402/schemes/stacks/types.py +380 -0
- t402/schemes/svm/__init__.py +29 -0
- t402/schemes/svm/exact/__init__.py +35 -0
- t402/schemes/svm/exact/client.py +23 -0
- t402/schemes/svm/exact/facilitator.py +24 -0
- t402/schemes/svm/exact/server.py +20 -0
- t402/schemes/tezos/__init__.py +84 -0
- t402/schemes/tezos/constants.py +372 -0
- t402/schemes/tezos/exact_direct/__init__.py +22 -0
- t402/schemes/tezos/exact_direct/client.py +226 -0
- t402/schemes/tezos/exact_direct/facilitator.py +491 -0
- t402/schemes/tezos/exact_direct/server.py +277 -0
- t402/schemes/tezos/types.py +220 -0
- t402/schemes/ton/__init__.py +9 -2
- t402/schemes/ton/exact/__init__.py +7 -0
- t402/schemes/ton/exact/facilitator.py +730 -0
- t402/schemes/ton/exact/server.py +1 -1
- t402/schemes/tron/__init__.py +11 -2
- t402/schemes/tron/exact/__init__.py +9 -0
- t402/schemes/tron/exact/facilitator.py +673 -0
- t402/schemes/tron/exact/server.py +1 -1
- t402/stacks_paywall_template.py +2 -0
- t402/svm.py +45 -11
- t402/svm_paywall_template.py +1 -1
- t402/ton.py +5 -1
- t402/ton_paywall_template.py +1 -192
- t402/tron.py +2 -0
- t402/tron_paywall_template.py +2 -0
- t402/types.py +3 -1
- t402/wdk/errors.py +15 -5
- t402/wdk/signer.py +11 -2
- {t402-1.9.0.dist-info → t402-1.9.1.dist-info}/METADATA +42 -1
- t402-1.9.1.dist-info/RECORD +125 -0
- t402-1.9.0.dist-info/RECORD +0 -72
- {t402-1.9.0.dist-info → t402-1.9.1.dist-info}/WHEEL +0 -0
- {t402-1.9.0.dist-info → t402-1.9.1.dist-info}/entry_points.txt +0 -0
|
@@ -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)
|