paynode-sdk-python 2.0.0__py3-none-any.whl → 2.2.0__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 CHANGED
@@ -1,3 +1,5 @@
1
+ import json
2
+ import base64
1
3
  import time
2
4
  import logging
3
5
  import threading
@@ -8,20 +10,21 @@ from eth_account.messages import encode_typed_data
8
10
  from web3 import Web3
9
11
  from requests.adapters import HTTPAdapter
10
12
  from urllib3.util.retry import Retry
11
- from .constants import PAYNODE_ROUTER_ADDRESS, BASE_USDC_ADDRESS, BASE_USDC_DECIMALS, BASE_RPC_URLS, ACCEPTED_TOKENS, MIN_PAYMENT_AMOUNT
13
+ from .constants import PAYNODE_ROUTER_ADDRESS, BASE_USDC_ADDRESS, BASE_USDC_DECIMALS, BASE_RPC_URLS, ACCEPTED_TOKENS, MIN_PAYMENT_AMOUNT, PAYNODE_ROUTER_ABI
12
14
  from .errors import PayNodeException, ErrorCode
13
15
 
14
16
  logger = logging.getLogger("paynode_sdk.client")
15
17
 
16
18
  class PayNodeAgentClient:
17
19
  """
18
- The main PayNode Client for AI Agents (v3.1).
20
+ The main PayNode Client for AI Agents (v2.2.0).
19
21
  Automatically handles the x402 'Payment Required' handshake.
20
22
  Supports RPC redundancy, EIP-2612 Permit, and EIP-3009 Authorization.
21
23
  """
22
24
  def __init__(self, private_key: str, rpc_urls: list | str = BASE_RPC_URLS):
23
25
  self.rpc_urls = rpc_urls if isinstance(rpc_urls, list) else [rpc_urls]
24
26
  self.w3 = self._init_w3()
27
+ self.current_rpc_index = 0
25
28
 
26
29
  # Initialize account and discard private key string
27
30
  self.account = self.w3.eth.account.from_key(private_key)
@@ -61,6 +64,24 @@ class PayNodeAgentClient:
61
64
 
62
65
  raise PayNodeException(ErrorCode.rpc_error, message="All provided RPC nodes are unreachable.")
63
66
 
67
+ def _rotate_rpc(self):
68
+ """Switches to the next available RPC node in the list."""
69
+ self.current_rpc_index = (self.current_rpc_index + 1) % len(self.rpc_urls)
70
+ new_url = self.rpc_urls[self.current_rpc_index]
71
+ logger.warning(f"⚠️ [PayNode-PY] RPC failure detected. Rotating to: {new_url}")
72
+ self.w3 = Web3(Web3.HTTPProvider(new_url, request_kwargs={'timeout': 10}))
73
+
74
+ def _call_with_failover(self, func, *args, **kwargs):
75
+ """Wrapper to retry a web3 call with RPC failover."""
76
+ for attempt in range(len(self.rpc_urls)):
77
+ try:
78
+ return func(*args, **kwargs)
79
+ except Exception as e:
80
+ if attempt < len(self.rpc_urls) - 1:
81
+ self._rotate_rpc()
82
+ else:
83
+ raise e
84
+
64
85
  def request_gate(self, url: str, method: str = "GET", **kwargs):
65
86
  """The high-level autonomous method handling 402 loop."""
66
87
  return self._request_with_402_retry(method.upper(), url, **kwargs)
@@ -71,16 +92,16 @@ class PayNodeAgentClient:
71
92
  def post(self, url, **kwargs):
72
93
  return self.request_gate(url, "POST", **kwargs)
73
94
 
74
- def _request_with_402_retry(self, method, url, max_retries=3, **kwargs):
95
+ def _request_with_402_retry(self, method: str, url: str, max_retries: int = 3, **kwargs) -> requests.Response:
75
96
  response = None
76
- for _ in range(max_retries):
77
- response = self.session.request(method, url, **kwargs)
97
+ for attempt in range(max_retries):
98
+ response = self._request_with_settlement_check(method, url, **kwargs)
78
99
  if response.status_code == 402:
79
- logger.info("💡 [PayNode-PY] 402 Detected. Analyzing protocol version...")
100
+ logger.info(f"💡 [PayNode-PY] 402 Detected (Attempt {attempt+1}/{max_retries}). Analyzing protocol version...")
80
101
 
81
- # Check for x402 v2 (JSON body or X-402-Required header)
102
+ # Check for x402 v2 (JSON body or official PAYMENT-REQUIRED header)
82
103
  content_type = response.headers.get('Content-Type', '')
83
- b64_required = response.headers.get('X-402-Required')
104
+ b64_required = response.headers.get('PAYMENT-REQUIRED') or response.headers.get('X-402-Required')
84
105
  order_id = response.headers.get('X-402-Order-Id')
85
106
 
86
107
  body = None
@@ -92,23 +113,29 @@ class PayNodeAgentClient:
92
113
 
93
114
  if not body and b64_required:
94
115
  try:
95
- import base64
96
- import json
97
116
  body = json.loads(base64.b64decode(b64_required).decode())
98
117
  except Exception as e:
99
- logger.warning(f"❌ [PayNode-PY] Failed to decode X-402-Required header: {e}")
118
+ logger.warning(f"❌ [PayNode-PY] Failed to decode PAYMENT-REQUIRED header: {e}")
100
119
 
101
120
  if body and body.get('x402Version') == 2:
102
121
  logger.info("🚀 [PayNode-PY] x402 v2 detected. Handling autonomous payment...")
103
- if order_id: body['orderId'] = order_id
104
- kwargs = self._handle_x402_v2(body, **kwargs)
122
+ if order_id and not body.get('orderId'): body['orderId'] = order_id
123
+ kwargs = self._handle_x402_v2(url, body, **kwargs)
105
124
  continue
106
125
 
107
126
  raise PayNodeException(ErrorCode.internal_error, message="Unsupported or malformed 402 response")
127
+
108
128
  return response
129
+
130
+ if response and response.status_code == 402:
131
+ raise PayNodeException(ErrorCode.internal_error, message="Still 402 after all payment attempts. The server may have rejected the payment or authorization.")
109
132
  return response
110
133
 
111
- def _handle_x402_v2(self, requirements, **kwargs):
134
+ def _handle_x402_v2(self, url: str, requirements: dict, **kwargs) -> dict:
135
+ """
136
+ Internal handler for X402 V2/V3.1 protocol.
137
+ Analyzes requirements, executes payment, and returns updated kwargs for retrying the request.
138
+ """
112
139
  chain_id = self.w3.eth.chain_id
113
140
  caip2_chain_id = f"eip155:{chain_id}"
114
141
 
@@ -119,13 +146,18 @@ class PayNodeAgentClient:
119
146
  if not requirement:
120
147
  raise PayNodeException(ErrorCode.internal_error, message=f"No compatible payment requirement found for network {caip2_chain_id}")
121
148
 
149
+ # 🛡️ Token Whitelist Check
150
+ chain_tokens = ACCEPTED_TOKENS.get(chain_id, [])
151
+ if chain_tokens and requirement.get('asset').lower() not in [t.lower() for t in chain_tokens]:
152
+ raise PayNodeException(ErrorCode.token_not_accepted, message=f"Token {requirement['asset']} is not in the whitelist for chain {chain_id}")
153
+
122
154
  logger.info(f"💡 [PayNode-PY] Payment request (v2): {requirement['amount']} atomic units of {requirement['asset']} to {requirement['payTo']}")
123
155
 
124
156
  # Dust limit check
125
157
  if int(requirement['amount']) < MIN_PAYMENT_AMOUNT:
126
158
  raise PayNodeException(ErrorCode.amount_too_low, message=f"Payment amount {requirement['amount']} is below the minimum dust limit of {MIN_PAYMENT_AMOUNT}")
127
159
 
128
- order_id = requirement.get('orderId') or requirements.get('orderId') or urlparse(kwargs.get('url', '')).path
160
+ order_id = requirement.get('orderId') or requirements.get('orderId') or urlparse(url).path
129
161
 
130
162
  payload_data = {}
131
163
  ptype = requirement.get('type', 'onchain')
@@ -160,35 +192,72 @@ class PayNodeAgentClient:
160
192
  allowance = self._get_allowance(asset, router_addr)
161
193
 
162
194
  if allowance >= amount:
163
- tx_hash = self.pay(router_addr, asset, requirement['payTo'], amount, order_id)
195
+ try:
196
+ tx_hash = self.pay(router_addr, asset, requirement['payTo'], amount, order_id)
197
+ except Exception as e:
198
+ logger.warning(f"⚠️ [PayNode-PY] Direct pay failed (possibly allowance race), falling back to permit: {e}")
199
+ tx_hash = self.pay_with_permit(router_addr, asset, requirement['payTo'], amount, order_id, version=requirement.get('extra', {}).get('version', '2'))
164
200
  else:
165
- tx_hash = self.pay_with_permit(router_addr, asset, requirement['payTo'], amount, order_id)
201
+ tx_hash = self.pay_with_permit(router_addr, asset, requirement['payTo'], amount, order_id, version=requirement.get('extra', {}).get('version', '2'))
166
202
 
167
203
  payload_data = {"txHash": tx_hash}
168
204
 
169
- # Unified Payload for v3.1
205
+ # Official X402 V2 Payload with PayNode extensions
170
206
  payment_payload = {
171
- "version": "3.1",
172
- "type": ptype,
173
- "orderId": order_id,
174
- "payload": payload_data
207
+ "x402Version": 2,
208
+ "resource": requirements.get('resource'),
209
+ "accepted": {
210
+ "scheme": requirement.get('scheme'),
211
+ "network": requirement.get('network'),
212
+ "amount": requirement.get('amount'),
213
+ "asset": requirement.get('asset'),
214
+ "payTo": requirement.get('payTo'),
215
+ "maxTimeoutSeconds": requirement.get('maxTimeoutSeconds'),
216
+ "extra": requirement.get('extra', {})
217
+ },
218
+ "payload": payload_data,
219
+ "_paynode": {
220
+ "version": "2.2.0",
221
+ "type": ptype,
222
+ "orderId": order_id
223
+ }
175
224
  }
225
+
226
+ # Add router for onchain
227
+ if ptype == 'onchain':
228
+ payment_payload["accepted"]["router"] = requirement.get('router')
176
229
 
177
230
  logger.info(f"✅ [PayNode-PY] {ptype} payment prepared. Retrying request...")
178
231
 
179
- import json
180
- import base64
181
232
  b64_payload = base64.b64encode(json.dumps(payment_payload).encode()).decode()
182
233
 
183
234
  retry_headers = kwargs.get('headers', {}).copy()
184
235
  retry_headers.update({
185
236
  'Content-Type': 'application/json',
186
- 'X-402-Payload': b64_payload,
237
+ 'PAYMENT-SIGNATURE': b64_payload,
238
+ 'X-402-Payload': b64_payload, # Backward compatibility
187
239
  'X-402-Order-Id': order_id
188
240
  })
189
241
  kwargs['headers'] = retry_headers
190
242
  return kwargs
191
243
 
244
+ def _request_with_settlement_check(self, method: str, url: str, **kwargs) -> requests.Response:
245
+ """Wrapper to check for PAYMENT-RESPONSE after retry."""
246
+ response = self.session.request(method, url, **kwargs)
247
+
248
+ settle_header = response.headers.get('PAYMENT-RESPONSE') or response.headers.get('X-PAYMENT-RESPONSE')
249
+ if settle_header:
250
+ try:
251
+ settle_data = json.loads(base64.b64decode(settle_header).decode())
252
+ if settle_data.get('success'):
253
+ logger.info(f"✅ [PayNode-PY] Settlement confirmed: {settle_data.get('transaction')}")
254
+ else:
255
+ logger.warning(f"⚠️ [PayNode-PY] Settlement failed: {settle_data.get('errorReason', 'Unknown error')}")
256
+ except Exception as e:
257
+ logger.warning(f"⚠️ [PayNode-PY] Failed to parse settlement response: {e}")
258
+
259
+ return response
260
+
192
261
  def sign_transfer_with_authorization(self, token_addr, to, amount, valid_after, valid_before, nonce, extra=None):
193
262
  extra = extra or {}
194
263
  token_addr = Web3.to_checksum_address(token_addr)
@@ -249,11 +318,14 @@ class PayNodeAgentClient:
249
318
  }
250
319
 
251
320
  def _get_allowance(self, token_addr, spender_addr):
321
+ return self._call_with_failover(self.__get_allowance_raw, token_addr, spender_addr)
322
+
323
+ def __get_allowance_raw(self, token_addr, spender_addr):
252
324
  abi = [{"constant": True, "inputs": [{"name": "o", "type": "address"}, {"name": "s", "type": "address"}], "name": "allowance", "outputs": [{"name": "", "type": "uint256"}], "type": "function"}]
253
325
  token = self.w3.eth.contract(address=Web3.to_checksum_address(token_addr), abi=abi)
254
326
  return token.functions.allowance(self.account.address, Web3.to_checksum_address(spender_addr)).call()
255
327
 
256
- def sign_permit(self, token_addr, spender_addr, amount, deadline=None):
328
+ def sign_permit(self, token_addr: str, spender_addr: str, amount: int, deadline: int = None, version: str = "2"):
257
329
  if deadline is None:
258
330
  deadline = int(time.time()) + 3600
259
331
 
@@ -269,7 +341,7 @@ class PayNodeAgentClient:
269
341
  name = token.functions.name().call()
270
342
  chain_id = self.w3.eth.chain_id
271
343
 
272
- domain = {"name": name, "version": "1", "chainId": chain_id, "verifyingContract": token_addr}
344
+ domain = {"name": name, "version": version, "chainId": chain_id, "verifyingContract": token_addr}
273
345
  message = {"owner": self.account.address, "spender": spender_addr, "value": amount, "nonce": nonce, "deadline": deadline}
274
346
  types = {
275
347
  "EIP712Domain": [
@@ -284,12 +356,19 @@ class PayNodeAgentClient:
284
356
  }
285
357
  structured_data = {"types": types, "domain": domain, "primaryType": "Permit", "message": message}
286
358
  signed = self.account.sign_typed_data(full_message=structured_data)
287
- return {"v": signed.v, "r": Web3.to_bytes(signed.r).rjust(32, b'\0'), "s": Web3.to_bytes(signed.s).rjust(32, b'\0'), "deadline": deadline}
359
+
360
+ # NOTE: r/s padding to 32 bytes ensures bytes32 compatibility
361
+ r_bytes = Web3.to_bytes(signed.r).rjust(32, b'\0')
362
+ s_bytes = Web3.to_bytes(signed.s).rjust(32, b'\0')
363
+
364
+ return {"v": signed.v, "r": r_bytes, "s": s_bytes, "deadline": deadline}
288
365
 
289
- def pay_with_permit(self, router_addr, token_addr, merchant_addr, amount, order_id):
290
- sig = self.sign_permit(token_addr, router_addr, amount)
291
- 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"}]
292
- router = self.w3.eth.contract(address=Web3.to_checksum_address(router_addr), abi=router_abi)
366
+ def pay_with_permit(self, router_addr, token_addr, merchant_addr, amount, order_id, version="2"):
367
+ return self._call_with_failover(self.__pay_with_permit_raw, router_addr, token_addr, merchant_addr, amount, order_id, version)
368
+
369
+ def __pay_with_permit_raw(self, router_addr, token_addr, merchant_addr, amount, order_id, version="2"):
370
+ sig = self.sign_permit(token_addr, router_addr, amount, version=version)
371
+ router = self.w3.eth.contract(address=Web3.to_checksum_address(router_addr), abi=PAYNODE_ROUTER_ABI)
293
372
  order_id_bytes = self.w3.keccak(text=order_id)
294
373
  current_gas_price = int(self.w3.eth.gas_price * 1.2)
295
374
  with self.nonce_lock:
@@ -301,8 +380,10 @@ class PayNodeAgentClient:
301
380
  return self.w3.to_hex(tx_h)
302
381
 
303
382
  def pay(self, router_addr, token_addr, merchant_addr, amount, order_id):
304
- 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"}]
305
- router = self.w3.eth.contract(address=Web3.to_checksum_address(router_addr), abi=router_abi)
383
+ return self._call_with_failover(self.__pay_raw, router_addr, token_addr, merchant_addr, amount, order_id)
384
+
385
+ def __pay_raw(self, router_addr, token_addr, merchant_addr, amount, order_id):
386
+ router = self.w3.eth.contract(address=Web3.to_checksum_address(router_addr), abi=PAYNODE_ROUTER_ABI)
306
387
  order_id_bytes = self.w3.keccak(text=order_id)
307
388
  current_gas_price = int(self.w3.eth.gas_price * 1.2)
308
389
  with self.nonce_lock:
paynode_sdk/constants.py CHANGED
@@ -2,34 +2,19 @@
2
2
  PAYNODE_ROUTER_ADDRESS = "0x4A73696ccF76E7381b044cB95127B3784369Ed63"
3
3
  PAYNODE_ROUTER_ADDRESS_SANDBOX = "0x24cD8b68aaC209217ff5a6ef1Bf55a59f2c8Ca6F"
4
4
  BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
5
- BASE_USDC_ADDRESS_SANDBOX = "0x109AEddD656Ed2761d1e210E179329105039c784"
5
+ BASE_USDC_ADDRESS_SANDBOX = "0x65c088EfBDB0E03185Dbe8e258Ad0cf4Ab7946b0"
6
6
  BASE_USDC_DECIMALS = 6
7
7
 
8
8
  PROTOCOL_TREASURY = "0x598bF63F5449876efafa7b36b77Deb2070621C0E"
9
9
  PROTOCOL_FEE_BPS = 100
10
10
  MIN_PAYMENT_AMOUNT = 1000
11
11
 
12
- BASE_RPC_URLS = ["https://mainnet.base.org", "https://base.meowrpc.com", "https://1rpc.io/base"]
13
- BASE_RPC_URLS_SANDBOX = ["https://sepolia.base.org", "https://base-sepolia-rpc.publicnode.com"]
12
+ BASE_RPC_URLS = ['https://mainnet.base.org', 'https://base.meowrpc.com', 'https://1rpc.io/base']
13
+ BASE_RPC_URLS_SANDBOX = ['https://sepolia.base.org', 'https://base-sepolia-rpc.publicnode.com']
14
14
 
15
15
  ACCEPTED_TOKENS = {
16
16
  8453: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],
17
- 84532: ["0x109AEddD656Ed2761d1e210E179329105039c784"]
17
+ 84532: ["0x65c088EfBDB0E03185Dbe8e258Ad0cf4Ab7946b0"]
18
18
  }
19
19
 
20
- PAYNODE_ROUTER_ABI = [
21
- {
22
- "anonymous": False,
23
- "inputs": [
24
- {"indexed": True, "name": "orderId", "type": "bytes32"},
25
- {"indexed": True, "name": "merchant", "type": "address"},
26
- {"indexed": True, "name": "payer", "type": "address"},
27
- {"indexed": False, "name": "token", "type": "address"},
28
- {"indexed": False, "name": "amount", "type": "uint256"},
29
- {"indexed": False, "name": "fee", "type": "uint256"},
30
- {"indexed": False, "name": "chainId", "type": "uint256"}
31
- ],
32
- "name": "PaymentReceived",
33
- "type": "event"
34
- }
35
- ]
20
+ PAYNODE_ROUTER_ABI = [{'type': 'constructor', 'inputs': [{'name': '_protocolTreasury', 'type': 'address', 'internalType': 'address'}], 'stateMutability': 'nonpayable'}, {'type': 'function', 'name': 'MAX_BPS', 'inputs': [], 'outputs': [{'name': '', 'type': 'uint256', 'internalType': 'uint256'}], 'stateMutability': 'view'}, {'type': 'function', 'name': 'MIN_PAYMENT_AMOUNT', 'inputs': [], 'outputs': [{'name': '', 'type': 'uint256', 'internalType': 'uint256'}], 'stateMutability': 'view'}, {'type': 'function', 'name': 'PROTOCOL_FEE_BPS', 'inputs': [], 'outputs': [{'name': '', 'type': 'uint256', 'internalType': 'uint256'}], 'stateMutability': 'view'}, {'type': 'function', 'name': 'acceptOwnership', 'inputs': [], 'outputs': [], 'stateMutability': 'nonpayable'}, {'type': 'function', 'name': 'owner', 'inputs': [], 'outputs': [{'name': '', 'type': 'address', 'internalType': 'address'}], 'stateMutability': 'view'}, {'type': 'function', 'name': 'pause', 'inputs': [], 'outputs': [], 'stateMutability': 'nonpayable'}, {'type': 'function', 'name': 'paused', 'inputs': [], 'outputs': [{'name': '', 'type': 'bool', 'internalType': 'bool'}], 'stateMutability': 'view'}, {'type': 'function', 'name': 'pay', 'inputs': [{'name': 'token', 'type': 'address', 'internalType': 'address'}, {'name': 'merchant', 'type': 'address', 'internalType': 'address'}, {'name': 'amount', 'type': 'uint256', 'internalType': 'uint256'}, {'name': 'orderId', 'type': 'bytes32', 'internalType': 'bytes32'}], 'outputs': [], 'stateMutability': 'nonpayable'}, {'type': 'function', 'name': 'payWithPermit', 'inputs': [{'name': 'payer', 'type': 'address', 'internalType': 'address'}, {'name': 'token', 'type': 'address', 'internalType': 'address'}, {'name': 'merchant', 'type': 'address', 'internalType': 'address'}, {'name': 'amount', 'type': 'uint256', 'internalType': 'uint256'}, {'name': 'orderId', 'type': 'bytes32', 'internalType': 'bytes32'}, {'name': 'deadline', 'type': 'uint256', 'internalType': 'uint256'}, {'name': 'v', 'type': 'uint8', 'internalType': 'uint8'}, {'name': 'r', 'type': 'bytes32', 'internalType': 'bytes32'}, {'name': 's', 'type': 'bytes32', 'internalType': 'bytes32'}], 'outputs': [], 'stateMutability': 'nonpayable'}, {'type': 'function', 'name': 'pendingOwner', 'inputs': [], 'outputs': [{'name': '', 'type': 'address', 'internalType': 'address'}], 'stateMutability': 'view'}, {'type': 'function', 'name': 'protocolTreasury', 'inputs': [], 'outputs': [{'name': '', 'type': 'address', 'internalType': 'address'}], 'stateMutability': 'view'}, {'type': 'function', 'name': 'renounceOwnership', 'inputs': [], 'outputs': [], 'stateMutability': 'nonpayable'}, {'type': 'function', 'name': 'transferOwnership', 'inputs': [{'name': 'newOwner', 'type': 'address', 'internalType': 'address'}], 'outputs': [], 'stateMutability': 'nonpayable'}, {'type': 'function', 'name': 'unpause', 'inputs': [], 'outputs': [], 'stateMutability': 'nonpayable'}, {'type': 'function', 'name': 'updateTreasury', 'inputs': [{'name': '_newTreasury', 'type': 'address', 'internalType': 'address'}], 'outputs': [], 'stateMutability': 'nonpayable'}, {'type': 'event', 'name': 'OwnershipTransferStarted', 'inputs': [{'name': 'previousOwner', 'type': 'address', 'indexed': True, 'internalType': 'address'}, {'name': 'newOwner', 'type': 'address', 'indexed': True, 'internalType': 'address'}], 'anonymous': False}, {'type': 'event', 'name': 'OwnershipTransferred', 'inputs': [{'name': 'previousOwner', 'type': 'address', 'indexed': True, 'internalType': 'address'}, {'name': 'newOwner', 'type': 'address', 'indexed': True, 'internalType': 'address'}], 'anonymous': False}, {'type': 'event', 'name': 'Paused', 'inputs': [{'name': 'account', 'type': 'address', 'indexed': False, 'internalType': 'address'}], 'anonymous': False}, {'type': 'event', 'name': 'PaymentReceived', 'inputs': [{'name': 'orderId', 'type': 'bytes32', 'indexed': True, 'internalType': 'bytes32'}, {'name': 'merchant', 'type': 'address', 'indexed': True, 'internalType': 'address'}, {'name': 'payer', 'type': 'address', 'indexed': True, 'internalType': 'address'}, {'name': 'token', 'type': 'address', 'indexed': False, 'internalType': 'address'}, {'name': 'amount', 'type': 'uint256', 'indexed': False, 'internalType': 'uint256'}, {'name': 'fee', 'type': 'uint256', 'indexed': False, 'internalType': 'uint256'}, {'name': 'chainId', 'type': 'uint256', 'indexed': False, 'internalType': 'uint256'}], 'anonymous': False}, {'type': 'event', 'name': 'TreasuryUpdated', 'inputs': [{'name': 'oldTreasury', 'type': 'address', 'indexed': True, 'internalType': 'address'}, {'name': 'newTreasury', 'type': 'address', 'indexed': True, 'internalType': 'address'}], 'anonymous': False}, {'type': 'event', 'name': 'Unpaused', 'inputs': [{'name': 'account', 'type': 'address', 'indexed': False, 'internalType': 'address'}], 'anonymous': False}, {'type': 'error', 'name': 'AmountTooLow', 'inputs': []}, {'type': 'error', 'name': 'EnforcedPause', 'inputs': []}, {'type': 'error', 'name': 'ExpectedPause', 'inputs': []}, {'type': 'error', 'name': 'InvalidAddress', 'inputs': []}, {'type': 'error', 'name': 'OwnableInvalidOwner', 'inputs': [{'name': 'owner', 'type': 'address', 'internalType': 'address'}]}, {'type': 'error', 'name': 'OwnableUnauthorizedAccount', 'inputs': [{'name': 'account', 'type': 'address', 'internalType': 'address'}]}, {'type': 'error', 'name': 'SafeERC20FailedOperation', 'inputs': [{'name': 'token', 'type': 'address', 'internalType': 'address'}]}, {'type': 'error', 'name': 'UnauthorizedCaller', 'inputs': []}]
paynode_sdk/errors.py CHANGED
@@ -28,7 +28,7 @@ ERROR_MESSAGES = {
28
28
  ErrorCode.transaction_not_found: "Transaction not found on-chain.",
29
29
  ErrorCode.wrong_contract: "Payment event was not emitted by the official PayNode contract.",
30
30
  ErrorCode.order_mismatch: "OrderId in receipt does not match requested ID.",
31
- ErrorCode.missing_receipt: "Please pay to PayNode contract and provide 'x-paynode-receipt' header.",
31
+ ErrorCode.missing_receipt: "Please pay to PayNode contract and provide 'PAYMENT-SIGNATURE' header.",
32
32
  }
33
33
 
34
34
  class PayNodeException(Exception):
@@ -18,27 +18,38 @@ class IdempotencyStore(ABC):
18
18
  """
19
19
  pass
20
20
 
21
+ import threading
22
+
21
23
  class MemoryIdempotencyStore(IdempotencyStore):
22
24
  def __init__(self):
23
25
  self.cache: Dict[str, float] = {}
26
+ self.last_cleanup = time.time()
27
+ self.lock = threading.Lock()
24
28
 
25
29
  async def check_and_set(self, tx_hash: str, ttl_seconds: int) -> bool:
26
- now = time.time()
27
- expiry = self.cache.get(tx_hash)
30
+ with self.lock:
31
+ now = time.time()
32
+ expiry = self.cache.get(tx_hash)
28
33
 
29
- if expiry and expiry > now:
30
- return False
34
+ if expiry and expiry > now:
35
+ return False
31
36
 
32
- self.cache[tx_hash] = now + ttl_seconds
33
- self._cleanup()
34
- return True
37
+ self.cache[tx_hash] = now + ttl_seconds
38
+
39
+ # BUG-5 FIX: Only cleanup periodically to avoid O(n) overhead on every call.
40
+ if now - self.last_cleanup > 60:
41
+ self._cleanup()
42
+ self.last_cleanup = now
43
+
44
+ return True
35
45
 
36
46
  async def delete(self, tx_hash: str) -> None:
37
- self.cache.pop(tx_hash, None)
47
+ with self.lock:
48
+ self.cache.pop(tx_hash, None)
38
49
 
39
50
  def _cleanup(self):
51
+ # Already inside lock when called from check_and_set
40
52
  now = time.time()
41
- # Simple cleanup logic: remove expired entries
42
53
  expired_keys = [k for k, v in self.cache.items() if v <= now]
43
54
  for k in expired_keys:
44
55
  del self.cache[k]
paynode_sdk/middleware.py CHANGED
@@ -47,24 +47,63 @@ class PayNodeMiddleware(BaseHTTPMiddleware):
47
47
  self.chain_id = chain_id
48
48
  self.generate_order_id = generate_order_id or (lambda r: f"agent_py_{int(time.time() * 1000)}")
49
49
 
50
- self.amount_int = int(float(price) * (10 ** decimals))
50
+ # DEV-2 FIX: Avoid float precision risks by using integer arithmetic or decimal string parsing
51
+ if "." in price:
52
+ parts = price.split(".")
53
+ integer_part = parts[0]
54
+ fraction_part = parts[1][:decimals].ljust(decimals, "0")
55
+ self.amount_int = int(integer_part + fraction_part)
56
+ else:
57
+ self.amount_int = int(price) * (10 ** decimals)
51
58
  self.description = kwargs.get('description', "Protected Resource")
52
59
  self.max_timeout_seconds = kwargs.get('max_timeout_seconds', 3600)
53
60
 
54
61
  async def dispatch(self, request: Request, call_next):
55
- v2_payload_header = request.headers.get('x-402-payload')
56
- order_id = request.headers.get('x-402-order-id')
62
+ v2_payload_header = request.headers.get('PAYMENT-SIGNATURE') or request.headers.get('X-402-Payload')
63
+ order_id = request.headers.get('X-402-Order-Id')
57
64
 
58
65
  if not order_id:
59
- order_id = self.generate_order_id(request)
66
+ order_id = (self.generate_order_id)(request)
60
67
 
61
68
  # Handle x402 v2 Unified Payload
62
69
  unified_payload = None
63
70
  if v2_payload_header:
64
71
  try:
65
- unified_payload = json.loads(base64.b64decode(v2_payload_header.encode()).decode())
72
+ parsed = json.loads(base64.b64decode(v2_payload_header.encode()).decode())
73
+
74
+ if parsed.get('x402Version') == 2 and parsed.get('accepted'):
75
+ # Official X402 V2 format - convert to internal format
76
+ internal_order_id = parsed.get('_paynode', {}).get('orderId') \
77
+ or order_id \
78
+ or f"auto_{int(time.time() * 1000)}"
79
+
80
+ # Infer type from payload content if missing
81
+ payload_content = parsed.get('payload', {})
82
+ inferred_type = 'onchain'
83
+ if payload_content.get('signature') or payload_content.get('authorization'):
84
+ inferred_type = 'eip3009'
85
+ elif payload_content.get('txHash'):
86
+ inferred_type = 'onchain'
87
+
88
+ p_type = parsed.get('_paynode', {}).get('type') or inferred_type
89
+
90
+ unified_payload = {
91
+ "version": "2.2.0",
92
+ "type": p_type,
93
+ "orderId": internal_order_id,
94
+ "router": parsed.get('accepted', {}).get('router'),
95
+ "payload": parsed.get('payload')
96
+ }
97
+ order_id = internal_order_id
98
+ elif parsed.get('version') == "2.2.0":
99
+ # Legacy PayNode format
100
+ unified_payload = parsed
101
+ if 'orderId' in unified_payload:
102
+ order_id = unified_payload['orderId']
103
+ elif 'order_id' in unified_payload:
104
+ order_id = unified_payload['order_id']
66
105
  except Exception as e:
67
- logger.error(f"❌ [PayNode-Middleware] Failed to decode X-402-Payload header: {e}")
106
+ logger.error(f"❌ [PayNode-Middleware] Failed to decode payment payload header: {e}")
68
107
 
69
108
  if unified_payload:
70
109
  try:
@@ -76,18 +115,51 @@ class PayNodeMiddleware(BaseHTTPMiddleware):
76
115
  "amount": str(self.amount_int),
77
116
  "orderId": order_id
78
117
  },
79
- unified_payload.get("payload", {}).get("extra", {}) if unified_payload.get("type") == "eip3009" else {}
118
+ # BUG-1 FIX: extra should come from our own config (v2Response schema), not the agent's payload
119
+ {
120
+ "name": self.currency,
121
+ "version": "2" # USDC v2
122
+ } if unified_payload.get("type") == "eip3009" else {}
80
123
  )
81
124
  if result.get("isValid"):
82
- request.state.paynode = {"unified_payload": unified_payload, "order_id": order_id}
83
- return await call_next(request)
125
+ request.state.paynode = {"unified_payload": unified_payload, "orderId": order_id}
126
+
127
+ # Construct settlement response header
128
+ settle_response = {
129
+ "success": True,
130
+ "transaction": unified_payload.get("payload", {}).get("txHash") or "",
131
+ "network": f"eip155:{self.chain_id}",
132
+ "payer": result.get("payer", "")
133
+ }
134
+ b64_settle = base64.b64encode(json.dumps(settle_response).encode()).decode()
135
+
136
+ response = await call_next(request)
137
+ response.headers["PAYMENT-RESPONSE"] = b64_settle
138
+ response.headers["X-PAYMENT-RESPONSE"] = b64_settle # Compatibility
139
+ return response
84
140
  else:
85
141
  err = result.get("error")
142
+ error_code = err.code if hasattr(err, 'code') else ErrorCode.invalid_receipt
143
+
144
+ # Also include PAYMENT-RESPONSE header on failure for protocol symmetry
145
+ settle_fail = {
146
+ "success": False,
147
+ "errorReason": error_code,
148
+ "transaction": "",
149
+ "network": f"eip155:{self.chain_id}"
150
+ }
151
+ b64_settle_fail = base64.b64encode(json.dumps(settle_fail).encode()).decode()
152
+
153
+ headers = {
154
+ "PAYMENT-RESPONSE": b64_settle_fail,
155
+ "X-PAYMENT-RESPONSE": b64_settle_fail
156
+ }
86
157
  return JSONResponse(
87
158
  status_code=403,
159
+ headers=headers,
88
160
  content={
89
161
  "error": "Forbidden",
90
- "code": err.code if hasattr(err, 'code') else ErrorCode.invalid_receipt,
162
+ "code": error_code,
91
163
  "message": str(err)
92
164
  }
93
165
  )
@@ -133,6 +205,7 @@ class PayNodeMiddleware(BaseHTTPMiddleware):
133
205
  b64_required = base64.b64encode(json.dumps(v2_response).encode()).decode()
134
206
 
135
207
  headers = {
208
+ 'PAYMENT-REQUIRED': b64_required,
136
209
  'X-402-Required': b64_required,
137
210
  'X-402-Order-Id': order_id,
138
211
  }
paynode_sdk/verifier.py CHANGED
@@ -3,7 +3,7 @@ import time
3
3
  import logging
4
4
  from concurrent.futures import ThreadPoolExecutor, as_completed
5
5
  from .errors import ErrorCode, PayNodeException
6
- from .constants import ACCEPTED_TOKENS, MIN_PAYMENT_AMOUNT
6
+ from .constants import ACCEPTED_TOKENS, MIN_PAYMENT_AMOUNT, PAYNODE_ROUTER_ABI
7
7
  from .idempotency import MemoryIdempotencyStore
8
8
  from web3 import Web3
9
9
  from eth_account import Account
@@ -55,6 +55,15 @@ class PayNodeVerifier:
55
55
  Routes to verify_onchain_payment or verify_transfer_with_authorization (eip3009).
56
56
  """
57
57
  try:
58
+ # 1. Double-check Protocol Dust Limit (>= 1000)
59
+ expected_amount = int(expected.get("amount", 0))
60
+ if expected_amount < MIN_PAYMENT_AMOUNT:
61
+ return {"isValid": False, "error": PayNodeException(ErrorCode.amount_too_low)}
62
+
63
+ # 2. Security: Token Whitelist Check
64
+ if self.accepted_tokens and expected.get("tokenAddress", "").lower() not in self.accepted_tokens:
65
+ return {"isValid": False, "error": PayNodeException(ErrorCode.token_not_accepted, message=f"Token {expected.get('tokenAddress')} not allowed")}
66
+
58
67
  payload_type = unified_payload.get("type")
59
68
  actual_payload = unified_payload.get("payload", {})
60
69
  order_id = unified_payload.get("orderId")
@@ -105,16 +114,21 @@ class PayNodeVerifier:
105
114
  if receipt.get("status") == 0:
106
115
  return {"isValid": False, "error": PayNodeException(ErrorCode.transaction_failed)}
107
116
 
108
- router_abi = [{"anonymous": False, "inputs": [{"indexed": True, "name": "merchant", "type": "address"}, {"indexed": True, "name": "token", "type": "address"}, {"indexed": False, "name": "amount", "type": "uint256"}, {"indexed": True, "name": "orderId", "type": "bytes32"}, {"indexed": False, "name": "chainId", "type": "uint256"}], "name": "PaymentReceived", "type": "event"}]
109
- contract = self.w3.eth.contract(address=Web3.to_checksum_address(self.contract_address), abi=router_abi)
117
+ contract = self.w3.eth.contract(address=Web3.to_checksum_address(self.contract_address), abi=PAYNODE_ROUTER_ABI)
110
118
 
119
+ # 1. Check if the router was even involved (against 'WrongContract' vs 'InvalidReceipt')
120
+ # Filter logs for current contract
121
+ relevant_logs = [log for log in receipt.get("logs", []) if log.get("address", "").lower() == self.contract_address.lower()]
122
+ if not relevant_logs:
123
+ return {"isValid": False, "error": PayNodeException(ErrorCode.wrong_contract, message="Transaction did not interact with the expected PayNodeRouter contract")}
124
+
111
125
  try:
112
- logs = await asyncio.to_thread(contract.events.PaymentReceived().process_receipt, receipt)
126
+ processed_logs = await asyncio.to_thread(contract.events.PaymentReceived().process_receipt, {"logs": relevant_logs})
113
127
  except Exception:
114
128
  return {"isValid": False, "error": PayNodeException(ErrorCode.invalid_receipt)}
115
129
 
116
- if not logs:
117
- return {"isValid": False, "error": PayNodeException(ErrorCode.invalid_receipt, message="No PaymentReceived event found")}
130
+ if not processed_logs:
131
+ return {"isValid": False, "error": PayNodeException(ErrorCode.invalid_receipt, message="No PaymentReceived event found in router logs")}
118
132
 
119
133
  merchant = expected.get("merchantAddress", "").lower()
120
134
  token = expected.get("tokenAddress", "").lower()
@@ -122,16 +136,26 @@ class PayNodeVerifier:
122
136
  order_id_bytes = self.w3.keccak(text=expected.get("orderId", ""))
123
137
 
124
138
  valid_log_found = False
125
- for log in logs:
139
+ found_payer = None
140
+ order_id_mismatch_found = False
141
+ for log in processed_logs:
126
142
  args = log.args
127
- if (args.get("merchant", "").lower() == merchant and
128
- args.get("token", "").lower() == token and
129
- args.get("amount", 0) >= amount and
130
- args.get("orderId") == order_id_bytes):
131
- valid_log_found = True
132
- break
143
+ is_merchant_match = args.get("merchant", "").lower() == merchant
144
+ is_token_match = args.get("token", "").lower() == token
145
+ is_amount_match = args.get("amount", 0) >= amount
146
+ is_order_match = args.get("orderId") == order_id_bytes
147
+
148
+ if is_merchant_match and is_token_match and is_amount_match:
149
+ if is_order_match:
150
+ valid_log_found = True
151
+ found_payer = args.get("payer")
152
+ break
153
+ else:
154
+ order_id_mismatch_found = True
133
155
 
134
156
  if not valid_log_found:
157
+ if order_id_mismatch_found:
158
+ return {"isValid": False, "error": PayNodeException(ErrorCode.order_mismatch, message="Payment log found but orderId does not match")}
135
159
  return {"isValid": False, "error": PayNodeException(ErrorCode.invalid_receipt, message="Payment event data mismatch")}
136
160
 
137
161
  if self.store:
@@ -139,7 +163,7 @@ class PayNodeVerifier:
139
163
  if not is_new:
140
164
  return {"isValid": False, "error": PayNodeException(ErrorCode.duplicate_transaction)}
141
165
 
142
- return {"isValid": True}
166
+ return {"isValid": True, "payer": found_payer}
143
167
 
144
168
  async def verify_transfer_with_authorization(
145
169
  self,
@@ -218,7 +242,9 @@ class PayNodeVerifier:
218
242
  "message": auth_msg
219
243
  }
220
244
 
221
- recovered_address = Account.recover_typed_data(structured_data, signature=signature)
245
+ from eth_account.messages import encode_typed_data
246
+ signable_msg = encode_typed_data(full_message=structured_data)
247
+ recovered_address = Account.recover_message(signable_msg, signature=signature)
222
248
 
223
249
  if recovered_address.lower() != auth["from"].lower():
224
250
  return {"isValid": False, "error": PayNodeException(ErrorCode.invalid_receipt, message="Invalid signature")}
@@ -261,19 +287,17 @@ class PayNodeVerifier:
261
287
 
262
288
  # Concurrent RPC calls
263
289
  try:
264
- def call_rpc():
265
- balance = token_contract.functions.balanceOf(authorizer_address).call()
266
- is_used = token_contract.functions.authorizationState(authorizer_address, nonce_bytes).call()
267
- return balance, is_used
268
-
269
- balance, is_nonce_used_on_chain = await asyncio.to_thread(call_rpc)
290
+ balance, is_nonce_used_on_chain = await asyncio.gather(
291
+ asyncio.to_thread(token_contract.functions.balanceOf(authorizer_address).call),
292
+ asyncio.to_thread(token_contract.functions.authorizationState(authorizer_address, nonce_bytes).call)
293
+ )
270
294
  except Exception as e:
271
- # If RPC fails (e.g. mock token doesn't support authorizationState), fallback or fail
272
295
  logger.warning(f"RPC state check failed for token {token_addr}: {e}")
273
- # We still keep the local idempotency check. For safety, we return true if balance is not checkable?
274
- # No, JS implementation fallbacks to 0 balance and False nonce.
275
- balance = payload_value # Optimistic if check fails? No, let's follow JS more closely but be safe.
276
- is_nonce_used_on_chain = False
296
+ if self.store: await self.store.delete(nonce)
297
+ return {
298
+ "isValid": False,
299
+ "error": PayNodeException(ErrorCode.rpc_error, message=f"Cannot verify on-chain state: {e}")
300
+ }
277
301
 
278
302
  if balance < payload_value:
279
303
  if self.store: await self.store.delete(nonce)
@@ -283,6 +307,6 @@ class PayNodeVerifier:
283
307
  if self.store: await self.store.delete(nonce)
284
308
  return {"isValid": False, "error": PayNodeException(ErrorCode.duplicate_transaction, message="Nonce already consumed on-chain")}
285
309
 
286
- return {"isValid": True}
310
+ return {"isValid": True, "payer": auth["from"]}
287
311
  except Exception as e:
288
312
  return {"isValid": False, "error": PayNodeException(ErrorCode.internal_error, message=str(e))}
paynode_sdk/webhook.py CHANGED
@@ -3,7 +3,7 @@ PayNode Webhook Notifier — monitors on-chain PaymentReceived events
3
3
  and delivers structured webhook POSTs to a merchant's endpoint.
4
4
 
5
5
  Features:
6
- - HMAC-SHA256 signature for authenticity (header: x-paynode-signature)
6
+ - HMAC-SHA256 signature for authenticity (header: X-402-Signature)
7
7
  - Configurable polling interval
8
8
  - Automatic retry with exponential backoff (3 attempts)
9
9
  - Async-first design
@@ -73,7 +73,7 @@ class PayNodeWebhookNotifier:
73
73
  Usage:
74
74
  notifier = PayNodeWebhookNotifier(
75
75
  rpc_url="https://mainnet.base.org",
76
- contract_address="0x92e20164FC457a2aC35f53D06268168e6352b200",
76
+ contract_address="0x4A73696ccF76E7381b044cB95127B3784369Ed63",
77
77
  webhook_url="https://myshop.com/api/paynode-webhook",
78
78
  webhook_secret="whsec_mysecretkey123",
79
79
  )
@@ -201,9 +201,9 @@ class PayNodeWebhookNotifier:
201
201
 
202
202
  headers = {
203
203
  "Content-Type": "application/json",
204
- "x-paynode-signature": f"sha256={signature}",
205
- "x-paynode-event": "payment.received",
206
- "x-paynode-delivery-id": f"{event.tx_hash}-{attempt}",
204
+ "X-402-Signature": f"sha256={signature}",
205
+ "X-402-Event": "payment.received",
206
+ "X-402-Delivery-Id": f"{event.tx_hash}-{attempt}",
207
207
  **self.custom_headers,
208
208
  }
209
209
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: paynode-sdk-python
3
- Version: 2.0.0
3
+ Version: 2.2.0
4
4
  Summary: PayNode Protocol Python SDK for AI Agents
5
5
  Author-email: PayNodeLabs <contact@paynode.dev>
6
6
  License: MIT
@@ -16,6 +16,8 @@ Provides-Extra: test
16
16
  Requires-Dist: pytest>=7.0.0; extra == "test"
17
17
  Requires-Dist: responses>=0.23.0; extra == "test"
18
18
  Requires-Dist: pytest-mock>=3.10.0; extra == "test"
19
+ Provides-Extra: webhook
20
+ Requires-Dist: aiohttp>=3.9.0; extra == "webhook"
19
21
  Dynamic: license-file
20
22
 
21
23
  # PayNode Python SDK
@@ -23,7 +25,7 @@ Dynamic: license-file
23
25
  [![Official Documentation](https://img.shields.io/badge/Docs-docs.paynode.dev-00ff88?style=for-the-badge&logo=readthedocs)](https://docs.paynode.dev)
24
26
  [![PyPI Version](https://img.shields.io/pypi/v/paynode-sdk-python.svg?style=for-the-badge)](https://pypi.org/project/paynode-sdk-python/)
25
27
 
26
- The official Python SDK for the **PayNode Protocol (v3.1)**. PayNode allows autonomous AI Agents to seamlessly pay for APIs and computational resources using USDC on Base L2, utilizing the standardized HTTP 402 protocol with support for both on-chain receipts and off-chain signatures (EIP-3009).
28
+ The official Python SDK for the **PayNode Protocol (v2.2.0)**. PayNode allows autonomous AI Agents to seamlessly pay for APIs and computational resources using USDC on Base L2, utilizing the standardized HTTP 402 protocol with support for both on-chain receipts and off-chain signatures (EIP-3009).
27
29
 
28
30
  ## 📖 Read the Docs
29
31
 
@@ -54,7 +56,7 @@ response = agent.request_gate("https://api.merchant.com/premium-data", method="P
54
56
  print(response.json())
55
57
  ```
56
58
 
57
- ### Key Features (v2.0)
59
+ ### Key Features (v2.2.0)
58
60
  - **EIP-3009 Support**: Sign payments off-chain using `TransferWithAuthorization`, allowing for gasless or relayer-mediated settlement.
59
61
  - **X402 V2 Protocol**: JSON-based handshake for more structured and machine-readable payment instructions.
60
62
  - **Dual Flow**: Automatic fallback to V1 (on-chain receipts) for legacy merchant support.
@@ -0,0 +1,13 @@
1
+ paynode_sdk/__init__.py,sha256=dDP3qUvuhpyeUcCRRIeaHMifaYPE_p6IwZjcmaHgAHU,1187
2
+ paynode_sdk/client.py,sha256=QMCm7y0bltBqToZvHl5VRjEmF7rlpF9SWpa1Wei5WHg,19543
3
+ paynode_sdk/constants.py,sha256=MaFiVkzD7pahM9Wn29Qgb9i5hvOafKpndJ8U4QylpWA,6346
4
+ paynode_sdk/errors.py,sha256=dpPXm-e0dHKOYkY87-DCB5LBf79hqPwUAaz9JmTomms,1965
5
+ paynode_sdk/idempotency.py,sha256=IOdyv8STj97EDGlwpGQnGE7K_NHRMmULvLTNTaglnB8,2450
6
+ paynode_sdk/middleware.py,sha256=plyoRmPft0r2WPuKyewYLW2V9HonhsHMTiDC7_ZYy3s,9604
7
+ paynode_sdk/verifier.py,sha256=AKy-OVwVKhShisycIywglHYgMBX1hhXFuj2zitK1EAE,14974
8
+ paynode_sdk/webhook.py,sha256=ngP0Az_-20gPVeHPzeaXuy1_AK1TTqqsmP-HCpAWEEM,8317
9
+ paynode_sdk_python-2.2.0.dist-info/licenses/LICENSE,sha256=U8RjGlEBtXN6PA-qN_N3Uh60jyu3qe26ZBmgt-LAHc4,1069
10
+ paynode_sdk_python-2.2.0.dist-info/METADATA,sha256=_n_jc3qWb3ZjKtA3dRUwzDNtsSqzhrLwaF58WIZKMHo,3835
11
+ paynode_sdk_python-2.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ paynode_sdk_python-2.2.0.dist-info/top_level.txt,sha256=c6Skc1Xx-9O-JJ7sHghLW8Kyn4hyJoVPUawH1Mu8iTU,12
13
+ paynode_sdk_python-2.2.0.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- paynode_sdk/__init__.py,sha256=dDP3qUvuhpyeUcCRRIeaHMifaYPE_p6IwZjcmaHgAHU,1187
2
- paynode_sdk/client.py,sha256=RSxF5p5k7JbFZW4dezr48Wb4NYKiPeI5bTwB5ApCYns,15530
3
- paynode_sdk/constants.py,sha256=-hR1P9B80-aQ0kCbY9hJEb1tpiRia1F0VCki9LpG7zs,1429
4
- paynode_sdk/errors.py,sha256=9Mnyctt-ekcbNAkgKi45i0UZr8QXDEBwLpQTNtuz9i0,1965
5
- paynode_sdk/idempotency.py,sha256=SZujxMC_YgrH7qpd8aW2cKp6qFaZCzLY888IIAk-iVc,2061
6
- paynode_sdk/middleware.py,sha256=0g1kT_mnwXq4c3O5Dkzxl_d6CQrhnfsfzyMqL7M4JOU,5814
7
- paynode_sdk/verifier.py,sha256=S1sy4dwBFhl9EWORcwAsM3g92a4ljsgUJBzTpYsTG9c,13743
8
- paynode_sdk/webhook.py,sha256=djpcsqWAlWts2Y-l2bS4_Rjbd_9FCir0rn3J48hLvnY,8333
9
- paynode_sdk_python-2.0.0.dist-info/licenses/LICENSE,sha256=U8RjGlEBtXN6PA-qN_N3Uh60jyu3qe26ZBmgt-LAHc4,1069
10
- paynode_sdk_python-2.0.0.dist-info/METADATA,sha256=Dn3wUeBZ75LE0Dg97c0bjNmQs8-Jz4RSkgdVNAZHvwU,3757
11
- paynode_sdk_python-2.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
- paynode_sdk_python-2.0.0.dist-info/top_level.txt,sha256=c6Skc1Xx-9O-JJ7sHghLW8Kyn4hyJoVPUawH1Mu8iTU,12
13
- paynode_sdk_python-2.0.0.dist-info/RECORD,,