@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 +21 -0
- package/README.md +47 -18
- package/dist/client.d.ts +5 -26
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +80 -71
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +8 -1
- package/dist/errors/index.d.ts +18 -10
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +26 -11
- package/dist/middleware/x402.d.ts.map +1 -1
- package/dist/middleware/x402.js +3 -2
- package/dist/utils/verifier.d.ts +6 -6
- package/dist/utils/verifier.d.ts.map +1 -1
- package/dist/utils/verifier.js +29 -27
- package/package.json +8 -3
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 {
|
|
24
|
+
import { PayNodeAgentClient } from "@paynodelabs/sdk-js";
|
|
25
25
|
|
|
26
|
-
const client = new
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
36
|
+
## 🚀 Run the Demo
|
|
37
37
|
|
|
38
|
-
|
|
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
|
-
|
|
40
|
+
### 1. Setup Environment
|
|
43
41
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
```bash
|
|
43
|
+
cp .env.example .env
|
|
44
|
+
# Edit .env with your private key and RPC URLs
|
|
45
|
+
```
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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:
|
|
17
|
+
v: 27 | 28;
|
|
39
18
|
r: string;
|
|
40
19
|
s: string;
|
|
41
20
|
}>;
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"
|
|
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,
|
|
18
|
-
this.
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
62
|
+
throw new errors_1.PayNodeException("Malformed 402 headers: missing metadata", errors_1.ErrorCode.InternalError);
|
|
48
63
|
}
|
|
49
64
|
const amount = BigInt(amountStr);
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
113
|
+
const receipt = await tx.wait();
|
|
85
114
|
return receipt.hash;
|
|
86
115
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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: '
|
|
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
|
|
148
|
-
const { v, r, s } = ethers_1.ethers.Signature.from(
|
|
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
|
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -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
|
package/dist/constants.d.ts.map
CHANGED
|
@@ -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
|
+
};
|
package/dist/errors/index.d.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
export declare enum ErrorCode {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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,
|
|
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"}
|
package/dist/errors/index.js
CHANGED
|
@@ -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["
|
|
7
|
-
ErrorCode["
|
|
8
|
-
ErrorCode["
|
|
9
|
-
ErrorCode["
|
|
10
|
-
ErrorCode["
|
|
11
|
-
ErrorCode["
|
|
12
|
-
ErrorCode["
|
|
13
|
-
ErrorCode["
|
|
14
|
-
ErrorCode["
|
|
15
|
-
ErrorCode["
|
|
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,
|
|
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"}
|
package/dist/middleware/x402.js
CHANGED
|
@@ -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.
|
|
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.
|
|
71
|
+
code: result.error?.code || errors_1.ErrorCode.InvalidReceipt,
|
|
71
72
|
message: result.error?.message || "Invalid receipt"
|
|
72
73
|
});
|
|
73
74
|
}
|
package/dist/utils/verifier.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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,
|
|
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"}
|
package/dist/utils/verifier.js
CHANGED
|
@@ -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
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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
|
}
|