@agentokratia/x402-escrow 2.0.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/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/client/index.cjs +524 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +51 -0
- package/dist/client/index.d.ts +51 -0
- package/dist/client/index.js +492 -0
- package/dist/client/index.js.map +1 -0
- package/dist/index.cjs +596 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +568 -0
- package/dist/index.js.map +1 -0
- package/dist/server/index.cjs +100 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +105 -0
- package/dist/server/index.d.ts +105 -0
- package/dist/server/index.js +71 -0
- package/dist/server/index.js.map +1 -0
- package/dist/session-wrapper-Cf7U8ObX.d.cts +372 -0
- package/dist/session-wrapper-Cf7U8ObX.d.ts +372 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Agentokratia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# @agentokratia/x402-escrow
|
|
2
|
+
|
|
3
|
+
Escrow payment scheme for the x402 protocol. Session-based payments for high-frequency APIs.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Session-based payments** - Sign once, make unlimited API calls
|
|
8
|
+
- **Zero per-request gas** - Facilitator handles on-chain transactions
|
|
9
|
+
- **100% reclaimable** - Withdraw unused funds anytime
|
|
10
|
+
- **ERC-3009 gasless** - Users sign off-chain, no wallet transaction needed
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @agentokratia/x402-escrow
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Client Usage
|
|
19
|
+
|
|
20
|
+
For apps and agents paying for APIs.
|
|
21
|
+
|
|
22
|
+
### Simple (recommended)
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { createEscrowFetch } from '@agentokratia/x402-escrow/client';
|
|
26
|
+
|
|
27
|
+
const { fetch: escrowFetch, scheme, x402 } = createEscrowFetch(walletClient);
|
|
28
|
+
|
|
29
|
+
// Payments handled automatically
|
|
30
|
+
const response = await escrowFetch('https://api.example.com/premium');
|
|
31
|
+
|
|
32
|
+
// Access sessions
|
|
33
|
+
scheme.sessions.getAll();
|
|
34
|
+
scheme.sessions.hasValid(receiverAddress, '10000');
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### With hooks
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
const { fetch: escrowFetch, x402 } = createEscrowFetch(walletClient);
|
|
41
|
+
|
|
42
|
+
// Add hooks for user control
|
|
43
|
+
x402.onBeforePaymentCreation(async (ctx) => {
|
|
44
|
+
console.log('About to pay:', ctx.paymentRequirements);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
x402.onAfterPaymentCreation(async (ctx) => {
|
|
48
|
+
console.log('Payment created:', ctx.paymentPayload);
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Advanced (manual setup)
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { x402Client } from '@x402/core/client';
|
|
56
|
+
import { wrapFetchWithPayment } from '@x402/fetch';
|
|
57
|
+
import { EscrowScheme, withSessionExtraction } from '@agentokratia/x402-escrow/client';
|
|
58
|
+
|
|
59
|
+
const escrowScheme = new EscrowScheme(walletClient);
|
|
60
|
+
const x402 = new x402Client().register('eip155:84532', escrowScheme);
|
|
61
|
+
const paidFetch = wrapFetchWithPayment(fetch, x402);
|
|
62
|
+
const escrowFetch = withSessionExtraction(paidFetch, escrowScheme);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Server Usage
|
|
66
|
+
|
|
67
|
+
For APIs accepting payments. Config is auto-discovered from facilitator.
|
|
68
|
+
|
|
69
|
+
### Express
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { x402ResourceServer, HTTPFacilitatorClient } from '@x402/core/server';
|
|
73
|
+
import { paymentMiddleware } from '@x402/express';
|
|
74
|
+
import { EscrowScheme } from '@agentokratia/x402-escrow/server';
|
|
75
|
+
|
|
76
|
+
const facilitator = new HTTPFacilitatorClient({
|
|
77
|
+
url: 'https://facilitator.agentokratia.com',
|
|
78
|
+
createAuthHeaders: async () => ({
|
|
79
|
+
verify: { Authorization: `Bearer ${process.env.X402_API_KEY}` },
|
|
80
|
+
settle: { Authorization: `Bearer ${process.env.X402_API_KEY}` },
|
|
81
|
+
supported: {},
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const server = new x402ResourceServer(facilitator).register('eip155:84532', new EscrowScheme());
|
|
86
|
+
|
|
87
|
+
app.use(
|
|
88
|
+
paymentMiddleware(
|
|
89
|
+
{
|
|
90
|
+
'GET /api/premium': {
|
|
91
|
+
accepts: {
|
|
92
|
+
scheme: 'escrow',
|
|
93
|
+
price: '$0.01',
|
|
94
|
+
network: 'eip155:84532',
|
|
95
|
+
payTo: ownerAddress,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
server
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Next.js
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { paymentProxy } from '@x402/next';
|
|
108
|
+
import { EscrowScheme } from '@agentokratia/x402-escrow/server';
|
|
109
|
+
|
|
110
|
+
const server = new x402ResourceServer(facilitator).register('eip155:84532', new EscrowScheme());
|
|
111
|
+
|
|
112
|
+
export const proxy = paymentProxy(
|
|
113
|
+
{
|
|
114
|
+
'/api/premium': {
|
|
115
|
+
accepts: { scheme: 'escrow', network: 'eip155:84532', payTo: ownerAddress, price: '$0.01' },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
server
|
|
119
|
+
);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## How It Works
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
1. User signs ERC-3009 authorization (gasless)
|
|
126
|
+
2. Facilitator deposits funds to escrow contract
|
|
127
|
+
3. Session created with balance
|
|
128
|
+
4. Each API call debits from session (no signature needed)
|
|
129
|
+
5. User can reclaim unused funds anytime
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Networks
|
|
133
|
+
|
|
134
|
+
| Network | Chain ID | Escrow Contract |
|
|
135
|
+
| ------------ | -------- | -------------------------------------------- |
|
|
136
|
+
| Base Mainnet | 8453 | `0xbDEa0d1BCc5966192b070fDF62ab4eF5B4420Cff` |
|
|
137
|
+
| Base Sepolia | 84532 | `0xbDEa0d1BCc5966192b070fDF62ab4eF5B4420Cff` |
|
|
138
|
+
|
|
139
|
+
## API
|
|
140
|
+
|
|
141
|
+
### Client
|
|
142
|
+
|
|
143
|
+
| Export | Description |
|
|
144
|
+
| ------------------------------------------- | --------------------------------------------- |
|
|
145
|
+
| `createEscrowFetch(walletClient, options?)` | Creates fetch with automatic payment handling |
|
|
146
|
+
| `EscrowScheme` | Core scheme class for x402Client |
|
|
147
|
+
| `withSessionExtraction(fetch, scheme)` | Wrapper to extract sessions from responses |
|
|
148
|
+
| `withAxiosSessionExtraction(scheme)` | Axios interceptor for session extraction |
|
|
149
|
+
|
|
150
|
+
### Server
|
|
151
|
+
|
|
152
|
+
| Export | Description |
|
|
153
|
+
| ----------------------- | ------------------------------------ |
|
|
154
|
+
| `EscrowScheme` | Server scheme for x402ResourceServer |
|
|
155
|
+
| `HTTPFacilitatorClient` | Re-export from @x402/core |
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/client/index.ts
|
|
21
|
+
var client_exports = {};
|
|
22
|
+
__export(client_exports, {
|
|
23
|
+
BrowserLocalStorage: () => BrowserLocalStorage,
|
|
24
|
+
EscrowScheme: () => EscrowScheme,
|
|
25
|
+
InMemoryStorage: () => InMemoryStorage,
|
|
26
|
+
SessionManager: () => SessionManager,
|
|
27
|
+
X402_VERSION: () => X402_VERSION,
|
|
28
|
+
computeEscrowNonce: () => computeEscrowNonce,
|
|
29
|
+
createEscrowFetch: () => createEscrowFetch,
|
|
30
|
+
createStorage: () => createStorage,
|
|
31
|
+
signERC3009: () => signERC3009,
|
|
32
|
+
withAxiosSessionExtraction: () => withAxiosSessionExtraction,
|
|
33
|
+
withSessionExtraction: () => withSessionExtraction
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(client_exports);
|
|
36
|
+
|
|
37
|
+
// src/client/session-wrapper.ts
|
|
38
|
+
var import_viem3 = require("viem");
|
|
39
|
+
var import_client = require("@x402/core/client");
|
|
40
|
+
var import_fetch = require("@x402/fetch");
|
|
41
|
+
|
|
42
|
+
// src/client/escrow.ts
|
|
43
|
+
var import_viem2 = require("viem");
|
|
44
|
+
|
|
45
|
+
// src/types.ts
|
|
46
|
+
var X402_VERSION = 2;
|
|
47
|
+
function fromBase64(str) {
|
|
48
|
+
try {
|
|
49
|
+
if (typeof atob !== "undefined") {
|
|
50
|
+
return decodeURIComponent(escape(atob(str)));
|
|
51
|
+
}
|
|
52
|
+
return Buffer.from(str, "base64").toString("utf-8");
|
|
53
|
+
} catch {
|
|
54
|
+
return Buffer.from(str, "base64").toString("utf-8");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function generateRequestId() {
|
|
58
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
59
|
+
return crypto.randomUUID();
|
|
60
|
+
}
|
|
61
|
+
return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}`;
|
|
62
|
+
}
|
|
63
|
+
function generateRandomBytes(length) {
|
|
64
|
+
const bytes = new Uint8Array(length);
|
|
65
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
66
|
+
crypto.getRandomValues(bytes);
|
|
67
|
+
} else {
|
|
68
|
+
for (let i = 0; i < length; i++) {
|
|
69
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return bytes;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/constants.ts
|
|
76
|
+
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
77
|
+
var DEFAULT_SESSION_DURATION = 3600;
|
|
78
|
+
var DEFAULT_REFUND_WINDOW = 86400;
|
|
79
|
+
|
|
80
|
+
// src/client/storage.ts
|
|
81
|
+
var BaseStorage = class {
|
|
82
|
+
constructor() {
|
|
83
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
84
|
+
}
|
|
85
|
+
get(network, receiver) {
|
|
86
|
+
const now = Date.now() / 1e3;
|
|
87
|
+
for (const session of this.sessions.values()) {
|
|
88
|
+
if (session.network === network && session.receiver.toLowerCase() === receiver.toLowerCase() && session.authorizationExpiry > now) {
|
|
89
|
+
return session;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
findBest(network, receiver, minAmount) {
|
|
95
|
+
const now = Date.now() / 1e3;
|
|
96
|
+
let best = null;
|
|
97
|
+
let bestBalance = 0n;
|
|
98
|
+
for (const session of this.sessions.values()) {
|
|
99
|
+
if (session.network === network && session.receiver.toLowerCase() === receiver.toLowerCase() && session.authorizationExpiry > now) {
|
|
100
|
+
const balance = BigInt(session.balance);
|
|
101
|
+
if (balance >= minAmount && balance > bestBalance) {
|
|
102
|
+
best = session;
|
|
103
|
+
bestBalance = balance;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return best;
|
|
108
|
+
}
|
|
109
|
+
set(session) {
|
|
110
|
+
this.sessions.set(session.sessionId, session);
|
|
111
|
+
this.onUpdate();
|
|
112
|
+
}
|
|
113
|
+
update(sessionId, balance) {
|
|
114
|
+
const session = this.sessions.get(sessionId);
|
|
115
|
+
if (session) {
|
|
116
|
+
this.sessions.set(sessionId, { ...session, balance });
|
|
117
|
+
this.onUpdate();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
list() {
|
|
121
|
+
return Array.from(this.sessions.values());
|
|
122
|
+
}
|
|
123
|
+
remove(sessionId) {
|
|
124
|
+
this.sessions.delete(sessionId);
|
|
125
|
+
this.onUpdate();
|
|
126
|
+
}
|
|
127
|
+
/** Override in subclasses for persistence */
|
|
128
|
+
onUpdate() {
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
var InMemoryStorage = class extends BaseStorage {
|
|
132
|
+
// No persistence needed
|
|
133
|
+
};
|
|
134
|
+
var BrowserLocalStorage = class extends BaseStorage {
|
|
135
|
+
constructor(key = "x402-sessions") {
|
|
136
|
+
super();
|
|
137
|
+
this.key = key;
|
|
138
|
+
this.load();
|
|
139
|
+
}
|
|
140
|
+
onUpdate() {
|
|
141
|
+
this.save();
|
|
142
|
+
}
|
|
143
|
+
load() {
|
|
144
|
+
if (typeof localStorage === "undefined") return;
|
|
145
|
+
try {
|
|
146
|
+
const data = localStorage.getItem(this.key);
|
|
147
|
+
if (data) {
|
|
148
|
+
const sessions = JSON.parse(data);
|
|
149
|
+
for (const s of sessions) this.sessions.set(s.sessionId, s);
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
if (process.env.NODE_ENV !== "production") {
|
|
153
|
+
console.warn("[x402] Failed to load sessions from localStorage");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
save() {
|
|
158
|
+
if (typeof localStorage === "undefined") return;
|
|
159
|
+
try {
|
|
160
|
+
localStorage.setItem(this.key, JSON.stringify(Array.from(this.sessions.values())));
|
|
161
|
+
} catch {
|
|
162
|
+
if (process.env.NODE_ENV !== "production") {
|
|
163
|
+
console.warn("[x402] Failed to save sessions to localStorage");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
function createStorage(type, storageKey) {
|
|
169
|
+
return type === "localStorage" ? new BrowserLocalStorage(storageKey) : new InMemoryStorage();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/client/session-manager.ts
|
|
173
|
+
var SessionManager = class {
|
|
174
|
+
constructor(network, options = {}) {
|
|
175
|
+
this.network = network;
|
|
176
|
+
this.storage = createStorage(options.storage ?? "memory", options.storageKey);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Store a session from escrow settlement response.
|
|
180
|
+
*/
|
|
181
|
+
store(session) {
|
|
182
|
+
this.storage.set({ ...session, createdAt: Date.now() });
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Get session for a specific receiver.
|
|
186
|
+
*/
|
|
187
|
+
getForReceiver(receiver) {
|
|
188
|
+
return this.storage.get(this.network, receiver);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Find best session for receiver with minimum balance.
|
|
192
|
+
*/
|
|
193
|
+
findBest(receiver, minAmount) {
|
|
194
|
+
return this.storage.findBest(this.network, receiver, minAmount);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Check if valid session exists for receiver.
|
|
198
|
+
*/
|
|
199
|
+
hasValid(receiver, minAmount) {
|
|
200
|
+
const session = minAmount ? this.storage.findBest(this.network, receiver, BigInt(minAmount)) : this.storage.get(this.network, receiver);
|
|
201
|
+
return session !== null;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Update session balance after debit.
|
|
205
|
+
*/
|
|
206
|
+
updateBalance(sessionId, newBalance) {
|
|
207
|
+
this.storage.update(sessionId, newBalance);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Get all stored sessions.
|
|
211
|
+
*/
|
|
212
|
+
getAll() {
|
|
213
|
+
return this.storage.list();
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Remove a specific session.
|
|
217
|
+
*/
|
|
218
|
+
remove(sessionId) {
|
|
219
|
+
this.storage.remove(sessionId);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Clear all sessions.
|
|
223
|
+
*/
|
|
224
|
+
clear() {
|
|
225
|
+
for (const session of this.storage.list()) {
|
|
226
|
+
this.storage.remove(session.sessionId);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// src/client/eip712.ts
|
|
232
|
+
var import_viem = require("viem");
|
|
233
|
+
var PAYMENT_INFO_TYPE = "PaymentInfo(address operator,address payer,address receiver,address token,uint120 maxAmount,uint48 preApprovalExpiry,uint48 authorizationExpiry,uint48 refundExpiry,uint16 minFeeBps,uint16 maxFeeBps,address feeReceiver,uint256 salt)";
|
|
234
|
+
var PAYMENT_INFO_TYPEHASH = (0, import_viem.keccak256)((0, import_viem.toHex)(new TextEncoder().encode(PAYMENT_INFO_TYPE)));
|
|
235
|
+
var PAYMENT_INFO_ABI_PARAMS = "bytes32, address, address, address, address, uint120, uint48, uint48, uint48, uint16, uint16, address, uint256";
|
|
236
|
+
var NONCE_ABI_PARAMS = "uint256, address, bytes32";
|
|
237
|
+
var ERC3009_TYPES = {
|
|
238
|
+
ReceiveWithAuthorization: [
|
|
239
|
+
{ name: "from", type: "address" },
|
|
240
|
+
{ name: "to", type: "address" },
|
|
241
|
+
{ name: "value", type: "uint256" },
|
|
242
|
+
{ name: "validAfter", type: "uint256" },
|
|
243
|
+
{ name: "validBefore", type: "uint256" },
|
|
244
|
+
{ name: "nonce", type: "bytes32" }
|
|
245
|
+
]
|
|
246
|
+
};
|
|
247
|
+
function computeEscrowNonce(chainId, escrowContract, paymentInfo) {
|
|
248
|
+
const paymentInfoHash = (0, import_viem.keccak256)(
|
|
249
|
+
(0, import_viem.encodeAbiParameters)((0, import_viem.parseAbiParameters)(PAYMENT_INFO_ABI_PARAMS), [
|
|
250
|
+
PAYMENT_INFO_TYPEHASH,
|
|
251
|
+
paymentInfo.operator,
|
|
252
|
+
ZERO_ADDRESS,
|
|
253
|
+
// payer = 0 for payer-agnostic
|
|
254
|
+
paymentInfo.receiver,
|
|
255
|
+
paymentInfo.token,
|
|
256
|
+
paymentInfo.maxAmount,
|
|
257
|
+
paymentInfo.preApprovalExpiry,
|
|
258
|
+
paymentInfo.authorizationExpiry,
|
|
259
|
+
paymentInfo.refundExpiry,
|
|
260
|
+
paymentInfo.minFeeBps,
|
|
261
|
+
paymentInfo.maxFeeBps,
|
|
262
|
+
paymentInfo.feeReceiver,
|
|
263
|
+
paymentInfo.salt
|
|
264
|
+
])
|
|
265
|
+
);
|
|
266
|
+
return (0, import_viem.keccak256)(
|
|
267
|
+
(0, import_viem.encodeAbiParameters)((0, import_viem.parseAbiParameters)(NONCE_ABI_PARAMS), [
|
|
268
|
+
BigInt(chainId),
|
|
269
|
+
escrowContract,
|
|
270
|
+
paymentInfoHash
|
|
271
|
+
])
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
async function signERC3009(wallet, authorization, domain) {
|
|
275
|
+
if (!wallet.account) {
|
|
276
|
+
throw new Error("WalletClient must have an account");
|
|
277
|
+
}
|
|
278
|
+
return wallet.signTypedData({
|
|
279
|
+
account: wallet.account,
|
|
280
|
+
domain,
|
|
281
|
+
types: ERC3009_TYPES,
|
|
282
|
+
primaryType: "ReceiveWithAuthorization",
|
|
283
|
+
message: {
|
|
284
|
+
from: authorization.from,
|
|
285
|
+
to: authorization.to,
|
|
286
|
+
value: authorization.value,
|
|
287
|
+
validAfter: authorization.validAfter,
|
|
288
|
+
validBefore: authorization.validBefore,
|
|
289
|
+
nonce: authorization.nonce
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/client/escrow.ts
|
|
295
|
+
var EscrowScheme = class {
|
|
296
|
+
constructor(walletClient, options = {}) {
|
|
297
|
+
this.scheme = "escrow";
|
|
298
|
+
if (!walletClient.account) {
|
|
299
|
+
throw new Error("WalletClient must have an account");
|
|
300
|
+
}
|
|
301
|
+
if (!walletClient.chain) {
|
|
302
|
+
throw new Error("WalletClient must have a chain");
|
|
303
|
+
}
|
|
304
|
+
this.wallet = walletClient;
|
|
305
|
+
this.chainId = walletClient.chain.id;
|
|
306
|
+
this.network = `eip155:${walletClient.chain.id}`;
|
|
307
|
+
this.sessionDuration = options.sessionDuration ?? DEFAULT_SESSION_DURATION;
|
|
308
|
+
this.refundWindow = options.refundWindow ?? DEFAULT_REFUND_WINDOW;
|
|
309
|
+
this.customDepositAmount = options.depositAmount ? BigInt(options.depositAmount) : void 0;
|
|
310
|
+
this.sessions = new SessionManager(this.network, {
|
|
311
|
+
storage: options.storage,
|
|
312
|
+
storageKey: options.storageKey
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
get address() {
|
|
316
|
+
return this.wallet.account.address;
|
|
317
|
+
}
|
|
318
|
+
// ========== Payment Payload Creation ==========
|
|
319
|
+
/**
|
|
320
|
+
* Creates payment payload for escrow scheme.
|
|
321
|
+
* Auto-detects whether to create new session or use existing one.
|
|
322
|
+
*/
|
|
323
|
+
async createPaymentPayload(x402Version, paymentRequirements) {
|
|
324
|
+
const receiver = (0, import_viem2.getAddress)(paymentRequirements.payTo);
|
|
325
|
+
const amount = BigInt(paymentRequirements.amount);
|
|
326
|
+
const existingSession = this.sessions.findBest(receiver, amount);
|
|
327
|
+
if (existingSession) {
|
|
328
|
+
return this.createUsagePayload(x402Version, existingSession, paymentRequirements.amount);
|
|
329
|
+
}
|
|
330
|
+
return this.createCreationPayload(x402Version, paymentRequirements);
|
|
331
|
+
}
|
|
332
|
+
// ========== Private: Payload Builders ==========
|
|
333
|
+
/**
|
|
334
|
+
* Session USAGE payload - uses existing session (no signature).
|
|
335
|
+
*/
|
|
336
|
+
createUsagePayload(x402Version, session, amount) {
|
|
337
|
+
return {
|
|
338
|
+
x402Version,
|
|
339
|
+
payload: {
|
|
340
|
+
session: {
|
|
341
|
+
id: session.sessionId,
|
|
342
|
+
token: session.sessionToken
|
|
343
|
+
},
|
|
344
|
+
amount,
|
|
345
|
+
requestId: generateRequestId()
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Session CREATION payload - requires wallet signature.
|
|
351
|
+
*/
|
|
352
|
+
async createCreationPayload(x402Version, paymentRequirements) {
|
|
353
|
+
const extra = paymentRequirements.extra;
|
|
354
|
+
if (!extra.escrowContract || !extra.facilitator || !extra.tokenCollector) {
|
|
355
|
+
throw new Error("Missing required escrow configuration in payment requirements");
|
|
356
|
+
}
|
|
357
|
+
const escrowContract = (0, import_viem2.getAddress)(extra.escrowContract);
|
|
358
|
+
const facilitator = (0, import_viem2.getAddress)(extra.facilitator);
|
|
359
|
+
const tokenCollector = (0, import_viem2.getAddress)(extra.tokenCollector);
|
|
360
|
+
const receiver = (0, import_viem2.getAddress)(paymentRequirements.payTo);
|
|
361
|
+
const token = (0, import_viem2.getAddress)(paymentRequirements.asset);
|
|
362
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
363
|
+
const salt = this.generateSalt();
|
|
364
|
+
const authorizationExpiry = now + this.sessionDuration;
|
|
365
|
+
const refundExpiry = authorizationExpiry + this.refundWindow;
|
|
366
|
+
const minDeposit = extra.minDeposit ? BigInt(extra.minDeposit) : BigInt(paymentRequirements.amount);
|
|
367
|
+
const maxDeposit = extra.maxDeposit ? BigInt(extra.maxDeposit) : minDeposit;
|
|
368
|
+
let amount;
|
|
369
|
+
if (this.customDepositAmount !== void 0) {
|
|
370
|
+
if (this.customDepositAmount < minDeposit) {
|
|
371
|
+
throw new Error(
|
|
372
|
+
`Deposit amount ${this.customDepositAmount} is below minimum ${minDeposit}`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
if (this.customDepositAmount > maxDeposit) {
|
|
376
|
+
throw new Error(`Deposit amount ${this.customDepositAmount} exceeds maximum ${maxDeposit}`);
|
|
377
|
+
}
|
|
378
|
+
amount = this.customDepositAmount;
|
|
379
|
+
} else {
|
|
380
|
+
amount = maxDeposit;
|
|
381
|
+
}
|
|
382
|
+
const validAfter = 0n;
|
|
383
|
+
const validBefore = BigInt(authorizationExpiry);
|
|
384
|
+
const nonce = computeEscrowNonce(this.chainId, escrowContract, {
|
|
385
|
+
operator: facilitator,
|
|
386
|
+
payer: this.address,
|
|
387
|
+
receiver,
|
|
388
|
+
token,
|
|
389
|
+
maxAmount: amount,
|
|
390
|
+
preApprovalExpiry: authorizationExpiry,
|
|
391
|
+
authorizationExpiry,
|
|
392
|
+
refundExpiry,
|
|
393
|
+
minFeeBps: 0,
|
|
394
|
+
maxFeeBps: 0,
|
|
395
|
+
feeReceiver: ZERO_ADDRESS,
|
|
396
|
+
salt: BigInt(salt)
|
|
397
|
+
});
|
|
398
|
+
const domain = {
|
|
399
|
+
name: extra.name,
|
|
400
|
+
version: extra.version,
|
|
401
|
+
chainId: this.chainId,
|
|
402
|
+
verifyingContract: token
|
|
403
|
+
};
|
|
404
|
+
const signature = await signERC3009(
|
|
405
|
+
this.wallet,
|
|
406
|
+
{ from: this.address, to: tokenCollector, value: amount, validAfter, validBefore, nonce },
|
|
407
|
+
domain
|
|
408
|
+
);
|
|
409
|
+
const payload = {
|
|
410
|
+
signature,
|
|
411
|
+
authorization: {
|
|
412
|
+
from: this.address,
|
|
413
|
+
to: tokenCollector,
|
|
414
|
+
value: amount.toString(),
|
|
415
|
+
validAfter: validAfter.toString(),
|
|
416
|
+
validBefore: validBefore.toString(),
|
|
417
|
+
nonce
|
|
418
|
+
},
|
|
419
|
+
sessionParams: {
|
|
420
|
+
salt,
|
|
421
|
+
authorizationExpiry,
|
|
422
|
+
refundExpiry
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
if (paymentRequirements.scheme === "escrow") {
|
|
426
|
+
payload.requestId = generateRequestId();
|
|
427
|
+
}
|
|
428
|
+
const accepted = {
|
|
429
|
+
scheme: paymentRequirements.scheme,
|
|
430
|
+
network: paymentRequirements.network,
|
|
431
|
+
asset: paymentRequirements.asset,
|
|
432
|
+
amount: paymentRequirements.amount,
|
|
433
|
+
payTo: paymentRequirements.payTo,
|
|
434
|
+
maxTimeoutSeconds: paymentRequirements.maxTimeoutSeconds,
|
|
435
|
+
extra: { ...paymentRequirements.extra, facilitator, escrowContract, tokenCollector }
|
|
436
|
+
};
|
|
437
|
+
return { x402Version, accepted, payload };
|
|
438
|
+
}
|
|
439
|
+
generateSalt() {
|
|
440
|
+
return (0, import_viem2.toHex)(generateRandomBytes(32));
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// src/client/session-wrapper.ts
|
|
445
|
+
function createEscrowFetch(walletClient, options) {
|
|
446
|
+
const scheme = new EscrowScheme(walletClient, options);
|
|
447
|
+
const x402 = new import_client.x402Client().register(scheme.network, scheme);
|
|
448
|
+
const baseFetch = options?.fetch ?? globalThis.fetch;
|
|
449
|
+
const paidFetch = (0, import_fetch.wrapFetchWithPayment)(baseFetch, x402);
|
|
450
|
+
return {
|
|
451
|
+
fetch: withSessionExtraction(paidFetch, scheme),
|
|
452
|
+
scheme,
|
|
453
|
+
x402
|
|
454
|
+
// Expose for adding hooks
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function extractSession(getHeader, escrowScheme) {
|
|
458
|
+
const paymentResponseHeader = getHeader("PAYMENT-RESPONSE") || getHeader("payment-response");
|
|
459
|
+
if (!paymentResponseHeader) return;
|
|
460
|
+
try {
|
|
461
|
+
const data = JSON.parse(fromBase64(paymentResponseHeader));
|
|
462
|
+
if (!data.session?.id) return;
|
|
463
|
+
if (!data.session.token) {
|
|
464
|
+
if (data.session.balance !== void 0) {
|
|
465
|
+
escrowScheme.sessions.updateBalance(data.session.id, data.session.balance);
|
|
466
|
+
}
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const receiver = data.requirements?.payTo || data.receiver;
|
|
470
|
+
if (!receiver) {
|
|
471
|
+
if (process.env.NODE_ENV !== "production") {
|
|
472
|
+
console.warn("[x402] Session missing receiver - cannot store");
|
|
473
|
+
}
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (!(0, import_viem3.isAddress)(receiver)) {
|
|
477
|
+
if (process.env.NODE_ENV !== "production") {
|
|
478
|
+
console.warn("[x402] Invalid receiver address in session:", receiver);
|
|
479
|
+
}
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
escrowScheme.sessions.store({
|
|
483
|
+
sessionId: data.session.id,
|
|
484
|
+
sessionToken: data.session.token,
|
|
485
|
+
network: escrowScheme.network,
|
|
486
|
+
payer: escrowScheme.address,
|
|
487
|
+
receiver: (0, import_viem3.getAddress)(receiver),
|
|
488
|
+
balance: data.session.balance || "0",
|
|
489
|
+
authorizationExpiry: data.session.expiresAt || 0
|
|
490
|
+
});
|
|
491
|
+
} catch (error) {
|
|
492
|
+
if (process.env.NODE_ENV !== "production") {
|
|
493
|
+
console.warn("[x402] Failed to parse PAYMENT-RESPONSE:", error);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function withSessionExtraction(paidFetch, escrowScheme) {
|
|
498
|
+
return async (input, init) => {
|
|
499
|
+
const response = await paidFetch(input, init);
|
|
500
|
+
extractSession((name) => response.headers.get(name), escrowScheme);
|
|
501
|
+
return response;
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function withAxiosSessionExtraction(escrowScheme) {
|
|
505
|
+
return (response) => {
|
|
506
|
+
extractSession((name) => response.headers[name.toLowerCase()], escrowScheme);
|
|
507
|
+
return response;
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
511
|
+
0 && (module.exports = {
|
|
512
|
+
BrowserLocalStorage,
|
|
513
|
+
EscrowScheme,
|
|
514
|
+
InMemoryStorage,
|
|
515
|
+
SessionManager,
|
|
516
|
+
X402_VERSION,
|
|
517
|
+
computeEscrowNonce,
|
|
518
|
+
createEscrowFetch,
|
|
519
|
+
createStorage,
|
|
520
|
+
signERC3009,
|
|
521
|
+
withAxiosSessionExtraction,
|
|
522
|
+
withSessionExtraction
|
|
523
|
+
});
|
|
524
|
+
//# sourceMappingURL=index.cjs.map
|