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,204 @@
|
|
|
1
|
+
"""NEAR Exact-Direct Scheme - Client Implementation.
|
|
2
|
+
|
|
3
|
+
This module provides the client-side implementation of the exact-direct payment
|
|
4
|
+
scheme for NEAR networks using NEP-141 ft_transfer.
|
|
5
|
+
|
|
6
|
+
Unlike other schemes where the client creates a signed message for the facilitator
|
|
7
|
+
to execute, the exact-direct scheme has the client execute the transfer directly.
|
|
8
|
+
The transaction hash is then used as proof of payment.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
from typing import Any, Dict, List, Optional, Union
|
|
16
|
+
|
|
17
|
+
from t402.types import PaymentRequirementsV2
|
|
18
|
+
from t402.schemes.near.constants import (
|
|
19
|
+
SCHEME_EXACT_DIRECT,
|
|
20
|
+
CAIP_FAMILY,
|
|
21
|
+
DEFAULT_GAS_INT,
|
|
22
|
+
STORAGE_DEPOSIT,
|
|
23
|
+
FUNCTION_FT_TRANSFER,
|
|
24
|
+
is_valid_network,
|
|
25
|
+
)
|
|
26
|
+
from t402.schemes.near.types import (
|
|
27
|
+
ClientNearSigner,
|
|
28
|
+
ExactDirectPayload,
|
|
29
|
+
is_valid_account_id,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ExactDirectNearClientConfig:
|
|
37
|
+
"""Configuration for the ExactDirectNearClientScheme.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
memo: Optional memo to include in the ft_transfer call.
|
|
41
|
+
gas_amount: Gas to attach to the ft_transfer call (default: 30 TGas).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
memo: Optional[str] = None,
|
|
47
|
+
gas_amount: Optional[int] = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
self.memo = memo
|
|
50
|
+
self.gas_amount = gas_amount or DEFAULT_GAS_INT
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ExactDirectNearClientScheme:
|
|
54
|
+
"""Client scheme for NEAR exact-direct payments using NEP-141 ft_transfer.
|
|
55
|
+
|
|
56
|
+
This scheme executes the token transfer on-chain directly and returns the
|
|
57
|
+
transaction hash as proof of payment. The facilitator then verifies the
|
|
58
|
+
transaction was successful.
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
```python
|
|
62
|
+
class MyNearSigner:
|
|
63
|
+
def account_id(self) -> str:
|
|
64
|
+
return "alice.near"
|
|
65
|
+
|
|
66
|
+
async def sign_and_send_transaction(
|
|
67
|
+
self, receiver_id, actions, network
|
|
68
|
+
) -> str:
|
|
69
|
+
# Execute the transaction
|
|
70
|
+
return "Abc123TxHash..."
|
|
71
|
+
|
|
72
|
+
signer = MyNearSigner()
|
|
73
|
+
scheme = ExactDirectNearClientScheme(signer)
|
|
74
|
+
|
|
75
|
+
payload = await scheme.create_payment_payload(
|
|
76
|
+
t402_version=2,
|
|
77
|
+
requirements=requirements,
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
signer: ClientNearSigner,
|
|
85
|
+
config: Optional[ExactDirectNearClientConfig] = None,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Initialize with a NEAR signer.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
signer: Any object implementing the ClientNearSigner protocol.
|
|
91
|
+
config: Optional configuration for the client scheme.
|
|
92
|
+
"""
|
|
93
|
+
self._signer = signer
|
|
94
|
+
self._config = config or ExactDirectNearClientConfig()
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def scheme(self) -> str:
|
|
98
|
+
"""The scheme identifier."""
|
|
99
|
+
return SCHEME_EXACT_DIRECT
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def caip_family(self) -> str:
|
|
103
|
+
"""The CAIP-2 family pattern for NEAR networks."""
|
|
104
|
+
return CAIP_FAMILY
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def account_id(self) -> str:
|
|
108
|
+
"""Get the signer's NEAR account ID."""
|
|
109
|
+
return self._signer.account_id()
|
|
110
|
+
|
|
111
|
+
async def create_payment_payload(
|
|
112
|
+
self,
|
|
113
|
+
t402_version: int,
|
|
114
|
+
requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
|
|
115
|
+
) -> Dict[str, Any]:
|
|
116
|
+
"""Create a payment payload by executing ft_transfer on-chain.
|
|
117
|
+
|
|
118
|
+
Executes a NEP-141 ft_transfer to the specified recipient and returns
|
|
119
|
+
the transaction hash as proof of payment.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
t402_version: The T402 protocol version.
|
|
123
|
+
requirements: Payment requirements with amount, asset, payTo, network.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Dict with t402Version and payload containing txHash, from, to, amount.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
ValueError: If requirements are invalid (bad network, missing fields,
|
|
130
|
+
invalid account IDs).
|
|
131
|
+
RuntimeError: If the transaction execution fails.
|
|
132
|
+
"""
|
|
133
|
+
# Extract requirements as dict
|
|
134
|
+
if hasattr(requirements, "model_dump"):
|
|
135
|
+
req = requirements.model_dump(by_alias=True)
|
|
136
|
+
else:
|
|
137
|
+
req = dict(requirements)
|
|
138
|
+
|
|
139
|
+
network = req.get("network", "")
|
|
140
|
+
asset = req.get("asset", "")
|
|
141
|
+
pay_to = req.get("payTo") or req.get("pay_to", "")
|
|
142
|
+
amount = req.get("amount", "")
|
|
143
|
+
|
|
144
|
+
# Validate network
|
|
145
|
+
if not is_valid_network(network):
|
|
146
|
+
raise ValueError(f"Unsupported network: {network}")
|
|
147
|
+
|
|
148
|
+
# Validate required fields
|
|
149
|
+
if not asset:
|
|
150
|
+
raise ValueError("Asset (token contract address) is required")
|
|
151
|
+
if not pay_to:
|
|
152
|
+
raise ValueError("payTo address is required")
|
|
153
|
+
if not amount:
|
|
154
|
+
raise ValueError("Amount is required")
|
|
155
|
+
|
|
156
|
+
# Validate account IDs
|
|
157
|
+
if not is_valid_account_id(pay_to):
|
|
158
|
+
raise ValueError(f"Invalid recipient account ID: {pay_to}")
|
|
159
|
+
sender_id = self._signer.account_id()
|
|
160
|
+
if not is_valid_account_id(sender_id):
|
|
161
|
+
raise ValueError(f"Invalid sender account ID: {sender_id}")
|
|
162
|
+
|
|
163
|
+
# Build ft_transfer arguments
|
|
164
|
+
ft_transfer_args: Dict[str, Any] = {
|
|
165
|
+
"receiver_id": pay_to,
|
|
166
|
+
"amount": amount,
|
|
167
|
+
}
|
|
168
|
+
if self._config.memo:
|
|
169
|
+
ft_transfer_args["memo"] = self._config.memo
|
|
170
|
+
|
|
171
|
+
# Build the function call action
|
|
172
|
+
actions: List[Dict[str, Any]] = [
|
|
173
|
+
{
|
|
174
|
+
"FunctionCall": {
|
|
175
|
+
"method_name": FUNCTION_FT_TRANSFER,
|
|
176
|
+
"args": json.dumps(ft_transfer_args),
|
|
177
|
+
"gas": self._config.gas_amount,
|
|
178
|
+
"deposit": STORAGE_DEPOSIT,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
# Execute the transfer via the signer
|
|
184
|
+
try:
|
|
185
|
+
tx_hash = await self._signer.sign_and_send_transaction(
|
|
186
|
+
receiver_id=asset,
|
|
187
|
+
actions=actions,
|
|
188
|
+
network=network,
|
|
189
|
+
)
|
|
190
|
+
except Exception as e:
|
|
191
|
+
raise RuntimeError(f"Failed to execute ft_transfer: {e}") from e
|
|
192
|
+
|
|
193
|
+
# Build the payload
|
|
194
|
+
payload = ExactDirectPayload(
|
|
195
|
+
tx_hash=tx_hash,
|
|
196
|
+
from_account=sender_id,
|
|
197
|
+
to_account=pay_to,
|
|
198
|
+
amount=amount,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
"t402Version": t402_version,
|
|
203
|
+
"payload": payload.to_map(),
|
|
204
|
+
}
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"""NEAR Exact-Direct Scheme - Facilitator Implementation.
|
|
2
|
+
|
|
3
|
+
This module provides the facilitator-side implementation of the exact-direct
|
|
4
|
+
payment scheme for NEAR networks.
|
|
5
|
+
|
|
6
|
+
The facilitator:
|
|
7
|
+
1. Verifies the on-chain transaction by querying the NEAR RPC.
|
|
8
|
+
2. Confirms the transaction was a successful ft_transfer with correct parameters.
|
|
9
|
+
3. Marks the transaction as settled (already executed in exact-direct).
|
|
10
|
+
|
|
11
|
+
Replay protection is built-in via an in-memory cache of used transaction hashes.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
from typing import Any, Dict, List, Optional, Union
|
|
20
|
+
|
|
21
|
+
from t402.types import (
|
|
22
|
+
PaymentRequirementsV2,
|
|
23
|
+
PaymentPayloadV2,
|
|
24
|
+
VerifyResponse,
|
|
25
|
+
SettleResponse,
|
|
26
|
+
Network,
|
|
27
|
+
)
|
|
28
|
+
from t402.schemes.near.constants import (
|
|
29
|
+
SCHEME_EXACT_DIRECT,
|
|
30
|
+
CAIP_FAMILY,
|
|
31
|
+
get_network_config,
|
|
32
|
+
)
|
|
33
|
+
from t402.schemes.near.types import (
|
|
34
|
+
FacilitatorNearSigner,
|
|
35
|
+
ExactDirectPayload,
|
|
36
|
+
parse_transaction_result,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ExactDirectNearFacilitatorConfig:
|
|
44
|
+
"""Configuration for the ExactDirectNearFacilitatorScheme.
|
|
45
|
+
|
|
46
|
+
Attributes:
|
|
47
|
+
max_transaction_age_seconds: Maximum age (in seconds) of a transaction to accept.
|
|
48
|
+
Default: 300 (5 minutes).
|
|
49
|
+
used_tx_cache_duration_seconds: How long (in seconds) to cache used transaction
|
|
50
|
+
hashes for replay protection. Default: 86400 (24 hours).
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
max_transaction_age_seconds: int = 300,
|
|
56
|
+
used_tx_cache_duration_seconds: int = 86400,
|
|
57
|
+
) -> None:
|
|
58
|
+
self.max_transaction_age_seconds = max_transaction_age_seconds
|
|
59
|
+
self.used_tx_cache_duration_seconds = used_tx_cache_duration_seconds
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ExactDirectNearFacilitatorScheme:
|
|
63
|
+
"""Facilitator scheme for NEAR exact-direct payments.
|
|
64
|
+
|
|
65
|
+
Verifies that an on-chain ft_transfer transaction was executed correctly
|
|
66
|
+
and marks it as settled. Since the client already executed the transfer,
|
|
67
|
+
settlement simply confirms the transaction is valid.
|
|
68
|
+
|
|
69
|
+
Features:
|
|
70
|
+
- Replay protection via used transaction hash cache.
|
|
71
|
+
- Validates transaction status, recipient, token contract, and amount.
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
```python
|
|
75
|
+
class MyNearRPC:
|
|
76
|
+
def get_addresses(self, network: str) -> List[str]:
|
|
77
|
+
return ["facilitator.near"]
|
|
78
|
+
|
|
79
|
+
async def query_transaction(
|
|
80
|
+
self, tx_hash, sender_id, network
|
|
81
|
+
) -> Dict:
|
|
82
|
+
# Query NEAR RPC
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
rpc = MyNearRPC()
|
|
86
|
+
facilitator = ExactDirectNearFacilitatorScheme(rpc)
|
|
87
|
+
|
|
88
|
+
result = await facilitator.verify(payload, requirements)
|
|
89
|
+
if result.is_valid:
|
|
90
|
+
settlement = await facilitator.settle(payload, requirements)
|
|
91
|
+
```
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
signer: FacilitatorNearSigner,
|
|
97
|
+
config: Optional[ExactDirectNearFacilitatorConfig] = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Initialize the facilitator scheme.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
signer: Any object implementing the FacilitatorNearSigner protocol.
|
|
103
|
+
config: Optional configuration. If not provided, defaults are used.
|
|
104
|
+
"""
|
|
105
|
+
self._signer = signer
|
|
106
|
+
self._config = config or ExactDirectNearFacilitatorConfig()
|
|
107
|
+
|
|
108
|
+
# Used transaction cache for replay protection
|
|
109
|
+
self._used_txs: Dict[str, float] = {}
|
|
110
|
+
self._used_txs_lock = threading.Lock()
|
|
111
|
+
|
|
112
|
+
# Start cleanup thread
|
|
113
|
+
self._cleanup_thread = threading.Thread(
|
|
114
|
+
target=self._cleanup_used_txs,
|
|
115
|
+
daemon=True,
|
|
116
|
+
name="near-facilitator-cleanup",
|
|
117
|
+
)
|
|
118
|
+
self._cleanup_thread.start()
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def scheme(self) -> str:
|
|
122
|
+
"""The scheme identifier."""
|
|
123
|
+
return SCHEME_EXACT_DIRECT
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def caip_family(self) -> str:
|
|
127
|
+
"""CAIP-2 family pattern for network matching."""
|
|
128
|
+
return CAIP_FAMILY
|
|
129
|
+
|
|
130
|
+
def get_extra(self, network: Network) -> Optional[Dict[str, Any]]:
|
|
131
|
+
"""Get mechanism-specific extra data for supported kinds.
|
|
132
|
+
|
|
133
|
+
Returns the default token symbol and decimals for the network.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
network: The network identifier.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Dict with assetSymbol and assetDecimals, or None if network unknown.
|
|
140
|
+
"""
|
|
141
|
+
config = get_network_config(network)
|
|
142
|
+
if not config:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
"assetSymbol": config.default_token.symbol,
|
|
147
|
+
"assetDecimals": config.default_token.decimals,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
def get_signers(self, network: Network) -> List[str]:
|
|
151
|
+
"""Get signer addresses for this facilitator.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
network: The network identifier.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
List of NEAR account IDs.
|
|
158
|
+
"""
|
|
159
|
+
return self._signer.get_addresses(network)
|
|
160
|
+
|
|
161
|
+
async def verify(
|
|
162
|
+
self,
|
|
163
|
+
payload: Union[PaymentPayloadV2, Dict[str, Any]],
|
|
164
|
+
requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
|
|
165
|
+
) -> VerifyResponse:
|
|
166
|
+
"""Verify a payment payload by checking the on-chain transaction.
|
|
167
|
+
|
|
168
|
+
Validates:
|
|
169
|
+
1. Payload has correct structure with txHash and from fields.
|
|
170
|
+
2. Transaction has not been used before (replay protection).
|
|
171
|
+
3. Transaction was successful on-chain.
|
|
172
|
+
4. Transaction was sent to the correct token contract.
|
|
173
|
+
5. The ft_transfer action has the correct recipient.
|
|
174
|
+
6. The transfer amount is >= the required amount.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
payload: The payment payload containing txHash and from.
|
|
178
|
+
requirements: The payment requirements to verify against.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
VerifyResponse indicating validity and payer address.
|
|
182
|
+
"""
|
|
183
|
+
try:
|
|
184
|
+
payload_data = self._extract_payload(payload)
|
|
185
|
+
req_data = self._extract_requirements(requirements)
|
|
186
|
+
|
|
187
|
+
network = req_data.get("network", "")
|
|
188
|
+
|
|
189
|
+
# Parse the NEAR payload
|
|
190
|
+
near_payload = ExactDirectPayload.from_map(payload_data)
|
|
191
|
+
|
|
192
|
+
# Validate required fields
|
|
193
|
+
if not near_payload.tx_hash:
|
|
194
|
+
return VerifyResponse(
|
|
195
|
+
is_valid=False,
|
|
196
|
+
invalid_reason="Missing transaction hash in payload",
|
|
197
|
+
payer=None,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
if not near_payload.from_account:
|
|
201
|
+
return VerifyResponse(
|
|
202
|
+
is_valid=False,
|
|
203
|
+
invalid_reason="Missing sender (from) in payload",
|
|
204
|
+
payer=None,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Check for replay attack
|
|
208
|
+
if self._is_tx_used(near_payload.tx_hash):
|
|
209
|
+
return VerifyResponse(
|
|
210
|
+
is_valid=False,
|
|
211
|
+
invalid_reason="Transaction has already been used",
|
|
212
|
+
payer=near_payload.from_account,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Query the transaction from the NEAR RPC
|
|
216
|
+
try:
|
|
217
|
+
tx_result = await self._signer.query_transaction(
|
|
218
|
+
tx_hash=near_payload.tx_hash,
|
|
219
|
+
sender_id=near_payload.from_account,
|
|
220
|
+
network=network,
|
|
221
|
+
)
|
|
222
|
+
except Exception as e:
|
|
223
|
+
return VerifyResponse(
|
|
224
|
+
is_valid=False,
|
|
225
|
+
invalid_reason=f"Transaction not found: {e}",
|
|
226
|
+
payer=near_payload.from_account,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Parse the transaction result
|
|
230
|
+
try:
|
|
231
|
+
parsed = parse_transaction_result(tx_result)
|
|
232
|
+
except ValueError as e:
|
|
233
|
+
return VerifyResponse(
|
|
234
|
+
is_valid=False,
|
|
235
|
+
invalid_reason=f"Failed to parse transaction: {e}",
|
|
236
|
+
payer=near_payload.from_account,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Verify transaction succeeded
|
|
240
|
+
status = parsed["status"]
|
|
241
|
+
if not status.is_success():
|
|
242
|
+
return VerifyResponse(
|
|
243
|
+
is_valid=False,
|
|
244
|
+
invalid_reason="Transaction failed on-chain",
|
|
245
|
+
payer=near_payload.from_account,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Verify the transaction was to the token contract
|
|
249
|
+
tx_receiver = parsed["transaction"]["receiver_id"]
|
|
250
|
+
required_asset = req_data.get("asset", "")
|
|
251
|
+
if tx_receiver != required_asset:
|
|
252
|
+
return VerifyResponse(
|
|
253
|
+
is_valid=False,
|
|
254
|
+
invalid_reason=(
|
|
255
|
+
f"Wrong token contract: expected {required_asset}, "
|
|
256
|
+
f"got {tx_receiver}"
|
|
257
|
+
),
|
|
258
|
+
payer=near_payload.from_account,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Find and verify ft_transfer action
|
|
262
|
+
ft_transfer_args = parsed["ft_transfer_args"]
|
|
263
|
+
if ft_transfer_args is None:
|
|
264
|
+
return VerifyResponse(
|
|
265
|
+
is_valid=False,
|
|
266
|
+
invalid_reason="No ft_transfer action found in transaction",
|
|
267
|
+
payer=near_payload.from_account,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Verify recipient
|
|
271
|
+
required_pay_to = req_data.get("payTo") or req_data.get("pay_to", "")
|
|
272
|
+
if ft_transfer_args.receiver_id != required_pay_to:
|
|
273
|
+
return VerifyResponse(
|
|
274
|
+
is_valid=False,
|
|
275
|
+
invalid_reason=(
|
|
276
|
+
f"Wrong recipient: expected {required_pay_to}, "
|
|
277
|
+
f"got {ft_transfer_args.receiver_id}"
|
|
278
|
+
),
|
|
279
|
+
payer=near_payload.from_account,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Verify amount
|
|
283
|
+
try:
|
|
284
|
+
tx_amount = int(ft_transfer_args.amount)
|
|
285
|
+
except (ValueError, TypeError):
|
|
286
|
+
return VerifyResponse(
|
|
287
|
+
is_valid=False,
|
|
288
|
+
invalid_reason=f"Invalid transaction amount: {ft_transfer_args.amount}",
|
|
289
|
+
payer=near_payload.from_account,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
required_amount_str = req_data.get("amount", "0")
|
|
293
|
+
try:
|
|
294
|
+
required_amount = int(required_amount_str)
|
|
295
|
+
except (ValueError, TypeError):
|
|
296
|
+
return VerifyResponse(
|
|
297
|
+
is_valid=False,
|
|
298
|
+
invalid_reason=f"Invalid required amount: {required_amount_str}",
|
|
299
|
+
payer=near_payload.from_account,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
if tx_amount < required_amount:
|
|
303
|
+
return VerifyResponse(
|
|
304
|
+
is_valid=False,
|
|
305
|
+
invalid_reason=(
|
|
306
|
+
f"Insufficient amount: expected {required_amount}, "
|
|
307
|
+
f"got {tx_amount}"
|
|
308
|
+
),
|
|
309
|
+
payer=near_payload.from_account,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Mark transaction as used
|
|
313
|
+
self._mark_tx_used(near_payload.tx_hash)
|
|
314
|
+
|
|
315
|
+
return VerifyResponse(
|
|
316
|
+
is_valid=True,
|
|
317
|
+
invalid_reason=None,
|
|
318
|
+
payer=near_payload.from_account,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
except Exception as e:
|
|
322
|
+
logger.error(f"NEAR verification failed: {e}")
|
|
323
|
+
return VerifyResponse(
|
|
324
|
+
is_valid=False,
|
|
325
|
+
invalid_reason=f"Verification error: {str(e)}",
|
|
326
|
+
payer=None,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
async def settle(
|
|
330
|
+
self,
|
|
331
|
+
payload: Union[PaymentPayloadV2, Dict[str, Any]],
|
|
332
|
+
requirements: Union[PaymentRequirementsV2, Dict[str, Any]],
|
|
333
|
+
) -> SettleResponse:
|
|
334
|
+
"""Settle a verified payment.
|
|
335
|
+
|
|
336
|
+
For exact-direct, the transfer was already executed by the client,
|
|
337
|
+
so settlement simply verifies the transaction and returns the tx hash.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
payload: The verified payment payload.
|
|
341
|
+
requirements: The payment requirements.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
SettleResponse with the transaction hash and status.
|
|
345
|
+
"""
|
|
346
|
+
try:
|
|
347
|
+
payload_data = self._extract_payload(payload)
|
|
348
|
+
req_data = self._extract_requirements(requirements)
|
|
349
|
+
|
|
350
|
+
network = req_data.get("network", "")
|
|
351
|
+
near_payload = ExactDirectPayload.from_map(payload_data)
|
|
352
|
+
|
|
353
|
+
# Verify the transaction first
|
|
354
|
+
verify_result = await self.verify(payload, requirements)
|
|
355
|
+
|
|
356
|
+
if not verify_result.is_valid:
|
|
357
|
+
return SettleResponse(
|
|
358
|
+
success=False,
|
|
359
|
+
error_reason=verify_result.invalid_reason,
|
|
360
|
+
transaction=None,
|
|
361
|
+
network=network,
|
|
362
|
+
payer=verify_result.payer,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# For exact-direct, settlement is already complete
|
|
366
|
+
return SettleResponse(
|
|
367
|
+
success=True,
|
|
368
|
+
error_reason=None,
|
|
369
|
+
transaction=near_payload.tx_hash,
|
|
370
|
+
network=network,
|
|
371
|
+
payer=verify_result.payer,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
except Exception as e:
|
|
375
|
+
logger.error(f"NEAR settlement failed: {e}")
|
|
376
|
+
return SettleResponse(
|
|
377
|
+
success=False,
|
|
378
|
+
error_reason=f"Settlement error: {str(e)}",
|
|
379
|
+
transaction=None,
|
|
380
|
+
network=None,
|
|
381
|
+
payer=None,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
def _is_tx_used(self, tx_hash: str) -> bool:
|
|
385
|
+
"""Check if a transaction has been used (replay protection).
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
tx_hash: The transaction hash to check.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
True if the transaction has already been used.
|
|
392
|
+
"""
|
|
393
|
+
with self._used_txs_lock:
|
|
394
|
+
return tx_hash in self._used_txs
|
|
395
|
+
|
|
396
|
+
def _mark_tx_used(self, tx_hash: str) -> None:
|
|
397
|
+
"""Mark a transaction as used.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
tx_hash: The transaction hash to mark.
|
|
401
|
+
"""
|
|
402
|
+
with self._used_txs_lock:
|
|
403
|
+
self._used_txs[tx_hash] = time.time()
|
|
404
|
+
|
|
405
|
+
def _cleanup_used_txs(self) -> None:
|
|
406
|
+
"""Periodically clean up old used transaction entries.
|
|
407
|
+
|
|
408
|
+
Runs in a background daemon thread.
|
|
409
|
+
"""
|
|
410
|
+
while True:
|
|
411
|
+
time.sleep(3600) # Clean up every hour
|
|
412
|
+
cutoff = time.time() - self._config.used_tx_cache_duration_seconds
|
|
413
|
+
with self._used_txs_lock:
|
|
414
|
+
expired = [
|
|
415
|
+
tx_hash
|
|
416
|
+
for tx_hash, used_at in self._used_txs.items()
|
|
417
|
+
if used_at < cutoff
|
|
418
|
+
]
|
|
419
|
+
for tx_hash in expired:
|
|
420
|
+
del self._used_txs[tx_hash]
|
|
421
|
+
|
|
422
|
+
def _extract_payload(
|
|
423
|
+
self, payload: Union[PaymentPayloadV2, Dict[str, Any]]
|
|
424
|
+
) -> Dict[str, Any]:
|
|
425
|
+
"""Extract payload data as a dict.
|
|
426
|
+
|
|
427
|
+
Handles both PaymentPayloadV2 models and plain dicts.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
payload: Payment payload (model or dict).
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
Dict containing the inner payload data.
|
|
434
|
+
"""
|
|
435
|
+
if hasattr(payload, "model_dump"):
|
|
436
|
+
data = payload.model_dump(by_alias=True)
|
|
437
|
+
return data.get("payload", data)
|
|
438
|
+
elif isinstance(payload, dict):
|
|
439
|
+
return payload.get("payload", payload)
|
|
440
|
+
return dict(payload)
|
|
441
|
+
|
|
442
|
+
def _extract_requirements(
|
|
443
|
+
self, requirements: Union[PaymentRequirementsV2, Dict[str, Any]]
|
|
444
|
+
) -> Dict[str, Any]:
|
|
445
|
+
"""Extract requirements data as a dict.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
requirements: Payment requirements (model or dict).
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
Dict containing requirement fields.
|
|
452
|
+
"""
|
|
453
|
+
if hasattr(requirements, "model_dump"):
|
|
454
|
+
return requirements.model_dump(by_alias=True)
|
|
455
|
+
return dict(requirements)
|