@paynodelabs/sdk-js 1.0.1 → 1.1.0

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
@@ -1,68 +1,55 @@
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.
6
7
 
7
- ```bash
8
- npm install @paynode/sdk
9
- ```
8
+ ## 📖 Read the Docs
10
9
 
11
- ## 🚀 Express 集成示例
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
12
 
13
- 只需几行代码,即可为现有 API 路由开启支付门禁。
13
+ ## Quick Start
14
14
 
15
- ```typescript
16
- import express from 'express';
17
- import { x402_gate } from '@paynode/sdk';
15
+ ### Installation
18
16
 
19
- const app = express();
17
+ ```bash
18
+ npm install @paynodelabs/sdk-js ethers
19
+ ```
20
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
- });
21
+ ### Agent Client (Payer)
35
22
 
36
- app.listen(3000);
37
- ```
23
+ ```typescript
24
+ import { PayNodeClient } from '@paynodelabs/sdk-js';
38
25
 
39
- ## 🏗️ 核心验证逻辑
26
+ const client = new PayNodeClient('YOUR_AGENT_PRIVATE_KEY');
40
27
 
41
- PayNode SDK 核心职责是对 Agent 提供的 `x-paynode-receipt` (Transaction Hash) 进行链上状态校验:
28
+ async function main() {
29
+ // Automatically handles 402 challenges, pays USDC, and retries the request
30
+ const response = await client.request('https://api.merchant.com/premium-data');
31
+ console.log(await response.text());
32
+ }
33
+ main();
34
+ ```
42
35
 
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。
36
+ ### Merchant Middleware (Receiver)
50
37
 
51
- ## 🧪 开发与测试
38
+ ```typescript
39
+ import express from 'express';
40
+ import { createPayNodeMiddleware } from '@paynodelabs/sdk-js';
52
41
 
53
- ```bash
54
- # 进入 SDK 目录
55
- cd packages/sdk-js
42
+ const app = express();
56
43
 
57
- # 安装依赖
58
- npm install
44
+ const requirePayment = createPayNodeMiddleware({
45
+ price: "1.50", // 1.50 USDC
46
+ merchantWallet: "0xYourWalletAddress..."
47
+ });
59
48
 
60
- # 运行单元测试
61
- npm test
49
+ app.get('/premium-data', requirePayment, (req, res) => {
50
+ res.json({ secret: "This is paid M2M data." });
51
+ });
62
52
  ```
63
53
 
64
- ## 🔗 资源
65
-
66
- - **目录位置:** `packages/sdk-js`
67
- - **主页:** [paynode.dev](https://paynode.dev)
68
- - **协议文档:** `/agentpay-docs/`
54
+ ---
55
+ *Built for the Autonomous AI Economy by PayNodeLabs.*
package/dist/client.d.ts CHANGED
@@ -13,5 +13,31 @@ export declare class PayNodeAgentClient {
13
13
  requestGate(url: string, options?: RequestOptions): Promise<Response>;
14
14
  private handlePaymentAndRetry;
15
15
  private executeChainPayment;
16
+ /**
17
+ * Executes a payment using EIP-2612 Permit — single-tx approve + pay.
18
+ * The payer signs the permit offline, and any relayer (e.g. AI Agent) can submit it on-chain.
19
+ * @param contractAddr PayNode Router address
20
+ * @param payerAddress The address that holds the tokens and signed the permit
21
+ * @param tokenAddr ERC20 token with EIP-2612 support (e.g. USDC)
22
+ * @param merchantAddr Merchant receiving 99% of payment
23
+ * @param amount Token amount in smallest unit (e.g. 1000000 = 1 USDC)
24
+ * @param orderId Order identifier as bytes32
25
+ * @param deadline Unix timestamp after which the permit is invalid
26
+ * @param v ECDSA recovery id
27
+ * @param r ECDSA signature component
28
+ * @param s ECDSA signature component
29
+ */
30
+ payWithPermit(contractAddr: string, payerAddress: string, tokenAddr: string, merchantAddr: string, amount: bigint, orderId: string, deadline: number, v: number, r: string, s: string): Promise<string>;
31
+ /**
32
+ * Helper: Generate an EIP-2612 Permit signature for USDC/ERC20.
33
+ * The wallet that calls this must be the token holder (payer).
34
+ * @returns { deadline, v, r, s } to pass to payWithPermit
35
+ */
36
+ signPermit(tokenAddr: string, spenderAddr: string, amount: bigint, deadlineSeconds?: number): Promise<{
37
+ deadline: number;
38
+ v: number;
39
+ r: string;
40
+ s: string;
41
+ }>;
16
42
  }
17
43
  //# 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":"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,CAGhB;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;IAoCjC;;;;;;;;;;;;;OAaG;IACG,aAAa,CACjB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,GACR,OAAO,CAAC,MAAM,CAAC;IAoBlB;;;;OAIG;IACG,UAAU,CACd,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,eAAe,GAAE,MAAa,GAC7B,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CA8ClE"}
package/dist/client.js CHANGED
@@ -11,7 +11,8 @@ class PayNodeAgentClient {
11
11
  "function balanceOf(address account) public view returns (uint256)"
12
12
  ];
13
13
  ROUTER_ABI = [
14
- "function pay(address token, address merchant, uint256 amount, bytes32 orderId) public"
14
+ "function pay(address token, address merchant, uint256 amount, bytes32 orderId) public",
15
+ "function payWithPermit(address payer, address token, address merchant, uint256 amount, bytes32 orderId, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public"
15
16
  ];
16
17
  constructor(privateKey, rpcUrl) {
17
18
  this.provider = new ethers_1.ethers.JsonRpcProvider(rpcUrl);
@@ -83,5 +84,69 @@ class PayNodeAgentClient {
83
84
  const receipt = await payTx.wait();
84
85
  return receipt.hash;
85
86
  }
87
+ /**
88
+ * Executes a payment using EIP-2612 Permit — single-tx approve + pay.
89
+ * The payer signs the permit offline, and any relayer (e.g. AI Agent) can submit it on-chain.
90
+ * @param contractAddr PayNode Router address
91
+ * @param payerAddress The address that holds the tokens and signed the permit
92
+ * @param tokenAddr ERC20 token with EIP-2612 support (e.g. USDC)
93
+ * @param merchantAddr Merchant receiving 99% of payment
94
+ * @param amount Token amount in smallest unit (e.g. 1000000 = 1 USDC)
95
+ * @param orderId Order identifier as bytes32
96
+ * @param deadline Unix timestamp after which the permit is invalid
97
+ * @param v ECDSA recovery id
98
+ * @param r ECDSA signature component
99
+ * @param s ECDSA signature component
100
+ */
101
+ async payWithPermit(contractAddr, payerAddress, tokenAddr, merchantAddr, amount, orderId, deadline, v, r, s) {
102
+ const routerContract = new ethers_1.ethers.Contract(contractAddr, this.ROUTER_ABI, this.wallet);
103
+ const tx = await routerContract.payWithPermit(payerAddress, tokenAddr, merchantAddr, amount, orderId, deadline, v, r, s, { gasLimit: 300000 });
104
+ const receipt = await tx.wait();
105
+ return receipt.hash;
106
+ }
107
+ /**
108
+ * Helper: Generate an EIP-2612 Permit signature for USDC/ERC20.
109
+ * The wallet that calls this must be the token holder (payer).
110
+ * @returns { deadline, v, r, s } to pass to payWithPermit
111
+ */
112
+ async signPermit(tokenAddr, spenderAddr, amount, deadlineSeconds = 3600) {
113
+ const deadline = Math.floor(Date.now() / 1000) + deadlineSeconds;
114
+ // EIP-2612 domain & types
115
+ const tokenContract = new ethers_1.ethers.Contract(tokenAddr, [
116
+ "function name() view returns (string)",
117
+ "function nonces(address owner) view returns (uint256)",
118
+ "function DOMAIN_SEPARATOR() view returns (bytes32)"
119
+ ], this.wallet);
120
+ const [name, nonce, chainId] = await Promise.all([
121
+ tokenContract.name(),
122
+ tokenContract.nonces(this.wallet.address),
123
+ this.provider.getNetwork().then(n => n.chainId)
124
+ ]);
125
+ const domain = {
126
+ name,
127
+ version: '2', // USDC uses version "2"
128
+ chainId: Number(chainId),
129
+ verifyingContract: tokenAddr
130
+ };
131
+ const types = {
132
+ Permit: [
133
+ { name: 'owner', type: 'address' },
134
+ { name: 'spender', type: 'address' },
135
+ { name: 'value', type: 'uint256' },
136
+ { name: 'nonce', type: 'uint256' },
137
+ { name: 'deadline', type: 'uint256' },
138
+ ]
139
+ };
140
+ const value = {
141
+ owner: this.wallet.address,
142
+ spender: spenderAddr,
143
+ value: amount,
144
+ nonce,
145
+ deadline
146
+ };
147
+ const sig = await this.wallet.signTypedData(domain, types, value);
148
+ const { v, r, s } = ethers_1.ethers.Signature.from(sig);
149
+ return { deadline, v, r, s };
150
+ }
86
151
  }
87
152
  exports.PayNodeAgentClient = PayNodeAgentClient;
@@ -0,0 +1,8 @@
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
+ //# 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"}
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ 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;
@@ -6,6 +6,8 @@ export declare enum ErrorCode {
6
6
  INSUFFICIENT_FUNDS = "INSUFFICIENT_FUNDS",
7
7
  RECEIPT_ALREADY_USED = "RECEIPT_ALREADY_USED",
8
8
  INVALID_RECEIPT = "INVALID_RECEIPT",
9
- MISSING_RECEIPT = "MISSING_RECEIPT"
9
+ MISSING_RECEIPT = "MISSING_RECEIPT",
10
+ TOKEN_NOT_ACCEPTED = "TOKEN_NOT_ACCEPTED",
11
+ INTERNAL_ERROR = "INTERNAL_ERROR"
10
12
  }
11
13
  //# 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,qBAAqB,0BAA0B;IAC/C,kBAAkB,uBAAuB;IACzC,cAAc,mBAAmB;IACjC,cAAc,mBAAmB;IACjC,kBAAkB,uBAAuB;IACzC,oBAAoB,yBAAyB;IAC7C,eAAe,oBAAoB;IACnC,eAAe,oBAAoB;IACnC,kBAAkB,uBAAuB;IACzC,cAAc,mBAAmB;CAClC"}
@@ -11,4 +11,6 @@ var ErrorCode;
11
11
  ErrorCode["RECEIPT_ALREADY_USED"] = "RECEIPT_ALREADY_USED";
12
12
  ErrorCode["INVALID_RECEIPT"] = "INVALID_RECEIPT";
13
13
  ErrorCode["MISSING_RECEIPT"] = "MISSING_RECEIPT";
14
+ ErrorCode["TOKEN_NOT_ACCEPTED"] = "TOKEN_NOT_ACCEPTED";
15
+ ErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
14
16
  })(ErrorCode || (exports.ErrorCode = ErrorCode = {}));
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,8 +1,32 @@
1
1
  import { ErrorCode } 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
+ export interface PayNodeVerifierConfig {
10
+ rpcUrls: string | string[];
11
+ chainId?: number;
12
+ store?: IdempotencyStore;
13
+ /** Override the default accepted token whitelist. If provided, only these addresses are allowed. */
14
+ acceptedTokens?: string[];
15
+ }
16
+ export interface ExpectedPayment {
17
+ merchantAddress: string;
18
+ tokenAddress: string;
19
+ amount: string | number | bigint;
20
+ orderId?: string;
21
+ verifyDepegPrice?: boolean;
22
+ }
2
23
  export declare class PayNodeVerifier {
3
- private usedHashes;
4
- constructor(config: any);
5
- verifyPayment(txHash: string, expected: any): Promise<{
24
+ private provider;
25
+ private chainId?;
26
+ private store?;
27
+ private acceptedTokens?;
28
+ constructor(config: PayNodeVerifierConfig);
29
+ verifyPayment(txHash: string, expected: ExpectedPayment): Promise<{
6
30
  isValid: boolean;
7
31
  error?: {
8
32
  code: ErrorCode;
@@ -10,7 +34,4 @@ export declare class PayNodeVerifier {
10
34
  };
11
35
  }>;
12
36
  }
13
- export interface ExpectedPayment {
14
- [key: string]: any;
15
- }
16
37
  //# 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,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjD;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAUpD,CAAC;AAEF,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;IACjB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;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;IAmCnC,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,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;CAiF5I"}
@@ -1,30 +1,138 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PayNodeVerifier = void 0;
3
+ exports.PayNodeVerifier = 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
+ '0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2', // USDT
16
+ ],
17
+ // Base Sepolia (chainId: 84532)
18
+ '84532': [
19
+ '0xeAC1f2C7099CdaFfB91Aa3b8Ffd653Ef16935798', // USDC (Sandbox)
20
+ ],
21
+ };
22
+ const PAYNODE_ABI = [
23
+ "event PaymentReceived(bytes32 indexed orderId, address indexed merchant, address indexed payer, address token, uint256 amount, uint256 fee, uint256 chainId)"
24
+ ];
25
+ const iface = new ethers_1.Interface(PAYNODE_ABI);
8
26
  class PayNodeVerifier {
9
- usedHashes = new Set();
10
- constructor(config) { }
27
+ provider;
28
+ chainId;
29
+ store;
30
+ acceptedTokens;
31
+ constructor(config) {
32
+ if (!config.rpcUrls || (Array.isArray(config.rpcUrls) && config.rpcUrls.length === 0)) {
33
+ throw new Error("rpcUrls must be provided");
34
+ }
35
+ // Support RpcPool / FallbackProvider
36
+ if (Array.isArray(config.rpcUrls)) {
37
+ const providers = config.rpcUrls.map((url, i) => {
38
+ return {
39
+ provider: new ethers_1.JsonRpcProvider(url, config.chainId),
40
+ priority: i,
41
+ stallTimeout: 1500,
42
+ weight: 1
43
+ };
44
+ });
45
+ this.provider = new ethers_1.FallbackProvider(providers);
46
+ }
47
+ else {
48
+ this.provider = new ethers_1.JsonRpcProvider(config.rpcUrls, config.chainId);
49
+ }
50
+ this.chainId = config.chainId;
51
+ this.store = config.store;
52
+ // Build accepted token set: user-provided or chain-default
53
+ // acceptedTokens=undefined → use chain default; acceptedTokens=[] → explicitly disable whitelist
54
+ let tokenList;
55
+ if (config.acceptedTokens !== undefined) {
56
+ tokenList = config.acceptedTokens;
57
+ }
58
+ else if (config.chainId) {
59
+ tokenList = exports.ACCEPTED_TOKENS[config.chainId.toString()];
60
+ }
61
+ if (tokenList && tokenList.length > 0) {
62
+ this.acceptedTokens = new Set(tokenList.map(t => t.toLowerCase()));
63
+ }
64
+ }
11
65
  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 };
66
+ try {
67
+ // 0. Token Whitelist Check (Anti-FakeToken)
68
+ if (this.acceptedTokens && !this.acceptedTokens.has(expected.tokenAddress.toLowerCase())) {
69
+ return { isValid: false, error: { code: errors_1.ErrorCode.TOKEN_NOT_ACCEPTED, message: `Token ${expected.tokenAddress} is not in the accepted whitelist.` } };
70
+ }
71
+ // 1. Idempotency Check
72
+ if (this.store) {
73
+ // Assume TTL of 24 hours for replay protection
74
+ const isNew = await this.store.checkAndSet(txHash, 86400);
75
+ if (!isNew) {
76
+ return { isValid: false, error: { code: errors_1.ErrorCode.RECEIPT_ALREADY_USED, message: 'Transaction hash has already been consumed.' } };
77
+ }
78
+ }
79
+ // 2. Fetch Receipt
80
+ const receipt = await this.provider.getTransactionReceipt(txHash);
81
+ if (!receipt) {
82
+ return { isValid: false, error: { code: errors_1.ErrorCode.TRANSACTION_NOT_FOUND, message: "Transaction not found on-chain." } };
83
+ }
84
+ if (receipt.status !== 1) {
85
+ return { isValid: false, error: { code: errors_1.ErrorCode.TRANSACTION_FAILED, message: "Transaction reverted on-chain." } };
86
+ }
87
+ // 3. Parse Logs
88
+ let paymentLog = null;
89
+ for (const log of receipt.logs) {
90
+ try {
91
+ const parsed = iface.parseLog({ topics: log.topics, data: log.data });
92
+ if (parsed && parsed.name === 'PaymentReceived') {
93
+ paymentLog = { parsed, logAddress: log.address };
94
+ break;
95
+ }
96
+ }
97
+ catch (e) {
98
+ continue;
99
+ }
100
+ }
101
+ if (!paymentLog) {
102
+ return { isValid: false, error: { code: errors_1.ErrorCode.ORDER_MISMATCH, message: "No PaymentReceived event found in transaction." } };
103
+ }
104
+ const args = paymentLog.parsed.args;
105
+ // 4. Verify Merchant
106
+ if (args.merchant.toLowerCase() !== expected.merchantAddress.toLowerCase()) {
107
+ return { isValid: false, error: { code: errors_1.ErrorCode.ORDER_MISMATCH, message: `Merchant mismatch. Expected ${expected.merchantAddress}, got ${args.merchant}` } };
108
+ }
109
+ // 5. Verify Token
110
+ if (args.token.toLowerCase() !== expected.tokenAddress.toLowerCase()) {
111
+ return { isValid: false, error: { code: errors_1.ErrorCode.ORDER_MISMATCH, message: `Token mismatch. Expected ${expected.tokenAddress}, got ${args.token}` } };
112
+ }
113
+ // 6. Verify Amount
114
+ if (BigInt(args.amount) < BigInt(expected.amount)) {
115
+ return { isValid: false, error: { code: errors_1.ErrorCode.INSUFFICIENT_FUNDS, message: `Expected amount ${expected.amount}, received ${args.amount}` } };
116
+ }
117
+ // 7. Verify ChainId (Cross-chain replay protection)
118
+ const expectedChainId = BigInt(this.chainId || (await this.provider.getNetwork()).chainId);
119
+ if (BigInt(args.chainId) !== expectedChainId) {
120
+ return { isValid: false, error: { code: errors_1.ErrorCode.ORDER_MISMATCH, message: "ChainId mismatch. Invalid network." } };
121
+ }
122
+ // 8. Order Id Check (Optional)
123
+ if (expected.orderId) {
124
+ // Contract orderId is bytes32. Just comparing strings directly if it was passed cleanly, or checking startsWith etc.
125
+ // Ethers returns bytes32 as 0x-prefixed hex string. We should format expected to bytes32 if it's text.
126
+ // For simplicity, assume they match format or we enforce formatting in the caller.
127
+ if (args.orderId !== expected.orderId) {
128
+ return { isValid: false, error: { code: errors_1.ErrorCode.ORDER_MISMATCH, message: "OrderId mismatch." } };
129
+ }
130
+ }
131
+ return { isValid: true };
132
+ }
133
+ catch (e) {
134
+ return { isValid: false, error: { code: errors_1.ErrorCode.INTERNAL_ERROR, message: e.message } };
135
+ }
28
136
  }
29
137
  }
30
138
  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.0",
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",
@@ -24,12 +24,15 @@
24
24
  "author": "PayNodeLabs",
25
25
  "license": "MIT",
26
26
  "dependencies": {
27
- "ethers": "^6.13.0"
27
+ "ethers": "^6.13.0",
28
+ "ioredis": "^5.10.1"
28
29
  },
29
30
  "devDependencies": {
31
+ "@types/express": "^5.0.6",
32
+ "@types/ioredis": "^4.28.10",
33
+ "@types/node": "^20.12.12",
30
34
  "dotenv": "^16.4.5",
31
35
  "ts-node": "^10.9.2",
32
- "typescript": "^5.4.5",
33
- "@types/node": "^20.12.12"
36
+ "typescript": "^5.4.5"
34
37
  }
35
38
  }