@paynodelabs/sdk-js 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -92
- package/dist/client.d.ts +7 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +40 -17
- package/dist/constants.d.ts +2 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -2
- package/dist/merchant/index.js +9 -5
- package/dist/merchant/middleware.d.ts +3 -1
- package/dist/merchant/middleware.d.ts.map +1 -1
- package/dist/merchant/middleware.js +7 -4
- package/dist/merchant/types.d.ts +2 -1
- package/dist/merchant/types.d.ts.map +1 -1
- package/dist/middleware/x402.d.ts.map +1 -1
- package/dist/middleware/x402.js +23 -16
- package/dist/types/x402.d.ts +19 -2
- package/dist/types/x402.d.ts.map +1 -1
- package/dist/utils/payload.d.ts.map +1 -1
- package/dist/utils/payload.js +11 -3
- package/dist/utils/signature.d.ts +2 -0
- package/dist/utils/signature.d.ts.map +1 -1
- package/dist/utils/signature.js +16 -7
- package/dist/utils/verifier.d.ts.map +1 -1
- package/dist/utils/verifier.js +19 -5
- package/dist/utils/webhook.d.ts +2 -0
- package/dist/utils/webhook.d.ts.map +1 -1
- package/dist/utils/webhook.js +14 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
|
-
# PayNode
|
|
1
|
+
# PayNode Master SDK (JS/TS)
|
|
2
|
+
|
|
3
|
+
> [!NOTE]
|
|
4
|
+
> Compatibility policy & legacy header aliases are documented in [COMPATIBILITY.md](./COMPATIBILITY.md).
|
|
2
5
|
|
|
3
6
|
[](https://docs.paynode.dev)
|
|
4
7
|
[](https://www.npmjs.com/package/@paynodelabs/sdk-js)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
The **Master SDK** for the [PayNode Protocol](https://paynode.dev). This JavaScript/TypeScript implementation serves as the **Reference Implementation** for the x402 V2 protocol. All future SDKs (Python, Go, Rust, etc.) are built following the architectural patterns and logic established in this codebase.
|
|
5
11
|
|
|
6
|
-
The
|
|
12
|
+
The current baseline and cleanup requirements for turning `sdk-js` into the formal multi-language source of truth are documented in [`meta/MULTI_LANGUAGE_BASELINE.md`](../../meta/MULTI_LANGUAGE_BASELINE.md). The protocol source of truth lives in [`meta/SPEC.md`](../../meta/SPEC.md).
|
|
7
13
|
|
|
8
|
-
|
|
14
|
+
PayNode is a stateless, non-custodial M2M payment gateway that standardizes the HTTP 402 "Payment Required" flow for AI Agents, enabling seamless USDC settlement on Base L2.
|
|
15
|
+
|
|
16
|
+
---
|
|
9
17
|
|
|
10
|
-
|
|
11
|
-
|
|
18
|
+
## 🏗️ Reference Implementation Status
|
|
19
|
+
|
|
20
|
+
As the **Master SDK**, `sdk-js` defines the gold standard for:
|
|
21
|
+
- **x402 V2 Handshake**: The JSON-based negotiation protocol between Agents and Merchants.
|
|
22
|
+
- **Dual-Mode Settlement**: Seamless switching between high-speed off-chain signatures (EIP-3009) and robust on-chain receipts.
|
|
23
|
+
- **Proof-of-Intent**: Standardized cryptographic signatures for M2M commerce.
|
|
24
|
+
- **Stateless Verification**: Logic for verifying payments without requiring complex backend databases.
|
|
25
|
+
|
|
26
|
+
> [!TIP]
|
|
27
|
+
> Developers building PayNode SDKs for other languages should treat this repository as the **Source of Truth** for internal logic and protocol compliance.
|
|
28
|
+
|
|
29
|
+
---
|
|
12
30
|
|
|
13
31
|
## ⚡ Quick Start
|
|
14
32
|
|
|
@@ -19,23 +37,25 @@ npm install @paynodelabs/sdk-js ethers
|
|
|
19
37
|
```
|
|
20
38
|
|
|
21
39
|
### Agent Client (Payer)
|
|
40
|
+
The `PayNodeAgentClient` automatically identifies 402 challenges and executes the best settlement path (On-chain or EIP-3009).
|
|
22
41
|
|
|
23
42
|
```typescript
|
|
24
43
|
import { PayNodeAgentClient } from "@paynodelabs/sdk-js";
|
|
25
44
|
|
|
26
|
-
|
|
45
|
+
// Initialize with Private Key and Base RPC
|
|
46
|
+
const client = new PayNodeAgentClient("YOUR_PRIVATE_KEY", ["https://mainnet.base.org"]);
|
|
27
47
|
|
|
28
48
|
async function main() {
|
|
29
|
-
// Automatically handles 402 challenges,
|
|
49
|
+
// Automatically handles 402 challenges, settles USDC, and retries the request
|
|
30
50
|
const response = await client.requestGate("https://api.merchant.com/premium-data");
|
|
31
|
-
|
|
51
|
+
const result = await response.json();
|
|
52
|
+
console.log("Success:", result);
|
|
32
53
|
}
|
|
33
54
|
main();
|
|
34
55
|
```
|
|
35
56
|
|
|
36
|
-
###
|
|
37
|
-
|
|
38
|
-
Starting from **v2.3.0**, PayNode SDK provides a unified merchant gateway for seamless [PayNode Market](https://mk.paynode.dev) integration.
|
|
57
|
+
### Merchant Gateway (Seller)
|
|
58
|
+
Starting from **v2.3.0**, the SDK provides a unified middleware for **PayNode Market Proxy** requests. It handles discovery probes, signature verification, and body unwrap in a single line.
|
|
39
59
|
|
|
40
60
|
```typescript
|
|
41
61
|
import { PayNodeMerchant } from "@paynodelabs/sdk-js";
|
|
@@ -43,109 +63,66 @@ import express from "express";
|
|
|
43
63
|
|
|
44
64
|
const app = express();
|
|
45
65
|
const merchant = new PayNodeMerchant({
|
|
46
|
-
sharedSecret: "YOUR_MARKET_SECRET" //
|
|
66
|
+
sharedSecret: "YOUR_MARKET_SECRET" // Obtainable from PayNode Market Hub
|
|
47
67
|
});
|
|
48
68
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
// Unified Middleware (Manifest + Auth + Discovery)
|
|
70
|
+
app.post("/api/v1/tools", merchant.middleware({
|
|
71
|
+
manifest: {
|
|
72
|
+
slug: "gpt-researcher",
|
|
73
|
+
name: "GPT Researcher",
|
|
74
|
+
description: "Research endpoint",
|
|
75
|
+
price_per_call: "0.05",
|
|
76
|
+
currency: "USDC"
|
|
77
|
+
},
|
|
78
|
+
price: "0.05" // Also set price directly — manifest.price_per_call is for Market Hub sync only
|
|
54
79
|
}), (req, res) => {
|
|
55
|
-
// req.body is automatically unwrapped
|
|
56
|
-
|
|
57
|
-
res.json({ data: "premium_content" });
|
|
80
|
+
// req.body is automatically unwrapped for valid Market Proxy requests
|
|
81
|
+
res.json({ result: "Data for " + req.body.query });
|
|
58
82
|
}
|
|
59
83
|
);
|
|
60
84
|
```
|
|
61
85
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
- **Zero-Wait Checkout**: API response speed drops from 5 seconds to **under 50ms** by using local signatures instead of waiting for on-chain inclusion.
|
|
65
|
-
- **Double-Spend Protection**:
|
|
66
|
-
- **L1 (Memory)**: High-speed local replay protection via `IdempotencyStore`.
|
|
67
|
-
- **L2 (RPC)**: Real-time on-chain `authorizationState` verification.
|
|
68
|
-
- **Empty-Wallet Proof**: Integrated `balanceOf` probes to block malicious agents using empty wallets to generate valid signatures.
|
|
69
|
-
- **EIP-3009 Support**: Sign payments off-chain using `TransferWithAuthorization`.
|
|
70
|
-
- **X402 V2 Protocol**: JSON-based handshake for structured agent interaction.
|
|
71
|
-
- **Dual Flow**: Automatic switch between V1 (on-chain receipts) and V2 (off-chain signatures).
|
|
72
|
-
|
|
73
|
-
## 🗺️ Roadmap
|
|
74
|
-
|
|
75
|
-
- **TRON Support**: USDT (TRC-20) payment integration.
|
|
76
|
-
- **Solana Support**: SPL USDC/USDT payment integration.
|
|
77
|
-
- **Cross-chain**: Universal settlement via bridges.
|
|
78
|
-
|
|
79
|
-
## 🚀 Run the Demo
|
|
80
|
-
|
|
81
|
-
The SDK includes a full merchant/agent demonstration in the `examples/` directory.
|
|
82
|
-
|
|
83
|
-
### 1. Setup Environment
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
cp .env.example .env
|
|
87
|
-
# Edit .env with your PRIVATE_KEY and RPC_URL
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### 2. Get Test Tokens (Required for Base Sepolia)
|
|
91
|
-
|
|
92
|
-
If you're testing on Sepolia, run the helper script to mint 1,000 mock USDC:
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
npx ts-node examples/mint-test-tokens.ts
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
### 3. Run the Merchant Server (Express)
|
|
86
|
+
---
|
|
99
87
|
|
|
100
|
-
|
|
101
|
-
npx ts-node examples/express-server.ts
|
|
102
|
-
```
|
|
88
|
+
## 🚀 Key Features
|
|
103
89
|
|
|
104
|
-
|
|
90
|
+
- **Zero-Wait Checkout**: Off-chain EIP-3009 signatures allow **<50ms** checkout times by bypassing block confirmation delays.
|
|
91
|
+
- **Autonomous Recovery**: Built-in RPC failover and retry logic for high-availability agent environments.
|
|
92
|
+
- **Double-Spend Protection**: Memory-efficient `IdempotencyStore` for high-frequency replay protection.
|
|
93
|
+
- **EIP-2612 & EIP-3009**: Native support for gasless approvals and off-chain transfers.
|
|
94
|
+
- **Market Proxy Middleware**: Drop-in Express support for PayNode Market discovery, authentication, and payload unwrap.
|
|
105
95
|
|
|
106
|
-
|
|
96
|
+
---
|
|
107
97
|
|
|
108
|
-
|
|
109
|
-
npx ts-node examples/agent-client.ts
|
|
110
|
-
```
|
|
98
|
+
## 🧭 Roadmap & Ecosystem
|
|
111
99
|
|
|
112
|
-
|
|
100
|
+
As the lead SDK, this package drives the protocol forward. Current initiatives:
|
|
101
|
+
- 🐍 **Python SDK**: Porting `sdk-js` logic to Python for LangChain/AutoGPT integration.
|
|
102
|
+
- 🦀 **Rust SDK**: High-performance implementation for edge compute.
|
|
103
|
+
- 📡 **Cross-chain Settlement**: Integration with Solana (USDC) and TRON (USDT).
|
|
113
104
|
|
|
114
105
|
---
|
|
115
106
|
|
|
116
|
-
##
|
|
107
|
+
## 🛠️ Development & Testing
|
|
117
108
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
```
|
|
124
|
-
2. **Login to NPM** (if not already):
|
|
125
|
-
```bash
|
|
126
|
-
npm login
|
|
127
|
-
```
|
|
128
|
-
3. **Publish**:
|
|
129
|
-
```bash
|
|
130
|
-
npm publish --access public
|
|
131
|
-
```
|
|
109
|
+
### Run the Demo
|
|
110
|
+
1. **Setup Env**: `cp .env.example .env` and set `CLIENT_PRIVATE_KEY`. If you want to run the merchant demo too, also set `MERCHANT_ADDRESS`.
|
|
111
|
+
2. **Mint Test USDC**: `npx ts-node examples/mint-test-tokens.ts` (Base Sepolia).
|
|
112
|
+
3. **Start Merchant**: `npm run example:server`
|
|
113
|
+
4. **Run Agent**: `npx ts-node examples/agent-client.ts`
|
|
132
114
|
|
|
133
115
|
---
|
|
134
116
|
|
|
135
|
-
## Versioning
|
|
136
|
-
|
|
137
|
-
PayNode uses two distinct versioning schemes:
|
|
117
|
+
## 📖 Versioning
|
|
138
118
|
|
|
139
|
-
|
|
|
140
|
-
|
|
|
141
|
-
| **Protocol
|
|
142
|
-
| **SDK
|
|
119
|
+
| Component | Current Version |
|
|
120
|
+
| :--- | :--- |
|
|
121
|
+
| **Protocol (x402)** | `v2` |
|
|
122
|
+
| **SDK Implementation** | `v2.4.0` |
|
|
143
123
|
|
|
144
124
|
> [!IMPORTANT]
|
|
145
|
-
>
|
|
146
|
-
|
|
147
|
-
> [!NOTE]
|
|
148
|
-
> Protocol Version changes imply breaking changes in verification or contract interfaces. SDK Version changes may include features, fixes, or performance improvements without altering protocol logic.
|
|
125
|
+
> Protocol and SDK package versioning are not the same thing. Treat the protocol as `x402 v2`, and treat `2.4.0` as the current JS SDK implementation version.
|
|
149
126
|
|
|
150
127
|
---
|
|
151
128
|
|
package/dist/client.d.ts
CHANGED
|
@@ -2,15 +2,22 @@ import { ExactEVMPayload } from './types/x402';
|
|
|
2
2
|
export interface RequestOptions extends RequestInit {
|
|
3
3
|
json?: any;
|
|
4
4
|
}
|
|
5
|
+
export interface AgentClientOptions {
|
|
6
|
+
rpcUrls?: string | string[];
|
|
7
|
+
maxRetries?: number;
|
|
8
|
+
quiet?: boolean;
|
|
9
|
+
}
|
|
5
10
|
export declare class PayNodeAgentClient {
|
|
6
11
|
private wallet;
|
|
7
12
|
private provider;
|
|
8
13
|
private rpcUrls;
|
|
9
14
|
private maxRetries;
|
|
15
|
+
private options;
|
|
10
16
|
private nonceLock;
|
|
11
17
|
private ERC20_ABI;
|
|
12
18
|
private ROUTER_ABI;
|
|
13
19
|
constructor(privateKey: string, rpcUrls?: string | string[], maxRetries?: number);
|
|
20
|
+
constructor(privateKey: string, options?: AgentClientOptions);
|
|
14
21
|
private _fetchWithRetry;
|
|
15
22
|
requestGate(url: string, options?: RequestOptions): Promise<Response>;
|
|
16
23
|
private _handleX402V2;
|
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,EAGL,eAAe,EAEhB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,eAAe,EAEhB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAKD,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,OAAO,CAAqB;IACpC,OAAO,CAAC,SAAS,CAAoC;IAErD,OAAO,CAAC,SAAS,CAMf;IAEF,OAAO,CAAC,UAAU,CAAsB;gBAE5B,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,UAAU,CAAC,EAAE,MAAM;gBACpE,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB;YAuB9C,eAAe;IAoCvB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,QAAQ,CAAC;YAuEjE,aAAa;IAoKrB,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;IA+CrB,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
|
+
options;
|
|
13
14
|
nonceLock = Promise.resolve();
|
|
14
15
|
ERC20_ABI = [
|
|
15
16
|
"function approve(address spender, uint256 value) public returns (bool)",
|
|
@@ -19,9 +20,17 @@ class PayNodeAgentClient {
|
|
|
19
20
|
"function nonces(address owner) view returns (uint256)"
|
|
20
21
|
];
|
|
21
22
|
ROUTER_ABI = constants_1.PAYNODE_ROUTER_ABI;
|
|
22
|
-
constructor(privateKey,
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
constructor(privateKey, arg2, arg3) {
|
|
24
|
+
if (typeof arg2 === 'object' && !Array.isArray(arg2)) {
|
|
25
|
+
this.options = { ...arg2 };
|
|
26
|
+
this.rpcUrls = Array.isArray(this.options.rpcUrls) ? this.options.rpcUrls : (this.options.rpcUrls ? [this.options.rpcUrls] : constants_1.BASE_RPC_URLS);
|
|
27
|
+
this.maxRetries = this.options.maxRetries ?? 3;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
this.rpcUrls = Array.isArray(arg2) ? arg2 : (arg2 ? [arg2] : constants_1.BASE_RPC_URLS);
|
|
31
|
+
this.maxRetries = arg3 ?? 3;
|
|
32
|
+
this.options = { rpcUrls: this.rpcUrls, maxRetries: this.maxRetries };
|
|
33
|
+
}
|
|
25
34
|
const configs = this.rpcUrls.map((url, index) => ({
|
|
26
35
|
provider: new ethers_1.ethers.JsonRpcProvider(url, undefined, { staticNetwork: true }),
|
|
27
36
|
priority: index,
|
|
@@ -44,7 +53,8 @@ class PayNodeAgentClient {
|
|
|
44
53
|
}
|
|
45
54
|
if (attempt < this.maxRetries - 1) {
|
|
46
55
|
const backoffMs = Math.pow(2, attempt) * 1000;
|
|
47
|
-
|
|
56
|
+
if (!this.options.quiet)
|
|
57
|
+
console.warn(`⚠️ [PayNode-JS] ${response.status} received. Retrying in ${backoffMs}ms...`);
|
|
48
58
|
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
|
49
59
|
continue;
|
|
50
60
|
}
|
|
@@ -54,7 +64,8 @@ class PayNodeAgentClient {
|
|
|
54
64
|
lastError = error;
|
|
55
65
|
if (attempt < this.maxRetries - 1) {
|
|
56
66
|
const backoffMs = Math.pow(2, attempt) * 1000;
|
|
57
|
-
|
|
67
|
+
if (!this.options.quiet)
|
|
68
|
+
console.warn(`⚠️ [PayNode-JS] Request failed: ${error.message}. Retrying in ${backoffMs}ms...`);
|
|
58
69
|
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
|
59
70
|
}
|
|
60
71
|
}
|
|
@@ -79,7 +90,8 @@ class PayNodeAgentClient {
|
|
|
79
90
|
try {
|
|
80
91
|
let response = await this._fetchWithRetry(url, fetchOptions);
|
|
81
92
|
if (response.status === 402) {
|
|
82
|
-
|
|
93
|
+
if (!this.options.quiet)
|
|
94
|
+
console.log(`💡 [PayNode-JS] 402 Payment Required detected. Analyzing protocol version...`);
|
|
83
95
|
const contentType = response.headers.get('content-type');
|
|
84
96
|
const b64Required = response.headers.get('PAYMENT-REQUIRED') || response.headers.get('X-402-Required');
|
|
85
97
|
const orderId = response.headers.get('X-402-Order-Id');
|
|
@@ -93,7 +105,8 @@ class PayNodeAgentClient {
|
|
|
93
105
|
headerBody = JSON.parse(decoded);
|
|
94
106
|
}
|
|
95
107
|
catch (e) {
|
|
96
|
-
|
|
108
|
+
if (!this.options.quiet)
|
|
109
|
+
console.debug('⚠️ [PayNode-JS] Failed to parse PAYMENT-REQUIRED header:', e);
|
|
97
110
|
}
|
|
98
111
|
}
|
|
99
112
|
if (contentType && contentType.includes('application/json')) {
|
|
@@ -107,7 +120,8 @@ class PayNodeAgentClient {
|
|
|
107
120
|
body = { ...body, ...headerBody };
|
|
108
121
|
}
|
|
109
122
|
if (body && body.x402Version === 2) {
|
|
110
|
-
|
|
123
|
+
if (!this.options.quiet)
|
|
124
|
+
console.log(`🚀 [PayNode-JS] x402 v2 detected. Handling autonomous payment...`);
|
|
111
125
|
if (orderId && !body.orderId)
|
|
112
126
|
body.orderId = orderId;
|
|
113
127
|
return await this._handleX402V2(url, fetchOptions, body);
|
|
@@ -119,7 +133,8 @@ class PayNodeAgentClient {
|
|
|
119
133
|
catch (error) {
|
|
120
134
|
if (error instanceof errors_1.PayNodeException || error?.name === "PayNodeException")
|
|
121
135
|
throw error;
|
|
122
|
-
|
|
136
|
+
if (!this.options.quiet)
|
|
137
|
+
console.error(`❌ [PayNode-JS] Critical error in requestGate:`, error);
|
|
123
138
|
throw new errors_1.PayNodeException(errors_1.ErrorCode.RpcError, undefined, error);
|
|
124
139
|
}
|
|
125
140
|
}
|
|
@@ -132,7 +147,8 @@ class PayNodeAgentClient {
|
|
|
132
147
|
if (!requirement) {
|
|
133
148
|
throw new errors_1.PayNodeException(errors_1.ErrorCode.TransactionFailed, `No compatible payment requirement found for network ${caip2ChainId}`);
|
|
134
149
|
}
|
|
135
|
-
|
|
150
|
+
if (!this.options.quiet)
|
|
151
|
+
console.log(`💡 [PayNode-JS] Selected payment method: ${requirement.type || 'onchain'} on ${requirement.network}`);
|
|
136
152
|
// 🛡️ Token Whitelist Check (Case-insensitive)
|
|
137
153
|
const chainTokens = constants_1.ACCEPTED_TOKENS[chainId]?.map(t => t.toLowerCase());
|
|
138
154
|
if (chainTokens && !chainTokens.includes(requirement.asset.toLowerCase())) {
|
|
@@ -154,6 +170,7 @@ class PayNodeAgentClient {
|
|
|
154
170
|
resource: requirements.resource,
|
|
155
171
|
accepted: {
|
|
156
172
|
scheme: requirement.scheme,
|
|
173
|
+
type: 'eip3009',
|
|
157
174
|
network: requirement.network,
|
|
158
175
|
amount: requirement.amount,
|
|
159
176
|
asset: requirement.asset,
|
|
@@ -163,7 +180,7 @@ class PayNodeAgentClient {
|
|
|
163
180
|
},
|
|
164
181
|
payload: authorization,
|
|
165
182
|
_paynode: {
|
|
166
|
-
|
|
183
|
+
sdkVersion: constants_1.SDK_VERSION,
|
|
167
184
|
type: 'eip3009',
|
|
168
185
|
orderId: orderId
|
|
169
186
|
}
|
|
@@ -175,7 +192,8 @@ class PayNodeAgentClient {
|
|
|
175
192
|
if (!routerAddr) {
|
|
176
193
|
throw new errors_1.PayNodeException(errors_1.ErrorCode.InternalError, "On-chain payment required but no router address provided.");
|
|
177
194
|
}
|
|
178
|
-
|
|
195
|
+
if (!this.options.quiet)
|
|
196
|
+
console.log(`⚡ [PayNode-JS] Executing on-chain payment to ${requirement.payTo}...`);
|
|
179
197
|
const amount = BigInt(requirement.amount);
|
|
180
198
|
const tokenContract = new ethers_1.ethers.Contract(requirement.asset, this.ERC20_ABI, this.wallet);
|
|
181
199
|
const allowance = await tokenContract.allowance(this.wallet.address, routerAddr);
|
|
@@ -185,7 +203,8 @@ class PayNodeAgentClient {
|
|
|
185
203
|
txHash = await this.pay(routerAddr, requirement.asset, requirement.payTo, amount, orderId);
|
|
186
204
|
}
|
|
187
205
|
catch (e) {
|
|
188
|
-
|
|
206
|
+
if (!this.options.quiet)
|
|
207
|
+
console.warn(`⚠️ [PayNode-JS] Direct pay failed (possibly allowance race), falling back to permit:`, e);
|
|
189
208
|
txHash = await this.payWithPermit(routerAddr, requirement.asset, requirement.payTo, amount, orderId, requirement.extra?.version || '2');
|
|
190
209
|
}
|
|
191
210
|
}
|
|
@@ -197,6 +216,7 @@ class PayNodeAgentClient {
|
|
|
197
216
|
resource: requirements.resource,
|
|
198
217
|
accepted: {
|
|
199
218
|
scheme: requirement.scheme,
|
|
219
|
+
type: 'onchain',
|
|
200
220
|
network: requirement.network,
|
|
201
221
|
amount: requirement.amount,
|
|
202
222
|
asset: requirement.asset,
|
|
@@ -207,7 +227,7 @@ class PayNodeAgentClient {
|
|
|
207
227
|
},
|
|
208
228
|
payload: { txHash },
|
|
209
229
|
_paynode: {
|
|
210
|
-
|
|
230
|
+
sdkVersion: constants_1.SDK_VERSION,
|
|
211
231
|
type: 'onchain',
|
|
212
232
|
orderId: orderId
|
|
213
233
|
}
|
|
@@ -248,14 +268,17 @@ class PayNodeAgentClient {
|
|
|
248
268
|
}
|
|
249
269
|
const settleData = JSON.parse(decoded);
|
|
250
270
|
if (settleData.success) {
|
|
251
|
-
|
|
271
|
+
if (!this.options.quiet)
|
|
272
|
+
console.log(`✅ [PayNode-JS] Settlement confirmed: ${settleData.transaction}`);
|
|
252
273
|
}
|
|
253
274
|
else {
|
|
254
|
-
|
|
275
|
+
if (!this.options.quiet)
|
|
276
|
+
console.warn(`⚠️ [PayNode-JS] Settlement failed: ${settleData.errorReason || 'Unknown error'}`);
|
|
255
277
|
}
|
|
256
278
|
}
|
|
257
279
|
catch (e) {
|
|
258
|
-
|
|
280
|
+
if (!this.options.quiet)
|
|
281
|
+
console.warn(`⚠️ [PayNode-JS] Failed to parse settlement response:`, e);
|
|
259
282
|
}
|
|
260
283
|
}
|
|
261
284
|
return retryResponse;
|
package/dist/constants.d.ts
CHANGED
|
@@ -6,7 +6,8 @@ export declare const BASE_USDC_ADDRESS_SANDBOX = "0x65c088EfBDB0E03185Dbe8e258Ad
|
|
|
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
|
-
export declare const
|
|
9
|
+
export declare const PROTOCOL_VERSION = 2;
|
|
10
|
+
export declare const SDK_VERSION = "2.4.0";
|
|
10
11
|
export declare const BASE_RPC_URLS: string[];
|
|
11
12
|
export declare const BASE_RPC_URLS_SANDBOX: string[];
|
|
12
13
|
export declare const ACCEPTED_TOKENS: Record<number, string[]>;
|
package/dist/constants.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,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;AAC/C,eAAO,MAAM,WAAW,UAAU,CAAC;AAEnC,eAAO,MAAM,aAAa,UAAmF,CAAC;AAC9G,eAAO,MAAM,qBAAqB,UAA0E,CAAC;AAE7G,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAGpD,CAAC;AAEF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAA25K,CAAC"}
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,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;AAC/C,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAClC,eAAO,MAAM,WAAW,UAAU,CAAC;AAEnC,eAAO,MAAM,aAAa,UAAmF,CAAC;AAC9G,eAAO,MAAM,qBAAqB,UAA0E,CAAC;AAE7G,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAGpD,CAAC;AAEF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAA25K,CAAC"}
|
package/dist/constants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PAYNODE_ROUTER_ABI = exports.ACCEPTED_TOKENS = exports.BASE_RPC_URLS_SANDBOX = exports.BASE_RPC_URLS = exports.SDK_VERSION = 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.SDK_VERSION = exports.PROTOCOL_VERSION = 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 meta/scripts/sync-config.py */
|
|
5
5
|
exports.PAYNODE_ROUTER_ADDRESS = "0x4A73696ccF76E7381b044cB95127B3784369Ed63";
|
|
6
6
|
exports.PAYNODE_ROUTER_ADDRESS_SANDBOX = "0x24cD8b68aaC209217ff5a6ef1Bf55a59f2c8Ca6F";
|
|
@@ -9,7 +9,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);
|
|
12
|
-
exports.
|
|
12
|
+
exports.PROTOCOL_VERSION = 2;
|
|
13
|
+
exports.SDK_VERSION = "2.4.0";
|
|
13
14
|
exports.BASE_RPC_URLS = ["https://mainnet.base.org", "https://base.meowrpc.com", "https://1rpc.io/base"];
|
|
14
15
|
exports.BASE_RPC_URLS_SANDBOX = ["https://sepolia.base.org", "https://base-sepolia-rpc.publicnode.com"];
|
|
15
16
|
exports.ACCEPTED_TOKENS = {
|
package/dist/merchant/index.js
CHANGED
|
@@ -19,7 +19,8 @@ class PayNodeMerchant {
|
|
|
19
19
|
* This ensures the market shows the correct price and input schema.
|
|
20
20
|
*/
|
|
21
21
|
async sync(manifest) {
|
|
22
|
-
|
|
22
|
+
if (!this.config.quiet)
|
|
23
|
+
console.log(`[PayNode-SDK] Syncing API manifest for ${manifest.slug} to ${this.config.marketUrl}`);
|
|
23
24
|
try {
|
|
24
25
|
const response = await fetch(`${this.config.marketUrl}/api/v1/merchant/apis`, {
|
|
25
26
|
method: 'POST',
|
|
@@ -31,16 +32,19 @@ class PayNodeMerchant {
|
|
|
31
32
|
});
|
|
32
33
|
const result = await response.json();
|
|
33
34
|
if (response.ok && result.success) {
|
|
34
|
-
|
|
35
|
+
if (!this.config.quiet)
|
|
36
|
+
console.log(`[PayNode-SDK] Successfully synced ${manifest.slug}. Status: ${result.api_id}`);
|
|
35
37
|
return true;
|
|
36
38
|
}
|
|
37
39
|
else {
|
|
38
|
-
|
|
40
|
+
if (!this.config.quiet)
|
|
41
|
+
console.warn(`[PayNode-SDK] Sync failed for ${manifest.slug}: ${result.error || response.statusText}`);
|
|
39
42
|
return false;
|
|
40
43
|
}
|
|
41
44
|
}
|
|
42
45
|
catch (err) {
|
|
43
|
-
|
|
46
|
+
if (!this.config.quiet)
|
|
47
|
+
console.error(`[PayNode-SDK] Network error during sync for ${manifest.slug}:`, err.message);
|
|
44
48
|
return false;
|
|
45
49
|
}
|
|
46
50
|
}
|
|
@@ -74,7 +78,7 @@ class PayNodeMerchant {
|
|
|
74
78
|
};
|
|
75
79
|
const signature = getHeader('X-PayNode-Signature');
|
|
76
80
|
const timestamp = getHeader('X-PayNode-Timestamp');
|
|
77
|
-
const requestId = getHeader('X-PayNode-Request-Id') || getHeader('X-402-Order-Id');
|
|
81
|
+
const requestId = getHeader('X-PayNode-Request-Id') || getHeader('X-402-Order-Id') || getHeader('PAYMENT-SIGNATURE'); // Try fallback
|
|
78
82
|
const isValid = (0, signature_1.verifyMarketSignature)({
|
|
79
83
|
signature,
|
|
80
84
|
orderId: requestId,
|
|
@@ -6,7 +6,9 @@ import { PayNodeMiddlewareOptions } from '../middleware/x402';
|
|
|
6
6
|
* Handles:
|
|
7
7
|
* 1. Market Proxy (Strict HMAC Signature + Body Unwrapping)
|
|
8
8
|
* 2. Discovery Probes (Auto-respond with API Manifest)
|
|
9
|
-
*
|
|
9
|
+
*
|
|
10
|
+
* Note: Standalone direct X402 payment flow should be handled
|
|
11
|
+
* via x402Gate component.
|
|
10
12
|
*/
|
|
11
13
|
export declare const createMerchantMiddleware: (config: MerchantConfig, options: MerchantMiddlewareOptions & PayNodeMiddlewareOptions) => (req: Request | any, res: Response | any, next: NextFunction) => Promise<any>;
|
|
12
14
|
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/merchant/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAC;AAEpE,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAE9D
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/merchant/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAC;AAEpE,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAE9D;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,GAAI,QAAQ,cAAc,EAAE,SAAS,yBAAyB,GAAG,wBAAwB,MAG9G,KAAK,OAAO,GAAG,GAAG,EAAE,KAAK,QAAQ,GAAG,GAAG,EAAE,MAAM,YAAY,iBAiE1E,CAAC"}
|
|
@@ -7,10 +7,12 @@ const signature_1 = require("../utils/signature");
|
|
|
7
7
|
* Handles:
|
|
8
8
|
* 1. Market Proxy (Strict HMAC Signature + Body Unwrapping)
|
|
9
9
|
* 2. Discovery Probes (Auto-respond with API Manifest)
|
|
10
|
-
*
|
|
10
|
+
*
|
|
11
|
+
* Note: Standalone direct X402 payment flow should be handled
|
|
12
|
+
* via x402Gate component.
|
|
11
13
|
*/
|
|
12
14
|
const createMerchantMiddleware = (config, options) => {
|
|
13
|
-
const { manifest
|
|
15
|
+
const { manifest } = options;
|
|
14
16
|
return async (req, res, next) => {
|
|
15
17
|
// 1. Check for Market Proxy Headers
|
|
16
18
|
const signature = req.header('X-PayNode-Signature');
|
|
@@ -26,14 +28,15 @@ const createMerchantMiddleware = (config, options) => {
|
|
|
26
28
|
sharedSecret: config.sharedSecret,
|
|
27
29
|
});
|
|
28
30
|
if (!isValid) {
|
|
29
|
-
|
|
31
|
+
if (!config.quiet)
|
|
32
|
+
console.error(`[PayNode-SDK] Invalid Market Proxy Signature for request ${requestId}`);
|
|
30
33
|
return res.status(401).json({ error: 'unauthorized', message: 'PayNode Market Signature verification failed.' });
|
|
31
34
|
}
|
|
32
35
|
// --- Scene A: Discovery Probe ---
|
|
33
36
|
if (isDiscovery) {
|
|
34
37
|
return res.status(200).json({
|
|
35
38
|
status: 'DISCOVERED',
|
|
36
|
-
|
|
39
|
+
x402Version: 2,
|
|
37
40
|
manifest: manifest || {},
|
|
38
41
|
last_synced: new Date().toISOString()
|
|
39
42
|
});
|
package/dist/merchant/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export interface MerchantConfig {
|
|
2
2
|
sharedSecret: string;
|
|
3
3
|
marketUrl?: string;
|
|
4
|
+
quiet?: boolean;
|
|
4
5
|
}
|
|
5
6
|
export interface ApiManifest {
|
|
6
7
|
slug: string;
|
|
@@ -11,6 +12,7 @@ export interface ApiManifest {
|
|
|
11
12
|
network?: 'mainnet' | 'testnet';
|
|
12
13
|
input_schema?: Record<string, any>;
|
|
13
14
|
sample_response?: Record<string, any>;
|
|
15
|
+
headers_template?: Record<string, string>;
|
|
14
16
|
}
|
|
15
17
|
export interface PayNodeRequestContext {
|
|
16
18
|
orderId: string;
|
|
@@ -22,6 +24,5 @@ export interface PayNodeRequestContext {
|
|
|
22
24
|
}
|
|
23
25
|
export interface MerchantMiddlewareOptions {
|
|
24
26
|
manifest?: Partial<ApiManifest>;
|
|
25
|
-
strict?: boolean;
|
|
26
27
|
}
|
|
27
28
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/merchant/types.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/merchant/types.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACtC,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;CACjC"}
|
|
@@ -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;
|
|
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;AAgBxD,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,iBAkK1E,CAAC"}
|
package/dist/middleware/x402.js
CHANGED
|
@@ -40,7 +40,7 @@ const x402Gate = (options) => {
|
|
|
40
40
|
return req.headers[name.toLowerCase()] || req.headers[name];
|
|
41
41
|
return null;
|
|
42
42
|
};
|
|
43
|
-
const v2PayloadHeader = getHeader('PAYMENT-SIGNATURE') || getHeader('X-402-Payload');
|
|
43
|
+
const v2PayloadHeader = getHeader('PAYMENT-SIGNATURE') || getHeader('X-402-Payload'); // COMPAT: X-402-Payload is a legacy alias for PAYMENT-SIGNATURE
|
|
44
44
|
let orderId = getHeader('X-402-Order-Id');
|
|
45
45
|
if (!orderId) {
|
|
46
46
|
orderId = (options.generateOrderId || defaultOrderIdGen)(req);
|
|
@@ -63,23 +63,29 @@ const x402Gate = (options) => {
|
|
|
63
63
|
inferredType = "onchain";
|
|
64
64
|
}
|
|
65
65
|
unifiedPayload = {
|
|
66
|
-
|
|
66
|
+
x402Version: constants_1.PROTOCOL_VERSION,
|
|
67
67
|
type: parsed._paynode?.type || inferredType,
|
|
68
68
|
orderId: internalOrderId,
|
|
69
69
|
router: parsed.accepted?.router,
|
|
70
|
-
payload: parsed.payload
|
|
70
|
+
payload: parsed.payload,
|
|
71
|
+
_paynode: {
|
|
72
|
+
sdkVersion: constants_1.SDK_VERSION
|
|
73
|
+
}
|
|
71
74
|
};
|
|
72
75
|
orderId = internalOrderId;
|
|
73
76
|
}
|
|
74
|
-
else if (typeof parsed.version === 'string'
|
|
75
|
-
// Legacy PayNode format
|
|
76
|
-
unifiedPayload =
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
else if (typeof (parsed.version || parsed.x402Version) === 'string' || typeof parsed.version === 'string') {
|
|
78
|
+
// Legacy PayNode format or old x402 V2 drafts
|
|
79
|
+
unifiedPayload = {
|
|
80
|
+
x402Version: constants_1.PROTOCOL_VERSION,
|
|
81
|
+
type: parsed.type || (parsed.payload?.txHash ? "onchain" : "eip3009"),
|
|
82
|
+
orderId: parsed.orderId || parsed.order_id || orderId || `legacy_${Date.now()}`,
|
|
83
|
+
payload: parsed.payload,
|
|
84
|
+
_paynode: {
|
|
85
|
+
sdkVersion: constants_1.SDK_VERSION
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
orderId = unifiedPayload.orderId;
|
|
83
89
|
}
|
|
84
90
|
}
|
|
85
91
|
catch (e) {
|
|
@@ -104,7 +110,7 @@ const x402Gate = (options) => {
|
|
|
104
110
|
const b64Response = Buffer.from(JSON.stringify(settleResponse)).toString('base64');
|
|
105
111
|
if (res.set) {
|
|
106
112
|
res.set('PAYMENT-RESPONSE', b64Response);
|
|
107
|
-
res.set('X-PAYMENT-RESPONSE', b64Response); //
|
|
113
|
+
res.set('X-PAYMENT-RESPONSE', b64Response); // COMPAT (legacy): deprecated alias for PAYMENT-RESPONSE
|
|
108
114
|
}
|
|
109
115
|
req.paynode = { unifiedPayload, orderId };
|
|
110
116
|
return next();
|
|
@@ -120,7 +126,7 @@ const x402Gate = (options) => {
|
|
|
120
126
|
const b64Response = Buffer.from(JSON.stringify(settleResponse)).toString('base64');
|
|
121
127
|
if (res.set) {
|
|
122
128
|
res.set('PAYMENT-RESPONSE', b64Response);
|
|
123
|
-
res.set('X-PAYMENT-RESPONSE', b64Response); //
|
|
129
|
+
res.set('X-PAYMENT-RESPONSE', b64Response); // COMPAT (legacy): deprecated alias for PAYMENT-RESPONSE
|
|
124
130
|
}
|
|
125
131
|
return res.status(403).json({
|
|
126
132
|
error: "Forbidden",
|
|
@@ -131,13 +137,14 @@ const x402Gate = (options) => {
|
|
|
131
137
|
}
|
|
132
138
|
// No valid payment found, return 402 with appropriate headers
|
|
133
139
|
const v2Response = {
|
|
134
|
-
x402Version:
|
|
140
|
+
x402Version: constants_1.PROTOCOL_VERSION,
|
|
135
141
|
error: "Payment Required by PayNode",
|
|
136
142
|
resource: {
|
|
137
143
|
url: req.protocol + '://' + req.get('host') + (req.originalUrl || req.url),
|
|
138
144
|
description: options.description || "Protected Resource",
|
|
139
145
|
mimeType: getHeader('accept') || "application/json"
|
|
140
146
|
},
|
|
147
|
+
orderId: orderId || undefined,
|
|
141
148
|
accepts: [
|
|
142
149
|
{
|
|
143
150
|
scheme: "exact",
|
|
@@ -167,7 +174,7 @@ const x402Gate = (options) => {
|
|
|
167
174
|
const b64Required = Buffer.from(JSON.stringify(v2Response)).toString('base64');
|
|
168
175
|
if (res.set) {
|
|
169
176
|
res.set('PAYMENT-REQUIRED', b64Required);
|
|
170
|
-
res.set('X-402-Required', b64Required);
|
|
177
|
+
res.set('X-402-Required', b64Required); // COMPAT (legacy): deprecated alias for PAYMENT-REQUIRED
|
|
171
178
|
res.set('X-402-Order-Id', orderId);
|
|
172
179
|
}
|
|
173
180
|
return res.status(402).json(v2Response);
|
package/dist/types/x402.d.ts
CHANGED
|
@@ -4,6 +4,19 @@ export interface ResourceInfo {
|
|
|
4
4
|
description?: string;
|
|
5
5
|
mimeType?: string;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* PaymentRequirements describes a single payment option returned
|
|
9
|
+
* in the `accepts[]` array of a 402 challenge.
|
|
10
|
+
*
|
|
11
|
+
* NOTE: The `orderId` (legacy) is DEPRECATED. All SDKs MUST read
|
|
12
|
+
* and write the request ID exclusively via the
|
|
13
|
+
* `X-402-Order-Id` HTTP header. This field exists only for
|
|
14
|
+
* backward compatibility with paynode-js <= 2.2.x and must
|
|
15
|
+
* not be used by new implementations.
|
|
16
|
+
*
|
|
17
|
+
* Protocol: x402 v2
|
|
18
|
+
* Canonical orderId transport: X-402-Order-Id header
|
|
19
|
+
*/
|
|
7
20
|
export interface PaymentRequirements {
|
|
8
21
|
scheme: string;
|
|
9
22
|
type?: "onchain" | "eip3009";
|
|
@@ -12,6 +25,7 @@ export interface PaymentRequirements {
|
|
|
12
25
|
asset: string;
|
|
13
26
|
payTo: string;
|
|
14
27
|
router?: string;
|
|
28
|
+
/** @deprecated Use X-402-Order-Id header exclusively. Legacy alias only. */
|
|
15
29
|
orderId?: string;
|
|
16
30
|
maxTimeoutSeconds: number;
|
|
17
31
|
extra?: Record<string, any>;
|
|
@@ -37,7 +51,7 @@ export interface PaymentPayload {
|
|
|
37
51
|
};
|
|
38
52
|
extensions?: Record<string, any>;
|
|
39
53
|
_paynode?: {
|
|
40
|
-
|
|
54
|
+
sdkVersion: string;
|
|
41
55
|
type: "onchain" | "eip3009";
|
|
42
56
|
orderId: string;
|
|
43
57
|
};
|
|
@@ -67,7 +81,7 @@ export interface VerifyResponse {
|
|
|
67
81
|
payer?: string;
|
|
68
82
|
}
|
|
69
83
|
export interface UnifiedPaymentPayload {
|
|
70
|
-
|
|
84
|
+
x402Version: X402Version;
|
|
71
85
|
type: "onchain" | "eip3009";
|
|
72
86
|
orderId: string;
|
|
73
87
|
router?: string;
|
|
@@ -76,6 +90,9 @@ export interface UnifiedPaymentPayload {
|
|
|
76
90
|
signature?: string;
|
|
77
91
|
authorization?: any;
|
|
78
92
|
} | ExactEVMPayload;
|
|
93
|
+
_paynode?: {
|
|
94
|
+
sdkVersion: string;
|
|
95
|
+
};
|
|
79
96
|
}
|
|
80
97
|
export interface SupportedKind {
|
|
81
98
|
x402Version: X402Version;
|
package/dist/types/x402.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"x402.d.ts","sourceRoot":"","sources":["../../src/types/x402.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC;AAE5B,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,WAAW,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,eAAe,GAAG;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,QAAQ,CAAC,EAAE;QACT,
|
|
1
|
+
{"version":3,"file":"x402.d.ts","sourceRoot":"","sources":["../../src/types/x402.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payload.d.ts","sourceRoot":"","sources":["../../src/utils/payload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"payload.d.ts","sourceRoot":"","sources":["../../src/utils/payload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAGtD,qBAAa,iBAAiB;IAC5B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,GAAG,qBAAqB;CA8CtF"}
|
package/dist/utils/payload.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.X402PayloadHelper = void 0;
|
|
4
|
+
const constants_1 = require("../constants");
|
|
4
5
|
class X402PayloadHelper {
|
|
5
6
|
/**
|
|
6
7
|
* Normalizes a raw payment payload (usually from PAYMENT-SIGNATURE or X-402-Payload headers)
|
|
@@ -29,18 +30,25 @@ class X402PayloadHelper {
|
|
|
29
30
|
inferredType = "onchain";
|
|
30
31
|
}
|
|
31
32
|
return {
|
|
32
|
-
|
|
33
|
+
x402Version: 2,
|
|
33
34
|
type: parsed._paynode?.type || inferredType,
|
|
34
35
|
orderId: parsed._paynode?.orderId || fallbackOrderId || "",
|
|
35
36
|
router: parsed.accepted?.router || parsed.router,
|
|
36
|
-
payload: parsed.payload
|
|
37
|
+
payload: parsed.payload,
|
|
38
|
+
_paynode: {
|
|
39
|
+
sdkVersion: constants_1.SDK_VERSION
|
|
40
|
+
}
|
|
37
41
|
};
|
|
38
42
|
}
|
|
39
43
|
// 2. Handle Legacy or already Unified Format
|
|
40
44
|
if (typeof parsed.version === 'string' && (parsed.version.startsWith("2.2") || parsed.version.startsWith("2.3"))) {
|
|
41
45
|
return {
|
|
42
46
|
...parsed,
|
|
43
|
-
|
|
47
|
+
x402Version: parsed.x402Version || 2,
|
|
48
|
+
orderId: parsed.orderId || parsed.order_id || fallbackOrderId || "",
|
|
49
|
+
_paynode: parsed._paynode || {
|
|
50
|
+
sdkVersion: parsed.version
|
|
51
|
+
}
|
|
44
52
|
};
|
|
45
53
|
}
|
|
46
54
|
// 3. Fallback for raw internal format
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signature.d.ts","sourceRoot":"","sources":["../../src/utils/signature.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"signature.d.ts","sourceRoot":"","sources":["../../src/utils/signature.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAuCxE"}
|
package/dist/utils/signature.js
CHANGED
|
@@ -15,19 +15,28 @@ function verifyMarketSignature(context) {
|
|
|
15
15
|
}
|
|
16
16
|
// Check for timestamp drift (default 5 minutes to prevent replay)
|
|
17
17
|
const tsDate = new Date(timestamp);
|
|
18
|
-
const
|
|
18
|
+
const checkTime = context.now || Date.now();
|
|
19
|
+
const driftWindow = context.driftWindow || 5 * 60 * 1000;
|
|
19
20
|
// Accept both ISO string and milliseconds
|
|
20
21
|
const tsMs = isNaN(tsDate.getTime()) ? parseInt(timestamp) : tsDate.getTime();
|
|
21
22
|
if (isNaN(tsMs))
|
|
22
23
|
return false;
|
|
23
|
-
const drift = Math.abs(
|
|
24
|
-
if (drift >
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
const drift = Math.abs(checkTime - tsMs);
|
|
25
|
+
if (drift > driftWindow) {
|
|
26
|
+
if (driftWindow > 0) {
|
|
27
|
+
console.warn(`[PayNode-SDK] Signature timestamp drift too high: ${drift}ms`);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
27
30
|
}
|
|
28
31
|
const expectedSig = crypto_1.default
|
|
29
32
|
.createHmac('sha256', sharedSecret)
|
|
30
|
-
.update(`${orderId}
|
|
33
|
+
.update(`${orderId}:${timestamp}`)
|
|
31
34
|
.digest('hex');
|
|
32
|
-
|
|
35
|
+
// Use constant-time comparison to prevent timing attacks
|
|
36
|
+
try {
|
|
37
|
+
return crypto_1.default.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expectedSig, 'hex'));
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
33
42
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../../src/utils/verifier.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../../src/utils/verifier.ts"],"names":[],"mappings":"AAEA,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,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IA8BpE,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAqFlI;;;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,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CA+G3E"}
|
package/dist/utils/verifier.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.PayNodeVerifier = void 0;
|
|
4
7
|
const ethers_1 = require("ethers");
|
|
8
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
5
9
|
const errors_1 = require("../errors");
|
|
6
10
|
const idempotency_1 = require("./idempotency");
|
|
7
11
|
const constants_1 = require("../constants");
|
|
@@ -108,10 +112,16 @@ class PayNodeVerifier {
|
|
|
108
112
|
const parsed = router.parseLog(log);
|
|
109
113
|
if (parsed && parsed.name === 'PaymentReceived') {
|
|
110
114
|
const { merchant, token, amount, orderId, payer } = parsed.args;
|
|
111
|
-
const
|
|
112
|
-
const
|
|
115
|
+
const merchantBuf = Buffer.from(merchant.toLowerCase().replace('0x', ''), 'hex');
|
|
116
|
+
const expectedMerchantBuf = Buffer.from(expected.merchantAddress.toLowerCase().replace('0x', ''), 'hex');
|
|
117
|
+
const isMerchantMatch = merchantBuf.length === expectedMerchantBuf.length && crypto_1.default.timingSafeEqual(merchantBuf, expectedMerchantBuf);
|
|
118
|
+
const tokenBuf = Buffer.from(token.toLowerCase().replace('0x', ''), 'hex');
|
|
119
|
+
const expectedTokenBuf = Buffer.from(expected.tokenAddress.toLowerCase().replace('0x', ''), 'hex');
|
|
120
|
+
const isTokenMatch = tokenBuf.length === expectedTokenBuf.length && crypto_1.default.timingSafeEqual(tokenBuf, expectedTokenBuf);
|
|
113
121
|
const isAmountMatch = BigInt(amount) >= BigInt(expected.amount);
|
|
114
|
-
const
|
|
122
|
+
const orderIdBuf = Buffer.from(orderId.replace('0x', ''), 'hex');
|
|
123
|
+
const targetOrderIdBuf = Buffer.from(targetOrderId.replace('0x', ''), 'hex');
|
|
124
|
+
const isOrderMatch = orderIdBuf.length === targetOrderIdBuf.length && crypto_1.default.timingSafeEqual(orderIdBuf, targetOrderIdBuf);
|
|
115
125
|
if (isMerchantMatch && isTokenMatch && isAmountMatch) {
|
|
116
126
|
if (isOrderMatch) {
|
|
117
127
|
validEventFound = true;
|
|
@@ -169,7 +179,9 @@ class PayNodeVerifier {
|
|
|
169
179
|
const expectedValue = BigInt(expected.value);
|
|
170
180
|
const payloadValue = BigInt(value);
|
|
171
181
|
// 2. 基础字段与金额校验 (防粉尘攻击)
|
|
172
|
-
|
|
182
|
+
const toBuf = Buffer.from(to.toLowerCase().replace('0x', ''), 'hex');
|
|
183
|
+
const expectedToBuf = Buffer.from(expected.to.toLowerCase().replace('0x', ''), 'hex');
|
|
184
|
+
if (toBuf.length !== expectedToBuf.length || !crypto_1.default.timingSafeEqual(toBuf, expectedToBuf)) {
|
|
173
185
|
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "Recipient mismatch") };
|
|
174
186
|
}
|
|
175
187
|
if (payloadValue < expectedValue) {
|
|
@@ -202,7 +214,9 @@ class PayNodeVerifier {
|
|
|
202
214
|
]
|
|
203
215
|
};
|
|
204
216
|
const recoveredAddress = ethers_1.ethers.verifyTypedData(domain, types, authorization, signature);
|
|
205
|
-
|
|
217
|
+
const recoveredBuf = Buffer.from(recoveredAddress.toLowerCase().replace('0x', ''), 'hex');
|
|
218
|
+
const fromBuf = Buffer.from(from.toLowerCase().replace('0x', ''), 'hex');
|
|
219
|
+
if (recoveredBuf.length !== fromBuf.length || !crypto_1.default.timingSafeEqual(recoveredBuf, fromBuf)) {
|
|
206
220
|
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "Invalid signature: recovered address mismatch") };
|
|
207
221
|
}
|
|
208
222
|
// 4. 内存幂等性校验 (防高频重放)
|
package/dist/utils/webhook.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface WebhookConfig {
|
|
|
20
20
|
onError?: (error: Error, event: PaymentEvent) => void;
|
|
21
21
|
/** Optional: callback when a webhook delivery succeeds */
|
|
22
22
|
onSuccess?: (event: PaymentEvent) => void;
|
|
23
|
+
/** Optional: suppress logging (default: false) */
|
|
24
|
+
quiet?: boolean;
|
|
23
25
|
}
|
|
24
26
|
/**
|
|
25
27
|
* Parsed PaymentReceived event data.
|
|
@@ -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,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;
|
|
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;IAC1C,kDAAkD;IAClD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;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
|
@@ -92,11 +92,13 @@ class PayNodeWebhookNotifier {
|
|
|
92
92
|
*/
|
|
93
93
|
async start(fromBlock) {
|
|
94
94
|
if (this.timer) {
|
|
95
|
-
|
|
95
|
+
if (!this.config.quiet)
|
|
96
|
+
console.warn('[PayNode Webhook] Already running.');
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
98
99
|
this.lastBlock = fromBlock ?? (await this.provider.getBlockNumber());
|
|
99
|
-
|
|
100
|
+
if (!this.config.quiet)
|
|
101
|
+
console.log(`🔔 [PayNode Webhook] Listening from block ${this.lastBlock} on ${this.config.contractAddress}`);
|
|
100
102
|
this.timer = setInterval(() => this.poll(), this.pollInterval);
|
|
101
103
|
}
|
|
102
104
|
/**
|
|
@@ -106,7 +108,8 @@ class PayNodeWebhookNotifier {
|
|
|
106
108
|
if (this.timer) {
|
|
107
109
|
clearInterval(this.timer);
|
|
108
110
|
this.timer = null;
|
|
109
|
-
|
|
111
|
+
if (!this.config.quiet)
|
|
112
|
+
console.log('🔕 [PayNode Webhook] Stopped.');
|
|
110
113
|
}
|
|
111
114
|
}
|
|
112
115
|
async poll() {
|
|
@@ -129,7 +132,8 @@ class PayNodeWebhookNotifier {
|
|
|
129
132
|
this.lastBlock = currentBlock;
|
|
130
133
|
}
|
|
131
134
|
catch (error) {
|
|
132
|
-
|
|
135
|
+
if (!this.config.quiet)
|
|
136
|
+
console.error(`❌ [PayNode Webhook] Poll error: ${error.message}`);
|
|
133
137
|
}
|
|
134
138
|
finally {
|
|
135
139
|
this.isProcessing = false;
|
|
@@ -183,17 +187,20 @@ class PayNodeWebhookNotifier {
|
|
|
183
187
|
if (!response.ok) {
|
|
184
188
|
throw new Error(`Webhook returned ${response.status}: ${response.statusText}`);
|
|
185
189
|
}
|
|
186
|
-
|
|
190
|
+
if (!this.config.quiet)
|
|
191
|
+
console.log(`✅ [PayNode Webhook] Delivered tx ${event.txHash.slice(0, 10)}... → ${response.status}`);
|
|
187
192
|
this.config.onSuccess?.(event);
|
|
188
193
|
}
|
|
189
194
|
catch (error) {
|
|
190
|
-
|
|
195
|
+
if (!this.config.quiet)
|
|
196
|
+
console.error(`⚠️ [PayNode Webhook] Delivery failed (attempt ${attempt}/${MAX_RETRIES}): ${error.message}`);
|
|
191
197
|
if (attempt < MAX_RETRIES) {
|
|
192
198
|
const backoffMs = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
|
|
193
199
|
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
|
194
200
|
return this.deliver(event, attempt + 1);
|
|
195
201
|
}
|
|
196
|
-
|
|
202
|
+
if (!this.config.quiet)
|
|
203
|
+
console.error(`❌ [PayNode Webhook] Gave up on tx ${event.txHash} after ${MAX_RETRIES} attempts.`);
|
|
197
204
|
this.config.onError?.(error, event);
|
|
198
205
|
}
|
|
199
206
|
}
|