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.
Files changed (20) hide show
  1. {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/PKG-INFO +1 -1
  2. {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk/client.py +13 -8
  3. paynode_sdk_python-1.1.3/paynode_sdk/errors.py +23 -0
  4. {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk/middleware.py +4 -4
  5. {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk/verifier.py +25 -15
  6. {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk/webhook.py +1 -1
  7. {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk_python.egg-info/PKG-INFO +1 -1
  8. {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/pyproject.toml +1 -1
  9. paynode_sdk_python-1.1.1/paynode_sdk/errors.py +0 -31
  10. {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/LICENSE +0 -0
  11. {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/README.md +0 -0
  12. {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk/__init__.py +0 -0
  13. {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk/constants.py +0 -0
  14. {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk/idempotency.py +0 -0
  15. {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk_python.egg-info/SOURCES.txt +0 -0
  16. {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk_python.egg-info/dependency_links.txt +0 -0
  17. {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk_python.egg-info/requires.txt +0 -0
  18. {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/paynode_sdk_python.egg-info/top_level.txt +0 -0
  19. {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/setup.cfg +0 -0
  20. {paynode_sdk_python-1.1.1 → paynode_sdk_python-1.1.3}/tests/test_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: paynode-sdk-python
3
- Version: 1.1.1
3
+ Version: 1.1.3
4
4
  Summary: PayNode Protocol Python SDK for AI Agents
5
5
  Author-email: PayNodeLabs <contact@paynode.dev>
6
6
  License: MIT
@@ -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.RPC_ERROR)
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.INTERNAL_ERROR)
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.INTERNAL_ERROR)
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.AMOUNT_TOO_LOW)
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.TRANSACTION_FAILED)
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 {"v": signed.v, "r": signed.r, "s": signed.s, "deadline": deadline}
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": "p", "type": "address"}, {"name": "t", "type": "address"}, {"name": "m", "type": "address"}, {"name": "a", "type": "uint256"}, {"name": "o", "type": "bytes32"}, {"name": "d", "type": "uint256"}, {"name": "v", "type": "uint8"}, {"name": "r", "type": "bytes32"}, {"name": "s", "type": "bytes32"}], "name": "payWithPermit", "outputs": [], "stateMutability": "nonpayable", "type": "function"}]
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": "t", "type": "address"}, {"name": "m", "type": "address"}, {"name": "a", "type": "uint256"}, {"name": "o", "type": "bytes32"}], "name": "pay", "outputs": [], "stateMutability": "nonpayable", "type": "function"}]
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
- rpc_url: str,
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(rpc_url, contract_address, chain_id, store=store)
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.MISSING_RECEIPT,
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.INVALID_RECEIPT,
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, rpc_url=None, contract_address=None, chain_id=None, w3=None, store=None, accepted_tokens=None):
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 rpc_url:
10
- self.w3 = Web3(Web3.HTTPProvider(rpc_url))
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.RPC_ERROR)}
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.AMOUNT_TOO_LOW
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.TOKEN_NOT_ACCEPTED
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.RECEIPT_ALREADY_USED)}
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.INTERNAL_ERROR, details=str(e))}
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.TRANSACTION_NOT_FOUND)}
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.TRANSACTION_NOT_FOUND)}
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.TRANSACTION_FAILED)}
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.WRONG_CONTRACT)}
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.INVALID_RECEIPT)}
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.INVALID_RECEIPT)}
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.INVALID_RECEIPT)}
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.INTERNAL_ERROR
216
+ ErrorCode.internal_error
217
217
  )
218
218
 
219
219
  logger.info(f"✅ [PayNode Webhook] Delivered tx {event.tx_hash[:10]}... → {resp.status}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: paynode-sdk-python
3
- Version: 1.1.1
3
+ Version: 1.1.3
4
4
  Summary: PayNode Protocol Python SDK for AI Agents
5
5
  Author-email: PayNodeLabs <contact@paynode.dev>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "paynode-sdk-python"
7
- version = "1.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