@paynodelabs/sdk-js 2.0.1 → 2.1.1

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.
package/README.md CHANGED
@@ -33,7 +33,7 @@ async function main() {
33
33
  main();
34
34
  ```
35
35
 
36
- ### Key Features (v2.0)
36
+ ### Key Features (v2.1)
37
37
  - **Zero-Wait Checkout**: API response speed drops from 5 seconds to **under 50ms** by using local signatures instead of waiting for on-chain inclusion.
38
38
  - **Double-Spend Protection**:
39
39
  - **L1 (Memory)**: High-speed local replay protection via `IdempotencyStore`.
package/dist/client.d.ts CHANGED
@@ -7,6 +7,7 @@ export declare class PayNodeAgentClient {
7
7
  private provider;
8
8
  private rpcUrls;
9
9
  private maxRetries;
10
+ private nonceLock;
10
11
  private ERC20_ABI;
11
12
  private ROUTER_ABI;
12
13
  constructor(privateKey: string, rpcUrls?: string | string[], maxRetries?: number);
@@ -15,12 +16,13 @@ export declare class PayNodeAgentClient {
15
16
  private _handleX402V2;
16
17
  signTransferWithAuthorization(tokenAddr: string, to: string, amount: bigint, validAfter: number, validBefore: number, nonce: string, extra?: Record<string, any>): Promise<ExactEVMPayload>;
17
18
  pay(contractAddr: string, tokenAddr: string, merchantAddr: string, amount: bigint, orderId: string): Promise<string>;
18
- payWithPermit(contractAddr: string, tokenAddr: string, merchantAddr: string, amount: bigint, orderId: string): Promise<string>;
19
- signPermit(tokenAddr: string, spenderAddr: string, amount: bigint, deadlineSeconds?: number): Promise<{
19
+ payWithPermit(contractAddr: string, tokenAddr: string, merchantAddr: string, amount: bigint, orderId: string, version?: string): Promise<string>;
20
+ signPermit(tokenAddr: string, spenderAddr: string, amount: bigint, deadlineSeconds?: number, version?: string): Promise<{
20
21
  deadline: number;
21
22
  v: 27 | 28;
22
23
  r: string;
23
24
  s: string;
24
25
  }>;
26
+ private _lockNonce;
25
27
  }
26
28
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,eAAe,EAEhB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAID,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,OAAO,CAAW;IAC1B,OAAO,CAAC,UAAU,CAAS;IAE3B,OAAO,CAAC,SAAS,CAMf;IAEF,OAAO,CAAC,UAAU,CAGhB;gBAEU,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,MAAM,GAAG,MAAM,EAAkB,EAAE,UAAU,GAAE,MAAU;YAepF,eAAe;IAgCvB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,QAAQ,CAAC;YAiDjE,aAAa;IAqFrB,6BAA6B,CACjC,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAC9B,OAAO,CAAC,eAAe,CAAC;IA6CrB,GAAG,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAepH,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAwB9H,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAE,MAAa;;;;;;CAwCxG"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,eAAe,EAEhB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAID,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,OAAO,CAAW;IAC1B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAoC;IAErD,OAAO,CAAC,SAAS,CAMf;IAEF,OAAO,CAAC,UAAU,CAAsB;gBAE5B,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,MAAM,GAAG,MAAM,EAAkB,EAAE,UAAU,GAAE,MAAU;YAepF,eAAe;IAgCvB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,QAAQ,CAAC;YAoDjE,aAAa;IAuGrB,6BAA6B,CACjC,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAC9B,OAAO,CAAC,eAAe,CAAC;IA6CrB,GAAG,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAqBpH,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,MAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IA8BrJ,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAE,MAAa,EAAE,OAAO,GAAE,MAAY;;;;;;YAyChH,UAAU;CAYzB"}
package/dist/client.js CHANGED
@@ -10,6 +10,7 @@ class PayNodeAgentClient {
10
10
  provider;
11
11
  rpcUrls;
12
12
  maxRetries;
13
+ nonceLock = Promise.resolve();
13
14
  ERC20_ABI = [
14
15
  "function approve(address spender, uint256 value) public returns (bool)",
15
16
  "function allowance(address owner, address spender) public view returns (uint256)",
@@ -17,10 +18,7 @@ class PayNodeAgentClient {
17
18
  "function name() view returns (string)",
18
19
  "function nonces(address owner) view returns (uint256)"
19
20
  ];
20
- ROUTER_ABI = [
21
- "function pay(address token, address merchant, uint256 amount, bytes32 orderId) public",
22
- "function payWithPermit(address payer, address token, address merchant, uint256 amount, bytes32 orderId, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public"
23
- ];
21
+ ROUTER_ABI = constants_1.PAYNODE_ROUTER_ABI;
24
22
  constructor(privateKey, rpcUrls = constants_1.BASE_RPC_URLS, maxRetries = 3) {
25
23
  this.rpcUrls = Array.isArray(rpcUrls) ? rpcUrls : [rpcUrls];
26
24
  this.maxRetries = maxRetries;
@@ -38,7 +36,7 @@ class PayNodeAgentClient {
38
36
  for (let attempt = 0; attempt < this.maxRetries; attempt++) {
39
37
  try {
40
38
  const response = await fetch(url, options);
41
- if (!RETRYABLE_STATUS_CODES.has(response.status)) {
39
+ if (!response || !RETRYABLE_STATUS_CODES.has(response.status)) {
42
40
  return response;
43
41
  }
44
42
  if (attempt < this.maxRetries - 1) {
@@ -82,7 +80,10 @@ class PayNodeAgentClient {
82
80
  }
83
81
  else if (b64Required) {
84
82
  try {
85
- body = JSON.parse(Buffer.from(b64Required, 'base64').toString());
83
+ const decoded = typeof globalThis.Buffer !== 'undefined'
84
+ ? globalThis.Buffer.from(b64Required, 'base64').toString()
85
+ : atob(b64Required);
86
+ body = JSON.parse(decoded);
86
87
  }
87
88
  catch (e) {
88
89
  console.debug('⚠️ [PayNode-JS] Failed to parse X-402-Required header:', e);
@@ -112,7 +113,12 @@ class PayNodeAgentClient {
112
113
  // Select suitable requirement
113
114
  const requirement = requirements.accepts.find((req) => req.network === caip2ChainId);
114
115
  if (!requirement) {
115
- throw new errors_1.PayNodeException(errors_1.ErrorCode.InternalError, `No compatible payment requirement found for network ${caip2ChainId}`);
116
+ throw new errors_1.PayNodeException(errors_1.ErrorCode.TransactionFailed, `No compatible payment requirement found for network ${caip2ChainId}`);
117
+ }
118
+ // 🛡️ Token Whitelist Check (Case-insensitive)
119
+ const chainTokens = constants_1.ACCEPTED_TOKENS[chainId]?.map(t => t.toLowerCase());
120
+ if (chainTokens && !chainTokens.includes(requirement.asset.toLowerCase())) {
121
+ throw new errors_1.PayNodeException(errors_1.ErrorCode.TokenNotAccepted, `Token ${requirement.asset} is not in the whitelist for chain ${chainId}`);
116
122
  }
117
123
  const orderId = requirement.orderId || requirements.orderId || url;
118
124
  // Dust limit check
@@ -144,10 +150,16 @@ class PayNodeAgentClient {
144
150
  const allowance = await tokenContract.allowance(this.wallet.address, routerAddr);
145
151
  let txHash;
146
152
  if (allowance >= amount) {
147
- txHash = await this.pay(routerAddr, requirement.asset, requirement.payTo, amount, orderId);
153
+ try {
154
+ txHash = await this.pay(routerAddr, requirement.asset, requirement.payTo, amount, orderId);
155
+ }
156
+ catch (e) {
157
+ console.warn(`⚠️ [PayNode-JS] Direct pay failed (possibly allowance race), falling back to permit:`, e);
158
+ txHash = await this.payWithPermit(routerAddr, requirement.asset, requirement.payTo, amount, orderId, requirement.extra?.version || '2');
159
+ }
148
160
  }
149
161
  else {
150
- txHash = await this.payWithPermit(routerAddr, requirement.asset, requirement.payTo, amount, orderId);
162
+ txHash = await this.payWithPermit(routerAddr, requirement.asset, requirement.payTo, amount, orderId, requirement.extra?.version || '2');
151
163
  }
152
164
  payload = {
153
165
  version: "3.1",
@@ -156,7 +168,10 @@ class PayNodeAgentClient {
156
168
  payload: { txHash }
157
169
  };
158
170
  }
159
- const b64Payload = Buffer.from(JSON.stringify(payload)).toString('base64');
171
+ const json = JSON.stringify(payload);
172
+ const b64Payload = typeof globalThis.Buffer !== 'undefined'
173
+ ? globalThis.Buffer.from(json).toString('base64')
174
+ : btoa(json);
160
175
  const retryOptions = {
161
176
  ...options,
162
177
  headers: {
@@ -166,7 +181,11 @@ class PayNodeAgentClient {
166
181
  'X-402-Order-Id': orderId
167
182
  }
168
183
  };
169
- return await this._fetchWithRetry(url, retryOptions);
184
+ const retryResponse = await this._fetchWithRetry(url, retryOptions);
185
+ if (retryResponse.status === 402) {
186
+ throw new errors_1.PayNodeException(errors_1.ErrorCode.TransactionFailed, "Still 402 after payment attempt. The server may have rejected the payment or authorization.");
187
+ }
188
+ return retryResponse;
170
189
  }
171
190
  async signTransferWithAuthorization(tokenAddr, to, amount, validAfter, validBefore, nonce, extra = {}) {
172
191
  const network = await this.provider.getNetwork();
@@ -208,28 +227,42 @@ class PayNodeAgentClient {
208
227
  };
209
228
  }
210
229
  async pay(contractAddr, tokenAddr, merchantAddr, amount, orderId) {
211
- const router = new ethers_1.ethers.Contract(contractAddr, this.ROUTER_ABI, this.wallet);
212
- const orderIdBytes = ethers_1.ethers.id(orderId);
213
- const feeData = await this.provider.getFeeData();
214
- const gasPrice = (feeData.gasPrice * 120n) / 100n;
215
- const tx = await router.pay(tokenAddr, merchantAddr, amount, orderIdBytes, {
216
- gasPrice,
217
- gasLimit: 200000
218
- });
219
- const receipt = await tx.wait();
220
- return receipt.hash;
230
+ const unlock = await this._lockNonce();
231
+ try {
232
+ const router = new ethers_1.ethers.Contract(contractAddr, this.ROUTER_ABI, this.wallet);
233
+ // convention: we hash the raw orderId string to bytes32 internally
234
+ const orderIdBytes = ethers_1.ethers.id(orderId);
235
+ const feeData = await this.provider.getFeeData();
236
+ const gasPrice = (feeData.gasPrice * 120n) / 100n;
237
+ const tx = await router.pay(tokenAddr, merchantAddr, amount, orderIdBytes, {
238
+ gasPrice,
239
+ gasLimit: 200000
240
+ });
241
+ const receipt = await tx.wait();
242
+ return receipt.hash;
243
+ }
244
+ finally {
245
+ unlock();
246
+ }
221
247
  }
222
- async payWithPermit(contractAddr, tokenAddr, merchantAddr, amount, orderId) {
223
- const sig = await this.signPermit(tokenAddr, contractAddr, amount);
224
- const router = new ethers_1.ethers.Contract(contractAddr, this.ROUTER_ABI, this.wallet);
225
- const orderIdBytes = ethers_1.ethers.id(orderId);
226
- const feeData = await this.provider.getFeeData();
227
- const gasPrice = (feeData.gasPrice * 120n) / 100n;
228
- const tx = await router.payWithPermit(this.wallet.address, tokenAddr, merchantAddr, amount, orderIdBytes, sig.deadline, sig.v, sig.r, sig.s, { gasPrice, gasLimit: 300000 });
229
- const receipt = await tx.wait();
230
- return receipt.hash;
248
+ async payWithPermit(contractAddr, tokenAddr, merchantAddr, amount, orderId, version = '2') {
249
+ const unlock = await this._lockNonce();
250
+ try {
251
+ const sig = await this.signPermit(tokenAddr, contractAddr, amount, 3600, version);
252
+ const router = new ethers_1.ethers.Contract(contractAddr, this.ROUTER_ABI, this.wallet);
253
+ // convention: we hash the raw orderId string to bytes32 internally
254
+ const orderIdBytes = ethers_1.ethers.id(orderId);
255
+ const feeData = await this.provider.getFeeData();
256
+ const gasPrice = (feeData.gasPrice * 120n) / 100n;
257
+ const tx = await router.payWithPermit(this.wallet.address, tokenAddr, merchantAddr, amount, orderIdBytes, sig.deadline, sig.v, sig.r, sig.s, { gasPrice, gasLimit: 300000 });
258
+ const receipt = await tx.wait();
259
+ return receipt.hash;
260
+ }
261
+ finally {
262
+ unlock();
263
+ }
231
264
  }
232
- async signPermit(tokenAddr, spenderAddr, amount, deadlineSeconds = 3600) {
265
+ async signPermit(tokenAddr, spenderAddr, amount, deadlineSeconds = 3600, version = '2') {
233
266
  const deadline = Math.floor(Date.now() / 1000) + deadlineSeconds;
234
267
  const token = new ethers_1.ethers.Contract(tokenAddr, this.ERC20_ABI, this.wallet);
235
268
  const [name, nonce, network] = await Promise.all([
@@ -239,7 +272,7 @@ class PayNodeAgentClient {
239
272
  ]);
240
273
  const domain = {
241
274
  name,
242
- version: '1',
275
+ version,
243
276
  chainId: Number(network.chainId),
244
277
  verifyingContract: tokenAddr
245
278
  };
@@ -263,5 +296,15 @@ class PayNodeAgentClient {
263
296
  const { v, r, s } = ethers_1.ethers.Signature.from(signature);
264
297
  return { deadline, v, r, s };
265
298
  }
299
+ async _lockNonce() {
300
+ let resolver;
301
+ const nextLock = new Promise((resolve) => {
302
+ resolver = resolve;
303
+ });
304
+ const currentLock = this.nonceLock;
305
+ this.nonceLock = nextLock;
306
+ await currentLock;
307
+ return resolver;
308
+ }
266
309
  }
267
310
  exports.PayNodeAgentClient = PayNodeAgentClient;
@@ -2,11 +2,12 @@
2
2
  export declare const PAYNODE_ROUTER_ADDRESS = "0x4A73696ccF76E7381b044cB95127B3784369Ed63";
3
3
  export declare const PAYNODE_ROUTER_ADDRESS_SANDBOX = "0x24cD8b68aaC209217ff5a6ef1Bf55a59f2c8Ca6F";
4
4
  export declare const BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
5
- export declare const BASE_USDC_ADDRESS_SANDBOX = "0x109AEddD656Ed2761d1e210E179329105039c784";
5
+ export declare const BASE_USDC_ADDRESS_SANDBOX = "0x65c088EfBDB0E03185Dbe8e258Ad0cf4Ab7946b0";
6
6
  export declare const PROTOCOL_TREASURY = "0x598bF63F5449876efafa7b36b77Deb2070621C0E";
7
7
  export declare const PROTOCOL_FEE_BPS = 100;
8
8
  export declare const MIN_PAYMENT_AMOUNT: bigint;
9
9
  export declare const BASE_RPC_URLS: string[];
10
10
  export declare const BASE_RPC_URLS_SANDBOX: string[];
11
11
  export declare const ACCEPTED_TOKENS: Record<number, string[]>;
12
+ export declare const PAYNODE_ROUTER_ABI: string[];
12
13
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,eAAO,MAAM,sBAAsB,+CAA+C,CAAC;AACnF,eAAO,MAAM,8BAA8B,+CAA+C,CAAC;AAC3F,eAAO,MAAM,iBAAiB,+CAA+C,CAAC;AAC9E,eAAO,MAAM,yBAAyB,+CAA+C,CAAC;AACtF,eAAO,MAAM,iBAAiB,+CAA+C,CAAC;AAC9E,eAAO,MAAM,gBAAgB,MAAM,CAAC;AACpC,eAAO,MAAM,kBAAkB,QAAe,CAAC;AAE/C,eAAO,MAAM,aAAa,UAAmF,CAAC;AAC9G,eAAO,MAAM,qBAAqB,UAA0E,CAAC;AAE7G,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAGpD,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,eAAO,MAAM,sBAAsB,+CAA+C,CAAC;AACnF,eAAO,MAAM,8BAA8B,+CAA+C,CAAC;AAC3F,eAAO,MAAM,iBAAiB,+CAA+C,CAAC;AAC9E,eAAO,MAAM,yBAAyB,+CAA+C,CAAC;AACtF,eAAO,MAAM,iBAAiB,+CAA+C,CAAC;AAC9E,eAAO,MAAM,gBAAgB,MAAM,CAAC;AACpC,eAAO,MAAM,kBAAkB,QAAe,CAAC;AAE/C,eAAO,MAAM,aAAa,UAAmF,CAAC;AAC9G,eAAO,MAAM,qBAAqB,UAA0E,CAAC;AAE7G,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAGpD,CAAC;AACF,eAAO,MAAM,kBAAkB,UAAqsC,CAAC"}
package/dist/constants.js CHANGED
@@ -1,11 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ACCEPTED_TOKENS = exports.BASE_RPC_URLS_SANDBOX = exports.BASE_RPC_URLS = exports.MIN_PAYMENT_AMOUNT = exports.PROTOCOL_FEE_BPS = exports.PROTOCOL_TREASURY = exports.BASE_USDC_ADDRESS_SANDBOX = exports.BASE_USDC_ADDRESS = exports.PAYNODE_ROUTER_ADDRESS_SANDBOX = exports.PAYNODE_ROUTER_ADDRESS = void 0;
3
+ exports.PAYNODE_ROUTER_ABI = exports.ACCEPTED_TOKENS = exports.BASE_RPC_URLS_SANDBOX = exports.BASE_RPC_URLS = exports.MIN_PAYMENT_AMOUNT = exports.PROTOCOL_FEE_BPS = exports.PROTOCOL_TREASURY = exports.BASE_USDC_ADDRESS_SANDBOX = exports.BASE_USDC_ADDRESS = exports.PAYNODE_ROUTER_ADDRESS_SANDBOX = exports.PAYNODE_ROUTER_ADDRESS = void 0;
4
4
  /** Generated by scripts/sync-config.py */
5
5
  exports.PAYNODE_ROUTER_ADDRESS = "0x4A73696ccF76E7381b044cB95127B3784369Ed63";
6
6
  exports.PAYNODE_ROUTER_ADDRESS_SANDBOX = "0x24cD8b68aaC209217ff5a6ef1Bf55a59f2c8Ca6F";
7
7
  exports.BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
8
- exports.BASE_USDC_ADDRESS_SANDBOX = "0x109AEddD656Ed2761d1e210E179329105039c784";
8
+ exports.BASE_USDC_ADDRESS_SANDBOX = "0x65c088EfBDB0E03185Dbe8e258Ad0cf4Ab7946b0";
9
9
  exports.PROTOCOL_TREASURY = "0x598bF63F5449876efafa7b36b77Deb2070621C0E";
10
10
  exports.PROTOCOL_FEE_BPS = 100;
11
11
  exports.MIN_PAYMENT_AMOUNT = BigInt(1000);
@@ -13,5 +13,6 @@ exports.BASE_RPC_URLS = ["https://mainnet.base.org", "https://base.meowrpc.com",
13
13
  exports.BASE_RPC_URLS_SANDBOX = ["https://sepolia.base.org", "https://base-sepolia-rpc.publicnode.com"];
14
14
  exports.ACCEPTED_TOKENS = {
15
15
  8453: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],
16
- 84532: ["0x109AEddD656Ed2761d1e210E179329105039c784"]
16
+ 84532: ["0x65c088EfBDB0E03185Dbe8e258Ad0cf4Ab7946b0"]
17
17
  };
18
+ exports.PAYNODE_ROUTER_ABI = ["function MAX_BPS() public", "function MIN_PAYMENT_AMOUNT() public", "function PROTOCOL_FEE_BPS() public", "function acceptOwnership() public", "function owner() public", "function pause() public", "function paused() public", "function pay(address token, address merchant, uint256 amount, bytes32 orderId) public", "function payWithPermit(address payer, address token, address merchant, uint256 amount, bytes32 orderId, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public", "function pendingOwner() public", "function protocolTreasury() public", "function renounceOwnership() public", "function transferOwnership(address newOwner) public", "function unpause() public", "function updateTreasury(address _newTreasury) public", "event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner)", "event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)", "event Paused(address account)", "event PaymentReceived(bytes32 indexed orderId, address indexed merchant, address indexed payer, address token, uint256 amount, uint256 fee, uint256 chainId)", "event TreasuryUpdated(address indexed oldTreasury, address indexed newTreasury)", "event Unpaused(address account)"];
@@ -15,6 +15,4 @@ export interface PayNodeMiddlewareOptions {
15
15
  maxTimeoutSeconds?: number;
16
16
  }
17
17
  export declare const x402Gate: (options: PayNodeMiddlewareOptions) => (req: Request | any, res: Response | any, next: NextFunction) => Promise<any>;
18
- /** @deprecated Use x402Gate instead. */
19
- export declare const x402_gate: (options: PayNodeMiddlewareOptions) => (req: Request | any, res: Response | any, next: NextFunction) => Promise<any>;
20
18
  //# sourceMappingURL=x402.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"x402.d.ts","sourceRoot":"","sources":["../../src/middleware/x402.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAcxD,MAAM,WAAW,wBAAwB;IACvC,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,KAAK,MAAM,CAAC;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,eAAO,MAAM,QAAQ,GAAI,SAAS,wBAAwB,MAwB1C,KAAK,OAAO,GAAG,GAAG,EAAE,KAAK,QAAQ,GAAG,GAAG,EAAE,MAAM,YAAY,iBA6F1E,CAAC;AAEF,wCAAwC;AACxC,eAAO,MAAM,SAAS,YAxHY,wBAAwB,MAwB1C,KAAK,OAAO,GAAG,GAAG,EAAE,KAAK,QAAQ,GAAG,GAAG,EAAE,MAAM,YAAY,iBAgG1C,CAAC"}
1
+ {"version":3,"file":"x402.d.ts","sourceRoot":"","sources":["../../src/middleware/x402.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAcxD,MAAM,WAAW,wBAAwB;IACvC,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,KAAK,MAAM,CAAC;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,eAAO,MAAM,QAAQ,GAAI,SAAS,wBAAwB,MA6B1C,KAAK,OAAO,GAAG,GAAG,EAAE,KAAK,QAAQ,GAAG,GAAG,EAAE,MAAM,YAAY,iBA8F1E,CAAC"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.x402_gate = exports.x402Gate = void 0;
3
+ exports.x402Gate = void 0;
4
4
  const errors_1 = require("../errors");
5
5
  const verifier_1 = require("../utils/verifier");
6
6
  const ethers_1 = require("ethers");
@@ -23,7 +23,12 @@ const x402Gate = (options) => {
23
23
  rawAmount = (0, ethers_1.parseUnits)(options.price, decimals);
24
24
  }
25
25
  catch (e) {
26
- rawAmount = BigInt(Math.floor(parseFloat(options.price) * (10 ** decimals)));
26
+ // Robust fallback for non-standard number strings (avoiding floating point math)
27
+ const parts = options.price.split('.');
28
+ const integerPart = parts[0] || '0';
29
+ let fractionPart = parts[1] || '0';
30
+ fractionPart = fractionPart.slice(0, decimals).padEnd(decimals, '0');
31
+ rawAmount = BigInt(integerPart + fractionPart);
27
32
  }
28
33
  const defaultOrderIdGen = (req) => `agent_js_${Date.now()}`;
29
34
  return async (req, res, next) => {
@@ -56,7 +61,7 @@ const x402Gate = (options) => {
56
61
  tokenAddress: tokenAddress,
57
62
  amount: rawAmount.toString(),
58
63
  orderId: orderId
59
- }, unifiedPayload.type === 'eip3009' ? unifiedPayload.payload?.extra : {});
64
+ }, unifiedPayload.type === 'eip3009' ? { name: currency, version: "2" } : {});
60
65
  if (result.isValid) {
61
66
  req.paynode = { unifiedPayload, orderId };
62
67
  return next();
@@ -107,10 +112,9 @@ const x402Gate = (options) => {
107
112
  const b64Required = Buffer.from(JSON.stringify(v2Response)).toString('base64');
108
113
  if (res.set) {
109
114
  res.set('X-402-Required', b64Required);
115
+ res.set('X-402-Order-Id', orderId);
110
116
  }
111
117
  return res.status(402).json(v2Response);
112
118
  };
113
119
  };
114
120
  exports.x402Gate = x402Gate;
115
- /** @deprecated Use x402Gate instead. */
116
- exports.x402_gate = exports.x402Gate;
@@ -1 +1 @@
1
- {"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../../src/utils/idempotency.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAElE;;;OAGG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED;;;;GAIG;AACH,qBAAa,sBAAuB,YAAW,gBAAgB;IAC7D,OAAO,CAAC,KAAK,CAAkC;;IAOzC,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAYjE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3C,OAAO,CAAC,OAAO;CAQhB;AAED;;;GAGG;AACH,qBAAa,qBAAsB,YAAW,gBAAgB;IAC5D,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,MAAM,CAAS;gBAEX,WAAW,EAAE,KAAK,EAAE,MAAM,GAAE,MAAsB;IAKxD,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAMjE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAI5C"}
1
+ {"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../../src/utils/idempotency.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAElE;;;OAGG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED;;;;GAIG;AACH,qBAAa,sBAAuB,YAAW,gBAAgB;IAC7D,OAAO,CAAC,KAAK,CAAkC;;IAUzC,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAYjE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3C,OAAO,CAAC,OAAO;CAQhB;AAED;;;GAGG;AACH,qBAAa,qBAAsB,YAAW,gBAAgB;IAC5D,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,MAAM,CAAS;gBAEX,WAAW,EAAE,KAAK,EAAE,MAAM,GAAE,MAAsB;IAKxD,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAMjE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAI5C"}
@@ -10,7 +10,10 @@ class MemoryIdempotencyStore {
10
10
  cache = new Map();
11
11
  constructor() {
12
12
  // Basic cleanup interval
13
- setInterval(() => this.cleanup(), 60000);
13
+ const interval = setInterval(() => this.cleanup(), 60000);
14
+ if (interval.unref) {
15
+ interval.unref();
16
+ }
14
17
  }
15
18
  async checkAndSet(txHash, ttlSeconds) {
16
19
  const now = Math.floor(Date.now() / 1000);
@@ -20,7 +20,7 @@ export declare class PayNodeVerifier {
20
20
  private contractAddress;
21
21
  private chainId?;
22
22
  private store?;
23
- private acceptedTokens?;
23
+ private acceptedTokens;
24
24
  constructor(config: PayNodeVerifierConfig);
25
25
  private static ROUTER_ABI;
26
26
  verify(unifiedPayload: UnifiedPaymentPayload, expected: ExpectedPayment, extra?: any): Promise<{
@@ -1 +1 @@
1
- {"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../../src/utils/verifier.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAA0B,MAAM,eAAe,CAAC;AAEzE,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAEvE,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,oGAAoG;IACpG,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAqC;IACrD,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,OAAO,CAAC,CAAS;IACzB,OAAO,CAAC,KAAK,CAAC,CAAmB;IACjC,OAAO,CAAC,cAAc,CAAC,CAAc;gBAEzB,MAAM,EAAE,qBAAqB;IAmCzC,OAAO,CAAC,MAAM,CAAC,UAAU,CAEvB;IAEI,MAAM,CACV,cAAc,EAAE,qBAAqB,EACrC,QAAQ,EAAE,eAAe,EACzB,KAAK,CAAC,EAAE,GAAG,GACV,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,gBAAgB,CAAA;KAAE,CAAC;IAkCpD,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,gBAAgB,CAAA;KAAE,CAAC;IAiDlH;;;OAGG;IACG,+BAA+B,CACnC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;KACjC,EACD,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAC9B,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,gBAAgB,CAAA;KAAE,CAAC;CA6F3D"}
1
+ {"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../../src/utils/verifier.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAA0B,MAAM,eAAe,CAAC;AAEzE,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAEvE,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,oGAAoG;IACpG,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAqC;IACrD,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,OAAO,CAAC,CAAS;IACzB,OAAO,CAAC,KAAK,CAAC,CAAmB;IACjC,OAAO,CAAC,cAAc,CAAc;gBAExB,MAAM,EAAE,qBAAqB;IAyCzC,OAAO,CAAC,MAAM,CAAC,UAAU,CAEvB;IAEI,MAAM,CACV,cAAc,EAAE,qBAAqB,EACrC,QAAQ,EAAE,eAAe,EACzB,KAAK,CAAC,EAAE,GAAG,GACV,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,gBAAgB,CAAA;KAAE,CAAC;IA8BpD,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,gBAAgB,CAAA;KAAE,CAAC;IA0ElH;;;OAGG;IACG,+BAA+B,CACnC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;KACjC,EACD,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAC9B,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,gBAAgB,CAAA;KAAE,CAAC;CA0G3D"}
@@ -40,9 +40,10 @@ class PayNodeVerifier {
40
40
  else if (config.chainId) {
41
41
  tokenList = constants_1.ACCEPTED_TOKENS[config.chainId];
42
42
  }
43
- if (tokenList && tokenList.length > 0) {
44
- this.acceptedTokens = new Set(tokenList.map(t => t.toLowerCase()));
43
+ if (!tokenList || tokenList.length === 0) {
44
+ throw new errors_1.PayNodeException(errors_1.ErrorCode.InternalError, "Verifier requires either a valid chainId or acceptedTokens to initialize its whitelist");
45
45
  }
46
+ this.acceptedTokens = new Set(tokenList.map(t => t.toLowerCase()));
46
47
  }
47
48
  static ROUTER_ABI = [
48
49
  "event PaymentReceived(bytes32 indexed orderId, address indexed merchant, address indexed payer, address token, uint256 amount, uint256 fee, uint256 chainId)"
@@ -51,12 +52,8 @@ class PayNodeVerifier {
51
52
  try {
52
53
  const { type, payload, orderId } = unifiedPayload;
53
54
  if (type === 'eip3009') {
54
- const tokenAddr = expected.tokenAddress;
55
- if (!tokenAddr) {
56
- return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.TokenNotAccepted, "tokenAddress is required for eip3009 verification") };
57
- }
58
55
  const actualPayload = payload;
59
- return await this.verifyTransferWithAuthorization(tokenAddr, actualPayload, {
56
+ return await this.verifyTransferWithAuthorization(expected.tokenAddress, actualPayload, {
60
57
  to: expected.merchantAddress,
61
58
  value: expected.amount
62
59
  }, extra);
@@ -85,24 +82,43 @@ class PayNodeVerifier {
85
82
  }
86
83
  async verifyOnchainPayment(txHash, expected) {
87
84
  try {
85
+ // 1. Security Checks
86
+ if (BigInt(expected.amount) < constants_1.MIN_PAYMENT_AMOUNT) {
87
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.AmountTooLow) };
88
+ }
89
+ if (!this.acceptedTokens.has(expected.tokenAddress.toLowerCase())) {
90
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.TokenNotAccepted) };
91
+ }
88
92
  const receipt = await this.provider.getTransactionReceipt(txHash);
89
93
  if (!receipt || receipt.status === 0) {
90
94
  return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.TransactionNotFound) };
91
95
  }
92
96
  const router = new ethers_1.ethers.Interface(PayNodeVerifier.ROUTER_ABI);
97
+ // convention: we hash the raw orderId string to bytes32 internally
93
98
  const targetOrderId = ethers_1.ethers.id(expected.orderId);
94
99
  let validEventFound = false;
100
+ let routerInteracted = false;
101
+ let orderIdMismatchFound = false;
95
102
  for (const log of receipt.logs) {
96
103
  try {
104
+ if (log.address.toLowerCase() !== this.contractAddress.toLowerCase())
105
+ continue;
106
+ routerInteracted = true;
97
107
  const parsed = router.parseLog(log);
98
108
  if (parsed && parsed.name === 'PaymentReceived') {
99
109
  const { merchant, token, amount, orderId } = parsed.args;
100
- if (merchant.toLowerCase() === expected.merchantAddress.toLowerCase() &&
101
- token.toLowerCase() === expected.tokenAddress.toLowerCase() &&
102
- BigInt(amount) >= BigInt(expected.amount) &&
103
- orderId === targetOrderId) {
104
- validEventFound = true;
105
- break;
110
+ const isMerchantMatch = merchant.toLowerCase() === expected.merchantAddress.toLowerCase();
111
+ const isTokenMatch = token.toLowerCase() === expected.tokenAddress.toLowerCase();
112
+ const isAmountMatch = BigInt(amount) >= BigInt(expected.amount);
113
+ const isOrderMatch = orderId === targetOrderId;
114
+ if (isMerchantMatch && isTokenMatch && isAmountMatch) {
115
+ if (isOrderMatch) {
116
+ validEventFound = true;
117
+ break;
118
+ }
119
+ else {
120
+ orderIdMismatchFound = true;
121
+ }
106
122
  }
107
123
  }
108
124
  }
@@ -110,7 +126,13 @@ class PayNodeVerifier {
110
126
  // Skip
111
127
  }
112
128
  }
129
+ if (!routerInteracted) {
130
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.WrongContract, "Transaction did not interact with the configured PayNodeRouter contract") };
131
+ }
113
132
  if (!validEventFound) {
133
+ if (orderIdMismatchFound) {
134
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.OrderMismatch, "Payment log found but orderId does not match") };
135
+ }
114
136
  return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "No matching PaymentReceived event found") };
115
137
  }
116
138
  if (this.store) {
@@ -130,12 +152,21 @@ class PayNodeVerifier {
130
152
  * 耗时: < 50ms (仅需一次 RPC Read)
131
153
  */
132
154
  async verifyTransferWithAuthorization(tokenAddr, payload, expected, extra = {}) {
155
+ let isLocked = false;
156
+ const { signature, authorization } = payload;
157
+ const nonce = authorization?.nonce;
133
158
  try {
134
- const { signature, authorization } = payload;
135
- const { from, to, value, validAfter, validBefore, nonce } = authorization;
159
+ // 1. Security Checks
160
+ if (BigInt(expected.value) < constants_1.MIN_PAYMENT_AMOUNT) {
161
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.AmountTooLow) };
162
+ }
163
+ if (!this.acceptedTokens.has(tokenAddr.toLowerCase())) {
164
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.TokenNotAccepted) };
165
+ }
166
+ const { from, to, value, validAfter, validBefore } = authorization;
136
167
  const expectedValue = BigInt(expected.value);
137
168
  const payloadValue = BigInt(value);
138
- // 1. 基础字段与金额校验 (防粉尘攻击)
169
+ // 2. 基础字段与金额校验 (防粉尘攻击)
139
170
  if (to.toLowerCase() !== expected.to.toLowerCase()) {
140
171
  return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "Recipient mismatch") };
141
172
  }
@@ -176,8 +207,9 @@ class PayNodeVerifier {
176
207
  if (this.store) {
177
208
  const isNew = await this.store.checkAndSet(nonce, 86400); // 锁定 24 小时
178
209
  if (!isNew) {
179
- return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.DuplicateTransaction, "Nonce already used in local memory") };
210
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.DuplicateTransaction, "Nonce already used or transaction already consumed") };
180
211
  }
212
+ isLocked = true;
181
213
  }
182
214
  // ================= 核心补全:RPC 状态只读校验 (<50ms) =================
183
215
  const tokenContract = new ethers_1.ethers.Contract(tokenAddr, [
@@ -193,14 +225,13 @@ class PayNodeVerifier {
193
225
  ]);
194
226
  // 5. 校验真实余额 (防止空钱包签署有效签名)
195
227
  if (BigInt(balance) < payloadValue) {
196
- // 如果验签失败,释放内存锁
197
- if (this.store)
228
+ if (isLocked && this.store)
198
229
  await this.store.delete(nonce);
199
230
  return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "Insufficient token balance") };
200
231
  }
201
232
  // 6. 校验链上 Nonce 状态 (防止该签名已被打包结算)
202
233
  if (isNonceUsedOnChain) {
203
- if (this.store)
234
+ if (isLocked && this.store)
204
235
  await this.store.delete(nonce);
205
236
  return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.DuplicateTransaction, "Nonce already consumed on-chain") };
206
237
  }
@@ -208,6 +239,10 @@ class PayNodeVerifier {
208
239
  return { isValid: true };
209
240
  }
210
241
  catch (e) {
242
+ if (isLocked && this.store)
243
+ await this.store.delete(nonce);
244
+ if (e instanceof errors_1.PayNodeException)
245
+ return { isValid: false, error: e };
211
246
  return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InternalError, e.message) };
212
247
  }
213
248
  }
@@ -8,7 +8,7 @@ export interface WebhookConfig {
8
8
  contractAddress?: string;
9
9
  /** The merchant's webhook endpoint URL */
10
10
  webhookUrl: string;
11
- /** Secret key for HMAC-SHA256 signature (header: x-paynode-signature) */
11
+ /** Secret key for HMAC-SHA256 signature (header: X-402-Signature) */
12
12
  webhookSecret: string;
13
13
  /** Optional: chain ID for payload enrichment */
14
14
  chainId?: number;
@@ -50,7 +50,7 @@ export interface PaymentEvent {
50
50
  * ```ts
51
51
  * const notifier = new PayNodeWebhookNotifier({
52
52
  * rpcUrl: 'https://mainnet.base.org',
53
- * contractAddress: '0x92e20164FC457a2aC35f53D06268168e6352b200',
53
+ * contractAddress: '0x4A73696ccF76E7381b044cB95127B3784369Ed63',
54
54
  * webhookUrl: 'https://myshop.com/api/paynode-webhook',
55
55
  * webhookSecret: 'whsec_mysecretkey123',
56
56
  * });
@@ -1 +1 @@
1
- {"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/utils/webhook.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,aAAa,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,uDAAuD;IACvD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACtD,0DAA0D;IAC1D,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;CAC3C;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAMD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,QAAQ,CAAW;IAC3B,OAAO,CAAC,KAAK,CAAY;IACzB,OAAO,CAAC,MAAM,CAAoG;IAClH,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,YAAY,CAAkB;gBAE1B,MAAM,EAAE,aAAa;IAgBjC;;;OAGG;IACG,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY9C;;OAEG;IACH,IAAI,IAAI,IAAI;YAQE,IAAI;IAgClB,OAAO,CAAC,UAAU;IAmBlB;;OAEG;YACW,OAAO;CA8CtB"}
1
+ {"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/utils/webhook.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,aAAa,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,uDAAuD;IACvD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACtD,0DAA0D;IAC1D,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;CAC3C;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAMD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,QAAQ,CAAW;IAC3B,OAAO,CAAC,KAAK,CAAY;IACzB,OAAO,CAAC,MAAM,CAAoG;IAClH,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,YAAY,CAAkB;gBAE1B,MAAM,EAAE,aAAa;IAgBjC;;;OAGG;IACG,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY9C;;OAEG;IACH,IAAI,IAAI,IAAI;YAQE,IAAI;IAgClB,OAAO,CAAC,UAAU;IAmBlB;;OAEG;YACW,OAAO;CA8CtB"}
@@ -54,7 +54,7 @@ const PAYNODE_ABI = [
54
54
  * ```ts
55
55
  * const notifier = new PayNodeWebhookNotifier({
56
56
  * rpcUrl: 'https://mainnet.base.org',
57
- * contractAddress: '0x92e20164FC457a2aC35f53D06268168e6352b200',
57
+ * contractAddress: '0x4A73696ccF76E7381b044cB95127B3784369Ed63',
58
58
  * webhookUrl: 'https://myshop.com/api/paynode-webhook',
59
59
  * webhookSecret: 'whsec_mysecretkey123',
60
60
  * });
@@ -169,9 +169,9 @@ class PayNodeWebhookNotifier {
169
169
  .digest('hex');
170
170
  const headers = {
171
171
  'Content-Type': 'application/json',
172
- 'x-paynode-signature': `sha256=${signature}`,
173
- 'x-paynode-event': 'payment.received',
174
- 'x-paynode-delivery-id': `${event.txHash}-${attempt}`,
172
+ 'X-402-Signature': `sha256=${signature}`,
173
+ 'X-402-Event': 'payment.received',
174
+ 'X-402-Delivery-Id': `${event.txHash}-${attempt}`,
175
175
  ...(this.config.customHeaders || {})
176
176
  };
177
177
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paynodelabs/sdk-js",
3
- "version": "2.0.1",
3
+ "version": "2.1.1",
4
4
  "description": "The official JavaScript/TypeScript SDK for PayNode x402 protocol on Base L2.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -39,5 +39,8 @@
39
39
  "ts-jest": "^29.1.4",
40
40
  "ts-node": "^10.9.2",
41
41
  "typescript": "^5.4.5"
42
+ },
43
+ "overrides": {
44
+ "brace-expansion": "^5.0.5"
42
45
  }
43
- }
46
+ }