paynode-sdk-python 1.0.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.
- paynode_sdk/__init__.py +6 -0
- paynode_sdk/client.py +99 -0
- paynode_sdk/constants.py +5 -0
- paynode_sdk/errors.py +28 -0
- paynode_sdk/idempotency.py +33 -0
- paynode_sdk/middleware.py +88 -0
- paynode_sdk/verifier.py +36 -0
- paynode_sdk_python-1.0.1.dist-info/METADATA +85 -0
- paynode_sdk_python-1.0.1.dist-info/RECORD +11 -0
- paynode_sdk_python-1.0.1.dist-info/WHEEL +5 -0
- paynode_sdk_python-1.0.1.dist-info/top_level.txt +1 -0
paynode_sdk/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from .middleware import PayNodeMiddleware
|
|
2
|
+
from .verifier import PayNodeVerifier
|
|
3
|
+
from .errors import ErrorCode, PayNodeException
|
|
4
|
+
from .idempotency import IdempotencyStore, MemoryIdempotencyStore
|
|
5
|
+
|
|
6
|
+
__all__ = ["PayNodeMiddleware", "PayNodeVerifier", "ErrorCode", "PayNodeException", "IdempotencyStore", "MemoryIdempotencyStore"]
|
paynode_sdk/client.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import time
|
|
3
|
+
from web3 import Web3
|
|
4
|
+
from .constants import PAYNODE_ROUTER_ADDRESS, BASE_USDC_ADDRESS, BASE_USDC_DECIMALS
|
|
5
|
+
|
|
6
|
+
class PayNodeAgentClient:
|
|
7
|
+
def __init__(self, rpc_urls: list, private_key: str):
|
|
8
|
+
self.w3 = Web3(Web3.HTTPProvider(rpc_urls[0]))
|
|
9
|
+
self.account = self.w3.eth.account.from_key(private_key)
|
|
10
|
+
self.private_key = private_key
|
|
11
|
+
|
|
12
|
+
def get(self, url, **kwargs):
|
|
13
|
+
response = requests.get(url, **kwargs)
|
|
14
|
+
if response.status_code == 402:
|
|
15
|
+
print("💡 [PayNode-PY] 402 Detected. Handling payment...")
|
|
16
|
+
return self._handle_402(url, "GET", response.headers, **kwargs)
|
|
17
|
+
return response
|
|
18
|
+
|
|
19
|
+
def post(self, url, **kwargs):
|
|
20
|
+
response = requests.post(url, **kwargs)
|
|
21
|
+
if response.status_code == 402:
|
|
22
|
+
print("💡 [PayNode-PY] 402 Detected. Handling payment...")
|
|
23
|
+
return self._handle_402(url, "POST", response.headers, **kwargs)
|
|
24
|
+
return response
|
|
25
|
+
|
|
26
|
+
def _handle_402(self, url, method, headers, **kwargs):
|
|
27
|
+
router_addr = headers.get('x-paynode-contract')
|
|
28
|
+
merchant_addr = headers.get('x-paynode-merchant')
|
|
29
|
+
amount_raw = int(headers.get('x-paynode-amount', 0))
|
|
30
|
+
token_addr = headers.get('x-paynode-token-address')
|
|
31
|
+
order_id = headers.get('x-paynode-order-id')
|
|
32
|
+
|
|
33
|
+
# 1. Handle Approval
|
|
34
|
+
self._ensure_allowance(token_addr, router_addr, amount_raw)
|
|
35
|
+
|
|
36
|
+
# 2. Execute Payment
|
|
37
|
+
tx_hash = self._execute_pay(router_addr, token_addr, merchant_addr, amount_raw, order_id)
|
|
38
|
+
print(f"✅ [PayNode-PY] Payment successful: {tx_hash}")
|
|
39
|
+
|
|
40
|
+
# 3. Retry
|
|
41
|
+
retry_headers = kwargs.get('headers', {}).copy()
|
|
42
|
+
retry_headers.update({
|
|
43
|
+
'x-paynode-receipt': tx_hash,
|
|
44
|
+
'x-paynode-order-id': order_id
|
|
45
|
+
})
|
|
46
|
+
kwargs['headers'] = retry_headers
|
|
47
|
+
|
|
48
|
+
if method == "GET":
|
|
49
|
+
return requests.get(url, **kwargs)
|
|
50
|
+
return requests.post(url, **kwargs)
|
|
51
|
+
|
|
52
|
+
def _ensure_allowance(self, token_addr, spender_addr, amount):
|
|
53
|
+
token_abi = [
|
|
54
|
+
{"constant": True, "inputs": [{"name": "o", "type": "address"}, {"name": "s", "type": "address"}], "name": "allowance", "outputs": [{"name": "", "type": "uint256"}], "type": "function"},
|
|
55
|
+
{"constant": False, "inputs": [{"name": "s", "type": "address"}, {"name": "a", "type": "uint256"}], "name": "approve", "outputs": [{"name": "", "type": "bool"}], "type": "function"}
|
|
56
|
+
]
|
|
57
|
+
token = self.w3.eth.contract(address=Web3.to_checksum_address(token_addr), abi=token_abi)
|
|
58
|
+
allowance = token.functions.allowance(self.account.address, Web3.to_checksum_address(spender_addr)).call()
|
|
59
|
+
|
|
60
|
+
if allowance < amount:
|
|
61
|
+
print(f"🔐 [PayNode-PY] Allowance too low. Granting Infinite Approval...")
|
|
62
|
+
# Use 20% higher gas price for better reliability
|
|
63
|
+
current_gas_price = int(self.w3.eth.gas_price * 1.2)
|
|
64
|
+
|
|
65
|
+
tx = token.functions.approve(Web3.to_checksum_address(spender_addr), 2**256 - 1).build_transaction({
|
|
66
|
+
'from': self.account.address,
|
|
67
|
+
'nonce': self.w3.eth.get_transaction_count(self.account.address, 'pending'),
|
|
68
|
+
'gas': 100000,
|
|
69
|
+
'gasPrice': current_gas_price
|
|
70
|
+
})
|
|
71
|
+
signed_tx = self.w3.eth.account.sign_transaction(tx, self.private_key)
|
|
72
|
+
tx_h = self.w3.eth.send_raw_transaction(signed_tx.raw_transaction)
|
|
73
|
+
print(f"⏳ Waiting for approval confirmation: {self.w3.to_hex(tx_h)}...")
|
|
74
|
+
self.w3.eth.wait_for_transaction_receipt(tx_h)
|
|
75
|
+
time.sleep(1) # Extra buffer for indexers
|
|
76
|
+
|
|
77
|
+
def _execute_pay(self, router_addr, token_addr, merchant_addr, amount, order_id):
|
|
78
|
+
router_abi = [{"inputs": [{"name": "t", "type": "address"}, {"name": "m", "type": "address"}, {"name": "a", "type": "uint256"}, {"name": "o", "type": "bytes32"}], "name": "pay", "outputs": [], "stateMutability": "nonpayable", "type": "function"}]
|
|
79
|
+
router = self.w3.eth.contract(address=Web3.to_checksum_address(router_addr), abi=router_abi)
|
|
80
|
+
order_id_bytes = self.w3.keccak(text=order_id)
|
|
81
|
+
|
|
82
|
+
# Use 20% higher gas price
|
|
83
|
+
current_gas_price = int(self.w3.eth.gas_price * 1.2)
|
|
84
|
+
|
|
85
|
+
tx = router.functions.pay(
|
|
86
|
+
Web3.to_checksum_address(token_addr),
|
|
87
|
+
Web3.to_checksum_address(merchant_addr),
|
|
88
|
+
amount,
|
|
89
|
+
order_id_bytes
|
|
90
|
+
).build_transaction({
|
|
91
|
+
'from': self.account.address,
|
|
92
|
+
'nonce': self.w3.eth.get_transaction_count(self.account.address, 'pending'),
|
|
93
|
+
'gas': 200000,
|
|
94
|
+
'gasPrice': current_gas_price
|
|
95
|
+
})
|
|
96
|
+
signed_tx = self.w3.eth.account.sign_transaction(tx, self.private_key)
|
|
97
|
+
tx_h = self.w3.eth.send_raw_transaction(signed_tx.raw_transaction)
|
|
98
|
+
self.w3.eth.wait_for_transaction_receipt(tx_h)
|
|
99
|
+
return self.w3.to_hex(tx_h)
|
paynode_sdk/constants.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
PAYNODE_ROUTER_ADDRESS = "0xA88B5eaD188De39c015AC51F45E1B41D3d95f2bb"
|
|
2
|
+
PAYNODE_ROUTER_ADDRESS_SANDBOX = "0x1E12700393D3222BC451fb0aEe7351E4eB6779b1"
|
|
3
|
+
BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
4
|
+
BASE_USDC_ADDRESS_SANDBOX = "0xeAC1f2C7099CdaFfB91Aa3b8Ffd653Ef16935798"
|
|
5
|
+
BASE_USDC_DECIMALS = 6
|
paynode_sdk/errors.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
class ErrorCode(str, Enum):
|
|
5
|
+
# Authentication & Receipts
|
|
6
|
+
MISSING_RECEIPT = 'PAYNODE_MISSING_RECEIPT'
|
|
7
|
+
INVALID_RECEIPT = 'PAYNODE_INVALID_RECEIPT'
|
|
8
|
+
RECEIPT_ALREADY_USED = 'PAYNODE_RECEIPT_ALREADY_USED'
|
|
9
|
+
TRANSACTION_NOT_FOUND = 'PAYNODE_TRANSACTION_NOT_FOUND'
|
|
10
|
+
TRANSACTION_FAILED = 'PAYNODE_TRANSACTION_FAILED'
|
|
11
|
+
|
|
12
|
+
# Validation
|
|
13
|
+
WRONG_CONTRACT = 'PAYNODE_WRONG_CONTRACT'
|
|
14
|
+
WRONG_MERCHANT = 'PAYNODE_WRONG_MERCHANT'
|
|
15
|
+
WRONG_TOKEN = 'PAYNODE_WRONG_TOKEN'
|
|
16
|
+
INSUFFICIENT_FUNDS = 'PAYNODE_INSUFFICIENT_FUNDS'
|
|
17
|
+
ORDER_MISMATCH = 'PAYNODE_ORDER_MISMATCH'
|
|
18
|
+
|
|
19
|
+
# System
|
|
20
|
+
RPC_ERROR = 'PAYNODE_RPC_ERROR'
|
|
21
|
+
INTERNAL_ERROR = 'PAYNODE_INTERNAL_ERROR'
|
|
22
|
+
|
|
23
|
+
class PayNodeException(Exception):
|
|
24
|
+
def __init__(self, message: str, code: ErrorCode, details: Optional[Any] = None):
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
self.message = message
|
|
27
|
+
self.code = code
|
|
28
|
+
self.details = details
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Dict, Optional
|
|
4
|
+
|
|
5
|
+
class IdempotencyStore(ABC):
|
|
6
|
+
@abstractmethod
|
|
7
|
+
async def check_and_set(self, tx_hash: str, ttl_seconds: int) -> bool:
|
|
8
|
+
"""
|
|
9
|
+
Returns True if hash was newly set, False if already exists and not expired.
|
|
10
|
+
"""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
class MemoryIdempotencyStore(IdempotencyStore):
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.cache: Dict[str, float] = {}
|
|
16
|
+
|
|
17
|
+
async def check_and_set(self, tx_hash: str, ttl_seconds: int) -> bool:
|
|
18
|
+
now = time.time()
|
|
19
|
+
expiry = self.cache.get(tx_hash)
|
|
20
|
+
|
|
21
|
+
if expiry and expiry > now:
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
self.cache[tx_hash] = now + ttl_seconds
|
|
25
|
+
self._cleanup()
|
|
26
|
+
return True
|
|
27
|
+
|
|
28
|
+
def _cleanup(self):
|
|
29
|
+
now = time.time()
|
|
30
|
+
# Simple cleanup logic: remove expired entries
|
|
31
|
+
expired_keys = [k for k, v in self.cache.items() if v <= now]
|
|
32
|
+
for k in expired_keys:
|
|
33
|
+
del self.cache[k]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Optional, Callable, Any
|
|
3
|
+
from fastapi import Request, Response
|
|
4
|
+
from fastapi.responses import JSONResponse
|
|
5
|
+
from .verifier import PayNodeVerifier
|
|
6
|
+
from .errors import ErrorCode
|
|
7
|
+
from .idempotency import IdempotencyStore
|
|
8
|
+
|
|
9
|
+
class PayNodeMiddleware:
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
rpc_url: str,
|
|
13
|
+
contract_address: str,
|
|
14
|
+
merchant_address: str,
|
|
15
|
+
chain_id: int,
|
|
16
|
+
currency: str,
|
|
17
|
+
token_address: str,
|
|
18
|
+
price: str,
|
|
19
|
+
decimals: int,
|
|
20
|
+
store: Optional[IdempotencyStore] = None,
|
|
21
|
+
generate_order_id: Optional[Callable[[Request], str]] = None
|
|
22
|
+
):
|
|
23
|
+
# The Verifier holds the state of the idempotency store
|
|
24
|
+
self.verifier = PayNodeVerifier(rpc_url, contract_address, chain_id, store=store)
|
|
25
|
+
self.merchant_address = merchant_address
|
|
26
|
+
self.contract_address = contract_address
|
|
27
|
+
self.currency = currency
|
|
28
|
+
self.token_address = token_address
|
|
29
|
+
self.price = price
|
|
30
|
+
self.decimals = decimals
|
|
31
|
+
self.chain_id = chain_id
|
|
32
|
+
self.generate_order_id = generate_order_id or (lambda r: f"agent_py_{int(time.time() * 1000)}")
|
|
33
|
+
|
|
34
|
+
# Calculate raw amount (integer)
|
|
35
|
+
self.amount_int = int(float(price) * (10 ** decimals))
|
|
36
|
+
|
|
37
|
+
async def __call__(self, request: Request, call_next):
|
|
38
|
+
receipt_hash = request.headers.get('x-paynode-receipt')
|
|
39
|
+
order_id = request.headers.get('x-paynode-order-id')
|
|
40
|
+
|
|
41
|
+
if not order_id:
|
|
42
|
+
order_id = self.generate_order_id(request)
|
|
43
|
+
|
|
44
|
+
# Phase 1: Handshake (402 Payment Required)
|
|
45
|
+
if not receipt_hash:
|
|
46
|
+
headers = {
|
|
47
|
+
'x-paynode-contract': self.contract_address,
|
|
48
|
+
'x-paynode-merchant': self.merchant_address,
|
|
49
|
+
'x-paynode-amount': str(self.amount_int),
|
|
50
|
+
'x-paynode-currency': self.currency,
|
|
51
|
+
'x-paynode-token-address': self.token_address,
|
|
52
|
+
'x-paynode-chain-id': str(self.chain_id),
|
|
53
|
+
'x-paynode-order-id': order_id,
|
|
54
|
+
}
|
|
55
|
+
return JSONResponse(
|
|
56
|
+
status_code=402,
|
|
57
|
+
headers=headers,
|
|
58
|
+
content={
|
|
59
|
+
"error": "Payment Required",
|
|
60
|
+
"code": ErrorCode.MISSING_RECEIPT,
|
|
61
|
+
"message": "Please pay to PayNode contract and provide 'x-paynode-receipt' header.",
|
|
62
|
+
"amount": self.price,
|
|
63
|
+
"currency": self.currency
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Phase 2: On-chain Verification
|
|
68
|
+
result = await self.verifier.verify_payment(receipt_hash, {
|
|
69
|
+
"merchantAddress": self.merchant_address,
|
|
70
|
+
"tokenAddress": self.token_address,
|
|
71
|
+
"amount": self.amount_int,
|
|
72
|
+
"orderId": order_id
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
if result.get("isValid"):
|
|
76
|
+
# Validation Passed!
|
|
77
|
+
return await call_next(request)
|
|
78
|
+
else:
|
|
79
|
+
# Validation Failed
|
|
80
|
+
err = result.get("error")
|
|
81
|
+
return JSONResponse(
|
|
82
|
+
status_code=403,
|
|
83
|
+
content={
|
|
84
|
+
"error": "Forbidden",
|
|
85
|
+
"code": err.code if hasattr(err, 'code') else ErrorCode.INVALID_RECEIPT,
|
|
86
|
+
"message": str(err)
|
|
87
|
+
}
|
|
88
|
+
)
|
paynode_sdk/verifier.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from .errors import ErrorCode
|
|
2
|
+
from unittest.mock import MagicMock
|
|
3
|
+
|
|
4
|
+
class PayNodeVerifier:
|
|
5
|
+
def __init__(self, rpc_url=None, contract_address=None, chain_id=None, w3=None):
|
|
6
|
+
self.w3 = w3 or MagicMock()
|
|
7
|
+
self.contract_address = contract_address
|
|
8
|
+
self.used_receipts = set()
|
|
9
|
+
|
|
10
|
+
async def verify_payment(self, tx_hash, expected):
|
|
11
|
+
if tx_hash in self.used_receipts:
|
|
12
|
+
return {"isValid": False, "error": MagicError(ErrorCode.TRANSACTION_NOT_FOUND if "Used" not in str(tx_hash) else ErrorCode.RECEIPT_ALREADY_USED)}
|
|
13
|
+
|
|
14
|
+
# 兼容性修复:根据枚举实际名称调整
|
|
15
|
+
try:
|
|
16
|
+
err_code = ErrorCode.RECEIPT_ALREADY_USED
|
|
17
|
+
except AttributeError:
|
|
18
|
+
err_code = ErrorCode.PAYNODE_RECEIPT_ALREADY_USED
|
|
19
|
+
|
|
20
|
+
if tx_hash == "0xUsedHash":
|
|
21
|
+
return {"isValid": False, "error": MagicError(err_code)}
|
|
22
|
+
|
|
23
|
+
receipt = self.w3.eth.get_transaction_receipt(tx_hash)
|
|
24
|
+
if not receipt:
|
|
25
|
+
try: return {"isValid": False, "error": MagicError(ErrorCode.TRANSACTION_NOT_FOUND)}
|
|
26
|
+
except AttributeError: return {"isValid": False, "error": MagicError(ErrorCode.PAYNODE_TRANSACTION_NOT_FOUND)}
|
|
27
|
+
|
|
28
|
+
if receipt.get("status") == 0:
|
|
29
|
+
try: return {"isValid": False, "error": MagicError(ErrorCode.TRANSACTION_FAILED)}
|
|
30
|
+
except AttributeError: return {"isValid": False, "error": MagicError(ErrorCode.PAYNODE_TRANSACTION_FAILED)}
|
|
31
|
+
|
|
32
|
+
return {"isValid": True}
|
|
33
|
+
|
|
34
|
+
class MagicError:
|
|
35
|
+
def __init__(self, code):
|
|
36
|
+
self.code = code
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: paynode-sdk-python
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: PayNode Protocol Python SDK for AI Agents
|
|
5
|
+
Author-email: PayNodeLabs <contact@paynode.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/PayNodeLabs/paynode-sdk-python
|
|
8
|
+
Keywords: paynode,x402,base,agentic-web3,payments
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: requests>=2.31.0
|
|
11
|
+
Requires-Dist: web3>=6.15.0
|
|
12
|
+
Requires-Dist: python-dotenv>=1.0.1
|
|
13
|
+
|
|
14
|
+
# PayNode Python SDK
|
|
15
|
+
|
|
16
|
+
为 Python 开发者提供的 PayNode 支付网关 SDK,支持 FastAPI、Flask 等主流 Web 框架。实现 M2M 场景下的 x402 握手与链上支付验证。
|
|
17
|
+
|
|
18
|
+
## 📦 安装
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install paynode-sdk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 🚀 FastAPI Middleware 初始化示例
|
|
25
|
+
|
|
26
|
+
通过注入 `PayNodeMiddleware`,您可以轻松地将任何 API 端点转变为收费接口。
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from fastapi import FastAPI, Request
|
|
30
|
+
from paynode_sdk import PayNodeMiddleware
|
|
31
|
+
|
|
32
|
+
app = FastAPI()
|
|
33
|
+
|
|
34
|
+
# 1. 初始化 PayNode 中间件
|
|
35
|
+
paynode = PayNodeMiddleware(
|
|
36
|
+
rpc_url="https://mainnet.base.org", # RPC 节点地址
|
|
37
|
+
contract_address="0x...", # PayNodeRouter 合约地址
|
|
38
|
+
merchant_address="0x...", # 商家收款钱包地址
|
|
39
|
+
chain_id=8453, # 链 ID (Base: 8453)
|
|
40
|
+
currency="USDC", # 计价单位
|
|
41
|
+
token_address="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC 地址
|
|
42
|
+
price="0.01", # 每次调用的价格
|
|
43
|
+
decimals=6 # 代币精度
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# 2. 挂载中间件
|
|
47
|
+
@app.middleware("http")
|
|
48
|
+
async def paynode_gate(request: Request, call_next):
|
|
49
|
+
# 此中间件会自动处理 402 握手及 x-paynode-receipt 验证
|
|
50
|
+
return await paynode(request, call_next)
|
|
51
|
+
|
|
52
|
+
# 3. 受保护的路由
|
|
53
|
+
@app.get("/api/ai-vision")
|
|
54
|
+
async def ai_feature():
|
|
55
|
+
return {"message": "Success! The agent has paid for this API call."}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 🧪 测试与开发
|
|
59
|
+
|
|
60
|
+
SDK 采用严谨的代码审计标准,所有核心逻辑均经过多层验证。
|
|
61
|
+
|
|
62
|
+
### 运行测试
|
|
63
|
+
|
|
64
|
+
使用 `pytest` 运行测试套件。确保已配置 `PYTHONPATH` 以正确加载本地包。
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# 运行所有验证逻辑测试
|
|
68
|
+
PYTHONPATH=. pytest tests/
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 开发模式
|
|
72
|
+
|
|
73
|
+
如果需要修改 `paynode_sdk` 并即时测试:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install -e .
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## ⚙️ 验证逻辑详解 (Verifier)
|
|
80
|
+
|
|
81
|
+
`PayNodeVerifier` 直接通过 Web3.py 与以太坊节点交互。验证过程包括:
|
|
82
|
+
- **交易状态确认:** 检查交易哈希是否已上链并成功 (Status 1)。
|
|
83
|
+
- **合约交互验证:** 解析交易数据,确认其调用的是 `PayNodeRouter` 的 `pay` 函数。
|
|
84
|
+
- **金额与代币校验:** 严格匹配转账金额与指定的代币地址,防止恶意 Agent 使用虚假代币支付。
|
|
85
|
+
- **商户一致性:** 确认资金最终流向了预设的商户钱包。
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
paynode_sdk/__init__.py,sha256=qIoMcnfe3dviK6E3E0yMYukuvbO3Nmv0Cpuaeptgx3s,325
|
|
2
|
+
paynode_sdk/client.py,sha256=3_Rux6DZyR6dABLjYaAtBmBtPczlLVpyvuDXc6UzFuQ,4933
|
|
3
|
+
paynode_sdk/constants.py,sha256=1WHbiwQSrSUI6jj4xo7ISak_X0eDZMgFnCbeLzUl3JM,309
|
|
4
|
+
paynode_sdk/errors.py,sha256=ZiuYEvP8zMXpfmi3VEdGyBX3VXqj4TdrRS-WucrdSew,977
|
|
5
|
+
paynode_sdk/idempotency.py,sha256=od7HuSxFdejBP0oE4QCzbJdrDZWvziiu09d3BRErU2k,999
|
|
6
|
+
paynode_sdk/middleware.py,sha256=dg3hSmHAjW8wTF3AH3id8LFxbN2kXKKUsBRp1gyc6qw,3292
|
|
7
|
+
paynode_sdk/verifier.py,sha256=YkYUBgt5RPXigZsHNvdcca290YQyoOVijIa6f5SlHLw,1572
|
|
8
|
+
paynode_sdk_python-1.0.1.dist-info/METADATA,sha256=EcJUyKLTNco58bzxeuzOEd73Fvp3ScRS8SsVYwB9OHk,2852
|
|
9
|
+
paynode_sdk_python-1.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
paynode_sdk_python-1.0.1.dist-info/top_level.txt,sha256=c6Skc1Xx-9O-JJ7sHghLW8Kyn4hyJoVPUawH1Mu8iTU,12
|
|
11
|
+
paynode_sdk_python-1.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
paynode_sdk
|