@paynodelabs/sdk-js 2.0.1 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/client.d.ts +4 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +75 -32
- package/dist/constants.d.ts +2 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +4 -3
- package/dist/middleware/x402.d.ts +0 -2
- package/dist/middleware/x402.d.ts.map +1 -1
- package/dist/middleware/x402.js +9 -5
- package/dist/utils/idempotency.d.ts.map +1 -1
- package/dist/utils/idempotency.js +4 -1
- package/dist/utils/verifier.d.ts +1 -1
- package/dist/utils/verifier.d.ts.map +1 -1
- package/dist/utils/verifier.js +55 -20
- package/dist/utils/webhook.d.ts +2 -2
- package/dist/utils/webhook.d.ts.map +1 -1
- package/dist/utils/webhook.js +4 -4
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ async function main() {
|
|
|
33
33
|
main();
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
### Key Features (v2.
|
|
36
|
+
### Key Features (v2.1)
|
|
37
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
38
|
- **Double-Spend Protection**:
|
|
39
39
|
- **L1 (Memory)**: High-speed local replay protection via `IdempotencyStore`.
|
package/dist/client.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare class PayNodeAgentClient {
|
|
|
7
7
|
private provider;
|
|
8
8
|
private rpcUrls;
|
|
9
9
|
private maxRetries;
|
|
10
|
+
private nonceLock;
|
|
10
11
|
private ERC20_ABI;
|
|
11
12
|
private ROUTER_ABI;
|
|
12
13
|
constructor(privateKey: string, rpcUrls?: string | string[], maxRetries?: number);
|
|
@@ -15,12 +16,13 @@ export declare class PayNodeAgentClient {
|
|
|
15
16
|
private _handleX402V2;
|
|
16
17
|
signTransferWithAuthorization(tokenAddr: string, to: string, amount: bigint, validAfter: number, validBefore: number, nonce: string, extra?: Record<string, any>): Promise<ExactEVMPayload>;
|
|
17
18
|
pay(contractAddr: string, tokenAddr: string, merchantAddr: string, amount: bigint, orderId: string): Promise<string>;
|
|
18
|
-
payWithPermit(contractAddr: string, tokenAddr: string, merchantAddr: string, amount: bigint, orderId: string): Promise<string>;
|
|
19
|
-
signPermit(tokenAddr: string, spenderAddr: string, amount: bigint, deadlineSeconds?: number): Promise<{
|
|
19
|
+
payWithPermit(contractAddr: string, tokenAddr: string, merchantAddr: string, amount: bigint, orderId: string, version?: string): Promise<string>;
|
|
20
|
+
signPermit(tokenAddr: string, spenderAddr: string, amount: bigint, deadlineSeconds?: number, version?: string): Promise<{
|
|
20
21
|
deadline: number;
|
|
21
22
|
v: 27 | 28;
|
|
22
23
|
r: string;
|
|
23
24
|
s: string;
|
|
24
25
|
}>;
|
|
26
|
+
private _lockNonce;
|
|
25
27
|
}
|
|
26
28
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;IAC3B,OAAO,CAAC,SAAS,CAAoC;IAErD,OAAO,CAAC,SAAS,CAMf;IAEF,OAAO,CAAC,UAAU,CAAsB;gBAE5B,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;YAoDjE,aAAa;IAuGrB,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;IAqBpH,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,MAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IA8BrJ,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAE,MAAa,EAAE,OAAO,GAAE,MAAY;;;;;;YAyChH,UAAU;CAYzB"}
|
package/dist/client.js
CHANGED
|
@@ -10,6 +10,7 @@ class PayNodeAgentClient {
|
|
|
10
10
|
provider;
|
|
11
11
|
rpcUrls;
|
|
12
12
|
maxRetries;
|
|
13
|
+
nonceLock = Promise.resolve();
|
|
13
14
|
ERC20_ABI = [
|
|
14
15
|
"function approve(address spender, uint256 value) public returns (bool)",
|
|
15
16
|
"function allowance(address owner, address spender) public view returns (uint256)",
|
|
@@ -17,10 +18,7 @@ class PayNodeAgentClient {
|
|
|
17
18
|
"function name() view returns (string)",
|
|
18
19
|
"function nonces(address owner) view returns (uint256)"
|
|
19
20
|
];
|
|
20
|
-
ROUTER_ABI =
|
|
21
|
-
"function pay(address token, address merchant, uint256 amount, bytes32 orderId) public",
|
|
22
|
-
"function payWithPermit(address payer, address token, address merchant, uint256 amount, bytes32 orderId, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public"
|
|
23
|
-
];
|
|
21
|
+
ROUTER_ABI = constants_1.PAYNODE_ROUTER_ABI;
|
|
24
22
|
constructor(privateKey, rpcUrls = constants_1.BASE_RPC_URLS, maxRetries = 3) {
|
|
25
23
|
this.rpcUrls = Array.isArray(rpcUrls) ? rpcUrls : [rpcUrls];
|
|
26
24
|
this.maxRetries = maxRetries;
|
|
@@ -38,7 +36,7 @@ class PayNodeAgentClient {
|
|
|
38
36
|
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
39
37
|
try {
|
|
40
38
|
const response = await fetch(url, options);
|
|
41
|
-
if (!RETRYABLE_STATUS_CODES.has(response.status)) {
|
|
39
|
+
if (!response || !RETRYABLE_STATUS_CODES.has(response.status)) {
|
|
42
40
|
return response;
|
|
43
41
|
}
|
|
44
42
|
if (attempt < this.maxRetries - 1) {
|
|
@@ -82,7 +80,10 @@ class PayNodeAgentClient {
|
|
|
82
80
|
}
|
|
83
81
|
else if (b64Required) {
|
|
84
82
|
try {
|
|
85
|
-
|
|
83
|
+
const decoded = typeof globalThis.Buffer !== 'undefined'
|
|
84
|
+
? globalThis.Buffer.from(b64Required, 'base64').toString()
|
|
85
|
+
: atob(b64Required);
|
|
86
|
+
body = JSON.parse(decoded);
|
|
86
87
|
}
|
|
87
88
|
catch (e) {
|
|
88
89
|
console.debug('⚠️ [PayNode-JS] Failed to parse X-402-Required header:', e);
|
|
@@ -112,7 +113,12 @@ class PayNodeAgentClient {
|
|
|
112
113
|
// Select suitable requirement
|
|
113
114
|
const requirement = requirements.accepts.find((req) => req.network === caip2ChainId);
|
|
114
115
|
if (!requirement) {
|
|
115
|
-
throw new errors_1.PayNodeException(errors_1.ErrorCode.
|
|
116
|
+
throw new errors_1.PayNodeException(errors_1.ErrorCode.TransactionFailed, `No compatible payment requirement found for network ${caip2ChainId}`);
|
|
117
|
+
}
|
|
118
|
+
// 🛡️ Token Whitelist Check (Case-insensitive)
|
|
119
|
+
const chainTokens = constants_1.ACCEPTED_TOKENS[chainId]?.map(t => t.toLowerCase());
|
|
120
|
+
if (chainTokens && !chainTokens.includes(requirement.asset.toLowerCase())) {
|
|
121
|
+
throw new errors_1.PayNodeException(errors_1.ErrorCode.TokenNotAccepted, `Token ${requirement.asset} is not in the whitelist for chain ${chainId}`);
|
|
116
122
|
}
|
|
117
123
|
const orderId = requirement.orderId || requirements.orderId || url;
|
|
118
124
|
// Dust limit check
|
|
@@ -144,10 +150,16 @@ class PayNodeAgentClient {
|
|
|
144
150
|
const allowance = await tokenContract.allowance(this.wallet.address, routerAddr);
|
|
145
151
|
let txHash;
|
|
146
152
|
if (allowance >= amount) {
|
|
147
|
-
|
|
153
|
+
try {
|
|
154
|
+
txHash = await this.pay(routerAddr, requirement.asset, requirement.payTo, amount, orderId);
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
console.warn(`⚠️ [PayNode-JS] Direct pay failed (possibly allowance race), falling back to permit:`, e);
|
|
158
|
+
txHash = await this.payWithPermit(routerAddr, requirement.asset, requirement.payTo, amount, orderId, requirement.extra?.version || '2');
|
|
159
|
+
}
|
|
148
160
|
}
|
|
149
161
|
else {
|
|
150
|
-
txHash = await this.payWithPermit(routerAddr, requirement.asset, requirement.payTo, amount, orderId);
|
|
162
|
+
txHash = await this.payWithPermit(routerAddr, requirement.asset, requirement.payTo, amount, orderId, requirement.extra?.version || '2');
|
|
151
163
|
}
|
|
152
164
|
payload = {
|
|
153
165
|
version: "3.1",
|
|
@@ -156,7 +168,10 @@ class PayNodeAgentClient {
|
|
|
156
168
|
payload: { txHash }
|
|
157
169
|
};
|
|
158
170
|
}
|
|
159
|
-
const
|
|
171
|
+
const json = JSON.stringify(payload);
|
|
172
|
+
const b64Payload = typeof globalThis.Buffer !== 'undefined'
|
|
173
|
+
? globalThis.Buffer.from(json).toString('base64')
|
|
174
|
+
: btoa(json);
|
|
160
175
|
const retryOptions = {
|
|
161
176
|
...options,
|
|
162
177
|
headers: {
|
|
@@ -166,7 +181,11 @@ class PayNodeAgentClient {
|
|
|
166
181
|
'X-402-Order-Id': orderId
|
|
167
182
|
}
|
|
168
183
|
};
|
|
169
|
-
|
|
184
|
+
const retryResponse = await this._fetchWithRetry(url, retryOptions);
|
|
185
|
+
if (retryResponse.status === 402) {
|
|
186
|
+
throw new errors_1.PayNodeException(errors_1.ErrorCode.TransactionFailed, "Still 402 after payment attempt. The server may have rejected the payment or authorization.");
|
|
187
|
+
}
|
|
188
|
+
return retryResponse;
|
|
170
189
|
}
|
|
171
190
|
async signTransferWithAuthorization(tokenAddr, to, amount, validAfter, validBefore, nonce, extra = {}) {
|
|
172
191
|
const network = await this.provider.getNetwork();
|
|
@@ -208,28 +227,42 @@ class PayNodeAgentClient {
|
|
|
208
227
|
};
|
|
209
228
|
}
|
|
210
229
|
async pay(contractAddr, tokenAddr, merchantAddr, amount, orderId) {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
230
|
+
const unlock = await this._lockNonce();
|
|
231
|
+
try {
|
|
232
|
+
const router = new ethers_1.ethers.Contract(contractAddr, this.ROUTER_ABI, this.wallet);
|
|
233
|
+
// convention: we hash the raw orderId string to bytes32 internally
|
|
234
|
+
const orderIdBytes = ethers_1.ethers.id(orderId);
|
|
235
|
+
const feeData = await this.provider.getFeeData();
|
|
236
|
+
const gasPrice = (feeData.gasPrice * 120n) / 100n;
|
|
237
|
+
const tx = await router.pay(tokenAddr, merchantAddr, amount, orderIdBytes, {
|
|
238
|
+
gasPrice,
|
|
239
|
+
gasLimit: 200000
|
|
240
|
+
});
|
|
241
|
+
const receipt = await tx.wait();
|
|
242
|
+
return receipt.hash;
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
unlock();
|
|
246
|
+
}
|
|
221
247
|
}
|
|
222
|
-
async payWithPermit(contractAddr, tokenAddr, merchantAddr, amount, orderId) {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
248
|
+
async payWithPermit(contractAddr, tokenAddr, merchantAddr, amount, orderId, version = '2') {
|
|
249
|
+
const unlock = await this._lockNonce();
|
|
250
|
+
try {
|
|
251
|
+
const sig = await this.signPermit(tokenAddr, contractAddr, amount, 3600, version);
|
|
252
|
+
const router = new ethers_1.ethers.Contract(contractAddr, this.ROUTER_ABI, this.wallet);
|
|
253
|
+
// convention: we hash the raw orderId string to bytes32 internally
|
|
254
|
+
const orderIdBytes = ethers_1.ethers.id(orderId);
|
|
255
|
+
const feeData = await this.provider.getFeeData();
|
|
256
|
+
const gasPrice = (feeData.gasPrice * 120n) / 100n;
|
|
257
|
+
const tx = await router.payWithPermit(this.wallet.address, tokenAddr, merchantAddr, amount, orderIdBytes, sig.deadline, sig.v, sig.r, sig.s, { gasPrice, gasLimit: 300000 });
|
|
258
|
+
const receipt = await tx.wait();
|
|
259
|
+
return receipt.hash;
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
unlock();
|
|
263
|
+
}
|
|
231
264
|
}
|
|
232
|
-
async signPermit(tokenAddr, spenderAddr, amount, deadlineSeconds = 3600) {
|
|
265
|
+
async signPermit(tokenAddr, spenderAddr, amount, deadlineSeconds = 3600, version = '2') {
|
|
233
266
|
const deadline = Math.floor(Date.now() / 1000) + deadlineSeconds;
|
|
234
267
|
const token = new ethers_1.ethers.Contract(tokenAddr, this.ERC20_ABI, this.wallet);
|
|
235
268
|
const [name, nonce, network] = await Promise.all([
|
|
@@ -239,7 +272,7 @@ class PayNodeAgentClient {
|
|
|
239
272
|
]);
|
|
240
273
|
const domain = {
|
|
241
274
|
name,
|
|
242
|
-
version
|
|
275
|
+
version,
|
|
243
276
|
chainId: Number(network.chainId),
|
|
244
277
|
verifyingContract: tokenAddr
|
|
245
278
|
};
|
|
@@ -263,5 +296,15 @@ class PayNodeAgentClient {
|
|
|
263
296
|
const { v, r, s } = ethers_1.ethers.Signature.from(signature);
|
|
264
297
|
return { deadline, v, r, s };
|
|
265
298
|
}
|
|
299
|
+
async _lockNonce() {
|
|
300
|
+
let resolver;
|
|
301
|
+
const nextLock = new Promise((resolve) => {
|
|
302
|
+
resolver = resolve;
|
|
303
|
+
});
|
|
304
|
+
const currentLock = this.nonceLock;
|
|
305
|
+
this.nonceLock = nextLock;
|
|
306
|
+
await currentLock;
|
|
307
|
+
return resolver;
|
|
308
|
+
}
|
|
266
309
|
}
|
|
267
310
|
exports.PayNodeAgentClient = PayNodeAgentClient;
|
package/dist/constants.d.ts
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
export declare const PAYNODE_ROUTER_ADDRESS = "0x4A73696ccF76E7381b044cB95127B3784369Ed63";
|
|
3
3
|
export declare const PAYNODE_ROUTER_ADDRESS_SANDBOX = "0x24cD8b68aaC209217ff5a6ef1Bf55a59f2c8Ca6F";
|
|
4
4
|
export declare const BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
5
|
-
export declare const BASE_USDC_ADDRESS_SANDBOX = "
|
|
5
|
+
export declare const BASE_USDC_ADDRESS_SANDBOX = "0x65c088EfBDB0E03185Dbe8e258Ad0cf4Ab7946b0";
|
|
6
6
|
export declare const PROTOCOL_TREASURY = "0x598bF63F5449876efafa7b36b77Deb2070621C0E";
|
|
7
7
|
export declare const PROTOCOL_FEE_BPS = 100;
|
|
8
8
|
export declare const MIN_PAYMENT_AMOUNT: bigint;
|
|
9
9
|
export declare const BASE_RPC_URLS: string[];
|
|
10
10
|
export declare const BASE_RPC_URLS_SANDBOX: string[];
|
|
11
11
|
export declare const ACCEPTED_TOKENS: Record<number, string[]>;
|
|
12
|
+
export declare const PAYNODE_ROUTER_ABI: string[];
|
|
12
13
|
//# 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;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"}
|
|
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;AACF,eAAO,MAAM,kBAAkB,UAAqsC,CAAC"}
|
package/dist/constants.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ACCEPTED_TOKENS = exports.BASE_RPC_URLS_SANDBOX = exports.BASE_RPC_URLS = exports.MIN_PAYMENT_AMOUNT = exports.PROTOCOL_FEE_BPS = exports.PROTOCOL_TREASURY = exports.BASE_USDC_ADDRESS_SANDBOX = exports.BASE_USDC_ADDRESS = exports.PAYNODE_ROUTER_ADDRESS_SANDBOX = exports.PAYNODE_ROUTER_ADDRESS = void 0;
|
|
3
|
+
exports.PAYNODE_ROUTER_ABI = 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 = "0x4A73696ccF76E7381b044cB95127B3784369Ed63";
|
|
6
6
|
exports.PAYNODE_ROUTER_ADDRESS_SANDBOX = "0x24cD8b68aaC209217ff5a6ef1Bf55a59f2c8Ca6F";
|
|
7
7
|
exports.BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
8
|
-
exports.BASE_USDC_ADDRESS_SANDBOX = "
|
|
8
|
+
exports.BASE_USDC_ADDRESS_SANDBOX = "0x65c088EfBDB0E03185Dbe8e258Ad0cf4Ab7946b0";
|
|
9
9
|
exports.PROTOCOL_TREASURY = "0x598bF63F5449876efafa7b36b77Deb2070621C0E";
|
|
10
10
|
exports.PROTOCOL_FEE_BPS = 100;
|
|
11
11
|
exports.MIN_PAYMENT_AMOUNT = BigInt(1000);
|
|
@@ -13,5 +13,6 @@ exports.BASE_RPC_URLS = ["https://mainnet.base.org", "https://base.meowrpc.com",
|
|
|
13
13
|
exports.BASE_RPC_URLS_SANDBOX = ["https://sepolia.base.org", "https://base-sepolia-rpc.publicnode.com"];
|
|
14
14
|
exports.ACCEPTED_TOKENS = {
|
|
15
15
|
8453: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],
|
|
16
|
-
84532: ["
|
|
16
|
+
84532: ["0x65c088EfBDB0E03185Dbe8e258Ad0cf4Ab7946b0"]
|
|
17
17
|
};
|
|
18
|
+
exports.PAYNODE_ROUTER_ABI = ["function MAX_BPS() public", "function MIN_PAYMENT_AMOUNT() public", "function PROTOCOL_FEE_BPS() public", "function acceptOwnership() public", "function owner() public", "function pause() public", "function paused() public", "function pay(address token, address merchant, uint256 amount, bytes32 orderId) public", "function payWithPermit(address payer, address token, address merchant, uint256 amount, bytes32 orderId, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public", "function pendingOwner() public", "function protocolTreasury() public", "function renounceOwnership() public", "function transferOwnership(address newOwner) public", "function unpause() public", "function updateTreasury(address _newTreasury) public", "event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner)", "event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)", "event Paused(address account)", "event PaymentReceived(bytes32 indexed orderId, address indexed merchant, address indexed payer, address token, uint256 amount, uint256 fee, uint256 chainId)", "event TreasuryUpdated(address indexed oldTreasury, address indexed newTreasury)", "event Unpaused(address account)"];
|
|
@@ -15,6 +15,4 @@ export interface PayNodeMiddlewareOptions {
|
|
|
15
15
|
maxTimeoutSeconds?: number;
|
|
16
16
|
}
|
|
17
17
|
export declare const x402Gate: (options: PayNodeMiddlewareOptions) => (req: Request | any, res: Response | any, next: NextFunction) => Promise<any>;
|
|
18
|
-
/** @deprecated Use x402Gate instead. */
|
|
19
|
-
export declare const x402_gate: (options: PayNodeMiddlewareOptions) => (req: Request | any, res: Response | any, next: NextFunction) => Promise<any>;
|
|
20
18
|
//# sourceMappingURL=x402.d.ts.map
|
|
@@ -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;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,
|
|
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,MA6B1C,KAAK,OAAO,GAAG,GAAG,EAAE,KAAK,QAAQ,GAAG,GAAG,EAAE,MAAM,YAAY,iBA8F1E,CAAC"}
|
package/dist/middleware/x402.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.x402Gate = void 0;
|
|
4
4
|
const errors_1 = require("../errors");
|
|
5
5
|
const verifier_1 = require("../utils/verifier");
|
|
6
6
|
const ethers_1 = require("ethers");
|
|
@@ -23,7 +23,12 @@ const x402Gate = (options) => {
|
|
|
23
23
|
rawAmount = (0, ethers_1.parseUnits)(options.price, decimals);
|
|
24
24
|
}
|
|
25
25
|
catch (e) {
|
|
26
|
-
|
|
26
|
+
// Robust fallback for non-standard number strings (avoiding floating point math)
|
|
27
|
+
const parts = options.price.split('.');
|
|
28
|
+
const integerPart = parts[0] || '0';
|
|
29
|
+
let fractionPart = parts[1] || '0';
|
|
30
|
+
fractionPart = fractionPart.slice(0, decimals).padEnd(decimals, '0');
|
|
31
|
+
rawAmount = BigInt(integerPart + fractionPart);
|
|
27
32
|
}
|
|
28
33
|
const defaultOrderIdGen = (req) => `agent_js_${Date.now()}`;
|
|
29
34
|
return async (req, res, next) => {
|
|
@@ -56,7 +61,7 @@ const x402Gate = (options) => {
|
|
|
56
61
|
tokenAddress: tokenAddress,
|
|
57
62
|
amount: rawAmount.toString(),
|
|
58
63
|
orderId: orderId
|
|
59
|
-
}, unifiedPayload.type === 'eip3009' ?
|
|
64
|
+
}, unifiedPayload.type === 'eip3009' ? { name: currency, version: "2" } : {});
|
|
60
65
|
if (result.isValid) {
|
|
61
66
|
req.paynode = { unifiedPayload, orderId };
|
|
62
67
|
return next();
|
|
@@ -107,10 +112,9 @@ const x402Gate = (options) => {
|
|
|
107
112
|
const b64Required = Buffer.from(JSON.stringify(v2Response)).toString('base64');
|
|
108
113
|
if (res.set) {
|
|
109
114
|
res.set('X-402-Required', b64Required);
|
|
115
|
+
res.set('X-402-Order-Id', orderId);
|
|
110
116
|
}
|
|
111
117
|
return res.status(402).json(v2Response);
|
|
112
118
|
};
|
|
113
119
|
};
|
|
114
120
|
exports.x402Gate = x402Gate;
|
|
115
|
-
/** @deprecated Use x402Gate instead. */
|
|
116
|
-
exports.x402_gate = exports.x402Gate;
|
|
@@ -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;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;;
|
|
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;;IAUzC,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"}
|
|
@@ -10,7 +10,10 @@ class MemoryIdempotencyStore {
|
|
|
10
10
|
cache = new Map();
|
|
11
11
|
constructor() {
|
|
12
12
|
// Basic cleanup interval
|
|
13
|
-
setInterval(() => this.cleanup(), 60000);
|
|
13
|
+
const interval = setInterval(() => this.cleanup(), 60000);
|
|
14
|
+
if (interval.unref) {
|
|
15
|
+
interval.unref();
|
|
16
|
+
}
|
|
14
17
|
}
|
|
15
18
|
async checkAndSet(txHash, ttlSeconds) {
|
|
16
19
|
const now = Math.floor(Date.now() / 1000);
|
package/dist/utils/verifier.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export declare class PayNodeVerifier {
|
|
|
20
20
|
private contractAddress;
|
|
21
21
|
private chainId?;
|
|
22
22
|
private store?;
|
|
23
|
-
private acceptedTokens
|
|
23
|
+
private acceptedTokens;
|
|
24
24
|
constructor(config: PayNodeVerifierConfig);
|
|
25
25
|
private static ROUTER_ABI;
|
|
26
26
|
verify(unifiedPayload: UnifiedPaymentPayload, expected: ExpectedPayment, extra?: any): Promise<{
|
|
@@ -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;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,
|
|
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;gBAExB,MAAM,EAAE,qBAAqB;IAyCzC,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;IA8BpD,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;IA0ElH;;;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;CA0G3D"}
|
package/dist/utils/verifier.js
CHANGED
|
@@ -40,9 +40,10 @@ class PayNodeVerifier {
|
|
|
40
40
|
else if (config.chainId) {
|
|
41
41
|
tokenList = constants_1.ACCEPTED_TOKENS[config.chainId];
|
|
42
42
|
}
|
|
43
|
-
if (tokenList
|
|
44
|
-
|
|
43
|
+
if (!tokenList || tokenList.length === 0) {
|
|
44
|
+
throw new errors_1.PayNodeException(errors_1.ErrorCode.InternalError, "Verifier requires either a valid chainId or acceptedTokens to initialize its whitelist");
|
|
45
45
|
}
|
|
46
|
+
this.acceptedTokens = new Set(tokenList.map(t => t.toLowerCase()));
|
|
46
47
|
}
|
|
47
48
|
static ROUTER_ABI = [
|
|
48
49
|
"event PaymentReceived(bytes32 indexed orderId, address indexed merchant, address indexed payer, address token, uint256 amount, uint256 fee, uint256 chainId)"
|
|
@@ -51,12 +52,8 @@ class PayNodeVerifier {
|
|
|
51
52
|
try {
|
|
52
53
|
const { type, payload, orderId } = unifiedPayload;
|
|
53
54
|
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
55
|
const actualPayload = payload;
|
|
59
|
-
return await this.verifyTransferWithAuthorization(
|
|
56
|
+
return await this.verifyTransferWithAuthorization(expected.tokenAddress, actualPayload, {
|
|
60
57
|
to: expected.merchantAddress,
|
|
61
58
|
value: expected.amount
|
|
62
59
|
}, extra);
|
|
@@ -85,24 +82,43 @@ class PayNodeVerifier {
|
|
|
85
82
|
}
|
|
86
83
|
async verifyOnchainPayment(txHash, expected) {
|
|
87
84
|
try {
|
|
85
|
+
// 1. Security Checks
|
|
86
|
+
if (BigInt(expected.amount) < constants_1.MIN_PAYMENT_AMOUNT) {
|
|
87
|
+
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.AmountTooLow) };
|
|
88
|
+
}
|
|
89
|
+
if (!this.acceptedTokens.has(expected.tokenAddress.toLowerCase())) {
|
|
90
|
+
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.TokenNotAccepted) };
|
|
91
|
+
}
|
|
88
92
|
const receipt = await this.provider.getTransactionReceipt(txHash);
|
|
89
93
|
if (!receipt || receipt.status === 0) {
|
|
90
94
|
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.TransactionNotFound) };
|
|
91
95
|
}
|
|
92
96
|
const router = new ethers_1.ethers.Interface(PayNodeVerifier.ROUTER_ABI);
|
|
97
|
+
// convention: we hash the raw orderId string to bytes32 internally
|
|
93
98
|
const targetOrderId = ethers_1.ethers.id(expected.orderId);
|
|
94
99
|
let validEventFound = false;
|
|
100
|
+
let routerInteracted = false;
|
|
101
|
+
let orderIdMismatchFound = false;
|
|
95
102
|
for (const log of receipt.logs) {
|
|
96
103
|
try {
|
|
104
|
+
if (log.address.toLowerCase() !== this.contractAddress.toLowerCase())
|
|
105
|
+
continue;
|
|
106
|
+
routerInteracted = true;
|
|
97
107
|
const parsed = router.parseLog(log);
|
|
98
108
|
if (parsed && parsed.name === 'PaymentReceived') {
|
|
99
109
|
const { merchant, token, amount, orderId } = parsed.args;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
const isMerchantMatch = merchant.toLowerCase() === expected.merchantAddress.toLowerCase();
|
|
111
|
+
const isTokenMatch = token.toLowerCase() === expected.tokenAddress.toLowerCase();
|
|
112
|
+
const isAmountMatch = BigInt(amount) >= BigInt(expected.amount);
|
|
113
|
+
const isOrderMatch = orderId === targetOrderId;
|
|
114
|
+
if (isMerchantMatch && isTokenMatch && isAmountMatch) {
|
|
115
|
+
if (isOrderMatch) {
|
|
116
|
+
validEventFound = true;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
orderIdMismatchFound = true;
|
|
121
|
+
}
|
|
106
122
|
}
|
|
107
123
|
}
|
|
108
124
|
}
|
|
@@ -110,7 +126,13 @@ class PayNodeVerifier {
|
|
|
110
126
|
// Skip
|
|
111
127
|
}
|
|
112
128
|
}
|
|
129
|
+
if (!routerInteracted) {
|
|
130
|
+
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.WrongContract, "Transaction did not interact with the configured PayNodeRouter contract") };
|
|
131
|
+
}
|
|
113
132
|
if (!validEventFound) {
|
|
133
|
+
if (orderIdMismatchFound) {
|
|
134
|
+
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.OrderMismatch, "Payment log found but orderId does not match") };
|
|
135
|
+
}
|
|
114
136
|
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "No matching PaymentReceived event found") };
|
|
115
137
|
}
|
|
116
138
|
if (this.store) {
|
|
@@ -130,12 +152,21 @@ class PayNodeVerifier {
|
|
|
130
152
|
* 耗时: < 50ms (仅需一次 RPC Read)
|
|
131
153
|
*/
|
|
132
154
|
async verifyTransferWithAuthorization(tokenAddr, payload, expected, extra = {}) {
|
|
155
|
+
let isLocked = false;
|
|
156
|
+
const { signature, authorization } = payload;
|
|
157
|
+
const nonce = authorization?.nonce;
|
|
133
158
|
try {
|
|
134
|
-
|
|
135
|
-
|
|
159
|
+
// 1. Security Checks
|
|
160
|
+
if (BigInt(expected.value) < constants_1.MIN_PAYMENT_AMOUNT) {
|
|
161
|
+
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.AmountTooLow) };
|
|
162
|
+
}
|
|
163
|
+
if (!this.acceptedTokens.has(tokenAddr.toLowerCase())) {
|
|
164
|
+
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.TokenNotAccepted) };
|
|
165
|
+
}
|
|
166
|
+
const { from, to, value, validAfter, validBefore } = authorization;
|
|
136
167
|
const expectedValue = BigInt(expected.value);
|
|
137
168
|
const payloadValue = BigInt(value);
|
|
138
|
-
//
|
|
169
|
+
// 2. 基础字段与金额校验 (防粉尘攻击)
|
|
139
170
|
if (to.toLowerCase() !== expected.to.toLowerCase()) {
|
|
140
171
|
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "Recipient mismatch") };
|
|
141
172
|
}
|
|
@@ -176,8 +207,9 @@ class PayNodeVerifier {
|
|
|
176
207
|
if (this.store) {
|
|
177
208
|
const isNew = await this.store.checkAndSet(nonce, 86400); // 锁定 24 小时
|
|
178
209
|
if (!isNew) {
|
|
179
|
-
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.DuplicateTransaction, "Nonce already used
|
|
210
|
+
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.DuplicateTransaction, "Nonce already used or transaction already consumed") };
|
|
180
211
|
}
|
|
212
|
+
isLocked = true;
|
|
181
213
|
}
|
|
182
214
|
// ================= 核心补全:RPC 状态只读校验 (<50ms) =================
|
|
183
215
|
const tokenContract = new ethers_1.ethers.Contract(tokenAddr, [
|
|
@@ -193,14 +225,13 @@ class PayNodeVerifier {
|
|
|
193
225
|
]);
|
|
194
226
|
// 5. 校验真实余额 (防止空钱包签署有效签名)
|
|
195
227
|
if (BigInt(balance) < payloadValue) {
|
|
196
|
-
|
|
197
|
-
if (this.store)
|
|
228
|
+
if (isLocked && this.store)
|
|
198
229
|
await this.store.delete(nonce);
|
|
199
230
|
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "Insufficient token balance") };
|
|
200
231
|
}
|
|
201
232
|
// 6. 校验链上 Nonce 状态 (防止该签名已被打包结算)
|
|
202
233
|
if (isNonceUsedOnChain) {
|
|
203
|
-
if (this.store)
|
|
234
|
+
if (isLocked && this.store)
|
|
204
235
|
await this.store.delete(nonce);
|
|
205
236
|
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.DuplicateTransaction, "Nonce already consumed on-chain") };
|
|
206
237
|
}
|
|
@@ -208,6 +239,10 @@ class PayNodeVerifier {
|
|
|
208
239
|
return { isValid: true };
|
|
209
240
|
}
|
|
210
241
|
catch (e) {
|
|
242
|
+
if (isLocked && this.store)
|
|
243
|
+
await this.store.delete(nonce);
|
|
244
|
+
if (e instanceof errors_1.PayNodeException)
|
|
245
|
+
return { isValid: false, error: e };
|
|
211
246
|
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InternalError, e.message) };
|
|
212
247
|
}
|
|
213
248
|
}
|
package/dist/utils/webhook.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export interface WebhookConfig {
|
|
|
8
8
|
contractAddress?: string;
|
|
9
9
|
/** The merchant's webhook endpoint URL */
|
|
10
10
|
webhookUrl: string;
|
|
11
|
-
/** Secret key for HMAC-SHA256 signature (header:
|
|
11
|
+
/** Secret key for HMAC-SHA256 signature (header: X-402-Signature) */
|
|
12
12
|
webhookSecret: string;
|
|
13
13
|
/** Optional: chain ID for payload enrichment */
|
|
14
14
|
chainId?: number;
|
|
@@ -50,7 +50,7 @@ export interface PaymentEvent {
|
|
|
50
50
|
* ```ts
|
|
51
51
|
* const notifier = new PayNodeWebhookNotifier({
|
|
52
52
|
* rpcUrl: 'https://mainnet.base.org',
|
|
53
|
-
* contractAddress: '
|
|
53
|
+
* contractAddress: '0x4A73696ccF76E7381b044cB95127B3784369Ed63',
|
|
54
54
|
* webhookUrl: 'https://myshop.com/api/paynode-webhook',
|
|
55
55
|
* webhookSecret: 'whsec_mysecretkey123',
|
|
56
56
|
* });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/utils/webhook.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/utils/webhook.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,aAAa,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,uDAAuD;IACvD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACtD,0DAA0D;IAC1D,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;CAC3C;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAMD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,QAAQ,CAAW;IAC3B,OAAO,CAAC,KAAK,CAAY;IACzB,OAAO,CAAC,MAAM,CAAoG;IAClH,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,YAAY,CAAkB;gBAE1B,MAAM,EAAE,aAAa;IAgBjC;;;OAGG;IACG,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY9C;;OAEG;IACH,IAAI,IAAI,IAAI;YAQE,IAAI;IAgClB,OAAO,CAAC,UAAU;IAmBlB;;OAEG;YACW,OAAO;CA8CtB"}
|
package/dist/utils/webhook.js
CHANGED
|
@@ -54,7 +54,7 @@ const PAYNODE_ABI = [
|
|
|
54
54
|
* ```ts
|
|
55
55
|
* const notifier = new PayNodeWebhookNotifier({
|
|
56
56
|
* rpcUrl: 'https://mainnet.base.org',
|
|
57
|
-
* contractAddress: '
|
|
57
|
+
* contractAddress: '0x4A73696ccF76E7381b044cB95127B3784369Ed63',
|
|
58
58
|
* webhookUrl: 'https://myshop.com/api/paynode-webhook',
|
|
59
59
|
* webhookSecret: 'whsec_mysecretkey123',
|
|
60
60
|
* });
|
|
@@ -169,9 +169,9 @@ class PayNodeWebhookNotifier {
|
|
|
169
169
|
.digest('hex');
|
|
170
170
|
const headers = {
|
|
171
171
|
'Content-Type': 'application/json',
|
|
172
|
-
'
|
|
173
|
-
'
|
|
174
|
-
'
|
|
172
|
+
'X-402-Signature': `sha256=${signature}`,
|
|
173
|
+
'X-402-Event': 'payment.received',
|
|
174
|
+
'X-402-Delivery-Id': `${event.txHash}-${attempt}`,
|
|
175
175
|
...(this.config.customHeaders || {})
|
|
176
176
|
};
|
|
177
177
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paynodelabs/sdk-js",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "The official JavaScript/TypeScript SDK for PayNode x402 protocol on Base L2.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -39,5 +39,8 @@
|
|
|
39
39
|
"ts-jest": "^29.1.4",
|
|
40
40
|
"ts-node": "^10.9.2",
|
|
41
41
|
"typescript": "^5.4.5"
|
|
42
|
+
},
|
|
43
|
+
"overrides": {
|
|
44
|
+
"brace-expansion": "^5.0.5"
|
|
42
45
|
}
|
|
43
|
-
}
|
|
46
|
+
}
|