@paynodelabs/sdk-js 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PayNode Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -21,35 +21,64 @@ npm install @paynodelabs/sdk-js ethers
21
21
  ### Agent Client (Payer)
22
22
 
23
23
  ```typescript
24
- import { PayNodeClient } from '@paynodelabs/sdk-js';
24
+ import { PayNodeAgentClient } from "@paynodelabs/sdk-js";
25
25
 
26
- const client = new PayNodeClient('YOUR_AGENT_PRIVATE_KEY');
26
+ const client = new PayNodeAgentClient("YOUR_AGENT_PRIVATE_KEY", ["https://mainnet.base.org", "https://rpc.ankr.com/base"]);
27
27
 
28
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());
29
+ // Automatically handles 402 challenges, pays USDC, and retries the request
30
+ const response = await client.requestGate("https://api.merchant.com/premium-data");
31
+ console.log(await response.text());
32
32
  }
33
33
  main();
34
34
  ```
35
35
 
36
- ### Merchant Middleware (Receiver)
36
+ ## 🚀 Run the Demo
37
37
 
38
- ```typescript
39
- import express from 'express';
40
- import { createPayNodeMiddleware } from '@paynodelabs/sdk-js';
38
+ The SDK includes a full merchant/agent demonstration in the `examples/` directory.
41
39
 
42
- const app = express();
40
+ ### 1. Setup Environment
43
41
 
44
- const requirePayment = createPayNodeMiddleware({
45
- price: "1.50", // 1.50 USDC
46
- merchantWallet: "0xYourWalletAddress..."
47
- });
42
+ ```bash
43
+ cp .env.example .env
44
+ # Edit .env with your private key and RPC URLs
45
+ ```
48
46
 
49
- app.get('/premium-data', requirePayment, (req, res) => {
50
- res.json({ secret: "This is paid M2M data." });
51
- });
47
+ ### 2. Run the Merchant Server (Express)
48
+
49
+ ```bash
50
+ npx ts-node examples/express-server.ts
52
51
  ```
53
52
 
53
+ ### 3. Run the Agent Client
54
+
55
+ In another terminal:
56
+
57
+ ```bash
58
+ npx ts-node examples/agent-client.ts
59
+ ```
60
+
61
+ The demo will perform a full loop: `402 Handshake -> On-chain Payment -> 200 Verification`.
62
+
63
+ ---
64
+
65
+ ## 📦 Publishing to NPM
66
+
67
+ To publish a new version of the SDK:
68
+
69
+ 1. **Build the project**:
70
+ ```bash
71
+ npm run build
72
+ ```
73
+ 2. **Login to NPM** (if not already):
74
+ ```bash
75
+ npm login
76
+ ```
77
+ 3. **Publish**:
78
+ ```bash
79
+ npm publish --access public
80
+ ```
81
+
54
82
  ---
55
- *Built for the Autonomous AI Economy by PayNodeLabs.*
83
+
84
+ _Built for the Autonomous AI Economy by PayNodeLabs._
package/dist/client.d.ts CHANGED
@@ -4,38 +4,17 @@ export interface RequestOptions extends RequestInit {
4
4
  export declare class PayNodeAgentClient {
5
5
  private wallet;
6
6
  private provider;
7
+ private rpcUrls;
7
8
  private ERC20_ABI;
8
9
  private ROUTER_ABI;
9
- constructor(privateKey: string, rpcUrl: string);
10
- /**
11
- * Executes a fetch request and automatically handles the 402 Payment loop if encountered.
12
- */
10
+ constructor(privateKey: string, rpcUrls: string | string[]);
13
11
  requestGate(url: string, options?: RequestOptions): Promise<Response>;
14
12
  private handlePaymentAndRetry;
15
- private executeChainPayment;
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
- */
13
+ private executeStandardPay;
14
+ private executePermitPay;
36
15
  signPermit(tokenAddr: string, spenderAddr: string, amount: bigint, deadlineSeconds?: number): Promise<{
37
16
  deadline: number;
38
- v: number;
17
+ v: 27 | 28;
39
18
  r: string;
40
19
  s: string;
41
20
  }>;
@@ -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,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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,OAAO,CAAW;IAE1B,OAAO,CAAC,SAAS,CAMf;IAEF,OAAO,CAAC,UAAU,CAGhB;gBAEU,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE;IAcpD,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,QAAQ,CAAC;YA0BjE,qBAAqB;YAwDrB,kBAAkB;YAelB,gBAAgB;IAwBxB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAE,MAAa;;;;;;CAwCxG"}
package/dist/client.js CHANGED
@@ -2,25 +2,33 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PayNodeAgentClient = void 0;
4
4
  const ethers_1 = require("ethers");
5
+ const errors_1 = require("./errors");
5
6
  class PayNodeAgentClient {
6
7
  wallet;
7
8
  provider;
9
+ rpcUrls;
8
10
  ERC20_ABI = [
9
11
  "function approve(address spender, uint256 value) public returns (bool)",
10
12
  "function allowance(address owner, address spender) public view returns (uint256)",
11
- "function balanceOf(address account) public view returns (uint256)"
13
+ "function balanceOf(address account) public view returns (uint256)",
14
+ "function name() view returns (string)",
15
+ "function nonces(address owner) view returns (uint256)"
12
16
  ];
13
17
  ROUTER_ABI = [
14
18
  "function pay(address token, address merchant, uint256 amount, bytes32 orderId) public",
15
19
  "function payWithPermit(address payer, address token, address merchant, uint256 amount, bytes32 orderId, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public"
16
20
  ];
17
- constructor(privateKey, rpcUrl) {
18
- this.provider = new ethers_1.ethers.JsonRpcProvider(rpcUrl);
21
+ constructor(privateKey, rpcUrls) {
22
+ this.rpcUrls = Array.isArray(rpcUrls) ? rpcUrls : [rpcUrls];
23
+ const configs = this.rpcUrls.map((url, index) => ({
24
+ provider: new ethers_1.ethers.JsonRpcProvider(url),
25
+ priority: index,
26
+ weight: 1,
27
+ stallTimeout: 3000
28
+ }));
29
+ this.provider = new ethers_1.ethers.FallbackProvider(configs);
19
30
  this.wallet = new ethers_1.ethers.Wallet(privateKey, this.provider);
20
31
  }
21
- /**
22
- * Executes a fetch request and automatically handles the 402 Payment loop if encountered.
23
- */
24
32
  async requestGate(url, options = {}) {
25
33
  const fetchOptions = { ...options };
26
34
  if (options.json && !fetchOptions.body) {
@@ -30,12 +38,19 @@ class PayNodeAgentClient {
30
38
  ...fetchOptions.headers
31
39
  };
32
40
  }
33
- let response = await fetch(url, fetchOptions);
34
- if (response.status === 402) {
35
- console.log(`💡 [PayNode-JS] 402 Payment Required detected. Handling autonomous payment...`);
36
- return await this.handlePaymentAndRetry(url, fetchOptions, response.headers);
41
+ try {
42
+ let response = await fetch(url, fetchOptions);
43
+ if (response.status === 402) {
44
+ console.log(`💡 [PayNode-JS] 402 Payment Required detected. Handling autonomous payment...`);
45
+ return await this.handlePaymentAndRetry(url, fetchOptions, response.headers);
46
+ }
47
+ return response;
48
+ }
49
+ catch (error) {
50
+ if (error instanceof errors_1.PayNodeException)
51
+ throw error;
52
+ throw new errors_1.PayNodeException(`Failed to connect to any provided RPC nodes.`, errors_1.ErrorCode.RpcError, error);
37
53
  }
38
- return response;
39
54
  }
40
55
  async handlePaymentAndRetry(url, options, headers) {
41
56
  const contractAddr = headers.get('x-paynode-contract');
@@ -44,11 +59,37 @@ class PayNodeAgentClient {
44
59
  const tokenAddr = headers.get('x-paynode-token-address');
45
60
  const orderIdStr = headers.get('x-paynode-order-id');
46
61
  if (!contractAddr || !merchantAddr || !amountStr || !tokenAddr || !orderIdStr) {
47
- throw new Error("Malformed 402 headers: missing PayNode metadata");
62
+ throw new errors_1.PayNodeException("Malformed 402 headers: missing metadata", errors_1.ErrorCode.InternalError);
48
63
  }
49
64
  const amount = BigInt(amountStr);
50
- const orderIdBytes = ethers_1.ethers.id(orderIdStr);
51
- const txHash = await this.executeChainPayment(contractAddr, merchantAddr, tokenAddr, amount, orderIdBytes);
65
+ // v1.3 Constraint: Min payment protection
66
+ if (amount < 1000n) {
67
+ throw new errors_1.PayNodeException("Payment amount is below the protocol minimum (1000).", errors_1.ErrorCode.AmountTooLow);
68
+ }
69
+ let txHash;
70
+ try {
71
+ const tokenContract = new ethers_1.ethers.Contract(tokenAddr, this.ERC20_ABI, this.wallet);
72
+ const [balance, allowance] = await Promise.all([
73
+ tokenContract.balanceOf(this.wallet.address),
74
+ tokenContract.allowance(this.wallet.address, contractAddr)
75
+ ]);
76
+ if (balance < amount) {
77
+ throw new errors_1.PayNodeException("Wallet lacks USDC or ETH for gas.", errors_1.ErrorCode.InsufficientFunds);
78
+ }
79
+ // Protocol v1.3: Permit-First Execution
80
+ if (allowance >= amount) {
81
+ txHash = await this.executeStandardPay(contractAddr, tokenAddr, merchantAddr, amount, orderIdStr);
82
+ }
83
+ else {
84
+ console.log(`⚡ [PayNode-JS] Insufficient allowance. Attempting Permit-First payment...`);
85
+ txHash = await this.executePermitPay(contractAddr, tokenAddr, merchantAddr, amount, orderIdStr);
86
+ }
87
+ }
88
+ catch (error) {
89
+ if (error instanceof errors_1.PayNodeException)
90
+ throw error;
91
+ throw new errors_1.PayNodeException(`On-chain transaction reverted or failed.`, errors_1.ErrorCode.TransactionFailed, error);
92
+ }
52
93
  console.log(`✅ [PayNode-JS] Payment confirmed on-chain: ${txHash}`);
53
94
  const retryOptions = {
54
95
  ...options,
@@ -60,72 +101,40 @@ class PayNodeAgentClient {
60
101
  };
61
102
  return await fetch(url, retryOptions);
62
103
  }
63
- async executeChainPayment(contractAddr, merchantAddr, tokenAddr, amount, orderId) {
64
- const tokenContract = new ethers_1.ethers.Contract(tokenAddr, this.ERC20_ABI, this.wallet);
65
- // 1. Check Balance
66
- const balance = await tokenContract.balanceOf(this.wallet.address);
67
- if (balance < amount) {
68
- throw new Error(`Insufficient USDC balance. Have: ${ethers_1.ethers.formatUnits(balance, 6)}, Need: ${ethers_1.ethers.formatUnits(amount, 6)}`);
69
- }
70
- // 2. Check and Handle Allowance
71
- const currentAllowance = await tokenContract.allowance(this.wallet.address, contractAddr);
72
- if (currentAllowance < amount) {
73
- console.log(`🔐 [PayNode-JS] Allowance too low (${currentAllowance}). Granting Infinite Approval to Router...`);
74
- const approveTx = await tokenContract.approve(contractAddr, ethers_1.ethers.MaxUint256);
75
- await approveTx.wait();
76
- console.log(`🔓 [PayNode-JS] Infinite Approval confirmed.`);
77
- }
78
- // 3. Execute Payment
79
- const routerContract = new ethers_1.ethers.Contract(contractAddr, this.ROUTER_ABI, this.wallet);
80
- // Use manually specified gas limit to avoid estimateGas issues with some RPCs
81
- const payTx = await routerContract.pay(tokenAddr, merchantAddr, amount, orderId, {
82
- gasLimit: 200000 // Safe overhead for Base
104
+ async executeStandardPay(contractAddr, tokenAddr, merchantAddr, amount, orderId) {
105
+ const router = new ethers_1.ethers.Contract(contractAddr, this.ROUTER_ABI, this.wallet);
106
+ const orderIdBytes = ethers_1.ethers.id(orderId);
107
+ const feeData = await this.provider.getFeeData();
108
+ const gasPrice = (feeData.gasPrice * 120n) / 100n; // GasPrice * 1.2
109
+ const tx = await router.pay(tokenAddr, merchantAddr, amount, orderIdBytes, {
110
+ gasPrice,
111
+ gasLimit: 200000
83
112
  });
84
- const receipt = await payTx.wait();
113
+ const receipt = await tx.wait();
85
114
  return receipt.hash;
86
115
  }
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 });
116
+ async executePermitPay(contractAddr, tokenAddr, merchantAddr, amount, orderId) {
117
+ const sig = await this.signPermit(tokenAddr, contractAddr, amount);
118
+ const router = new ethers_1.ethers.Contract(contractAddr, this.ROUTER_ABI, this.wallet);
119
+ const orderIdBytes = ethers_1.ethers.id(orderId);
120
+ const feeData = await this.provider.getFeeData();
121
+ const gasPrice = (feeData.gasPrice * 120n) / 100n;
122
+ const tx = await router.payWithPermit(this.wallet.address, tokenAddr, merchantAddr, amount, orderIdBytes, sig.deadline, sig.v, sig.r, sig.s, { gasPrice, gasLimit: 300000 });
104
123
  const receipt = await tx.wait();
105
124
  return receipt.hash;
106
125
  }
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
126
  async signPermit(tokenAddr, spenderAddr, amount, deadlineSeconds = 3600) {
113
127
  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)
128
+ const token = new ethers_1.ethers.Contract(tokenAddr, this.ERC20_ABI, this.wallet);
129
+ const [name, nonce, network] = await Promise.all([
130
+ token.name(),
131
+ token.nonces(this.wallet.address),
132
+ this.provider.getNetwork()
124
133
  ]);
125
134
  const domain = {
126
135
  name,
127
- version: '2', // USDC uses version "2"
128
- chainId: Number(chainId),
136
+ version: '1', // USDC on Base uses version 1
137
+ chainId: Number(network.chainId),
129
138
  verifyingContract: tokenAddr
130
139
  };
131
140
  const types = {
@@ -144,8 +153,8 @@ class PayNodeAgentClient {
144
153
  nonce,
145
154
  deadline
146
155
  };
147
- const sig = await this.wallet.signTypedData(domain, types, value);
148
- const { v, r, s } = ethers_1.ethers.Signature.from(sig);
156
+ const signature = await this.wallet.signTypedData(domain, types, value);
157
+ const { v, r, s } = ethers_1.ethers.Signature.from(signature);
149
158
  return { deadline, v, r, s };
150
159
  }
151
160
  }
@@ -5,4 +5,8 @@ export declare const BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA0
5
5
  export declare const BASE_USDC_ADDRESS_SANDBOX = "0xeAC1f2C7099CdaFfB91Aa3b8Ffd653Ef16935798";
6
6
  export declare const PROTOCOL_TREASURY = "0x598bF63F5449876efafa7b36b77Deb2070621C0E";
7
7
  export declare const PROTOCOL_FEE_BPS = 100;
8
+ export declare const MIN_PAYMENT_AMOUNT: bigint;
9
+ export declare const BASE_RPC_URLS: string[];
10
+ export declare const BASE_RPC_URLS_SANDBOX: string[];
11
+ export declare const ACCEPTED_TOKENS: Record<number, string[]>;
8
12
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,eAAO,MAAM,sBAAsB,+CAA+C,CAAC;AACnF,eAAO,MAAM,8BAA8B,+CAA+C,CAAC;AAC3F,eAAO,MAAM,iBAAiB,+CAA+C,CAAC;AAC9E,eAAO,MAAM,yBAAyB,+CAA+C,CAAC;AACtF,eAAO,MAAM,iBAAiB,+CAA+C,CAAC;AAC9E,eAAO,MAAM,gBAAgB,MAAM,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,eAAO,MAAM,sBAAsB,+CAA+C,CAAC;AACnF,eAAO,MAAM,8BAA8B,+CAA+C,CAAC;AAC3F,eAAO,MAAM,iBAAiB,+CAA+C,CAAC;AAC9E,eAAO,MAAM,yBAAyB,+CAA+C,CAAC;AACtF,eAAO,MAAM,iBAAiB,+CAA+C,CAAC;AAC9E,eAAO,MAAM,gBAAgB,MAAM,CAAC;AACpC,eAAO,MAAM,kBAAkB,QAAe,CAAC;AAE/C,eAAO,MAAM,aAAa,UAAmF,CAAC;AAC9G,eAAO,MAAM,qBAAqB,UAA0E,CAAC;AAE7G,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAGpD,CAAC"}
package/dist/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
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;
3
+ exports.ACCEPTED_TOKENS = exports.BASE_RPC_URLS_SANDBOX = exports.BASE_RPC_URLS = exports.MIN_PAYMENT_AMOUNT = exports.PROTOCOL_FEE_BPS = exports.PROTOCOL_TREASURY = exports.BASE_USDC_ADDRESS_SANDBOX = exports.BASE_USDC_ADDRESS = exports.PAYNODE_ROUTER_ADDRESS_SANDBOX = exports.PAYNODE_ROUTER_ADDRESS = void 0;
4
4
  /** Generated by scripts/sync-config.py */
5
5
  exports.PAYNODE_ROUTER_ADDRESS = "0x92e20164FC457a2aC35f53D06268168e6352b200";
6
6
  exports.PAYNODE_ROUTER_ADDRESS_SANDBOX = "0xB587Bc36aaCf65962eCd6Ba59e2DA76f2f575408";
@@ -8,3 +8,10 @@ exports.BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
8
8
  exports.BASE_USDC_ADDRESS_SANDBOX = "0xeAC1f2C7099CdaFfB91Aa3b8Ffd653Ef16935798";
9
9
  exports.PROTOCOL_TREASURY = "0x598bF63F5449876efafa7b36b77Deb2070621C0E";
10
10
  exports.PROTOCOL_FEE_BPS = 100;
11
+ exports.MIN_PAYMENT_AMOUNT = BigInt(1000);
12
+ exports.BASE_RPC_URLS = ["https://mainnet.base.org", "https://base.meowrpc.com", "https://1rpc.io/base"];
13
+ exports.BASE_RPC_URLS_SANDBOX = ["https://sepolia.base.org", "https://base-sepolia-rpc.publicnode.com"];
14
+ exports.ACCEPTED_TOKENS = {
15
+ 8453: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],
16
+ 84532: ["0xeAC1f2C7099CdaFfB91Aa3b8Ffd653Ef16935798"]
17
+ };
@@ -1,13 +1,21 @@
1
1
  export declare enum ErrorCode {
2
- TRANSACTION_NOT_FOUND = "TRANSACTION_NOT_FOUND",
3
- TRANSACTION_FAILED = "TRANSACTION_FAILED",
4
- WRONG_CONTRACT = "WRONG_CONTRACT",
5
- ORDER_MISMATCH = "ORDER_MISMATCH",
6
- INSUFFICIENT_FUNDS = "INSUFFICIENT_FUNDS",
7
- RECEIPT_ALREADY_USED = "RECEIPT_ALREADY_USED",
8
- INVALID_RECEIPT = "INVALID_RECEIPT",
9
- MISSING_RECEIPT = "MISSING_RECEIPT",
10
- TOKEN_NOT_ACCEPTED = "TOKEN_NOT_ACCEPTED",
11
- INTERNAL_ERROR = "INTERNAL_ERROR"
2
+ RpcError = "RpcError",
3
+ InsufficientFunds = "InsufficientFunds",
4
+ AmountTooLow = "AmountTooLow",
5
+ TokenNotAccepted = "TokenNotAccepted",
6
+ TransactionFailed = "TransactionFailed",
7
+ DuplicateTransaction = "DuplicateTransaction",
8
+ InvalidReceipt = "InvalidReceipt",
9
+ InternalError = "InternalError",
10
+ TransactionNotFound = "TransactionNotFound",
11
+ WrongContract = "WrongContract",
12
+ OrderMismatch = "OrderMismatch",
13
+ MissingReceipt = "MissingReceipt"
14
+ }
15
+ export declare class PayNodeException extends Error {
16
+ message: string;
17
+ code: ErrorCode;
18
+ details?: any | undefined;
19
+ constructor(message: string, code: ErrorCode, details?: any | undefined);
12
20
  }
13
21
  //# 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;IACnC,kBAAkB,uBAAuB;IACzC,cAAc,mBAAmB;CAClC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"AAAA,oBAAY,SAAS;IACnB,QAAQ,aAAa;IACrB,iBAAiB,sBAAsB;IACvC,YAAY,iBAAiB;IAC7B,gBAAgB,qBAAqB;IACrC,iBAAiB,sBAAsB;IACvC,oBAAoB,yBAAyB;IAC7C,cAAc,mBAAmB;IACjC,aAAa,kBAAkB;IAC/B,mBAAmB,wBAAwB;IAC3C,aAAa,kBAAkB;IAC/B,aAAa,kBAAkB;IAC/B,cAAc,mBAAmB;CAClC;AAED,qBAAa,gBAAiB,SAAQ,KAAK;IACtB,OAAO,EAAE,MAAM;IAAS,IAAI,EAAE,SAAS;IAAS,OAAO,CAAC,EAAE,GAAG;gBAA7D,OAAO,EAAE,MAAM,EAAS,IAAI,EAAE,SAAS,EAAS,OAAO,CAAC,EAAE,GAAG,YAAA;CAIjF"}
@@ -1,16 +1,31 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ErrorCode = void 0;
3
+ exports.PayNodeException = exports.ErrorCode = void 0;
4
4
  var ErrorCode;
5
5
  (function (ErrorCode) {
6
- ErrorCode["TRANSACTION_NOT_FOUND"] = "TRANSACTION_NOT_FOUND";
7
- ErrorCode["TRANSACTION_FAILED"] = "TRANSACTION_FAILED";
8
- ErrorCode["WRONG_CONTRACT"] = "WRONG_CONTRACT";
9
- ErrorCode["ORDER_MISMATCH"] = "ORDER_MISMATCH";
10
- ErrorCode["INSUFFICIENT_FUNDS"] = "INSUFFICIENT_FUNDS";
11
- ErrorCode["RECEIPT_ALREADY_USED"] = "RECEIPT_ALREADY_USED";
12
- ErrorCode["INVALID_RECEIPT"] = "INVALID_RECEIPT";
13
- ErrorCode["MISSING_RECEIPT"] = "MISSING_RECEIPT";
14
- ErrorCode["TOKEN_NOT_ACCEPTED"] = "TOKEN_NOT_ACCEPTED";
15
- ErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
6
+ ErrorCode["RpcError"] = "RpcError";
7
+ ErrorCode["InsufficientFunds"] = "InsufficientFunds";
8
+ ErrorCode["AmountTooLow"] = "AmountTooLow";
9
+ ErrorCode["TokenNotAccepted"] = "TokenNotAccepted";
10
+ ErrorCode["TransactionFailed"] = "TransactionFailed";
11
+ ErrorCode["DuplicateTransaction"] = "DuplicateTransaction";
12
+ ErrorCode["InvalidReceipt"] = "InvalidReceipt";
13
+ ErrorCode["InternalError"] = "InternalError";
14
+ ErrorCode["TransactionNotFound"] = "TransactionNotFound";
15
+ ErrorCode["WrongContract"] = "WrongContract";
16
+ ErrorCode["OrderMismatch"] = "OrderMismatch";
17
+ ErrorCode["MissingReceipt"] = "MissingReceipt";
16
18
  })(ErrorCode || (exports.ErrorCode = ErrorCode = {}));
19
+ class PayNodeException extends Error {
20
+ message;
21
+ code;
22
+ details;
23
+ constructor(message, code, details) {
24
+ super(message);
25
+ this.message = message;
26
+ this.code = code;
27
+ this.details = details;
28
+ this.name = "PayNodeException";
29
+ }
30
+ }
31
+ exports.PayNodeException = PayNodeException;
@@ -1 +1 @@
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"}
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,MAiB3C,KAAK,GAAG,EAAE,KAAK,GAAG,EAAE,MAAM,YAAY,iBAwDrD,CAAC"}
@@ -8,6 +8,7 @@ const x402_gate = (options) => {
8
8
  const verifier = new verifier_1.PayNodeVerifier({
9
9
  rpcUrls: options.rpcUrls,
10
10
  chainId: options.chainId,
11
+ contractAddress: options.contractAddress,
11
12
  store: options.store
12
13
  });
13
14
  let rawAmount;
@@ -46,7 +47,7 @@ const x402_gate = (options) => {
46
47
  }
47
48
  return res.status(402).json({
48
49
  error: "Payment Required",
49
- code: errors_1.ErrorCode.MISSING_RECEIPT,
50
+ code: errors_1.ErrorCode.MissingReceipt,
50
51
  message: "Please pay to PayNode contract and provide 'x-paynode-receipt' header.",
51
52
  amount: options.price,
52
53
  currency: options.currency
@@ -67,7 +68,7 @@ const x402_gate = (options) => {
67
68
  else {
68
69
  return res.status(403).json({
69
70
  error: "Forbidden",
70
- code: result.error?.code || errors_1.ErrorCode.INVALID_RECEIPT,
71
+ code: result.error?.code || errors_1.ErrorCode.InvalidReceipt,
71
72
  message: result.error?.message || "Invalid receipt"
72
73
  });
73
74
  }
@@ -1,4 +1,4 @@
1
- import { ErrorCode } from '../errors';
1
+ import { PayNodeException } from '../errors';
2
2
  import { IdempotencyStore } from './idempotency';
3
3
  /**
4
4
  * Default accepted token addresses across supported chains.
@@ -6,8 +6,11 @@ import { IdempotencyStore } from './idempotency';
6
6
  * preventing fake-token attacks at the verification layer.
7
7
  */
8
8
  export declare const ACCEPTED_TOKENS: Record<string, string[]>;
9
+ /** Minimum allowed payment amount to prevent dust exploits (1000 = 0.001 USDC) */
10
+ export declare const MIN_PAYMENT_AMOUNT = 1000n;
9
11
  export interface PayNodeVerifierConfig {
10
12
  rpcUrls: string | string[];
13
+ contractAddress: string;
11
14
  chainId?: number;
12
15
  store?: IdempotencyStore;
13
16
  /** Override the default accepted token whitelist. If provided, only these addresses are allowed. */
@@ -18,20 +21,17 @@ export interface ExpectedPayment {
18
21
  tokenAddress: string;
19
22
  amount: string | number | bigint;
20
23
  orderId?: string;
21
- verifyDepegPrice?: boolean;
22
24
  }
23
25
  export declare class PayNodeVerifier {
24
26
  private provider;
27
+ private contractAddress;
25
28
  private chainId?;
26
29
  private store?;
27
30
  private acceptedTokens?;
28
31
  constructor(config: PayNodeVerifierConfig);
29
32
  verifyPayment(txHash: string, expected: ExpectedPayment): Promise<{
30
33
  isValid: boolean;
31
- error?: {
32
- code: ErrorCode;
33
- message: string;
34
- };
34
+ error?: PayNodeException;
35
35
  }>;
36
36
  }
37
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;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
+ {"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../../src/utils/verifier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAExD,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjD;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CASpD,CAAC;AAEF,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,QAAQ,CAAC;AAExC,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,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;CAkFxH"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PayNodeVerifier = exports.ACCEPTED_TOKENS = void 0;
3
+ exports.PayNodeVerifier = exports.MIN_PAYMENT_AMOUNT = exports.ACCEPTED_TOKENS = void 0;
4
4
  const errors_1 = require("../errors");
5
5
  const ethers_1 = require("ethers");
6
6
  /**
@@ -12,25 +12,27 @@ exports.ACCEPTED_TOKENS = {
12
12
  // Base Mainnet (chainId: 8453)
13
13
  '8453': [
14
14
  '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC
15
- '0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2', // USDT
16
15
  ],
17
16
  // Base Sepolia (chainId: 84532)
18
17
  '84532': [
19
18
  '0xeAC1f2C7099CdaFfB91Aa3b8Ffd653Ef16935798', // USDC (Sandbox)
20
19
  ],
21
20
  };
21
+ /** Minimum allowed payment amount to prevent dust exploits (1000 = 0.001 USDC) */
22
+ exports.MIN_PAYMENT_AMOUNT = 1000n;
22
23
  const PAYNODE_ABI = [
23
24
  "event PaymentReceived(bytes32 indexed orderId, address indexed merchant, address indexed payer, address token, uint256 amount, uint256 fee, uint256 chainId)"
24
25
  ];
25
26
  const iface = new ethers_1.Interface(PAYNODE_ABI);
26
27
  class PayNodeVerifier {
27
28
  provider;
29
+ contractAddress;
28
30
  chainId;
29
31
  store;
30
32
  acceptedTokens;
31
33
  constructor(config) {
32
34
  if (!config.rpcUrls || (Array.isArray(config.rpcUrls) && config.rpcUrls.length === 0)) {
33
- throw new Error("rpcUrls must be provided");
35
+ throw new errors_1.PayNodeException("Failed to connect to any provided RPC nodes.", errors_1.ErrorCode.RpcError);
34
36
  }
35
37
  // Support RpcPool / FallbackProvider
36
38
  if (Array.isArray(config.rpcUrls)) {
@@ -47,10 +49,9 @@ class PayNodeVerifier {
47
49
  else {
48
50
  this.provider = new ethers_1.JsonRpcProvider(config.rpcUrls, config.chainId);
49
51
  }
52
+ this.contractAddress = config.contractAddress;
50
53
  this.chainId = config.chainId;
51
54
  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
55
  let tokenList;
55
56
  if (config.acceptedTokens !== undefined) {
56
57
  tokenList = config.acceptedTokens;
@@ -64,30 +65,38 @@ class PayNodeVerifier {
64
65
  }
65
66
  async verifyPayment(txHash, expected) {
66
67
  try {
67
- // 0. Token Whitelist Check (Anti-FakeToken)
68
+ // 0. Dust Exploit Check (Minimum Payment)
69
+ const expectedAmount = BigInt(expected.amount);
70
+ if (expectedAmount < exports.MIN_PAYMENT_AMOUNT) {
71
+ return { isValid: false, error: new errors_1.PayNodeException("Payment amount is below the protocol minimum (1000).", errors_1.ErrorCode.AmountTooLow) };
72
+ }
73
+ // 1. Token Whitelist Check (Anti-FakeToken)
68
74
  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.` } };
75
+ return { isValid: false, error: new errors_1.PayNodeException("The provided token address is not in the whitelist.", errors_1.ErrorCode.TokenNotAccepted) };
70
76
  }
71
77
  // 1. Idempotency Check
72
78
  if (this.store) {
73
- // Assume TTL of 24 hours for replay protection
74
79
  const isNew = await this.store.checkAndSet(txHash, 86400);
75
80
  if (!isNew) {
76
- return { isValid: false, error: { code: errors_1.ErrorCode.RECEIPT_ALREADY_USED, message: 'Transaction hash has already been consumed.' } };
81
+ return { isValid: false, error: new errors_1.PayNodeException("This transaction hash has already been consumed.", errors_1.ErrorCode.DuplicateTransaction) };
77
82
  }
78
83
  }
79
84
  // 2. Fetch Receipt
80
85
  const receipt = await this.provider.getTransactionReceipt(txHash);
81
86
  if (!receipt) {
82
- return { isValid: false, error: { code: errors_1.ErrorCode.TRANSACTION_NOT_FOUND, message: "Transaction not found on-chain." } };
87
+ return { isValid: false, error: new errors_1.PayNodeException("The provided receipt (TxHash) is malformed or invalid.", errors_1.ErrorCode.InvalidReceipt) };
83
88
  }
84
89
  if (receipt.status !== 1) {
85
- return { isValid: false, error: { code: errors_1.ErrorCode.TRANSACTION_FAILED, message: "Transaction reverted on-chain." } };
90
+ return { isValid: false, error: new errors_1.PayNodeException("On-chain transaction reverted or failed.", errors_1.ErrorCode.TransactionFailed) };
86
91
  }
87
- // 3. Parse Logs
92
+ // 3. Parse Logs & Verify Contract Source
88
93
  let paymentLog = null;
89
94
  for (const log of receipt.logs) {
90
95
  try {
96
+ // Security Fix: Verify the log address matches the official router address
97
+ if (log.address.toLowerCase() !== this.contractAddress.toLowerCase()) {
98
+ continue;
99
+ }
91
100
  const parsed = iface.parseLog({ topics: log.topics, data: log.data });
92
101
  if (parsed && parsed.name === 'PaymentReceived') {
93
102
  paymentLog = { parsed, logAddress: log.address };
@@ -99,39 +108,32 @@ class PayNodeVerifier {
99
108
  }
100
109
  }
101
110
  if (!paymentLog) {
102
- return { isValid: false, error: { code: errors_1.ErrorCode.ORDER_MISMATCH, message: "No PaymentReceived event found in transaction." } };
111
+ return { isValid: false, error: new errors_1.PayNodeException("No valid PaymentReceived event from official contract found in transaction.", errors_1.ErrorCode.WrongContract) };
103
112
  }
104
113
  const args = paymentLog.parsed.args;
105
114
  // 4. Verify Merchant
106
115
  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}` } };
116
+ return { isValid: false, error: new errors_1.PayNodeException("Payment went to a different merchant.", errors_1.ErrorCode.InvalidReceipt) };
108
117
  }
109
118
  // 5. Verify Token
110
119
  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}` } };
120
+ return { isValid: false, error: new errors_1.PayNodeException("Payment used unexpected token.", errors_1.ErrorCode.InvalidReceipt) };
112
121
  }
113
122
  // 6. Verify Amount
114
123
  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}` } };
124
+ return { isValid: false, error: new errors_1.PayNodeException("Payment amount is below required price.", errors_1.ErrorCode.InvalidReceipt) };
116
125
  }
117
126
  // 7. Verify ChainId (Cross-chain replay protection)
118
127
  const expectedChainId = BigInt(this.chainId || (await this.provider.getNetwork()).chainId);
119
128
  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
- }
129
+ return { isValid: false, error: new errors_1.PayNodeException("ChainId mismatch. Invalid network.", errors_1.ErrorCode.InvalidReceipt) };
130
130
  }
131
131
  return { isValid: true };
132
132
  }
133
133
  catch (e) {
134
- return { isValid: false, error: { code: errors_1.ErrorCode.INTERNAL_ERROR, message: e.message } };
134
+ if (e instanceof errors_1.PayNodeException)
135
+ return { isValid: false, error: e };
136
+ return { isValid: false, error: new errors_1.PayNodeException(`An unexpected error occurred: ${e.message}`, errors_1.ErrorCode.InternalError) };
135
137
  }
136
138
  }
137
139
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paynodelabs/sdk-js",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "The official JavaScript/TypeScript SDK for PayNode x402 protocol on Base L2.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -11,7 +11,8 @@
11
11
  "scripts": {
12
12
  "build": "tsc",
13
13
  "prepublishOnly": "npm run build",
14
- "test": "ts-node tests/mainnet_live.ts"
14
+ "test": "jest",
15
+ "example:server": "ts-node examples/express-server.ts"
15
16
  },
16
17
  "keywords": [
17
18
  "paynode",
@@ -25,13 +26,17 @@
25
26
  "license": "MIT",
26
27
  "dependencies": {
27
28
  "ethers": "^6.13.0",
28
- "ioredis": "^5.10.1"
29
+ "ioredis": "^5.10.1",
30
+ "express": "^4.19.2"
29
31
  },
30
32
  "devDependencies": {
31
33
  "@types/express": "^5.0.6",
32
34
  "@types/ioredis": "^4.28.10",
35
+ "@types/jest": "^29.5.12",
33
36
  "@types/node": "^20.12.12",
34
37
  "dotenv": "^16.4.5",
38
+ "jest": "^29.7.0",
39
+ "ts-jest": "^29.1.4",
35
40
  "ts-node": "^10.9.2",
36
41
  "typescript": "^5.4.5"
37
42
  }