paynode-sdk-python 1.1.1__tar.gz → 1.1.3__tar.gz
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_python-1.1.1 → paynode_sdk_python-1.1.3}/PKG-INFO +1 -1
- {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk/client.py +13 -8
- paynode_sdk_python-1.1.3/paynode_sdk/errors.py +23 -0
- {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk/middleware.py +4 -4
- {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk/verifier.py +25 -15
- {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk/webhook.py +1 -1
- {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk_python.egg-info/PKG-INFO +1 -1
- {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/pyproject.toml +1 -1
- paynode_sdk_python-1.1.1/paynode_sdk/errors.py +0 -31
- {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/LICENSE +0 -0
- {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/README.md +0 -0
- {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk/__init__.py +0 -0
- {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk/constants.py +0 -0
- {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk/idempotency.py +0 -0
- {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk_python.egg-info/SOURCES.txt +0 -0
- {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk_python.egg-info/dependency_links.txt +0 -0
- {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk_python.egg-info/requires.txt +0 -0
- {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk_python.egg-info/top_level.txt +0 -0
- {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/setup.cfg +0 -0
- {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/tests/test_client.py +0 -0
|
@@ -46,7 +46,7 @@ class PayNodeAgentClient:
|
|
|
46
46
|
except Exception as e:
|
|
47
47
|
logger.warning(f"⚠️ [PayNode-PY] RPC {rpc} failed: {str(e)}")
|
|
48
48
|
continue
|
|
49
|
-
raise PayNodeException("Failed to connect to any provided RPC nodes.", ErrorCode.
|
|
49
|
+
raise PayNodeException("Failed to connect to any provided RPC nodes.", ErrorCode.rpc_error)
|
|
50
50
|
|
|
51
51
|
def request_gate(self, url: str, method: str = "GET", **kwargs):
|
|
52
52
|
"""The high-level autonomous method handling 402 loop."""
|
|
@@ -67,7 +67,7 @@ class PayNodeAgentClient:
|
|
|
67
67
|
kwargs = self._handle_402(response.headers, **kwargs)
|
|
68
68
|
except Exception as e:
|
|
69
69
|
if isinstance(e, PayNodeException): raise
|
|
70
|
-
raise PayNodeException(f"An unexpected error occurred: {str(e)}", ErrorCode.
|
|
70
|
+
raise PayNodeException(f"An unexpected error occurred: {str(e)}", ErrorCode.internal_error)
|
|
71
71
|
continue
|
|
72
72
|
return response
|
|
73
73
|
return response
|
|
@@ -80,11 +80,11 @@ class PayNodeAgentClient:
|
|
|
80
80
|
order_id = headers.get('x-paynode-order-id')
|
|
81
81
|
|
|
82
82
|
if not all([router_addr, merchant_addr, amount_raw, token_addr, order_id]):
|
|
83
|
-
raise PayNodeException("Malformed 402 headers: missing metadata", ErrorCode.
|
|
83
|
+
raise PayNodeException("Malformed 402 headers: missing metadata", ErrorCode.internal_error)
|
|
84
84
|
|
|
85
85
|
# v1.3 Constraint: Min payment protection
|
|
86
86
|
if amount_raw < 1000:
|
|
87
|
-
raise PayNodeException("Payment amount is below the protocol minimum (1000).", ErrorCode.
|
|
87
|
+
raise PayNodeException("Payment amount is below the protocol minimum (1000).", ErrorCode.amount_too_low)
|
|
88
88
|
|
|
89
89
|
# Protocol v1.3: Permit-First Execution
|
|
90
90
|
try:
|
|
@@ -99,7 +99,7 @@ class PayNodeAgentClient:
|
|
|
99
99
|
logger.info(f"✅ [PayNode-PY] Payment successful: {tx_hash}")
|
|
100
100
|
except Exception as e:
|
|
101
101
|
if isinstance(e, PayNodeException): raise
|
|
102
|
-
raise PayNodeException(f"On-chain transaction reverted or failed: {str(e)}", ErrorCode.
|
|
102
|
+
raise PayNodeException(f"On-chain transaction reverted or failed: {str(e)}", ErrorCode.transaction_failed)
|
|
103
103
|
|
|
104
104
|
retry_headers = kwargs.get('headers', {}).copy()
|
|
105
105
|
retry_headers.update({'x-paynode-receipt': tx_hash, 'x-paynode-order-id': order_id})
|
|
@@ -166,12 +166,17 @@ class PayNodeAgentClient:
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
signed = self.account.sign_typed_data(full_message=structured_data)
|
|
169
|
-
return {
|
|
169
|
+
return {
|
|
170
|
+
"v": signed.v,
|
|
171
|
+
"r": Web3.to_bytes(signed.r).rjust(32, b'\0'),
|
|
172
|
+
"s": Web3.to_bytes(signed.s).rjust(32, b'\0'),
|
|
173
|
+
"deadline": deadline
|
|
174
|
+
}
|
|
170
175
|
|
|
171
176
|
def pay_with_permit_auto(self, router_addr, token_addr, merchant_addr, amount, order_id):
|
|
172
177
|
"""Combines sign_permit and on-chain submission."""
|
|
173
178
|
sig = self.sign_permit(token_addr, router_addr, amount)
|
|
174
|
-
router_abi = [{"inputs": [{"name": "
|
|
179
|
+
router_abi = [{"inputs": [{"name": "payer", "type": "address"}, {"name": "token", "type": "address"}, {"name": "merchant", "type": "address"}, {"name": "amount", "type": "uint256"}, {"name": "orderId", "type": "bytes32"}, {"name": "deadline", "type": "uint256"}, {"name": "v", "type": "uint8"}, {"name": "r", "type": "bytes32"}, {"name": "s", "type": "bytes32"}], "name": "payWithPermit", "outputs": [], "stateMutability": "nonpayable", "type": "function"}]
|
|
175
180
|
router = self.w3.eth.contract(address=Web3.to_checksum_address(router_addr), abi=router_abi)
|
|
176
181
|
order_id_bytes = self.w3.keccak(text=order_id)
|
|
177
182
|
|
|
@@ -199,7 +204,7 @@ class PayNodeAgentClient:
|
|
|
199
204
|
|
|
200
205
|
def _execute_pay(self, router_addr, token_addr, merchant_addr, amount, order_id):
|
|
201
206
|
"""Standard pay method (fallback)."""
|
|
202
|
-
router_abi = [{"inputs": [{"name": "
|
|
207
|
+
router_abi = [{"inputs": [{"name": "token", "type": "address"}, {"name": "merchant", "type": "address"}, {"name": "amount", "type": "uint256"}, {"name": "orderId", "type": "bytes32"}], "name": "pay", "outputs": [], "stateMutability": "nonpayable", "type": "function"}]
|
|
203
208
|
router = self.w3.eth.contract(address=Web3.to_checksum_address(router_addr), abi=router_abi)
|
|
204
209
|
order_id_bytes = self.w3.keccak(text=order_id)
|
|
205
210
|
current_gas_price = int(self.w3.eth.gas_price * 1.2)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
class ErrorCode(str, Enum):
|
|
5
|
+
rpc_error = 'rpc_error'
|
|
6
|
+
insufficient_funds = 'insufficient_funds'
|
|
7
|
+
amount_too_low = 'amount_too_low'
|
|
8
|
+
token_not_accepted = 'token_not_accepted'
|
|
9
|
+
transaction_failed = 'transaction_failed'
|
|
10
|
+
duplicate_transaction = 'duplicate_transaction'
|
|
11
|
+
invalid_receipt = 'invalid_receipt'
|
|
12
|
+
internal_error = 'internal_error'
|
|
13
|
+
transaction_not_found = 'transaction_not_found'
|
|
14
|
+
wrong_contract = 'wrong_contract'
|
|
15
|
+
order_mismatch = 'order_mismatch'
|
|
16
|
+
missing_receipt = 'missing_receipt'
|
|
17
|
+
|
|
18
|
+
class PayNodeException(Exception):
|
|
19
|
+
def __init__(self, message: str, code: ErrorCode, details: Optional[Any] = None):
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.message = message
|
|
22
|
+
self.code = code
|
|
23
|
+
self.details = details
|
|
@@ -12,7 +12,7 @@ class PayNodeMiddleware(BaseHTTPMiddleware):
|
|
|
12
12
|
def __init__(
|
|
13
13
|
self,
|
|
14
14
|
app: Any,
|
|
15
|
-
|
|
15
|
+
rpc_urls: list | str,
|
|
16
16
|
contract_address: str,
|
|
17
17
|
merchant_address: str,
|
|
18
18
|
chain_id: int,
|
|
@@ -25,7 +25,7 @@ class PayNodeMiddleware(BaseHTTPMiddleware):
|
|
|
25
25
|
):
|
|
26
26
|
super().__init__(app)
|
|
27
27
|
# The Verifier holds the state of the idempotency store
|
|
28
|
-
self.verifier = PayNodeVerifier(
|
|
28
|
+
self.verifier = PayNodeVerifier(rpc_urls=rpc_urls, contract_address=contract_address, chain_id=chain_id, store=store)
|
|
29
29
|
self.merchant_address = merchant_address
|
|
30
30
|
self.contract_address = contract_address
|
|
31
31
|
self.currency = currency
|
|
@@ -61,7 +61,7 @@ class PayNodeMiddleware(BaseHTTPMiddleware):
|
|
|
61
61
|
headers=headers,
|
|
62
62
|
content={
|
|
63
63
|
"error": "Payment Required",
|
|
64
|
-
"code": ErrorCode.
|
|
64
|
+
"code": ErrorCode.missing_receipt,
|
|
65
65
|
"message": "Please pay to PayNode contract and provide 'x-paynode-receipt' header.",
|
|
66
66
|
"amount": self.price,
|
|
67
67
|
"currency": self.currency
|
|
@@ -87,7 +87,7 @@ class PayNodeMiddleware(BaseHTTPMiddleware):
|
|
|
87
87
|
status_code=403,
|
|
88
88
|
content={
|
|
89
89
|
"error": "Forbidden",
|
|
90
|
-
"code": err.code if hasattr(err, 'code') else ErrorCode.
|
|
90
|
+
"code": err.code if hasattr(err, 'code') else ErrorCode.invalid_receipt,
|
|
91
91
|
"message": str(err)
|
|
92
92
|
}
|
|
93
93
|
)
|
|
@@ -4,10 +4,20 @@ from .idempotency import MemoryIdempotencyStore
|
|
|
4
4
|
from web3 import Web3
|
|
5
5
|
|
|
6
6
|
class PayNodeVerifier:
|
|
7
|
-
def __init__(self,
|
|
7
|
+
def __init__(self, rpc_urls=None, contract_address=None, chain_id=None, w3=None, store=None, accepted_tokens=None):
|
|
8
8
|
self.w3 = w3
|
|
9
|
-
if not self.w3 and
|
|
10
|
-
|
|
9
|
+
if not self.w3 and rpc_urls:
|
|
10
|
+
urls = rpc_urls if isinstance(rpc_urls, list) else [rpc_urls]
|
|
11
|
+
for rpc in urls:
|
|
12
|
+
try:
|
|
13
|
+
temp_w3 = Web3(Web3.HTTPProvider(rpc, request_kwargs={'timeout': 5}))
|
|
14
|
+
if temp_w3.is_connected():
|
|
15
|
+
self.w3 = temp_w3
|
|
16
|
+
break
|
|
17
|
+
except Exception:
|
|
18
|
+
continue
|
|
19
|
+
if not self.w3:
|
|
20
|
+
raise PayNodeException("Failed to connect to any provided RPC nodes.", ErrorCode.rpc_error)
|
|
11
21
|
self.contract_address = contract_address
|
|
12
22
|
self.chain_id = int(chain_id) if chain_id else None
|
|
13
23
|
self.store = store or MemoryIdempotencyStore()
|
|
@@ -24,14 +34,14 @@ class PayNodeVerifier:
|
|
|
24
34
|
|
|
25
35
|
async def verify_payment(self, tx_hash, expected):
|
|
26
36
|
if not self.w3:
|
|
27
|
-
return {"isValid": False, "error": PayNodeException("Verifier Provider Missing", ErrorCode.
|
|
37
|
+
return {"isValid": False, "error": PayNodeException("Verifier Provider Missing", ErrorCode.rpc_error)}
|
|
28
38
|
|
|
29
39
|
# 0. Dust Exploit Check (Minimum Payment)
|
|
30
40
|
amount = int(expected.get("amount", 0))
|
|
31
41
|
if amount < MIN_PAYMENT_AMOUNT:
|
|
32
42
|
return {"isValid": False, "error": PayNodeException(
|
|
33
43
|
f"Payment amount {amount} is below the minimum threshold of {MIN_PAYMENT_AMOUNT}.",
|
|
34
|
-
ErrorCode.
|
|
44
|
+
ErrorCode.amount_too_low
|
|
35
45
|
)}
|
|
36
46
|
|
|
37
47
|
# 1. Token Whitelist Check (Anti-FakeToken)
|
|
@@ -39,39 +49,39 @@ class PayNodeVerifier:
|
|
|
39
49
|
if self.accepted_tokens and expected_token not in self.accepted_tokens:
|
|
40
50
|
return {"isValid": False, "error": PayNodeException(
|
|
41
51
|
f"Token {expected.get('tokenAddress')} is not in the accepted whitelist.",
|
|
42
|
-
ErrorCode.
|
|
52
|
+
ErrorCode.token_not_accepted
|
|
43
53
|
)}
|
|
44
54
|
|
|
45
55
|
try:
|
|
46
56
|
is_new = await self.store.check_and_set(tx_hash, 86400) # 24 hour TTL
|
|
47
57
|
if not is_new:
|
|
48
|
-
return {"isValid": False, "error": PayNodeException("Receipt already used", ErrorCode.
|
|
58
|
+
return {"isValid": False, "error": PayNodeException("Receipt already used", ErrorCode.receipt_already_used)}
|
|
49
59
|
except Exception as e:
|
|
50
|
-
return {"isValid": False, "error": PayNodeException("Store Error", ErrorCode.
|
|
60
|
+
return {"isValid": False, "error": PayNodeException("Store Error", ErrorCode.internal_error, details=str(e))}
|
|
51
61
|
|
|
52
62
|
try:
|
|
53
63
|
receipt = self.w3.eth.get_transaction_receipt(tx_hash)
|
|
54
64
|
except Exception:
|
|
55
|
-
return {"isValid": False, "error": PayNodeException("Transaction not found", ErrorCode.
|
|
65
|
+
return {"isValid": False, "error": PayNodeException("Transaction not found", ErrorCode.transaction_not_found)}
|
|
56
66
|
|
|
57
67
|
if not receipt:
|
|
58
|
-
return {"isValid": False, "error": PayNodeException("Transaction not found", ErrorCode.
|
|
68
|
+
return {"isValid": False, "error": PayNodeException("Transaction not found", ErrorCode.transaction_not_found)}
|
|
59
69
|
|
|
60
70
|
if receipt.get("status") == 0:
|
|
61
|
-
return {"isValid": False, "error": PayNodeException("Transaction failed", ErrorCode.
|
|
71
|
+
return {"isValid": False, "error": PayNodeException("Transaction failed", ErrorCode.transaction_failed)}
|
|
62
72
|
|
|
63
73
|
if not receipt.get("to") or receipt.get("to", "").lower() != self.contract_address.lower():
|
|
64
|
-
return {"isValid": False, "error": PayNodeException("Wrong contract", ErrorCode.
|
|
74
|
+
return {"isValid": False, "error": PayNodeException("Wrong contract", ErrorCode.wrong_contract)}
|
|
65
75
|
|
|
66
76
|
contract = self.w3.eth.contract(address=Web3.to_checksum_address(self.contract_address), abi=PAYNODE_ROUTER_ABI)
|
|
67
77
|
|
|
68
78
|
try:
|
|
69
79
|
logs = contract.events.PaymentReceived().process_receipt(receipt)
|
|
70
80
|
except Exception:
|
|
71
|
-
return {"isValid": False, "error": PayNodeException("Invalid receipt format", ErrorCode.
|
|
81
|
+
return {"isValid": False, "error": PayNodeException("Invalid receipt format", ErrorCode.invalid_receipt)}
|
|
72
82
|
|
|
73
83
|
if not logs:
|
|
74
|
-
return {"isValid": False, "error": PayNodeException("No valid PaymentReceived event found", ErrorCode.
|
|
84
|
+
return {"isValid": False, "error": PayNodeException("No valid PaymentReceived event found", ErrorCode.invalid_receipt)}
|
|
75
85
|
|
|
76
86
|
# Find the valid log
|
|
77
87
|
merchant = expected.get("merchantAddress", "").lower()
|
|
@@ -103,6 +113,6 @@ class PayNodeVerifier:
|
|
|
103
113
|
break
|
|
104
114
|
|
|
105
115
|
if not valid:
|
|
106
|
-
return {"isValid": False, "error": PayNodeException("Payment criteria mismatch", ErrorCode.
|
|
116
|
+
return {"isValid": False, "error": PayNodeException("Payment criteria mismatch", ErrorCode.invalid_receipt)}
|
|
107
117
|
|
|
108
118
|
return {"isValid": True}
|
|
@@ -213,7 +213,7 @@ class PayNodeWebhookNotifier:
|
|
|
213
213
|
if resp.status >= 400:
|
|
214
214
|
raise PayNodeException(
|
|
215
215
|
f"Webhook returned {resp.status}",
|
|
216
|
-
ErrorCode.
|
|
216
|
+
ErrorCode.internal_error
|
|
217
217
|
)
|
|
218
218
|
|
|
219
219
|
logger.info(f"✅ [PayNode Webhook] Delivered tx {event.tx_hash[:10]}... → {resp.status}")
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "paynode-sdk-python"
|
|
7
|
-
version = "1.1.
|
|
7
|
+
version = "1.1.3"
|
|
8
8
|
description = "PayNode Protocol Python SDK for AI Agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "PayNodeLabs", email = "contact@paynode.dev" }]
|
|
@@ -1,31 +0,0 @@
|
|
|
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
|
-
TOKEN_NOT_ACCEPTED = 'PAYNODE_TOKEN_NOT_ACCEPTED'
|
|
17
|
-
AMOUNT_TOO_LOW = 'PAYNODE_AMOUNT_TOO_LOW'
|
|
18
|
-
INSUFFICIENT_FUNDS = 'PAYNODE_INSUFFICIENT_FUNDS'
|
|
19
|
-
ORDER_MISMATCH = 'PAYNODE_ORDER_MISMATCH'
|
|
20
|
-
PERMIT_FAILED = 'PAYNODE_PERMIT_FAILED'
|
|
21
|
-
|
|
22
|
-
# System
|
|
23
|
-
RPC_ERROR = 'PAYNODE_RPC_ERROR'
|
|
24
|
-
INTERNAL_ERROR = 'PAYNODE_INTERNAL_ERROR'
|
|
25
|
-
|
|
26
|
-
class PayNodeException(Exception):
|
|
27
|
-
def __init__(self, message: str, code: ErrorCode, details: Optional[Any] = None):
|
|
28
|
-
super().__init__(message)
|
|
29
|
-
self.message = message
|
|
30
|
-
self.code = code
|
|
31
|
-
self.details = details
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk_python.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk_python.egg-info/requires.txt
RENAMED
|
File without changes
|
{paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk_python.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|