@paynodelabs/sdk-js 1.0.1 → 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PayNode Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,68 +1,84 @@
1
- # PayNode Node.js/TypeScript SDK
1
+ # PayNode JavaScript SDK
2
2
 
3
- PayNode SDK 为 Node.js 应用提供轻量级、无感集成的支付网关。基于 **x402 (Payment Required)** 协议,使您的 API 能够直接对 AI Agent 收取 USDC 微支付。
3
+ [![Official Documentation](https://img.shields.io/badge/Docs-docs.paynode.dev-00ff88?style=for-the-badge&logo=readthedocs)](https://docs.paynode.dev)
4
+ [![NPM Version](https://img.shields.io/npm/v/@paynodelabs/sdk-js.svg?style=for-the-badge)](https://www.npmjs.com/package/@paynodelabs/sdk-js)
4
5
 
5
- ## 📦 安装
6
+ The official TypeScript/JavaScript SDK for the **PayNode Protocol**. PayNode is a stateless, non-custodial M2M payment gateway that standardizes the HTTP 402 "Payment Required" flow for AI Agents, settling instantly in USDC on Base L2.
7
+
8
+ ## 📖 Read the Docs
9
+
10
+ **For complete installation guides, advanced usage, API references, and architecture details, please visit our official documentation:**
11
+ 👉 **[docs.paynode.dev](https://docs.paynode.dev)**
12
+
13
+ ## ⚡ Quick Start
14
+
15
+ ### Installation
6
16
 
7
17
  ```bash
8
- npm install @paynode/sdk
18
+ npm install @paynodelabs/sdk-js ethers
9
19
  ```
10
20
 
11
- ## 🚀 Express 集成示例
12
-
13
- 只需几行代码,即可为现有 API 路由开启支付门禁。
21
+ ### Agent Client (Payer)
14
22
 
15
23
  ```typescript
16
- import express from 'express';
17
- import { x402_gate } from '@paynode/sdk';
18
-
19
- const app = express();
20
-
21
- // 为指定路由挂载 PayNode 支付网关
22
- app.use('/api/premium-service', x402_gate({
23
- rpcUrl: "https://mainnet.base.org",
24
- contractAddress: "0x...", // PayNodeRouter 部署地址
25
- merchantAddress: "0x...", // 商家收款地址
26
- chainId: 8453, // Base Mainnet
27
- currency: "USDC",
28
- price: "0.01", // 单次调用价格 (USDC)
29
- tokenAddress: "0x8335..." // USDC 代币合约地址
30
- }));
31
-
32
- app.get('/api/premium-service', (req, res) => {
33
- res.json({ data: "This content is paid and verified on-chain." });
34
- });
35
-
36
- app.listen(3000);
24
+ import { PayNodeAgentClient } from "@paynodelabs/sdk-js";
25
+
26
+ const client = new PayNodeAgentClient("YOUR_AGENT_PRIVATE_KEY", ["https://mainnet.base.org", "https://rpc.ankr.com/base"]);
27
+
28
+ async function main() {
29
+ // Automatically handles 402 challenges, pays USDC, and retries the request
30
+ const response = await client.requestGate("https://api.merchant.com/premium-data");
31
+ console.log(await response.text());
32
+ }
33
+ main();
37
34
  ```
38
35
 
39
- ## 🏗️ 核心验证逻辑
36
+ ## 🚀 Run the Demo
40
37
 
41
- PayNode SDK 核心职责是对 Agent 提供的 `x-paynode-receipt` (Transaction Hash) 进行链上状态校验:
38
+ The SDK includes a full merchant/agent demonstration in the `examples/` directory.
42
39
 
43
- 1. **402 握手阶段:**
44
- - 当请求头中缺失 `x-paynode-receipt` 时,SDK 自动返回 `402 Payment Required`。
45
- - 响应头中包含支付所需的全部元数据 (价格、收款地址、合约、ChainId、OrderId)。
46
- 2. **链上验证阶段:**
47
- - 接收到交易哈希后,SDK 通过 RPC 实时查询交易状态。
48
- - 解析 Event Log,比对 `orderId`、`merchant`、`token` 和 `amount` 是否完全一致。
49
- - **防重放 (Anti-Replay):** 建议结合本地缓存或数据库记录已核销的 Receipt。
40
+ ### 1. Setup Environment
41
+
42
+ ```bash
43
+ cp .env.example .env
44
+ # Edit .env with your private key and RPC URLs
45
+ ```
50
46
 
51
- ## 🧪 开发与测试
47
+ ### 2. Run the Merchant Server (Express)
52
48
 
53
49
  ```bash
54
- # 进入 SDK 目录
55
- cd packages/sdk-js
50
+ npx ts-node examples/express-server.ts
51
+ ```
52
+
53
+ ### 3. Run the Agent Client
56
54
 
57
- # 安装依赖
58
- npm install
55
+ In another terminal:
59
56
 
60
- # 运行单元测试
61
- npm test
57
+ ```bash
58
+ npx ts-node examples/agent-client.ts
62
59
  ```
63
60
 
64
- ## 🔗 资源
61
+ The demo will perform a full loop: `402 Handshake -> On-chain Payment -> 200 Verification`.
62
+
63
+ ---
64
+
65
+ ## 📦 Publishing to NPM
66
+
67
+ To publish a new version of the SDK:
68
+
69
+ 1. **Build the project**:
70
+ ```bash
71
+ npm run build
72
+ ```
73
+ 2. **Login to NPM** (if not already):
74
+ ```bash
75
+ npm login
76
+ ```
77
+ 3. **Publish**:
78
+ ```bash
79
+ npm publish --access public
80
+ ```
81
+
82
+ ---
65
83
 
66
- - **目录位置:** `packages/sdk-js`
67
- - **主页:** [paynode.dev](https://paynode.dev)
68
- - **协议文档:** `/agentpay-docs/`
84
+ _Built for the Autonomous AI Economy by PayNodeLabs._
package/dist/client.d.ts CHANGED
@@ -4,14 +4,19 @@ export interface RequestOptions extends RequestInit {
4
4
  export declare class PayNodeAgentClient {
5
5
  private wallet;
6
6
  private provider;
7
+ private rpcUrls;
7
8
  private ERC20_ABI;
8
9
  private ROUTER_ABI;
9
- constructor(privateKey: string, rpcUrl: string);
10
- /**
11
- * Executes a fetch request and automatically handles the 402 Payment loop if encountered.
12
- */
10
+ constructor(privateKey: string, rpcUrls: string | string[]);
13
11
  requestGate(url: string, options?: RequestOptions): Promise<Response>;
14
12
  private handlePaymentAndRetry;
15
- private executeChainPayment;
13
+ private executeStandardPay;
14
+ private executePermitPay;
15
+ signPermit(tokenAddr: string, spenderAddr: string, amount: bigint, deadlineSeconds?: number): Promise<{
16
+ deadline: number;
17
+ v: 27 | 28;
18
+ r: string;
19
+ s: string;
20
+ }>;
16
21
  }
17
22
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,QAAQ,CAAyB;IAEzC,OAAO,CAAC,SAAS,CAIf;IAEF,OAAO,CAAC,UAAU,CAEhB;gBAEU,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAK9C;;OAEG;IACG,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,QAAQ,CAAC;YAqBjE,qBAAqB;YA6BrB,mBAAmB;CAmClC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,OAAO,CAAW;IAE1B,OAAO,CAAC,SAAS,CAMf;IAEF,OAAO,CAAC,UAAU,CAGhB;gBAEU,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE;IAcpD,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,QAAQ,CAAC;YA0BjE,qBAAqB;YAwDrB,kBAAkB;YAelB,gBAAgB;IAwBxB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAE,MAAa;;;;;;CAwCxG"}
package/dist/client.js CHANGED
@@ -2,24 +2,33 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PayNodeAgentClient = void 0;
4
4
  const ethers_1 = require("ethers");
5
+ const errors_1 = require("./errors");
5
6
  class PayNodeAgentClient {
6
7
  wallet;
7
8
  provider;
9
+ rpcUrls;
8
10
  ERC20_ABI = [
9
11
  "function approve(address spender, uint256 value) public returns (bool)",
10
12
  "function allowance(address owner, address spender) public view returns (uint256)",
11
- "function balanceOf(address account) public view returns (uint256)"
13
+ "function balanceOf(address account) public view returns (uint256)",
14
+ "function name() view returns (string)",
15
+ "function nonces(address owner) view returns (uint256)"
12
16
  ];
13
17
  ROUTER_ABI = [
14
- "function pay(address token, address merchant, uint256 amount, bytes32 orderId) public"
18
+ "function pay(address token, address merchant, uint256 amount, bytes32 orderId) public",
19
+ "function payWithPermit(address payer, address token, address merchant, uint256 amount, bytes32 orderId, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public"
15
20
  ];
16
- constructor(privateKey, rpcUrl) {
17
- this.provider = new ethers_1.ethers.JsonRpcProvider(rpcUrl);
21
+ constructor(privateKey, rpcUrls) {
22
+ this.rpcUrls = Array.isArray(rpcUrls) ? rpcUrls : [rpcUrls];
23
+ const configs = this.rpcUrls.map((url, index) => ({
24
+ provider: new ethers_1.ethers.JsonRpcProvider(url),
25
+ priority: index,
26
+ weight: 1,
27
+ stallTimeout: 3000
28
+ }));
29
+ this.provider = new ethers_1.ethers.FallbackProvider(configs);
18
30
  this.wallet = new ethers_1.ethers.Wallet(privateKey, this.provider);
19
31
  }
20
- /**
21
- * Executes a fetch request and automatically handles the 402 Payment loop if encountered.
22
- */
23
32
  async requestGate(url, options = {}) {
24
33
  const fetchOptions = { ...options };
25
34
  if (options.json && !fetchOptions.body) {
@@ -29,12 +38,19 @@ class PayNodeAgentClient {
29
38
  ...fetchOptions.headers
30
39
  };
31
40
  }
32
- let response = await fetch(url, fetchOptions);
33
- if (response.status === 402) {
34
- console.log(`💡 [PayNode-JS] 402 Payment Required detected. Handling autonomous payment...`);
35
- return await this.handlePaymentAndRetry(url, fetchOptions, response.headers);
41
+ try {
42
+ let response = await fetch(url, fetchOptions);
43
+ if (response.status === 402) {
44
+ console.log(`💡 [PayNode-JS] 402 Payment Required detected. Handling autonomous payment...`);
45
+ return await this.handlePaymentAndRetry(url, fetchOptions, response.headers);
46
+ }
47
+ return response;
48
+ }
49
+ catch (error) {
50
+ if (error instanceof errors_1.PayNodeException)
51
+ throw error;
52
+ throw new errors_1.PayNodeException(`Failed to connect to any provided RPC nodes.`, errors_1.ErrorCode.RPC_ERROR, error);
36
53
  }
37
- return response;
38
54
  }
39
55
  async handlePaymentAndRetry(url, options, headers) {
40
56
  const contractAddr = headers.get('x-paynode-contract');
@@ -43,11 +59,37 @@ class PayNodeAgentClient {
43
59
  const tokenAddr = headers.get('x-paynode-token-address');
44
60
  const orderIdStr = headers.get('x-paynode-order-id');
45
61
  if (!contractAddr || !merchantAddr || !amountStr || !tokenAddr || !orderIdStr) {
46
- throw new Error("Malformed 402 headers: missing PayNode metadata");
62
+ throw new errors_1.PayNodeException("Malformed 402 headers: missing metadata", errors_1.ErrorCode.INTERNAL_ERROR);
47
63
  }
48
64
  const amount = BigInt(amountStr);
49
- const orderIdBytes = ethers_1.ethers.id(orderIdStr);
50
- const txHash = await this.executeChainPayment(contractAddr, merchantAddr, tokenAddr, amount, orderIdBytes);
65
+ // v1.3 Constraint: Min payment protection
66
+ if (amount < 1000n) {
67
+ throw new errors_1.PayNodeException("Payment amount is below the protocol minimum (1000).", errors_1.ErrorCode.AMOUNT_TOO_LOW);
68
+ }
69
+ let txHash;
70
+ try {
71
+ const tokenContract = new ethers_1.ethers.Contract(tokenAddr, this.ERC20_ABI, this.wallet);
72
+ const [balance, allowance] = await Promise.all([
73
+ tokenContract.balanceOf(this.wallet.address),
74
+ tokenContract.allowance(this.wallet.address, contractAddr)
75
+ ]);
76
+ if (balance < amount) {
77
+ throw new errors_1.PayNodeException("Wallet lacks USDC or ETH for gas.", errors_1.ErrorCode.INSUFFICIENT_FUNDS);
78
+ }
79
+ // Protocol v1.3: Permit-First Execution
80
+ if (allowance >= amount) {
81
+ txHash = await this.executeStandardPay(contractAddr, tokenAddr, merchantAddr, amount, orderIdStr);
82
+ }
83
+ else {
84
+ console.log(`⚡ [PayNode-JS] Insufficient allowance. Attempting Permit-First payment...`);
85
+ txHash = await this.executePermitPay(contractAddr, tokenAddr, merchantAddr, amount, orderIdStr);
86
+ }
87
+ }
88
+ catch (error) {
89
+ if (error instanceof errors_1.PayNodeException)
90
+ throw error;
91
+ throw new errors_1.PayNodeException(`On-chain transaction reverted or failed.`, errors_1.ErrorCode.TRANSACTION_FAILED, error);
92
+ }
51
93
  console.log(`✅ [PayNode-JS] Payment confirmed on-chain: ${txHash}`);
52
94
  const retryOptions = {
53
95
  ...options,
@@ -59,29 +101,61 @@ class PayNodeAgentClient {
59
101
  };
60
102
  return await fetch(url, retryOptions);
61
103
  }
62
- async executeChainPayment(contractAddr, merchantAddr, tokenAddr, amount, orderId) {
63
- const tokenContract = new ethers_1.ethers.Contract(tokenAddr, this.ERC20_ABI, this.wallet);
64
- // 1. Check Balance
65
- const balance = await tokenContract.balanceOf(this.wallet.address);
66
- if (balance < amount) {
67
- throw new Error(`Insufficient USDC balance. Have: ${ethers_1.ethers.formatUnits(balance, 6)}, Need: ${ethers_1.ethers.formatUnits(amount, 6)}`);
68
- }
69
- // 2. Check and Handle Allowance
70
- const currentAllowance = await tokenContract.allowance(this.wallet.address, contractAddr);
71
- if (currentAllowance < amount) {
72
- console.log(`🔐 [PayNode-JS] Allowance too low (${currentAllowance}). Granting Infinite Approval to Router...`);
73
- const approveTx = await tokenContract.approve(contractAddr, ethers_1.ethers.MaxUint256);
74
- await approveTx.wait();
75
- console.log(`🔓 [PayNode-JS] Infinite Approval confirmed.`);
76
- }
77
- // 3. Execute Payment
78
- const routerContract = new ethers_1.ethers.Contract(contractAddr, this.ROUTER_ABI, this.wallet);
79
- // Use manually specified gas limit to avoid estimateGas issues with some RPCs
80
- const payTx = await routerContract.pay(tokenAddr, merchantAddr, amount, orderId, {
81
- gasLimit: 200000 // Safe overhead for Base
104
+ async executeStandardPay(contractAddr, tokenAddr, merchantAddr, amount, orderId) {
105
+ const router = new ethers_1.ethers.Contract(contractAddr, this.ROUTER_ABI, this.wallet);
106
+ const orderIdBytes = ethers_1.ethers.id(orderId);
107
+ const feeData = await this.provider.getFeeData();
108
+ const gasPrice = (feeData.gasPrice * 120n) / 100n; // GasPrice * 1.2
109
+ const tx = await router.pay(tokenAddr, merchantAddr, amount, orderIdBytes, {
110
+ gasPrice,
111
+ gasLimit: 200000
82
112
  });
83
- const receipt = await payTx.wait();
113
+ const receipt = await tx.wait();
114
+ return receipt.hash;
115
+ }
116
+ async executePermitPay(contractAddr, tokenAddr, merchantAddr, amount, orderId) {
117
+ const sig = await this.signPermit(tokenAddr, contractAddr, amount);
118
+ const router = new ethers_1.ethers.Contract(contractAddr, this.ROUTER_ABI, this.wallet);
119
+ const orderIdBytes = ethers_1.ethers.id(orderId);
120
+ const feeData = await this.provider.getFeeData();
121
+ const gasPrice = (feeData.gasPrice * 120n) / 100n;
122
+ const tx = await router.payWithPermit(this.wallet.address, tokenAddr, merchantAddr, amount, orderIdBytes, sig.deadline, sig.v, sig.r, sig.s, { gasPrice, gasLimit: 300000 });
123
+ const receipt = await tx.wait();
84
124
  return receipt.hash;
85
125
  }
126
+ async signPermit(tokenAddr, spenderAddr, amount, deadlineSeconds = 3600) {
127
+ const deadline = Math.floor(Date.now() / 1000) + deadlineSeconds;
128
+ const token = new ethers_1.ethers.Contract(tokenAddr, this.ERC20_ABI, this.wallet);
129
+ const [name, nonce, network] = await Promise.all([
130
+ token.name(),
131
+ token.nonces(this.wallet.address),
132
+ this.provider.getNetwork()
133
+ ]);
134
+ const domain = {
135
+ name,
136
+ version: '1', // USDC on Base uses version 1
137
+ chainId: Number(network.chainId),
138
+ verifyingContract: tokenAddr
139
+ };
140
+ const types = {
141
+ Permit: [
142
+ { name: 'owner', type: 'address' },
143
+ { name: 'spender', type: 'address' },
144
+ { name: 'value', type: 'uint256' },
145
+ { name: 'nonce', type: 'uint256' },
146
+ { name: 'deadline', type: 'uint256' },
147
+ ]
148
+ };
149
+ const value = {
150
+ owner: this.wallet.address,
151
+ spender: spenderAddr,
152
+ value: amount,
153
+ nonce,
154
+ deadline
155
+ };
156
+ const signature = await this.wallet.signTypedData(domain, types, value);
157
+ const { v, r, s } = ethers_1.ethers.Signature.from(signature);
158
+ return { deadline, v, r, s };
159
+ }
86
160
  }
87
161
  exports.PayNodeAgentClient = PayNodeAgentClient;
@@ -0,0 +1,12 @@
1
+ /** Generated by scripts/sync-config.py */
2
+ export declare const PAYNODE_ROUTER_ADDRESS = "0x92e20164FC457a2aC35f53D06268168e6352b200";
3
+ export declare const PAYNODE_ROUTER_ADDRESS_SANDBOX = "0xB587Bc36aaCf65962eCd6Ba59e2DA76f2f575408";
4
+ export declare const BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
5
+ export declare const BASE_USDC_ADDRESS_SANDBOX = "0xeAC1f2C7099CdaFfB91Aa3b8Ffd653Ef16935798";
6
+ export declare const PROTOCOL_TREASURY = "0x598bF63F5449876efafa7b36b77Deb2070621C0E";
7
+ export declare const PROTOCOL_FEE_BPS = 100;
8
+ export declare const MIN_PAYMENT_AMOUNT: bigint;
9
+ export declare const BASE_RPC_URLS: string[];
10
+ export declare const BASE_RPC_URLS_SANDBOX: string[];
11
+ export declare const ACCEPTED_TOKENS: Record<number, string[]>;
12
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,17 @@
1
+ "use strict";
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;
4
+ /** Generated by scripts/sync-config.py */
5
+ exports.PAYNODE_ROUTER_ADDRESS = "0x92e20164FC457a2aC35f53D06268168e6352b200";
6
+ exports.PAYNODE_ROUTER_ADDRESS_SANDBOX = "0xB587Bc36aaCf65962eCd6Ba59e2DA76f2f575408";
7
+ exports.BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
8
+ exports.BASE_USDC_ADDRESS_SANDBOX = "0xeAC1f2C7099CdaFfB91Aa3b8Ffd653Ef16935798";
9
+ exports.PROTOCOL_TREASURY = "0x598bF63F5449876efafa7b36b77Deb2070621C0E";
10
+ exports.PROTOCOL_FEE_BPS = 100;
11
+ exports.MIN_PAYMENT_AMOUNT = BigInt(1000);
12
+ exports.BASE_RPC_URLS = ["https://mainnet.base.org", "https://base.meowrpc.com", "https://1rpc.io/base"];
13
+ exports.BASE_RPC_URLS_SANDBOX = ["https://sepolia.base.org", "https://base-sepolia-rpc.publicnode.com"];
14
+ exports.ACCEPTED_TOKENS = {
15
+ 8453: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],
16
+ 84532: ["0xeAC1f2C7099CdaFfB91Aa3b8Ffd653Ef16935798"]
17
+ };
@@ -1,11 +1,22 @@
1
1
  export declare enum ErrorCode {
2
- TRANSACTION_NOT_FOUND = "TRANSACTION_NOT_FOUND",
2
+ RPC_ERROR = "RPC_ERROR",
3
+ INSUFFICIENT_FUNDS = "INSUFFICIENT_FUNDS",
4
+ AMOUNT_TOO_LOW = "AMOUNT_TOO_LOW",
5
+ TOKEN_NOT_ACCEPTED = "TOKEN_NOT_ACCEPTED",
3
6
  TRANSACTION_FAILED = "TRANSACTION_FAILED",
7
+ DUPLICATE_TRANSACTION = "DUPLICATE_TRANSACTION",
8
+ INVALID_RECEIPT = "INVALID_RECEIPT",
9
+ INTERNAL_ERROR = "INTERNAL_ERROR",
10
+ TRANSACTION_NOT_FOUND = "TRANSACTION_NOT_FOUND",
4
11
  WRONG_CONTRACT = "WRONG_CONTRACT",
5
12
  ORDER_MISMATCH = "ORDER_MISMATCH",
6
- INSUFFICIENT_FUNDS = "INSUFFICIENT_FUNDS",
7
13
  RECEIPT_ALREADY_USED = "RECEIPT_ALREADY_USED",
8
- INVALID_RECEIPT = "INVALID_RECEIPT",
9
14
  MISSING_RECEIPT = "MISSING_RECEIPT"
10
15
  }
16
+ export declare class PayNodeException extends Error {
17
+ message: string;
18
+ code: ErrorCode;
19
+ details?: any | undefined;
20
+ constructor(message: string, code: ErrorCode, details?: any | undefined);
21
+ }
11
22
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"AAAA,oBAAY,SAAS;IACnB,qBAAqB,0BAA0B;IAC/C,kBAAkB,uBAAuB;IACzC,cAAc,mBAAmB;IACjC,cAAc,mBAAmB;IACjC,kBAAkB,uBAAuB;IACzC,oBAAoB,yBAAyB;IAC7C,eAAe,oBAAoB;IACnC,eAAe,oBAAoB;CACpC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"AAAA,oBAAY,SAAS;IACnB,SAAS,cAAc;IACvB,kBAAkB,uBAAuB;IACzC,cAAc,mBAAmB;IACjC,kBAAkB,uBAAuB;IACzC,kBAAkB,uBAAuB;IACzC,qBAAqB,0BAA0B;IAC/C,eAAe,oBAAoB;IACnC,cAAc,mBAAmB;IAEjC,qBAAqB,0BAA0B;IAC/C,cAAc,mBAAmB;IACjC,cAAc,mBAAmB;IACjC,oBAAoB,yBAAyB;IAC7C,eAAe,oBAAoB;CACpC;AAED,qBAAa,gBAAiB,SAAQ,KAAK;IACtB,OAAO,EAAE,MAAM;IAAS,IAAI,EAAE,SAAS;IAAS,OAAO,CAAC,EAAE,GAAG;gBAA7D,OAAO,EAAE,MAAM,EAAS,IAAI,EAAE,SAAS,EAAS,OAAO,CAAC,EAAE,GAAG,YAAA;CAIjF"}
@@ -1,14 +1,33 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ErrorCode = void 0;
3
+ exports.PayNodeException = exports.ErrorCode = void 0;
4
4
  var ErrorCode;
5
5
  (function (ErrorCode) {
6
- ErrorCode["TRANSACTION_NOT_FOUND"] = "TRANSACTION_NOT_FOUND";
6
+ ErrorCode["RPC_ERROR"] = "RPC_ERROR";
7
+ ErrorCode["INSUFFICIENT_FUNDS"] = "INSUFFICIENT_FUNDS";
8
+ ErrorCode["AMOUNT_TOO_LOW"] = "AMOUNT_TOO_LOW";
9
+ ErrorCode["TOKEN_NOT_ACCEPTED"] = "TOKEN_NOT_ACCEPTED";
7
10
  ErrorCode["TRANSACTION_FAILED"] = "TRANSACTION_FAILED";
11
+ ErrorCode["DUPLICATE_TRANSACTION"] = "DUPLICATE_TRANSACTION";
12
+ ErrorCode["INVALID_RECEIPT"] = "INVALID_RECEIPT";
13
+ ErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
14
+ // Verification specific
15
+ ErrorCode["TRANSACTION_NOT_FOUND"] = "TRANSACTION_NOT_FOUND";
8
16
  ErrorCode["WRONG_CONTRACT"] = "WRONG_CONTRACT";
9
17
  ErrorCode["ORDER_MISMATCH"] = "ORDER_MISMATCH";
10
- ErrorCode["INSUFFICIENT_FUNDS"] = "INSUFFICIENT_FUNDS";
11
18
  ErrorCode["RECEIPT_ALREADY_USED"] = "RECEIPT_ALREADY_USED";
12
- ErrorCode["INVALID_RECEIPT"] = "INVALID_RECEIPT";
13
19
  ErrorCode["MISSING_RECEIPT"] = "MISSING_RECEIPT";
14
20
  })(ErrorCode || (exports.ErrorCode = ErrorCode = {}));
21
+ class PayNodeException extends Error {
22
+ message;
23
+ code;
24
+ details;
25
+ constructor(message, code, details) {
26
+ super(message);
27
+ this.message = message;
28
+ this.code = code;
29
+ this.details = details;
30
+ this.name = "PayNodeException";
31
+ }
32
+ }
33
+ exports.PayNodeException = PayNodeException;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from './middleware/x402';
2
2
  export * from './utils/verifier';
3
3
  export * from './utils/idempotency';
4
+ export * from './utils/webhook';
4
5
  export * from './client';
6
+ export * from './errors';
5
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC"}
package/dist/index.js CHANGED
@@ -17,4 +17,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./middleware/x402"), exports);
18
18
  __exportStar(require("./utils/verifier"), exports);
19
19
  __exportStar(require("./utils/idempotency"), exports);
20
+ __exportStar(require("./utils/webhook"), exports);
20
21
  __exportStar(require("./client"), exports);
22
+ __exportStar(require("./errors"), exports);
@@ -1,6 +1,16 @@
1
- import { NextFunction } from 'express';
2
- export interface PayNodeOptions {
3
- [key: string]: any;
1
+ import { Request, NextFunction } from 'express';
2
+ import { IdempotencyStore } from '../utils/idempotency';
3
+ export interface PayNodeMiddlewareOptions {
4
+ rpcUrls: string | string[];
5
+ chainId: number;
6
+ contractAddress: string;
7
+ merchantAddress: string;
8
+ tokenAddress: string;
9
+ currency: string;
10
+ price: string;
11
+ decimals: number;
12
+ store?: IdempotencyStore;
13
+ generateOrderId?: (req: Request | any) => string;
4
14
  }
5
- export declare const x402_gate: (options: PayNodeOptions) => (req: any, res: any, next: NextFunction) => Promise<any>;
15
+ export declare const x402_gate: (options: PayNodeMiddlewareOptions) => (req: any, res: any, next: NextFunction) => Promise<any>;
6
16
  //# sourceMappingURL=x402.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"x402.d.ts","sourceRoot":"","sources":["../../src/middleware/x402.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,YAAY,EAAE,MAAM,SAAS,CAAC;AAG1D,MAAM,WAAW,cAAc;IAAG,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CAAE;AAEvD,eAAO,MAAM,SAAS,GAAI,SAAS,cAAc,MACjC,KAAK,GAAG,EAAE,KAAK,GAAG,EAAE,MAAM,YAAY,iBA2BrD,CAAC"}
1
+ {"version":3,"file":"x402.d.ts","sourceRoot":"","sources":["../../src/middleware/x402.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAY,YAAY,EAAE,MAAM,SAAS,CAAC;AAG1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAGxD,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,KAAK,MAAM,CAAC;CAClD;AAED,eAAO,MAAM,SAAS,GAAI,SAAS,wBAAwB,MAgB3C,KAAK,GAAG,EAAE,KAAK,GAAG,EAAE,MAAM,YAAY,iBAwDrD,CAAC"}
@@ -2,9 +2,24 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.x402_gate = void 0;
4
4
  const errors_1 = require("../errors");
5
+ const verifier_1 = require("../utils/verifier");
6
+ const ethers_1 = require("ethers");
5
7
  const x402_gate = (options) => {
8
+ const verifier = new verifier_1.PayNodeVerifier({
9
+ rpcUrls: options.rpcUrls,
10
+ chainId: options.chainId,
11
+ store: options.store
12
+ });
13
+ let rawAmount;
14
+ try {
15
+ rawAmount = (0, ethers_1.parseUnits)(options.price, options.decimals);
16
+ }
17
+ catch (e) {
18
+ rawAmount = BigInt(Math.floor(parseFloat(options.price) * (10 ** options.decimals)));
19
+ }
20
+ const defaultOrderIdGen = (req) => `agent_js_${Date.now()}`;
6
21
  return async (req, res, next) => {
7
- // 兼容多种 Mock 形式
22
+ // Compatibility with different mock/real environments
8
23
  const getHeader = (name) => {
9
24
  if (req.header && typeof req.header === 'function')
10
25
  return req.header(name);
@@ -12,22 +27,50 @@ const x402_gate = (options) => {
12
27
  return req.headers[name.toLowerCase()] || req.headers[name];
13
28
  return null;
14
29
  };
15
- const txHash = getHeader('X-PayNode-TxHash');
16
- if (!txHash) {
17
- if (res.set)
30
+ const receiptHash = getHeader('x-paynode-receipt') || getHeader('X-PayNode-TxHash');
31
+ let orderId = getHeader('x-paynode-order-id');
32
+ if (!orderId) {
33
+ orderId = (options.generateOrderId || defaultOrderIdGen)(req);
34
+ }
35
+ if (!receiptHash) {
36
+ if (res.set) {
18
37
  res.set({
19
- 'x-paynode-contract': options.payNodeContractAddress,
20
- 'x-paynode-amount': '1000000',
21
- 'x-paynode-currency': 'USDC'
38
+ 'x-paynode-contract': options.contractAddress,
39
+ 'x-paynode-merchant': options.merchantAddress,
40
+ 'x-paynode-amount': rawAmount.toString(),
41
+ 'x-paynode-currency': options.currency,
42
+ 'x-paynode-token-address': options.tokenAddress,
43
+ 'x-paynode-chain-id': options.chainId.toString(),
44
+ 'x-paynode-order-id': orderId
22
45
  });
23
- return res.status(402).json({ code: errors_1.ErrorCode.MISSING_RECEIPT });
46
+ }
47
+ return res.status(402).json({
48
+ error: "Payment Required",
49
+ code: errors_1.ErrorCode.MISSING_RECEIPT,
50
+ message: "Please pay to PayNode contract and provide 'x-paynode-receipt' header.",
51
+ amount: options.price,
52
+ currency: options.currency
53
+ });
54
+ }
55
+ // Phase 2: On-chain Verification
56
+ const result = await verifier.verifyPayment(receiptHash, {
57
+ merchantAddress: options.merchantAddress,
58
+ tokenAddress: options.tokenAddress,
59
+ amount: rawAmount,
60
+ orderId: orderId
61
+ });
62
+ if (result.isValid) {
63
+ // Expose to downstream handlers
64
+ req.paynode = { receiptHash, orderId };
65
+ return next();
24
66
  }
25
- // 测试用例 3: 无效支付
26
- if (txHash === 'invalid_tx_hash') {
27
- return res.status(403).json({ code: errors_1.ErrorCode.INSUFFICIENT_FUNDS });
67
+ else {
68
+ return res.status(403).json({
69
+ error: "Forbidden",
70
+ code: result.error?.code || errors_1.ErrorCode.INVALID_RECEIPT,
71
+ message: result.error?.message || "Invalid receipt"
72
+ });
28
73
  }
29
- // 测试用例 2: 有效支付
30
- next();
31
74
  };
32
75
  };
33
76
  exports.x402_gate = x402_gate;
@@ -1,3 +1,4 @@
1
+ import type { Redis } from 'ioredis';
1
2
  export interface IdempotencyStore {
2
3
  /**
3
4
  * Attempts to mark a transaction hash as used.
@@ -8,6 +9,7 @@ export interface IdempotencyStore {
8
9
  /**
9
10
  * Default implementation for MVP.
10
11
  * Uses a Map with expiration logic.
12
+ * @deprecated Use RedisIdempotencyStore for production environments.
11
13
  */
12
14
  export declare class MemoryIdempotencyStore implements IdempotencyStore {
13
15
  private cache;
@@ -15,4 +17,14 @@ export declare class MemoryIdempotencyStore implements IdempotencyStore {
15
17
  checkAndSet(txHash: string, ttlSeconds: number): Promise<boolean>;
16
18
  private cleanup;
17
19
  }
20
+ /**
21
+ * Production-ready implementation using Redis.
22
+ * Uses `SET txHash 1 NX EX ttlSeconds` for atomic check-and-set.
23
+ */
24
+ export declare class RedisIdempotencyStore implements IdempotencyStore {
25
+ private redis;
26
+ private prefix;
27
+ constructor(redisClient: Redis, prefix?: string);
28
+ checkAndSet(txHash: string, ttlSeconds: number): Promise<boolean>;
29
+ }
18
30
  //# sourceMappingURL=idempotency.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../../src/utils/idempotency.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAED;;;GAGG;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;IAYvE,OAAO,CAAC,OAAO;CAQhB"}
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;CACnE;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;IAYvE,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;CAKxE"}
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MemoryIdempotencyStore = void 0;
3
+ exports.RedisIdempotencyStore = exports.MemoryIdempotencyStore = void 0;
4
4
  /**
5
5
  * Default implementation for MVP.
6
6
  * Uses a Map with expiration logic.
7
+ * @deprecated Use RedisIdempotencyStore for production environments.
7
8
  */
8
9
  class MemoryIdempotencyStore {
9
10
  cache = new Map();
@@ -30,3 +31,21 @@ class MemoryIdempotencyStore {
30
31
  }
31
32
  }
32
33
  exports.MemoryIdempotencyStore = MemoryIdempotencyStore;
34
+ /**
35
+ * Production-ready implementation using Redis.
36
+ * Uses `SET txHash 1 NX EX ttlSeconds` for atomic check-and-set.
37
+ */
38
+ class RedisIdempotencyStore {
39
+ redis;
40
+ prefix;
41
+ constructor(redisClient, prefix = 'paynode:tx:') {
42
+ this.redis = redisClient;
43
+ this.prefix = prefix;
44
+ }
45
+ async checkAndSet(txHash, ttlSeconds) {
46
+ const key = `${this.prefix}${txHash}`;
47
+ const result = await this.redis.set(key, '1', 'EX', ttlSeconds, 'NX');
48
+ return result === 'OK';
49
+ }
50
+ }
51
+ exports.RedisIdempotencyStore = RedisIdempotencyStore;
@@ -1,16 +1,35 @@
1
- import { ErrorCode } from '../errors';
1
+ import { PayNodeException } from '../errors';
2
+ import { IdempotencyStore } from './idempotency';
3
+ /**
4
+ * Default accepted token addresses across supported chains.
5
+ * SDK will reject any payment involving a token NOT in this whitelist,
6
+ * preventing fake-token attacks at the verification layer.
7
+ */
8
+ export declare const ACCEPTED_TOKENS: Record<string, string[]>;
9
+ /** Minimum allowed payment amount to prevent dust exploits (1000 = 0.001 USDC) */
10
+ export declare const MIN_PAYMENT_AMOUNT = 1000n;
11
+ export interface PayNodeVerifierConfig {
12
+ rpcUrls: string | string[];
13
+ chainId?: number;
14
+ store?: IdempotencyStore;
15
+ /** Override the default accepted token whitelist. If provided, only these addresses are allowed. */
16
+ acceptedTokens?: string[];
17
+ }
18
+ export interface ExpectedPayment {
19
+ merchantAddress: string;
20
+ tokenAddress: string;
21
+ amount: string | number | bigint;
22
+ orderId?: string;
23
+ }
2
24
  export declare class PayNodeVerifier {
3
- private usedHashes;
4
- constructor(config: any);
5
- verifyPayment(txHash: string, expected: any): Promise<{
25
+ private provider;
26
+ private chainId?;
27
+ private store?;
28
+ private acceptedTokens?;
29
+ constructor(config: PayNodeVerifierConfig);
30
+ verifyPayment(txHash: string, expected: ExpectedPayment): Promise<{
6
31
  isValid: boolean;
7
- error?: {
8
- code: ErrorCode;
9
- message: string;
10
- };
32
+ error?: PayNodeException;
11
33
  }>;
12
34
  }
13
- export interface ExpectedPayment {
14
- [key: string]: any;
15
- }
16
35
  //# sourceMappingURL=verifier.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../../src/utils/verifier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAKtC,qBAAa,eAAe;IAC1B,OAAO,CAAC,UAAU,CAAqB;gBAC3B,MAAM,EAAE,GAAG;IAEjB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE;YAAE,IAAI,EAAE,SAAS,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;CAgBhI;AACD,MAAM,WAAW,eAAe;IAAG,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CAAE"}
1
+ {"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../../src/utils/verifier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAExD,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjD;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CASpD,CAAC;AAEF,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,QAAQ,CAAC;AAExC,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,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;AAQD,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAqC;IACrD,OAAO,CAAC,OAAO,CAAC,CAAS;IACzB,OAAO,CAAC,KAAK,CAAC,CAAmB;IACjC,OAAO,CAAC,cAAc,CAAC,CAAc;gBAEzB,MAAM,EAAE,qBAAqB;IAiCnC,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,gBAAgB,CAAA;KAAE,CAAC;CA6ExH"}
@@ -1,30 +1,134 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PayNodeVerifier = void 0;
3
+ exports.PayNodeVerifier = exports.MIN_PAYMENT_AMOUNT = exports.ACCEPTED_TOKENS = void 0;
4
4
  const errors_1 = require("../errors");
5
- // 使用全局计数器应对 Jest 重试/并发
6
- let globalCallCount = 0;
7
- globalCallCount = 0;
5
+ const ethers_1 = require("ethers");
6
+ /**
7
+ * Default accepted token addresses across supported chains.
8
+ * SDK will reject any payment involving a token NOT in this whitelist,
9
+ * preventing fake-token attacks at the verification layer.
10
+ */
11
+ exports.ACCEPTED_TOKENS = {
12
+ // Base Mainnet (chainId: 8453)
13
+ '8453': [
14
+ '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC
15
+ ],
16
+ // Base Sepolia (chainId: 84532)
17
+ '84532': [
18
+ '0xeAC1f2C7099CdaFfB91Aa3b8Ffd653Ef16935798', // USDC (Sandbox)
19
+ ],
20
+ };
21
+ /** Minimum allowed payment amount to prevent dust exploits (1000 = 0.001 USDC) */
22
+ exports.MIN_PAYMENT_AMOUNT = 1000n;
23
+ const PAYNODE_ABI = [
24
+ "event PaymentReceived(bytes32 indexed orderId, address indexed merchant, address indexed payer, address token, uint256 amount, uint256 fee, uint256 chainId)"
25
+ ];
26
+ const iface = new ethers_1.Interface(PAYNODE_ABI);
8
27
  class PayNodeVerifier {
9
- usedHashes = new Set();
10
- constructor(config) { }
28
+ provider;
29
+ chainId;
30
+ store;
31
+ acceptedTokens;
32
+ constructor(config) {
33
+ if (!config.rpcUrls || (Array.isArray(config.rpcUrls) && config.rpcUrls.length === 0)) {
34
+ throw new errors_1.PayNodeException("Failed to connect to any provided RPC nodes.", errors_1.ErrorCode.RPC_ERROR);
35
+ }
36
+ // Support RpcPool / FallbackProvider
37
+ if (Array.isArray(config.rpcUrls)) {
38
+ const providers = config.rpcUrls.map((url, i) => {
39
+ return {
40
+ provider: new ethers_1.JsonRpcProvider(url, config.chainId),
41
+ priority: i,
42
+ stallTimeout: 1500,
43
+ weight: 1
44
+ };
45
+ });
46
+ this.provider = new ethers_1.FallbackProvider(providers);
47
+ }
48
+ else {
49
+ this.provider = new ethers_1.JsonRpcProvider(config.rpcUrls, config.chainId);
50
+ }
51
+ this.chainId = config.chainId;
52
+ this.store = config.store;
53
+ let tokenList;
54
+ if (config.acceptedTokens !== undefined) {
55
+ tokenList = config.acceptedTokens;
56
+ }
57
+ else if (config.chainId) {
58
+ tokenList = exports.ACCEPTED_TOKENS[config.chainId.toString()];
59
+ }
60
+ if (tokenList && tokenList.length > 0) {
61
+ this.acceptedTokens = new Set(tokenList.map(t => t.toLowerCase()));
62
+ }
63
+ }
11
64
  async verifyPayment(txHash, expected) {
12
- if (this.usedHashes.has(txHash))
13
- return { isValid: false, error: { code: errors_1.ErrorCode.RECEIPT_ALREADY_USED, message: 'Used' } };
14
- if (txHash === '0xFakeHash')
15
- return { isValid: false, error: { code: errors_1.ErrorCode.TRANSACTION_NOT_FOUND, message: 'x' } };
16
- if (txHash === '0xFailedHash')
17
- return { isValid: false, error: { code: errors_1.ErrorCode.TRANSACTION_FAILED, message: 'x' } };
18
- if (txHash === '0xHash') {
19
- globalCallCount++;
20
- if (globalCallCount === 1)
21
- return { isValid: false, error: { code: errors_1.ErrorCode.WRONG_CONTRACT, message: 'x' } };
22
- if (globalCallCount === 2)
23
- return { isValid: false, error: { code: errors_1.ErrorCode.ORDER_MISMATCH, message: 'x' } };
24
- return { isValid: false, error: { code: errors_1.ErrorCode.INSUFFICIENT_FUNDS, message: 'x' } };
25
- }
26
- this.usedHashes.add(txHash);
27
- return { isValid: true };
65
+ try {
66
+ // 0. Dust Exploit Check (Minimum Payment)
67
+ const expectedAmount = BigInt(expected.amount);
68
+ if (expectedAmount < exports.MIN_PAYMENT_AMOUNT) {
69
+ return { isValid: false, error: new errors_1.PayNodeException("Payment amount is below the protocol minimum (1000).", errors_1.ErrorCode.AMOUNT_TOO_LOW) };
70
+ }
71
+ // 1. Token Whitelist Check (Anti-FakeToken)
72
+ if (this.acceptedTokens && !this.acceptedTokens.has(expected.tokenAddress.toLowerCase())) {
73
+ return { isValid: false, error: new errors_1.PayNodeException("The provided token address is not in the whitelist.", errors_1.ErrorCode.TOKEN_NOT_ACCEPTED) };
74
+ }
75
+ // 1. Idempotency Check
76
+ if (this.store) {
77
+ const isNew = await this.store.checkAndSet(txHash, 86400);
78
+ if (!isNew) {
79
+ return { isValid: false, error: new errors_1.PayNodeException("This transaction hash has already been consumed.", errors_1.ErrorCode.DUPLICATE_TRANSACTION) };
80
+ }
81
+ }
82
+ // 2. Fetch Receipt
83
+ const receipt = await this.provider.getTransactionReceipt(txHash);
84
+ if (!receipt) {
85
+ return { isValid: false, error: new errors_1.PayNodeException("The provided receipt (TxHash) is malformed or invalid.", errors_1.ErrorCode.INVALID_RECEIPT) };
86
+ }
87
+ if (receipt.status !== 1) {
88
+ return { isValid: false, error: new errors_1.PayNodeException("On-chain transaction reverted or failed.", errors_1.ErrorCode.TRANSACTION_FAILED) };
89
+ }
90
+ // 3. Parse Logs
91
+ let paymentLog = null;
92
+ for (const log of receipt.logs) {
93
+ try {
94
+ const parsed = iface.parseLog({ topics: log.topics, data: log.data });
95
+ if (parsed && parsed.name === 'PaymentReceived') {
96
+ paymentLog = { parsed, logAddress: log.address };
97
+ break;
98
+ }
99
+ }
100
+ catch (e) {
101
+ continue;
102
+ }
103
+ }
104
+ if (!paymentLog) {
105
+ return { isValid: false, error: new errors_1.PayNodeException("No valid PaymentReceived event found in transaction.", errors_1.ErrorCode.INVALID_RECEIPT) };
106
+ }
107
+ const args = paymentLog.parsed.args;
108
+ // 4. Verify Merchant
109
+ if (args.merchant.toLowerCase() !== expected.merchantAddress.toLowerCase()) {
110
+ return { isValid: false, error: new errors_1.PayNodeException("Payment went to a different merchant.", errors_1.ErrorCode.INVALID_RECEIPT) };
111
+ }
112
+ // 5. Verify Token
113
+ if (args.token.toLowerCase() !== expected.tokenAddress.toLowerCase()) {
114
+ return { isValid: false, error: new errors_1.PayNodeException("Payment used unexpected token.", errors_1.ErrorCode.INVALID_RECEIPT) };
115
+ }
116
+ // 6. Verify Amount
117
+ if (BigInt(args.amount) < BigInt(expected.amount)) {
118
+ return { isValid: false, error: new errors_1.PayNodeException("Payment amount is below required price.", errors_1.ErrorCode.INVALID_RECEIPT) };
119
+ }
120
+ // 7. Verify ChainId (Cross-chain replay protection)
121
+ const expectedChainId = BigInt(this.chainId || (await this.provider.getNetwork()).chainId);
122
+ if (BigInt(args.chainId) !== expectedChainId) {
123
+ return { isValid: false, error: new errors_1.PayNodeException("ChainId mismatch. Invalid network.", errors_1.ErrorCode.INVALID_RECEIPT) };
124
+ }
125
+ return { isValid: true };
126
+ }
127
+ catch (e) {
128
+ if (e instanceof errors_1.PayNodeException)
129
+ return { isValid: false, error: e };
130
+ return { isValid: false, error: new errors_1.PayNodeException(`An unexpected error occurred: ${e.message}`, errors_1.ErrorCode.INTERNAL_ERROR) };
131
+ }
28
132
  }
29
133
  }
30
134
  exports.PayNodeVerifier = PayNodeVerifier;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Configuration for the PayNode Webhook Notifier.
3
+ */
4
+ export interface WebhookConfig {
5
+ /** RPC URL to connect to the chain */
6
+ rpcUrl: string;
7
+ /** PayNode Router contract address to monitor (Defaults to Mainnet) */
8
+ contractAddress?: string;
9
+ /** The merchant's webhook endpoint URL */
10
+ webhookUrl: string;
11
+ /** Secret key for HMAC-SHA256 signature (header: x-paynode-signature) */
12
+ webhookSecret: string;
13
+ /** Optional: chain ID for payload enrichment */
14
+ chainId?: number;
15
+ /** Polling interval in milliseconds (default: 5000ms) */
16
+ pollIntervalMs?: number;
17
+ /** Optional: custom headers to send with webhook POST */
18
+ customHeaders?: Record<string, string>;
19
+ /** Optional: callback when a webhook delivery fails */
20
+ onError?: (error: Error, event: PaymentEvent) => void;
21
+ /** Optional: callback when a webhook delivery succeeds */
22
+ onSuccess?: (event: PaymentEvent) => void;
23
+ }
24
+ /**
25
+ * Parsed PaymentReceived event data.
26
+ */
27
+ export interface PaymentEvent {
28
+ txHash: string;
29
+ blockNumber: number;
30
+ orderId: string;
31
+ merchant: string;
32
+ payer: string;
33
+ token: string;
34
+ amount: string;
35
+ fee: string;
36
+ chainId: string;
37
+ timestamp: number;
38
+ }
39
+ /**
40
+ * PayNodeWebhookNotifier — listens to on-chain PaymentReceived events
41
+ * and delivers structured webhook POSTs to the merchant's endpoint.
42
+ *
43
+ * Features:
44
+ * - HMAC-SHA256 signature for authenticity verification
45
+ * - Configurable polling interval
46
+ * - Automatic retry with exponential backoff (3 attempts)
47
+ * - Error/Success callbacks
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * const notifier = new PayNodeWebhookNotifier({
52
+ * rpcUrl: 'https://mainnet.base.org',
53
+ * contractAddress: '0x92e20164FC457a2aC35f53D06268168e6352b200',
54
+ * webhookUrl: 'https://myshop.com/api/paynode-webhook',
55
+ * webhookSecret: 'whsec_mysecretkey123',
56
+ * });
57
+ * notifier.start();
58
+ * ```
59
+ */
60
+ export declare class PayNodeWebhookNotifier {
61
+ private provider;
62
+ private contract;
63
+ private iface;
64
+ private config;
65
+ private pollInterval;
66
+ private lastBlock;
67
+ private timer;
68
+ private isProcessing;
69
+ constructor(config: WebhookConfig);
70
+ /**
71
+ * Start polling for new PaymentReceived events.
72
+ * @param fromBlock Optional starting block number. Defaults to 'latest'.
73
+ */
74
+ start(fromBlock?: number): Promise<void>;
75
+ /**
76
+ * Stop polling.
77
+ */
78
+ stop(): void;
79
+ private poll;
80
+ private parseEvent;
81
+ /**
82
+ * Deliver a webhook POST with HMAC signature and retry logic.
83
+ */
84
+ private deliver;
85
+ }
86
+ //# sourceMappingURL=webhook.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PayNodeWebhookNotifier = void 0;
37
+ const ethers_1 = require("ethers");
38
+ const crypto = __importStar(require("crypto"));
39
+ const constants_1 = require("../constants");
40
+ const PAYNODE_ABI = [
41
+ "event PaymentReceived(bytes32 indexed orderId, address indexed merchant, address indexed payer, address token, uint256 amount, uint256 fee, uint256 chainId)"
42
+ ];
43
+ /**
44
+ * PayNodeWebhookNotifier — listens to on-chain PaymentReceived events
45
+ * and delivers structured webhook POSTs to the merchant's endpoint.
46
+ *
47
+ * Features:
48
+ * - HMAC-SHA256 signature for authenticity verification
49
+ * - Configurable polling interval
50
+ * - Automatic retry with exponential backoff (3 attempts)
51
+ * - Error/Success callbacks
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * const notifier = new PayNodeWebhookNotifier({
56
+ * rpcUrl: 'https://mainnet.base.org',
57
+ * contractAddress: '0x92e20164FC457a2aC35f53D06268168e6352b200',
58
+ * webhookUrl: 'https://myshop.com/api/paynode-webhook',
59
+ * webhookSecret: 'whsec_mysecretkey123',
60
+ * });
61
+ * notifier.start();
62
+ * ```
63
+ */
64
+ class PayNodeWebhookNotifier {
65
+ provider;
66
+ contract;
67
+ iface;
68
+ config;
69
+ pollInterval;
70
+ lastBlock = 0;
71
+ timer = null;
72
+ isProcessing = false;
73
+ constructor(config) {
74
+ if (!config.rpcUrl)
75
+ throw new Error('rpcUrl is required');
76
+ if (!config.webhookUrl)
77
+ throw new Error('webhookUrl is required');
78
+ if (!config.webhookSecret)
79
+ throw new Error('webhookSecret is required');
80
+ this.config = {
81
+ ...config,
82
+ contractAddress: config.contractAddress || constants_1.PAYNODE_ROUTER_ADDRESS
83
+ };
84
+ this.pollInterval = config.pollIntervalMs || 5000;
85
+ this.provider = new ethers_1.JsonRpcProvider(config.rpcUrl, config.chainId);
86
+ this.iface = new ethers_1.Interface(PAYNODE_ABI);
87
+ this.contract = new ethers_1.Contract(this.config.contractAddress, PAYNODE_ABI, this.provider);
88
+ }
89
+ /**
90
+ * Start polling for new PaymentReceived events.
91
+ * @param fromBlock Optional starting block number. Defaults to 'latest'.
92
+ */
93
+ async start(fromBlock) {
94
+ if (this.timer) {
95
+ console.warn('[PayNode Webhook] Already running.');
96
+ return;
97
+ }
98
+ this.lastBlock = fromBlock ?? (await this.provider.getBlockNumber());
99
+ console.log(`🔔 [PayNode Webhook] Listening from block ${this.lastBlock} on ${this.config.contractAddress}`);
100
+ this.timer = setInterval(() => this.poll(), this.pollInterval);
101
+ }
102
+ /**
103
+ * Stop polling.
104
+ */
105
+ stop() {
106
+ if (this.timer) {
107
+ clearInterval(this.timer);
108
+ this.timer = null;
109
+ console.log('🔕 [PayNode Webhook] Stopped.');
110
+ }
111
+ }
112
+ async poll() {
113
+ if (this.isProcessing)
114
+ return;
115
+ this.isProcessing = true;
116
+ try {
117
+ const currentBlock = await this.provider.getBlockNumber();
118
+ if (currentBlock <= this.lastBlock) {
119
+ this.isProcessing = false;
120
+ return;
121
+ }
122
+ const events = await this.contract.queryFilter('PaymentReceived', this.lastBlock + 1, currentBlock);
123
+ for (const event of events) {
124
+ const paymentEvent = this.parseEvent(event);
125
+ if (paymentEvent) {
126
+ await this.deliver(paymentEvent);
127
+ }
128
+ }
129
+ this.lastBlock = currentBlock;
130
+ }
131
+ catch (error) {
132
+ console.error(`[PayNode Webhook] Poll error: ${error.message}`);
133
+ }
134
+ finally {
135
+ this.isProcessing = false;
136
+ }
137
+ }
138
+ parseEvent(event) {
139
+ try {
140
+ return {
141
+ txHash: event.transactionHash,
142
+ blockNumber: event.blockNumber,
143
+ orderId: event.args[0], // bytes32 indexed orderId
144
+ merchant: event.args[1], // address indexed merchant
145
+ payer: event.args[2], // address indexed payer
146
+ token: event.args[3], // address token
147
+ amount: event.args[4].toString(), // uint256 amount
148
+ fee: event.args[5].toString(), // uint256 fee
149
+ chainId: event.args[6].toString(), // uint256 chainId
150
+ timestamp: Date.now()
151
+ };
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ /**
158
+ * Deliver a webhook POST with HMAC signature and retry logic.
159
+ */
160
+ async deliver(event, attempt = 1) {
161
+ const MAX_RETRIES = 3;
162
+ const payload = JSON.stringify({
163
+ event: 'payment.received',
164
+ data: event
165
+ });
166
+ const signature = crypto
167
+ .createHmac('sha256', this.config.webhookSecret)
168
+ .update(payload)
169
+ .digest('hex');
170
+ const headers = {
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}`,
175
+ ...(this.config.customHeaders || {})
176
+ };
177
+ try {
178
+ const response = await fetch(this.config.webhookUrl, {
179
+ method: 'POST',
180
+ headers,
181
+ body: payload
182
+ });
183
+ if (!response.ok) {
184
+ throw new Error(`Webhook returned ${response.status}: ${response.statusText}`);
185
+ }
186
+ console.log(`✅ [PayNode Webhook] Delivered tx ${event.txHash.slice(0, 10)}... → ${response.status}`);
187
+ this.config.onSuccess?.(event);
188
+ }
189
+ catch (error) {
190
+ console.error(`[PayNode Webhook] Delivery failed (attempt ${attempt}/${MAX_RETRIES}): ${error.message}`);
191
+ if (attempt < MAX_RETRIES) {
192
+ const backoffMs = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
193
+ await new Promise(resolve => setTimeout(resolve, backoffMs));
194
+ return this.deliver(event, attempt + 1);
195
+ }
196
+ console.error(`[PayNode Webhook] Gave up on tx ${event.txHash} after ${MAX_RETRIES} attempts.`);
197
+ this.config.onError?.(error, event);
198
+ }
199
+ }
200
+ }
201
+ exports.PayNodeWebhookNotifier = PayNodeWebhookNotifier;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paynodelabs/sdk-js",
3
- "version": "1.0.1",
3
+ "version": "1.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",
@@ -11,7 +11,8 @@
11
11
  "scripts": {
12
12
  "build": "tsc",
13
13
  "prepublishOnly": "npm run build",
14
- "test": "ts-node tests/mainnet_live.ts"
14
+ "test": "jest",
15
+ "example:server": "ts-node examples/express-server.ts"
15
16
  },
16
17
  "keywords": [
17
18
  "paynode",
@@ -24,12 +25,19 @@
24
25
  "author": "PayNodeLabs",
25
26
  "license": "MIT",
26
27
  "dependencies": {
27
- "ethers": "^6.13.0"
28
+ "ethers": "^6.13.0",
29
+ "ioredis": "^5.10.1",
30
+ "express": "^4.19.2"
28
31
  },
29
32
  "devDependencies": {
33
+ "@types/express": "^5.0.6",
34
+ "@types/ioredis": "^4.28.10",
35
+ "@types/jest": "^29.5.12",
36
+ "@types/node": "^20.12.12",
30
37
  "dotenv": "^16.4.5",
38
+ "jest": "^29.7.0",
39
+ "ts-jest": "^29.1.4",
31
40
  "ts-node": "^10.9.2",
32
- "typescript": "^5.4.5",
33
- "@types/node": "^20.12.12"
41
+ "typescript": "^5.4.5"
34
42
  }
35
43
  }