paynode-sdk-python 1.1.2__py3-none-any.whl → 1.1.3__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/client.py +11 -6
- paynode_sdk/errors.py +12 -20
- paynode_sdk/middleware.py +2 -2
- paynode_sdk/verifier.py +13 -13
- paynode_sdk/webhook.py +1 -1
- {paynode_sdk_python-1.1.2.dist-info → paynode_sdk_python-1.1.3.dist-info}/METADATA +1 -1
- paynode_sdk_python-1.1.3.dist-info/RECORD +13 -0
- paynode_sdk_python-1.1.2.dist-info/RECORD +0 -13
- {paynode_sdk_python-1.1.2.dist-info → paynode_sdk_python-1.1.3.dist-info}/WHEEL +0 -0
- {paynode_sdk_python-1.1.2.dist-info → paynode_sdk_python-1.1.3.dist-info}/licenses/LICENSE +0 -0
- {paynode_sdk_python-1.1.2.dist-info → paynode_sdk_python-1.1.3.dist-info}/top_level.txt +0 -0
paynode_sdk/client.py
CHANGED
|
@@ -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,7 +166,12 @@ 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."""
|
paynode_sdk/errors.py
CHANGED
|
@@ -2,26 +2,18 @@ from enum import Enum
|
|
|
2
2
|
from typing import Any, Optional
|
|
3
3
|
|
|
4
4
|
class ErrorCode(str, Enum):
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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'
|
|
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'
|
|
25
17
|
|
|
26
18
|
class PayNodeException(Exception):
|
|
27
19
|
def __init__(self, message: str, code: ErrorCode, details: Optional[Any] = None):
|
paynode_sdk/middleware.py
CHANGED
|
@@ -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
|
)
|
paynode_sdk/verifier.py
CHANGED
|
@@ -17,7 +17,7 @@ class PayNodeVerifier:
|
|
|
17
17
|
except Exception:
|
|
18
18
|
continue
|
|
19
19
|
if not self.w3:
|
|
20
|
-
raise PayNodeException("Failed to connect to any provided RPC nodes.", ErrorCode.
|
|
20
|
+
raise PayNodeException("Failed to connect to any provided RPC nodes.", ErrorCode.rpc_error)
|
|
21
21
|
self.contract_address = contract_address
|
|
22
22
|
self.chain_id = int(chain_id) if chain_id else None
|
|
23
23
|
self.store = store or MemoryIdempotencyStore()
|
|
@@ -34,14 +34,14 @@ class PayNodeVerifier:
|
|
|
34
34
|
|
|
35
35
|
async def verify_payment(self, tx_hash, expected):
|
|
36
36
|
if not self.w3:
|
|
37
|
-
return {"isValid": False, "error": PayNodeException("Verifier Provider Missing", ErrorCode.
|
|
37
|
+
return {"isValid": False, "error": PayNodeException("Verifier Provider Missing", ErrorCode.rpc_error)}
|
|
38
38
|
|
|
39
39
|
# 0. Dust Exploit Check (Minimum Payment)
|
|
40
40
|
amount = int(expected.get("amount", 0))
|
|
41
41
|
if amount < MIN_PAYMENT_AMOUNT:
|
|
42
42
|
return {"isValid": False, "error": PayNodeException(
|
|
43
43
|
f"Payment amount {amount} is below the minimum threshold of {MIN_PAYMENT_AMOUNT}.",
|
|
44
|
-
ErrorCode.
|
|
44
|
+
ErrorCode.amount_too_low
|
|
45
45
|
)}
|
|
46
46
|
|
|
47
47
|
# 1. Token Whitelist Check (Anti-FakeToken)
|
|
@@ -49,39 +49,39 @@ class PayNodeVerifier:
|
|
|
49
49
|
if self.accepted_tokens and expected_token not in self.accepted_tokens:
|
|
50
50
|
return {"isValid": False, "error": PayNodeException(
|
|
51
51
|
f"Token {expected.get('tokenAddress')} is not in the accepted whitelist.",
|
|
52
|
-
ErrorCode.
|
|
52
|
+
ErrorCode.token_not_accepted
|
|
53
53
|
)}
|
|
54
54
|
|
|
55
55
|
try:
|
|
56
56
|
is_new = await self.store.check_and_set(tx_hash, 86400) # 24 hour TTL
|
|
57
57
|
if not is_new:
|
|
58
|
-
return {"isValid": False, "error": PayNodeException("Receipt already used", ErrorCode.
|
|
58
|
+
return {"isValid": False, "error": PayNodeException("Receipt already used", ErrorCode.receipt_already_used)}
|
|
59
59
|
except Exception as e:
|
|
60
|
-
return {"isValid": False, "error": PayNodeException("Store Error", ErrorCode.
|
|
60
|
+
return {"isValid": False, "error": PayNodeException("Store Error", ErrorCode.internal_error, details=str(e))}
|
|
61
61
|
|
|
62
62
|
try:
|
|
63
63
|
receipt = self.w3.eth.get_transaction_receipt(tx_hash)
|
|
64
64
|
except Exception:
|
|
65
|
-
return {"isValid": False, "error": PayNodeException("Transaction not found", ErrorCode.
|
|
65
|
+
return {"isValid": False, "error": PayNodeException("Transaction not found", ErrorCode.transaction_not_found)}
|
|
66
66
|
|
|
67
67
|
if not receipt:
|
|
68
|
-
return {"isValid": False, "error": PayNodeException("Transaction not found", ErrorCode.
|
|
68
|
+
return {"isValid": False, "error": PayNodeException("Transaction not found", ErrorCode.transaction_not_found)}
|
|
69
69
|
|
|
70
70
|
if receipt.get("status") == 0:
|
|
71
|
-
return {"isValid": False, "error": PayNodeException("Transaction failed", ErrorCode.
|
|
71
|
+
return {"isValid": False, "error": PayNodeException("Transaction failed", ErrorCode.transaction_failed)}
|
|
72
72
|
|
|
73
73
|
if not receipt.get("to") or receipt.get("to", "").lower() != self.contract_address.lower():
|
|
74
|
-
return {"isValid": False, "error": PayNodeException("Wrong contract", ErrorCode.
|
|
74
|
+
return {"isValid": False, "error": PayNodeException("Wrong contract", ErrorCode.wrong_contract)}
|
|
75
75
|
|
|
76
76
|
contract = self.w3.eth.contract(address=Web3.to_checksum_address(self.contract_address), abi=PAYNODE_ROUTER_ABI)
|
|
77
77
|
|
|
78
78
|
try:
|
|
79
79
|
logs = contract.events.PaymentReceived().process_receipt(receipt)
|
|
80
80
|
except Exception:
|
|
81
|
-
return {"isValid": False, "error": PayNodeException("Invalid receipt format", ErrorCode.
|
|
81
|
+
return {"isValid": False, "error": PayNodeException("Invalid receipt format", ErrorCode.invalid_receipt)}
|
|
82
82
|
|
|
83
83
|
if not logs:
|
|
84
|
-
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)}
|
|
85
85
|
|
|
86
86
|
# Find the valid log
|
|
87
87
|
merchant = expected.get("merchantAddress", "").lower()
|
|
@@ -113,6 +113,6 @@ class PayNodeVerifier:
|
|
|
113
113
|
break
|
|
114
114
|
|
|
115
115
|
if not valid:
|
|
116
|
-
return {"isValid": False, "error": PayNodeException("Payment criteria mismatch", ErrorCode.
|
|
116
|
+
return {"isValid": False, "error": PayNodeException("Payment criteria mismatch", ErrorCode.invalid_receipt)}
|
|
117
117
|
|
|
118
118
|
return {"isValid": True}
|
paynode_sdk/webhook.py
CHANGED
|
@@ -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}")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
paynode_sdk/__init__.py,sha256=p6URBqxFz1AErG7rIRWKnfwnzAC19qrMURtG7YhVR1I,821
|
|
2
|
+
paynode_sdk/client.py,sha256=tNQE3dizUnIc15Z9M1NU_0jZcoLJe01oN1TI40Ynl2k,10761
|
|
3
|
+
paynode_sdk/constants.py,sha256=puqz09qeKcMoigWrqdIzkhtAzFpECxEwHGLvDd3U_CQ,1429
|
|
4
|
+
paynode_sdk/errors.py,sha256=m49zByht9_nrmgX4_zHUR5eLsCHX_iR-d_BJZ2VNOx8,829
|
|
5
|
+
paynode_sdk/idempotency.py,sha256=od7HuSxFdejBP0oE4QCzbJdrDZWvziiu09d3BRErU2k,999
|
|
6
|
+
paynode_sdk/middleware.py,sha256=ubSFv2fiAjJ16cxrORDfWbsUHffhBbD4Wfvt0zM7lvE,3563
|
|
7
|
+
paynode_sdk/verifier.py,sha256=CzWO9IsXwc3AeCkmXy8gvcNTUbPhXnbfxmAa7lr1mhE,5274
|
|
8
|
+
paynode_sdk/webhook.py,sha256=DaCIuZ_rI7Kynt60Drw2EKQJuhNW0GoMk_u9EZy4Jxs,8302
|
|
9
|
+
paynode_sdk_python-1.1.3.dist-info/licenses/LICENSE,sha256=U8RjGlEBtXN6PA-qN_N3Uh60jyu3qe26ZBmgt-LAHc4,1069
|
|
10
|
+
paynode_sdk_python-1.1.3.dist-info/METADATA,sha256=nluXsilwB805frjjWE2rKgJ_ZC98hjQK_06O4HGSIao,2858
|
|
11
|
+
paynode_sdk_python-1.1.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
12
|
+
paynode_sdk_python-1.1.3.dist-info/top_level.txt,sha256=c6Skc1Xx-9O-JJ7sHghLW8Kyn4hyJoVPUawH1Mu8iTU,12
|
|
13
|
+
paynode_sdk_python-1.1.3.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
paynode_sdk/__init__.py,sha256=p6URBqxFz1AErG7rIRWKnfwnzAC19qrMURtG7YhVR1I,821
|
|
2
|
-
paynode_sdk/client.py,sha256=rwn6bTSyIuIXd8aLnK5Nnd7R5p8Hx25ETzfNQil6y1k,10639
|
|
3
|
-
paynode_sdk/constants.py,sha256=puqz09qeKcMoigWrqdIzkhtAzFpECxEwHGLvDd3U_CQ,1429
|
|
4
|
-
paynode_sdk/errors.py,sha256=GN0qycgjATT47dF4yZ6Ulg18jf2OwholvYl0pgCvuzo,1121
|
|
5
|
-
paynode_sdk/idempotency.py,sha256=od7HuSxFdejBP0oE4QCzbJdrDZWvziiu09d3BRErU2k,999
|
|
6
|
-
paynode_sdk/middleware.py,sha256=KAcSVaVyApO4evby4vnv1erifqi3VzSBFbWelGGSF_w,3563
|
|
7
|
-
paynode_sdk/verifier.py,sha256=V7VXWqXzkObfN9v9agb7CWiLLwKbX1f2gsIWQE0qDdk,5274
|
|
8
|
-
paynode_sdk/webhook.py,sha256=KxlXGkXe3wpR8ueO8FMW2iypJgWaLwMfxAAZLNRvNDo,8302
|
|
9
|
-
paynode_sdk_python-1.1.2.dist-info/licenses/LICENSE,sha256=U8RjGlEBtXN6PA-qN_N3Uh60jyu3qe26ZBmgt-LAHc4,1069
|
|
10
|
-
paynode_sdk_python-1.1.2.dist-info/METADATA,sha256=_qVjPReImedgxEqo1w4FlZVgead-vXFxAGvR3nZVyx4,2858
|
|
11
|
-
paynode_sdk_python-1.1.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
12
|
-
paynode_sdk_python-1.1.2.dist-info/top_level.txt,sha256=c6Skc1Xx-9O-JJ7sHghLW8Kyn4hyJoVPUawH1Mu8iTU,12
|
|
13
|
-
paynode_sdk_python-1.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|