@paynodelabs/sdk-js 1.4.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Official Documentation](https://img.shields.io/badge/Docs-docs.paynode.dev-00ff88?style=for-the-badge&logo=readthedocs)](https://docs.paynode.dev)
4
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)
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
+ The official TypeScript/JavaScript SDK for the **PayNode Protocol (v3.1)**. PayNode is a stateless, non-custodial M2M payment gateway that standardizes the HTTP 402 "Payment Required" flow for AI Agents, with support for both on-chain receipts and off-chain signatures (EIP-3009).
7
7
 
8
8
  ## 📖 Read the Docs
9
9
 
@@ -33,6 +33,21 @@ async function main() {
33
33
  main();
34
34
  ```
35
35
 
36
+ ### Key Features (v2.0)
37
+ - **Zero-Wait Checkout**: API response speed drops from 5 seconds to **under 50ms** by using local signatures instead of waiting for on-chain inclusion.
38
+ - **Double-Spend Protection**:
39
+ - **L1 (Memory)**: High-speed local replay protection via `IdempotencyStore`.
40
+ - **L2 (RPC)**: Real-time on-chain `authorizationState` verification.
41
+ - **Empty-Wallet Proof**: Integrated `balanceOf` probes to block malicious agents using empty wallets to generate valid signatures.
42
+ - **EIP-3009 Support**: Sign payments off-chain using `TransferWithAuthorization`.
43
+ - **X402 V2 Protocol**: JSON-based handshake for structured agent interaction.
44
+ - **Dual Flow**: Automatic switch between V1 (on-chain receipts) and V2 (off-chain signatures).
45
+
46
+ ## 🗺️ Roadmap
47
+ - **TRON Support**: USDT (TRC-20) payment integration.
48
+ - **Solana Support**: SPL USDC/USDT payment integration.
49
+ - **Cross-chain**: Universal settlement via bridges.
50
+
36
51
  ## 🚀 Run the Demo
37
52
 
38
53
  The SDK includes a full merchant/agent demonstration in the `examples/` directory.
package/dist/client.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { ExactEVMPayload } from './types/x402';
1
2
  export interface RequestOptions extends RequestInit {
2
3
  json?: any;
3
4
  }
@@ -5,11 +6,14 @@ export declare class PayNodeAgentClient {
5
6
  private wallet;
6
7
  private provider;
7
8
  private rpcUrls;
9
+ private maxRetries;
8
10
  private ERC20_ABI;
9
11
  private ROUTER_ABI;
10
- constructor(privateKey: string, rpcUrls?: string | string[]);
12
+ constructor(privateKey: string, rpcUrls?: string | string[], maxRetries?: number);
13
+ private _fetchWithRetry;
11
14
  requestGate(url: string, options?: RequestOptions): Promise<Response>;
12
- private handlePaymentAndRetry;
15
+ private _handleX402V2;
16
+ signTransferWithAuthorization(tokenAddr: string, to: string, amount: bigint, validAfter: number, validBefore: number, nonce: string, extra?: Record<string, any>): Promise<ExactEVMPayload>;
13
17
  pay(contractAddr: string, tokenAddr: string, merchantAddr: string, amount: bigint, orderId: string): Promise<string>;
14
18
  payWithPermit(contractAddr: string, tokenAddr: string, merchantAddr: string, amount: bigint, orderId: string): Promise<string>;
15
19
  signPermit(tokenAddr: string, spenderAddr: string, amount: bigint, deadlineSeconds?: number): Promise<{
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAIA,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,GAAE,MAAM,GAAG,MAAM,EAAkB;IAcpE,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,QAAQ,CAAC;YA0BjE,qBAAqB;IA0E7B,GAAG,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAepH,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAwB9H,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAE,MAAa;;;;;;CAwCxG"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,eAAe,EAEhB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAID,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,OAAO,CAAW;IAC1B,OAAO,CAAC,UAAU,CAAS;IAE3B,OAAO,CAAC,SAAS,CAMf;IAEF,OAAO,CAAC,UAAU,CAGhB;gBAEU,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,MAAM,GAAG,MAAM,EAAkB,EAAE,UAAU,GAAE,MAAU;YAepF,eAAe;IAgCvB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,QAAQ,CAAC;YAiDjE,aAAa;IAqFrB,6BAA6B,CACjC,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAC9B,OAAO,CAAC,eAAe,CAAC;IA6CrB,GAAG,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAepH,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAwB9H,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAE,MAAa;;;;;;CAwCxG"}
package/dist/client.js CHANGED
@@ -4,10 +4,12 @@ exports.PayNodeAgentClient = void 0;
4
4
  const ethers_1 = require("ethers");
5
5
  const errors_1 = require("./errors");
6
6
  const constants_1 = require("./constants");
7
+ const RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503, 504]);
7
8
  class PayNodeAgentClient {
8
9
  wallet;
9
10
  provider;
10
11
  rpcUrls;
12
+ maxRetries;
11
13
  ERC20_ABI = [
12
14
  "function approve(address spender, uint256 value) public returns (bool)",
13
15
  "function allowance(address owner, address spender) public view returns (uint256)",
@@ -19,8 +21,9 @@ class PayNodeAgentClient {
19
21
  "function pay(address token, address merchant, uint256 amount, bytes32 orderId) public",
20
22
  "function payWithPermit(address payer, address token, address merchant, uint256 amount, bytes32 orderId, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public"
21
23
  ];
22
- constructor(privateKey, rpcUrls = constants_1.BASE_RPC_URLS) {
24
+ constructor(privateKey, rpcUrls = constants_1.BASE_RPC_URLS, maxRetries = 3) {
23
25
  this.rpcUrls = Array.isArray(rpcUrls) ? rpcUrls : [rpcUrls];
26
+ this.maxRetries = maxRetries;
24
27
  const configs = this.rpcUrls.map((url, index) => ({
25
28
  provider: new ethers_1.ethers.JsonRpcProvider(url),
26
29
  priority: index,
@@ -30,6 +33,33 @@ class PayNodeAgentClient {
30
33
  this.provider = new ethers_1.ethers.FallbackProvider(configs);
31
34
  this.wallet = new ethers_1.ethers.Wallet(privateKey, this.provider);
32
35
  }
36
+ async _fetchWithRetry(url, options) {
37
+ let lastError = null;
38
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
39
+ try {
40
+ const response = await fetch(url, options);
41
+ if (!RETRYABLE_STATUS_CODES.has(response.status)) {
42
+ return response;
43
+ }
44
+ if (attempt < this.maxRetries - 1) {
45
+ const backoffMs = Math.pow(2, attempt) * 1000;
46
+ console.warn(`⚠️ [PayNode-JS] ${response.status} received. Retrying in ${backoffMs}ms...`);
47
+ await new Promise(resolve => setTimeout(resolve, backoffMs));
48
+ continue;
49
+ }
50
+ return response;
51
+ }
52
+ catch (error) {
53
+ lastError = error;
54
+ if (attempt < this.maxRetries - 1) {
55
+ const backoffMs = Math.pow(2, attempt) * 1000;
56
+ console.warn(`⚠️ [PayNode-JS] Request failed: ${error.message}. Retrying in ${backoffMs}ms...`);
57
+ await new Promise(resolve => setTimeout(resolve, backoffMs));
58
+ }
59
+ }
60
+ }
61
+ throw lastError || new Error('Request failed after max retries');
62
+ }
33
63
  async requestGate(url, options = {}) {
34
64
  const fetchOptions = { ...options };
35
65
  if (options.json && !fetchOptions.body) {
@@ -40,89 +70,148 @@ class PayNodeAgentClient {
40
70
  };
41
71
  }
42
72
  try {
43
- let response = await fetch(url, fetchOptions);
73
+ let response = await this._fetchWithRetry(url, fetchOptions);
44
74
  if (response.status === 402) {
45
- console.log(`💡 [PayNode-JS] 402 Payment Required detected. Handling autonomous payment...`);
46
- return await this.handlePaymentAndRetry(url, fetchOptions, response.headers);
75
+ console.log(`💡 [PayNode-JS] 402 Payment Required detected. Analyzing protocol version...`);
76
+ const contentType = response.headers.get('content-type');
77
+ const b64Required = response.headers.get('X-402-Required');
78
+ const orderId = response.headers.get('X-402-Order-Id');
79
+ let body = null;
80
+ if (contentType && contentType.includes('application/json')) {
81
+ body = await response.clone().json();
82
+ }
83
+ else if (b64Required) {
84
+ try {
85
+ body = JSON.parse(Buffer.from(b64Required, 'base64').toString());
86
+ }
87
+ catch (e) {
88
+ console.debug('⚠️ [PayNode-JS] Failed to parse X-402-Required header:', e);
89
+ }
90
+ }
91
+ if (body && body.x402Version === 2) {
92
+ console.log(`🚀 [PayNode-JS] x402 v2 detected. Handling autonomous payment...`);
93
+ if (orderId)
94
+ body.orderId = orderId;
95
+ return await this._handleX402V2(url, fetchOptions, body);
96
+ }
97
+ throw new errors_1.PayNodeException(errors_1.ErrorCode.InternalError, "Unsupported or malformed 402 response");
47
98
  }
48
99
  return response;
49
100
  }
50
101
  catch (error) {
51
- if (error instanceof errors_1.PayNodeException)
102
+ if (error instanceof errors_1.PayNodeException || error?.name === "PayNodeException")
52
103
  throw error;
104
+ console.error(`❌ [PayNode-JS] Critical error in requestGate:`, error);
53
105
  throw new errors_1.PayNodeException(errors_1.ErrorCode.RpcError, undefined, error);
54
106
  }
55
107
  }
56
- async handlePaymentAndRetry(url, options, headers) {
57
- const contractAddr = headers.get('x-paynode-contract');
58
- const merchantAddr = headers.get('x-paynode-merchant');
59
- const amountStr = headers.get('x-paynode-amount');
60
- const tokenAddr = headers.get('x-paynode-token-address');
61
- const orderIdStr = headers.get('x-paynode-order-id');
62
- const chainIdStr = headers.get('x-paynode-chain-id');
63
- const currency = headers.get('x-paynode-currency') || 'USDC';
64
- if (!contractAddr || !merchantAddr || !amountStr || !tokenAddr || !orderIdStr) {
65
- throw new errors_1.PayNodeException(errors_1.ErrorCode.InternalError, "Malformed 402 headers: missing metadata");
108
+ async _handleX402V2(url, options, requirements) {
109
+ const network = await this.provider.getNetwork();
110
+ const chainId = Number(network.chainId);
111
+ const caip2ChainId = `eip155:${chainId}`;
112
+ // Select suitable requirement
113
+ const requirement = requirements.accepts.find((req) => req.network === caip2ChainId);
114
+ if (!requirement) {
115
+ throw new errors_1.PayNodeException(errors_1.ErrorCode.InternalError, `No compatible payment requirement found for network ${caip2ChainId}`);
66
116
  }
67
- // Network safety check (v1.4)
68
- if (chainIdStr) {
69
- const network = await this.provider.getNetwork();
70
- if (BigInt(chainIdStr) !== network.chainId) {
71
- throw new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, `Network mismatch: Current ${network.chainId}, Request ${chainIdStr}.`);
72
- }
73
- }
74
- console.log(`💡 [PayNode-JS] Payment request: ${amountStr} ${currency} to ${merchantAddr}`);
75
- const amount = BigInt(amountStr);
76
- // v1.3 Constraint: Min payment protection
77
- if (amount < 1000n) {
78
- throw new errors_1.PayNodeException(errors_1.ErrorCode.AmountTooLow);
117
+ const orderId = requirement.orderId || requirements.orderId || url;
118
+ // Dust limit check
119
+ if (BigInt(requirement.amount) < BigInt(constants_1.MIN_PAYMENT_AMOUNT)) {
120
+ throw new errors_1.PayNodeException(errors_1.ErrorCode.AmountTooLow, `Payment amount ${requirement.amount} is below the minimum dust limit`);
79
121
  }
80
- // v1.4 Constraint: Token whitelist pre-flight (Anti-FakeToken)
81
- const resolvedChainId = chainIdStr ? Number(chainIdStr) : 8453;
82
- const whitelist = constants_1.ACCEPTED_TOKENS[resolvedChainId];
83
- if (whitelist && whitelist.length > 0 && !whitelist.some(t => t.toLowerCase() === tokenAddr.toLowerCase())) {
84
- throw new errors_1.PayNodeException(errors_1.ErrorCode.TokenNotAccepted);
122
+ let payload;
123
+ if (requirement.type === 'eip3009') {
124
+ const validAfter = Math.floor(Date.now() / 1000) - 60;
125
+ const validBefore = Math.floor(Date.now() / 1000) + (requirement.maxTimeoutSeconds || 3600);
126
+ const nonce = ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(32));
127
+ const authorization = await this.signTransferWithAuthorization(requirement.asset, requirement.payTo, BigInt(requirement.amount), validAfter, validBefore, nonce, requirement.extra);
128
+ payload = {
129
+ version: "3.1",
130
+ type: 'eip3009',
131
+ orderId,
132
+ payload: authorization
133
+ };
85
134
  }
86
- let txHash;
87
- try {
88
- const tokenContract = new ethers_1.ethers.Contract(tokenAddr, this.ERC20_ABI, this.wallet);
89
- const [balance, allowance] = await Promise.all([
90
- tokenContract.balanceOf(this.wallet.address),
91
- tokenContract.allowance(this.wallet.address, contractAddr)
92
- ]);
93
- if (balance < amount) {
94
- throw new errors_1.PayNodeException(errors_1.ErrorCode.InsufficientFunds);
135
+ else {
136
+ // type: 'onchain' or fallback
137
+ const routerAddr = requirement.router;
138
+ if (!routerAddr) {
139
+ throw new errors_1.PayNodeException(errors_1.ErrorCode.InternalError, "On-chain payment required but no router address provided.");
95
140
  }
96
- // Protocol v1.3: Permit-First Execution
141
+ console.log(`⚡ [PayNode-JS] Executing on-chain payment to ${requirement.payTo}...`);
142
+ const amount = BigInt(requirement.amount);
143
+ const tokenContract = new ethers_1.ethers.Contract(requirement.asset, this.ERC20_ABI, this.wallet);
144
+ const allowance = await tokenContract.allowance(this.wallet.address, routerAddr);
145
+ let txHash;
97
146
  if (allowance >= amount) {
98
- txHash = await this.pay(contractAddr, tokenAddr, merchantAddr, amount, orderIdStr);
147
+ txHash = await this.pay(routerAddr, requirement.asset, requirement.payTo, amount, orderId);
99
148
  }
100
149
  else {
101
- console.log(`⚡ [PayNode-JS] Insufficient allowance. Attempting Permit-First payment...`);
102
- txHash = await this.payWithPermit(contractAddr, tokenAddr, merchantAddr, amount, orderIdStr);
150
+ txHash = await this.payWithPermit(routerAddr, requirement.asset, requirement.payTo, amount, orderId);
103
151
  }
152
+ payload = {
153
+ version: "3.1",
154
+ type: 'onchain',
155
+ orderId,
156
+ payload: { txHash }
157
+ };
104
158
  }
105
- catch (error) {
106
- if (error instanceof errors_1.PayNodeException)
107
- throw error;
108
- throw new errors_1.PayNodeException(errors_1.ErrorCode.TransactionFailed, undefined, error);
109
- }
110
- console.log(`✅ [PayNode-JS] Payment confirmed on-chain: ${txHash}`);
159
+ const b64Payload = Buffer.from(JSON.stringify(payload)).toString('base64');
111
160
  const retryOptions = {
112
161
  ...options,
113
162
  headers: {
114
163
  ...options.headers,
115
- 'x-paynode-receipt': txHash,
116
- 'x-paynode-order-id': orderIdStr
164
+ 'Content-Type': 'application/json',
165
+ 'X-402-Payload': b64Payload,
166
+ 'X-402-Order-Id': orderId
167
+ }
168
+ };
169
+ return await this._fetchWithRetry(url, retryOptions);
170
+ }
171
+ async signTransferWithAuthorization(tokenAddr, to, amount, validAfter, validBefore, nonce, extra = {}) {
172
+ const network = await this.provider.getNetwork();
173
+ const domain = {
174
+ name: extra.name || "USD Coin",
175
+ version: extra.version || "2",
176
+ chainId: Number(network.chainId),
177
+ verifyingContract: tokenAddr
178
+ };
179
+ const types = {
180
+ TransferWithAuthorization: [
181
+ { name: "from", type: "address" },
182
+ { name: "to", type: "address" },
183
+ { name: "value", type: "uint256" },
184
+ { name: "validAfter", type: "uint256" },
185
+ { name: "validBefore", type: "uint256" },
186
+ { name: "nonce", type: "bytes32" }
187
+ ]
188
+ };
189
+ const value = {
190
+ from: this.wallet.address,
191
+ to,
192
+ value: amount,
193
+ validAfter,
194
+ validBefore,
195
+ nonce
196
+ };
197
+ const signature = await this.wallet.signTypedData(domain, types, value);
198
+ return {
199
+ signature,
200
+ authorization: {
201
+ from: this.wallet.address,
202
+ to,
203
+ value: amount.toString(),
204
+ validAfter: validAfter.toString(),
205
+ validBefore: validBefore.toString(),
206
+ nonce
117
207
  }
118
208
  };
119
- return await fetch(url, retryOptions);
120
209
  }
121
210
  async pay(contractAddr, tokenAddr, merchantAddr, amount, orderId) {
122
211
  const router = new ethers_1.ethers.Contract(contractAddr, this.ROUTER_ABI, this.wallet);
123
212
  const orderIdBytes = ethers_1.ethers.id(orderId);
124
213
  const feeData = await this.provider.getFeeData();
125
- const gasPrice = (feeData.gasPrice * 120n) / 100n; // GasPrice * 1.2
214
+ const gasPrice = (feeData.gasPrice * 120n) / 100n;
126
215
  const tx = await router.pay(tokenAddr, merchantAddr, amount, orderIdBytes, {
127
216
  gasPrice,
128
217
  gasLimit: 200000
@@ -150,7 +239,7 @@ class PayNodeAgentClient {
150
239
  ]);
151
240
  const domain = {
152
241
  name,
153
- version: '1', // USDC on Base uses version 1
242
+ version: '1',
154
243
  chainId: Number(network.chainId),
155
244
  verifyingContract: tokenAddr
156
245
  };
@@ -29,7 +29,7 @@ exports.ERROR_MESSAGES = {
29
29
  "transaction_not_found": "Transaction not found on-chain.",
30
30
  "wrong_contract": "Payment event was not emitted by the official PayNode contract.",
31
31
  "order_mismatch": "OrderId in receipt does not match requested ID.",
32
- "missing_receipt": "Please pay to PayNode contract and provide 'x-paynode-receipt' header.",
32
+ "missing_receipt": "Please pay to PayNode contract and provide 'X-402-Payload' header.",
33
33
  };
34
34
  class PayNodeException extends Error {
35
35
  code;
package/dist/index.d.ts CHANGED
@@ -5,4 +5,5 @@ export * from './utils/webhook';
5
5
  export * from './client';
6
6
  export * from './errors';
7
7
  export * from './constants';
8
+ export * from './types/x402';
8
9
  //# 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,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,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;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -21,3 +21,4 @@ __exportStar(require("./utils/webhook"), exports);
21
21
  __exportStar(require("./client"), exports);
22
22
  __exportStar(require("./errors"), exports);
23
23
  __exportStar(require("./constants"), exports);
24
+ __exportStar(require("./types/x402"), exports);
@@ -11,6 +11,8 @@ export interface PayNodeMiddlewareOptions {
11
11
  decimals?: number;
12
12
  store?: IdempotencyStore;
13
13
  generateOrderId?: (req: Request | any) => string;
14
+ description?: string;
15
+ maxTimeoutSeconds?: number;
14
16
  }
15
17
  export declare const x402Gate: (options: PayNodeMiddlewareOptions) => (req: Request | any, res: Response | any, next: NextFunction) => Promise<any>;
16
18
  /** @deprecated Use x402Gate instead. */
@@ -1 +1 @@
1
- {"version":3,"file":"x402.d.ts","sourceRoot":"","sources":["../../src/middleware/x402.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAQxD,MAAM,WAAW,wBAAwB;IACvC,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,KAAK,MAAM,CAAC;CAClD;AAED,eAAO,MAAM,QAAQ,GAAI,SAAS,wBAAwB,MAwB1C,KAAK,OAAO,GAAG,GAAG,EAAE,KAAK,QAAQ,GAAG,GAAG,EAAE,MAAM,YAAY,iBAwD1E,CAAC;AAEF,wCAAwC;AACxC,eAAO,MAAM,SAAS,YAnFY,wBAAwB,MAwB1C,KAAK,OAAO,GAAG,GAAG,EAAE,KAAK,QAAQ,GAAG,GAAG,EAAE,MAAM,YAAY,iBA2D1C,CAAC"}
1
+ {"version":3,"file":"x402.d.ts","sourceRoot":"","sources":["../../src/middleware/x402.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAcxD,MAAM,WAAW,wBAAwB;IACvC,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,KAAK,MAAM,CAAC;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,eAAO,MAAM,QAAQ,GAAI,SAAS,wBAAwB,MAwB1C,KAAK,OAAO,GAAG,GAAG,EAAE,KAAK,QAAQ,GAAG,GAAG,EAAE,MAAM,YAAY,iBA6F1E,CAAC;AAEF,wCAAwC;AACxC,eAAO,MAAM,SAAS,YAxHY,wBAAwB,MAwB1C,KAAK,OAAO,GAAG,GAAG,EAAE,KAAK,QAAQ,GAAG,GAAG,EAAE,MAAM,YAAY,iBAgG1C,CAAC"}
@@ -35,50 +35,80 @@ const x402Gate = (options) => {
35
35
  return req.headers[name.toLowerCase()] || req.headers[name];
36
36
  return null;
37
37
  };
38
- const receiptHash = getHeader('x-paynode-receipt') || getHeader('X-PayNode-TxHash');
39
- let orderId = getHeader('x-paynode-order-id');
38
+ const v2PayloadHeader = getHeader('X-402-Payload');
39
+ let orderId = getHeader('X-402-Order-Id');
40
40
  if (!orderId) {
41
41
  orderId = (options.generateOrderId || defaultOrderIdGen)(req);
42
42
  }
43
- if (!receiptHash) {
44
- if (res.set) {
45
- res.set({
46
- 'x-paynode-contract': contractAddress,
47
- 'x-paynode-merchant': options.merchantAddress,
48
- 'x-paynode-amount': rawAmount.toString(),
49
- 'x-paynode-currency': currency,
50
- 'x-paynode-token-address': tokenAddress,
51
- 'x-paynode-chain-id': chainId.toString(),
52
- 'x-paynode-order-id': orderId
53
- });
43
+ // Handle x402 v2 Unified Payload
44
+ let unifiedPayload = null;
45
+ if (v2PayloadHeader) {
46
+ try {
47
+ unifiedPayload = JSON.parse(Buffer.from(v2PayloadHeader, 'base64').toString());
48
+ }
49
+ catch (e) {
50
+ console.error("❌ [PayNode-Middleware] Failed to decode X-402-Payload header:", e);
54
51
  }
55
- return res.status(402).json({
56
- error: "Payment Required",
57
- code: errors_1.ErrorCode.MissingReceipt,
58
- message: "Please pay to PayNode contract and provide 'x-paynode-receipt' header.",
59
- amount: options.price,
60
- currency: currency
61
- });
62
52
  }
63
- // Phase 2: On-chain Verification
64
- const result = await verifier.verifyPayment(receiptHash, {
65
- merchantAddress: options.merchantAddress,
66
- tokenAddress: tokenAddress,
67
- amount: rawAmount,
68
- orderId: orderId
69
- });
70
- if (result.isValid) {
71
- // Expose to downstream handlers
72
- req.paynode = { receiptHash, orderId };
73
- return next();
53
+ if (unifiedPayload) {
54
+ const result = await verifier.verify(unifiedPayload, {
55
+ merchantAddress: options.merchantAddress,
56
+ tokenAddress: tokenAddress,
57
+ amount: rawAmount.toString(),
58
+ orderId: orderId
59
+ }, unifiedPayload.type === 'eip3009' ? unifiedPayload.payload?.extra : {});
60
+ if (result.isValid) {
61
+ req.paynode = { unifiedPayload, orderId };
62
+ return next();
63
+ }
64
+ else {
65
+ return res.status(403).json({
66
+ error: "Forbidden",
67
+ code: result.error?.code || errors_1.ErrorCode.InvalidReceipt,
68
+ message: result.error?.message || "Invalid X402 payment payload"
69
+ });
70
+ }
74
71
  }
75
- else {
76
- return res.status(403).json({
77
- error: "Forbidden",
78
- code: result.error?.code || errors_1.ErrorCode.InvalidReceipt,
79
- message: result.error?.message || "Invalid receipt"
80
- });
72
+ // No valid payment found, return 402 with X-402-Required
73
+ const v2Response = {
74
+ x402Version: 2,
75
+ error: "Payment Required by PayNode",
76
+ resource: {
77
+ url: req.protocol + '://' + req.get('host') + req.originalUrl,
78
+ description: options.description || "Protected Resource",
79
+ mimeType: req.header('accept') || "application/json"
80
+ },
81
+ accepts: [
82
+ {
83
+ scheme: "exact",
84
+ type: "eip3009",
85
+ network: `eip155:${chainId}`,
86
+ amount: rawAmount.toString(),
87
+ asset: tokenAddress,
88
+ payTo: options.merchantAddress,
89
+ maxTimeoutSeconds: options.maxTimeoutSeconds || 3600,
90
+ extra: {
91
+ name: currency,
92
+ version: "2"
93
+ }
94
+ },
95
+ {
96
+ scheme: "exact",
97
+ type: "onchain",
98
+ network: `eip155:${chainId}`,
99
+ amount: rawAmount.toString(),
100
+ asset: tokenAddress,
101
+ payTo: options.merchantAddress,
102
+ maxTimeoutSeconds: options.maxTimeoutSeconds || 3600,
103
+ router: contractAddress
104
+ }
105
+ ]
106
+ };
107
+ const b64Required = Buffer.from(JSON.stringify(v2Response)).toString('base64');
108
+ if (res.set) {
109
+ res.set('X-402-Required', b64Required);
81
110
  }
111
+ return res.status(402).json(v2Response);
82
112
  };
83
113
  };
84
114
  exports.x402Gate = x402Gate;
@@ -0,0 +1,83 @@
1
+ export type X402Version = 2;
2
+ export interface ResourceInfo {
3
+ url: string;
4
+ description?: string;
5
+ mimeType?: string;
6
+ }
7
+ export interface PaymentRequirements {
8
+ scheme: string;
9
+ type?: "onchain" | "eip3009";
10
+ network: string;
11
+ amount: string;
12
+ asset: string;
13
+ payTo: string;
14
+ router?: string;
15
+ orderId?: string;
16
+ maxTimeoutSeconds: number;
17
+ extra?: Record<string, any>;
18
+ }
19
+ export interface Extension {
20
+ info: Record<string, any>;
21
+ schema: Record<string, any>;
22
+ }
23
+ export interface PaymentRequiredResponse {
24
+ x402Version: X402Version;
25
+ error?: string;
26
+ orderId?: string;
27
+ resource: ResourceInfo;
28
+ accepts: PaymentRequirements[];
29
+ extensions?: Record<string, Extension>;
30
+ }
31
+ export interface PaymentPayload {
32
+ x402Version: X402Version;
33
+ resource?: ResourceInfo;
34
+ accepted: PaymentRequirements;
35
+ payload: Record<string, any>;
36
+ extensions?: Record<string, any>;
37
+ }
38
+ export interface ExactEVMPayload {
39
+ signature: string;
40
+ authorization: {
41
+ from: string;
42
+ to: string;
43
+ value: string;
44
+ validAfter: string;
45
+ validBefore: string;
46
+ nonce: string;
47
+ };
48
+ }
49
+ export interface SettlementResponse {
50
+ success: boolean;
51
+ errorReason?: string;
52
+ payer?: string;
53
+ transaction: string;
54
+ network: string;
55
+ extensions?: Record<string, any>;
56
+ }
57
+ export interface VerifyResponse {
58
+ isValid: boolean;
59
+ invalidReason?: string;
60
+ payer?: string;
61
+ }
62
+ export interface UnifiedPaymentPayload {
63
+ version: "3.1";
64
+ type: "onchain" | "eip3009";
65
+ orderId: string;
66
+ payload: {
67
+ txHash?: string;
68
+ signature?: string;
69
+ authorization?: any;
70
+ } | ExactEVMPayload;
71
+ }
72
+ export interface SupportedKind {
73
+ x402Version: X402Version;
74
+ scheme: string;
75
+ network: string;
76
+ extra?: Record<string, any>;
77
+ }
78
+ export interface SupportedResponse {
79
+ kinds: SupportedKind[];
80
+ extensions: string[];
81
+ signers: Record<string, string[]>;
82
+ }
83
+ //# sourceMappingURL=x402.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"x402.d.ts","sourceRoot":"","sources":["../../src/types/x402.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -5,6 +5,11 @@ export interface IdempotencyStore {
5
5
  * @returns true if the hash was newly added, false if it already exists.
6
6
  */
7
7
  checkAndSet(txHash: string, ttlSeconds: number): Promise<boolean>;
8
+ /**
9
+ * Deletes a transaction hash from the store.
10
+ * Used for rolling back a lock if subsequent verification fails.
11
+ */
12
+ delete(txHash: string): Promise<void>;
8
13
  }
9
14
  /**
10
15
  * Default implementation for MVP.
@@ -15,6 +20,7 @@ export declare class MemoryIdempotencyStore implements IdempotencyStore {
15
20
  private cache;
16
21
  constructor();
17
22
  checkAndSet(txHash: string, ttlSeconds: number): Promise<boolean>;
23
+ delete(txHash: string): Promise<void>;
18
24
  private cleanup;
19
25
  }
20
26
  /**
@@ -26,5 +32,6 @@ export declare class RedisIdempotencyStore implements IdempotencyStore {
26
32
  private prefix;
27
33
  constructor(redisClient: Redis, prefix?: string);
28
34
  checkAndSet(txHash: string, ttlSeconds: number): Promise<boolean>;
35
+ delete(txHash: string): Promise<void>;
29
36
  }
30
37
  //# sourceMappingURL=idempotency.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../../src/utils/idempotency.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;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
+ {"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../../src/utils/idempotency.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAElE;;;OAGG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED;;;;GAIG;AACH,qBAAa,sBAAuB,YAAW,gBAAgB;IAC7D,OAAO,CAAC,KAAK,CAAkC;;IAOzC,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAYjE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3C,OAAO,CAAC,OAAO;CAQhB;AAED;;;GAGG;AACH,qBAAa,qBAAsB,YAAW,gBAAgB;IAC5D,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,MAAM,CAAS;gBAEX,WAAW,EAAE,KAAK,EAAE,MAAM,GAAE,MAAsB;IAKxD,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAMjE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAI5C"}
@@ -21,6 +21,9 @@ class MemoryIdempotencyStore {
21
21
  this.cache.set(txHash, now + ttlSeconds);
22
22
  return true;
23
23
  }
24
+ async delete(txHash) {
25
+ this.cache.delete(txHash);
26
+ }
24
27
  cleanup() {
25
28
  const now = Math.floor(Date.now() / 1000);
26
29
  for (const [key, expiry] of this.cache.entries()) {
@@ -47,5 +50,9 @@ class RedisIdempotencyStore {
47
50
  const result = await this.redis.set(key, '1', 'EX', ttlSeconds, 'NX');
48
51
  return result === 'OK';
49
52
  }
53
+ async delete(txHash) {
54
+ const key = `${this.prefix}${txHash}`;
55
+ await this.redis.del(key);
56
+ }
50
57
  }
51
58
  exports.RedisIdempotencyStore = RedisIdempotencyStore;
@@ -1,5 +1,6 @@
1
1
  import { PayNodeException } from '../errors';
2
2
  import { IdempotencyStore } from './idempotency';
3
+ import { ExactEVMPayload, UnifiedPaymentPayload } from '../types/x402';
3
4
  export interface PayNodeVerifierConfig {
4
5
  rpcUrls: string | string[];
5
6
  contractAddress: string;
@@ -21,7 +22,23 @@ export declare class PayNodeVerifier {
21
22
  private store?;
22
23
  private acceptedTokens?;
23
24
  constructor(config: PayNodeVerifierConfig);
24
- verifyPayment(txHash: string, expected: ExpectedPayment): Promise<{
25
+ private static ROUTER_ABI;
26
+ verify(unifiedPayload: UnifiedPaymentPayload, expected: ExpectedPayment, extra?: any): Promise<{
27
+ isValid: boolean;
28
+ error?: PayNodeException;
29
+ }>;
30
+ verifyOnchainPayment(txHash: string, expected: any): Promise<{
31
+ isValid: boolean;
32
+ error?: PayNodeException;
33
+ }>;
34
+ /**
35
+ * 亚秒级离线签名验证 (V2 核心)
36
+ * 耗时: < 50ms (仅需一次 RPC Read)
37
+ */
38
+ verifyTransferWithAuthorization(tokenAddr: string, payload: ExactEVMPayload, expected: {
39
+ to: string;
40
+ value: string | number | bigint;
41
+ }, extra?: Record<string, any>): Promise<{
25
42
  isValid: boolean;
26
43
  error?: PayNodeException;
27
44
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../../src/utils/verifier.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAA0B,MAAM,eAAe,CAAC;AAGzE,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,oGAAoG;IACpG,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAQD,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAqC;IACrD,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,OAAO,CAAC,CAAS;IACzB,OAAO,CAAC,KAAK,CAAC,CAAmB;IACjC,OAAO,CAAC,cAAc,CAAC,CAAc;gBAEzB,MAAM,EAAE,qBAAqB;IAmCnC,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;CA2FxH"}
1
+ {"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../../src/utils/verifier.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAA0B,MAAM,eAAe,CAAC;AAEzE,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAEvE,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,oGAAoG;IACpG,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAqC;IACrD,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,OAAO,CAAC,CAAS;IACzB,OAAO,CAAC,KAAK,CAAC,CAAmB;IACjC,OAAO,CAAC,cAAc,CAAC,CAAc;gBAEzB,MAAM,EAAE,qBAAqB;IAmCzC,OAAO,CAAC,MAAM,CAAC,UAAU,CAEvB;IAEI,MAAM,CACV,cAAc,EAAE,qBAAqB,EACrC,QAAQ,EAAE,eAAe,EACzB,KAAK,CAAC,EAAE,GAAG,GACV,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,gBAAgB,CAAA;KAAE,CAAC;IAkCpD,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,gBAAgB,CAAA;KAAE,CAAC;IAiDlH;;;OAGG;IACG,+BAA+B,CACnC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;KACjC,EACD,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAC9B,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,gBAAgB,CAAA;KAAE,CAAC;CA6F3D"}
@@ -5,10 +5,6 @@ const ethers_1 = require("ethers");
5
5
  const errors_1 = require("../errors");
6
6
  const idempotency_1 = require("./idempotency");
7
7
  const constants_1 = require("../constants");
8
- const PAYNODE_ABI = [
9
- "event PaymentReceived(bytes32 indexed orderId, address indexed merchant, address indexed payer, address token, uint256 amount, uint256 fee, uint256 chainId)"
10
- ];
11
- const iface = new ethers_1.Interface(PAYNODE_ABI);
12
8
  class PayNodeVerifier {
13
9
  provider;
14
10
  contractAddress;
@@ -25,7 +21,7 @@ class PayNodeVerifier {
25
21
  return {
26
22
  provider: new ethers_1.JsonRpcProvider(url, config.chainId),
27
23
  priority: i,
28
- stallTimeout: 1500,
24
+ stallTimeout: 3000,
29
25
  weight: 1
30
26
  };
31
27
  });
@@ -48,83 +44,171 @@ class PayNodeVerifier {
48
44
  this.acceptedTokens = new Set(tokenList.map(t => t.toLowerCase()));
49
45
  }
50
46
  }
51
- async verifyPayment(txHash, expected) {
47
+ static ROUTER_ABI = [
48
+ "event PaymentReceived(bytes32 indexed orderId, address indexed merchant, address indexed payer, address token, uint256 amount, uint256 fee, uint256 chainId)"
49
+ ];
50
+ async verify(unifiedPayload, expected, extra) {
52
51
  try {
53
- // 0. Dust Exploit Check (Minimum Payment)
54
- const expectedAmount = BigInt(expected.amount);
55
- if (expectedAmount < constants_1.MIN_PAYMENT_AMOUNT) {
56
- return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.AmountTooLow) };
52
+ const { type, payload, orderId } = unifiedPayload;
53
+ if (type === 'eip3009') {
54
+ const tokenAddr = expected.tokenAddress;
55
+ if (!tokenAddr) {
56
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.TokenNotAccepted, "tokenAddress is required for eip3009 verification") };
57
+ }
58
+ const actualPayload = payload;
59
+ return await this.verifyTransferWithAuthorization(tokenAddr, actualPayload, {
60
+ to: expected.merchantAddress,
61
+ value: expected.amount
62
+ }, extra);
57
63
  }
58
- // 1. Token Whitelist Check (Anti-FakeToken)
59
- if (this.acceptedTokens && !this.acceptedTokens.has(expected.tokenAddress.toLowerCase())) {
60
- return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.TokenNotAccepted) };
64
+ else if (type === 'onchain') {
65
+ const { txHash } = payload;
66
+ if (!txHash) {
67
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "Missing txHash in onchain payload") };
68
+ }
69
+ return await this.verifyOnchainPayment(txHash, {
70
+ merchantAddress: expected.merchantAddress,
71
+ tokenAddress: expected.tokenAddress,
72
+ amount: expected.amount,
73
+ orderId: orderId
74
+ });
61
75
  }
62
- // 2. Fetch Receipt
63
- const receipt = await this.provider.getTransactionReceipt(txHash);
64
- if (!receipt) {
65
- return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt) };
76
+ else {
77
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InternalError, `Unsupported payload type: ${type}`) };
66
78
  }
67
- if (receipt.status !== 1) {
68
- return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.TransactionFailed) };
79
+ }
80
+ catch (e) {
81
+ if (e instanceof errors_1.PayNodeException)
82
+ return { isValid: false, error: e };
83
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InternalError, e.message) };
84
+ }
85
+ }
86
+ async verifyOnchainPayment(txHash, expected) {
87
+ try {
88
+ const receipt = await this.provider.getTransactionReceipt(txHash);
89
+ if (!receipt || receipt.status === 0) {
90
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.TransactionNotFound) };
69
91
  }
70
- // 3. Parse Logs & Verify Contract Source
71
- let paymentLog = null;
92
+ const router = new ethers_1.ethers.Interface(PayNodeVerifier.ROUTER_ABI);
93
+ const targetOrderId = ethers_1.ethers.id(expected.orderId);
94
+ let validEventFound = false;
72
95
  for (const log of receipt.logs) {
73
96
  try {
74
- // Security Fix: Verify the log address matches the official router address
75
- if (log.address.toLowerCase() !== this.contractAddress.toLowerCase()) {
76
- continue;
77
- }
78
- const parsed = iface.parseLog({ topics: log.topics, data: log.data });
97
+ const parsed = router.parseLog(log);
79
98
  if (parsed && parsed.name === 'PaymentReceived') {
80
- paymentLog = { parsed, logAddress: log.address };
81
- break;
99
+ const { merchant, token, amount, orderId } = parsed.args;
100
+ if (merchant.toLowerCase() === expected.merchantAddress.toLowerCase() &&
101
+ token.toLowerCase() === expected.tokenAddress.toLowerCase() &&
102
+ BigInt(amount) >= BigInt(expected.amount) &&
103
+ orderId === targetOrderId) {
104
+ validEventFound = true;
105
+ break;
106
+ }
82
107
  }
83
108
  }
84
109
  catch (e) {
85
- continue;
110
+ // Skip
86
111
  }
87
112
  }
88
- if (!paymentLog) {
89
- return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.WrongContract) };
113
+ if (!validEventFound) {
114
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "No matching PaymentReceived event found") };
90
115
  }
91
- const args = paymentLog.parsed.args;
92
- // 4. Verify OrderId (bytes32 keccak256 hash comparison)
93
- if (expected.orderId) {
94
- if (args.orderId !== ethers_1.ethers.id(expected.orderId)) {
95
- return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.OrderMismatch) };
116
+ if (this.store) {
117
+ const isNew = await this.store.checkAndSet(txHash, 86400);
118
+ if (!isNew) {
119
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.DuplicateTransaction) };
96
120
  }
97
121
  }
98
- // 5. Verify Merchant
99
- if (args.merchant.toLowerCase() !== expected.merchantAddress.toLowerCase()) {
100
- return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "Payment went to a different merchant.") };
122
+ return { isValid: true };
123
+ }
124
+ catch (error) {
125
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.RpcError, undefined, error) };
126
+ }
127
+ }
128
+ /**
129
+ * 亚秒级离线签名验证 (V2 核心)
130
+ * 耗时: < 50ms (仅需一次 RPC Read)
131
+ */
132
+ async verifyTransferWithAuthorization(tokenAddr, payload, expected, extra = {}) {
133
+ try {
134
+ const { signature, authorization } = payload;
135
+ const { from, to, value, validAfter, validBefore, nonce } = authorization;
136
+ const expectedValue = BigInt(expected.value);
137
+ const payloadValue = BigInt(value);
138
+ // 1. 基础字段与金额校验 (防粉尘攻击)
139
+ if (to.toLowerCase() !== expected.to.toLowerCase()) {
140
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "Recipient mismatch") };
101
141
  }
102
- // 5. Verify Token
103
- if (args.token.toLowerCase() !== expected.tokenAddress.toLowerCase()) {
104
- return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "Payment used unexpected token.") };
142
+ if (payloadValue < expectedValue) {
143
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.AmountTooLow) };
105
144
  }
106
- // 6. Verify Amount
107
- if (BigInt(args.amount) < BigInt(expected.amount)) {
108
- return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "Payment amount is below required price.") };
145
+ // 2. 时间窗口校验
146
+ const now = Math.floor(Date.now() / 1000);
147
+ if (now < Number(validAfter)) {
148
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "Authorization not yet valid") };
109
149
  }
110
- // 7. Verify ChainId (Cross-chain replay protection)
111
- const expectedChainId = BigInt(this.chainId || (await this.provider.getNetwork()).chainId);
112
- if (BigInt(args.chainId) !== expectedChainId) {
113
- return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "ChainId mismatch. Invalid network.") };
150
+ if (now > Number(validBefore)) {
151
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "Authorization expired") };
114
152
  }
115
- // 8. Idempotency Check
153
+ // 3. 密码学验签 (EIP-712 / EIP-3009) - 纯本地计算 0ms
154
+ const chainId = Number(this.chainId || (await this.provider.getNetwork()).chainId);
155
+ const domain = {
156
+ name: extra.name || "USD Coin",
157
+ version: extra.version || "2",
158
+ chainId,
159
+ verifyingContract: tokenAddr
160
+ };
161
+ const types = {
162
+ TransferWithAuthorization: [
163
+ { name: "from", type: "address" },
164
+ { name: "to", type: "address" },
165
+ { name: "value", type: "uint256" },
166
+ { name: "validAfter", type: "uint256" },
167
+ { name: "validBefore", type: "uint256" },
168
+ { name: "nonce", type: "bytes32" }
169
+ ]
170
+ };
171
+ const recoveredAddress = ethers_1.ethers.verifyTypedData(domain, types, authorization, signature);
172
+ if (recoveredAddress.toLowerCase() !== from.toLowerCase()) {
173
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "Invalid signature: recovered address mismatch") };
174
+ }
175
+ // 4. 内存幂等性校验 (防高频重放)
116
176
  if (this.store) {
117
- const isNew = await this.store.checkAndSet(txHash, 86400);
177
+ const isNew = await this.store.checkAndSet(nonce, 86400); // 锁定 24 小时
118
178
  if (!isNew) {
119
- return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.DuplicateTransaction) };
179
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.DuplicateTransaction, "Nonce already used in local memory") };
120
180
  }
121
181
  }
182
+ // ================= 核心补全:RPC 状态只读校验 (<50ms) =================
183
+ const tokenContract = new ethers_1.ethers.Contract(tokenAddr, [
184
+ "function balanceOf(address account) view returns (uint256)",
185
+ "function authorizationState(address authorizer, bytes32 nonce) view returns (bool)"
186
+ ], this.provider);
187
+ // 并发执行 RPC 查询以追求极限速度
188
+ const [balance, isNonceUsedOnChain] = await Promise.all([
189
+ tokenContract.balanceOf(from).catch(() => 0n),
190
+ // Note: For mock tokens that don't support EIP-3009 view methods, this will fallback to false.
191
+ // We still have L1 protection (IdempotencyStore) to prevent immediate replays.
192
+ tokenContract.authorizationState(from, nonce).catch(() => false)
193
+ ]);
194
+ // 5. 校验真实余额 (防止空钱包签署有效签名)
195
+ if (BigInt(balance) < payloadValue) {
196
+ // 如果验签失败,释放内存锁
197
+ if (this.store)
198
+ await this.store.delete(nonce);
199
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "Insufficient token balance") };
200
+ }
201
+ // 6. 校验链上 Nonce 状态 (防止该签名已被打包结算)
202
+ if (isNonceUsedOnChain) {
203
+ if (this.store)
204
+ await this.store.delete(nonce);
205
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.DuplicateTransaction, "Nonce already consumed on-chain") };
206
+ }
207
+ // =======================================================================
122
208
  return { isValid: true };
123
209
  }
124
210
  catch (e) {
125
- if (e instanceof errors_1.PayNodeException)
126
- return { isValid: false, error: e };
127
- return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InternalError, `An unexpected error occurred: ${e.message}`) };
211
+ return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InternalError, e.message) };
128
212
  }
129
213
  }
130
214
  }
@@ -129,7 +129,7 @@ class PayNodeWebhookNotifier {
129
129
  this.lastBlock = currentBlock;
130
130
  }
131
131
  catch (error) {
132
- console.error(`[PayNode Webhook] Poll error: ${error.message}`);
132
+ console.error(`❌ [PayNode Webhook] Poll error: ${error.message}`);
133
133
  }
134
134
  finally {
135
135
  this.isProcessing = false;
@@ -187,13 +187,13 @@ class PayNodeWebhookNotifier {
187
187
  this.config.onSuccess?.(event);
188
188
  }
189
189
  catch (error) {
190
- console.error(`[PayNode Webhook] Delivery failed (attempt ${attempt}/${MAX_RETRIES}): ${error.message}`);
190
+ console.error(`⚠️ [PayNode Webhook] Delivery failed (attempt ${attempt}/${MAX_RETRIES}): ${error.message}`);
191
191
  if (attempt < MAX_RETRIES) {
192
192
  const backoffMs = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
193
193
  await new Promise(resolve => setTimeout(resolve, backoffMs));
194
194
  return this.deliver(event, attempt + 1);
195
195
  }
196
- console.error(`[PayNode Webhook] Gave up on tx ${event.txHash} after ${MAX_RETRIES} attempts.`);
196
+ console.error(`❌ [PayNode Webhook] Gave up on tx ${event.txHash} after ${MAX_RETRIES} attempts.`);
197
197
  this.config.onError?.(error, event);
198
198
  }
199
199
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paynodelabs/sdk-js",
3
- "version": "1.4.0",
3
+ "version": "2.0.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",