pay-lobster 1.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/README.md +401 -0
- package/README.md.bak +401 -0
- package/dist/agent.d.ts +132 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +224 -0
- package/dist/agent.js.map +1 -0
- package/dist/analytics.d.ts +120 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +345 -0
- package/dist/analytics.js.map +1 -0
- package/dist/approvals.d.ts +168 -0
- package/dist/approvals.d.ts.map +1 -0
- package/dist/approvals.js +406 -0
- package/dist/approvals.js.map +1 -0
- package/dist/circle-client.d.ts +152 -0
- package/dist/circle-client.d.ts.map +1 -0
- package/dist/circle-client.js +266 -0
- package/dist/circle-client.js.map +1 -0
- package/dist/commission.d.ts +191 -0
- package/dist/commission.d.ts.map +1 -0
- package/dist/commission.js +475 -0
- package/dist/commission.js.map +1 -0
- package/dist/condition-builder.d.ts +98 -0
- package/dist/condition-builder.d.ts.map +1 -0
- package/dist/condition-builder.js +193 -0
- package/dist/condition-builder.js.map +1 -0
- package/dist/contacts.d.ts +179 -0
- package/dist/contacts.d.ts.map +1 -0
- package/dist/contacts.js +445 -0
- package/dist/contacts.js.map +1 -0
- package/dist/easy.d.ts +22 -0
- package/dist/easy.d.ts.map +1 -0
- package/dist/easy.js +40 -0
- package/dist/easy.js.map +1 -0
- package/dist/erc8004/constants.d.ts +152 -0
- package/dist/erc8004/constants.d.ts.map +1 -0
- package/dist/erc8004/constants.js +114 -0
- package/dist/erc8004/constants.js.map +1 -0
- package/dist/erc8004/discovery.d.ts +84 -0
- package/dist/erc8004/discovery.d.ts.map +1 -0
- package/dist/erc8004/discovery.js +217 -0
- package/dist/erc8004/discovery.js.map +1 -0
- package/dist/erc8004/identity.d.ts +91 -0
- package/dist/erc8004/identity.d.ts.map +1 -0
- package/dist/erc8004/identity.js +250 -0
- package/dist/erc8004/identity.js.map +1 -0
- package/dist/erc8004/index.d.ts +147 -0
- package/dist/erc8004/index.d.ts.map +1 -0
- package/dist/erc8004/index.js +225 -0
- package/dist/erc8004/index.js.map +1 -0
- package/dist/erc8004/reputation.d.ts +133 -0
- package/dist/erc8004/reputation.d.ts.map +1 -0
- package/dist/erc8004/reputation.js +277 -0
- package/dist/erc8004/reputation.js.map +1 -0
- package/dist/escrow-templates.d.ts +38 -0
- package/dist/escrow-templates.d.ts.map +1 -0
- package/dist/escrow-templates.js +419 -0
- package/dist/escrow-templates.js.map +1 -0
- package/dist/escrow.d.ts +320 -0
- package/dist/escrow.d.ts.map +1 -0
- package/dist/escrow.js +854 -0
- package/dist/escrow.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/invoices.d.ts +212 -0
- package/dist/invoices.d.ts.map +1 -0
- package/dist/invoices.js +393 -0
- package/dist/invoices.js.map +1 -0
- package/dist/notifications.d.ts +141 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +350 -0
- package/dist/notifications.js.map +1 -0
- package/dist/tips.d.ts +171 -0
- package/dist/tips.d.ts.map +1 -0
- package/dist/tips.js +390 -0
- package/dist/tips.js.map +1 -0
- package/dist/types.d.ts +100 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/x402-client.d.ts +127 -0
- package/dist/x402-client.d.ts.map +1 -0
- package/dist/x402-client.js +350 -0
- package/dist/x402-client.js.map +1 -0
- package/dist/x402-server.d.ts +133 -0
- package/dist/x402-server.d.ts.map +1 -0
- package/dist/x402-server.js +330 -0
- package/dist/x402-server.js.map +1 -0
- package/lib/agent.ts +273 -0
- package/lib/analytics.ts +474 -0
- package/lib/analytics.ts.bak +474 -0
- package/lib/approvals.ts +585 -0
- package/lib/approvals.ts.bak +585 -0
- package/lib/circle-client.ts +376 -0
- package/lib/circle-client.ts.bak +376 -0
- package/lib/commission.ts +680 -0
- package/lib/commission.ts.bak +680 -0
- package/lib/condition-builder.ts +223 -0
- package/lib/condition-builder.ts.bak +223 -0
- package/lib/contacts.ts +615 -0
- package/lib/contacts.ts.bak +615 -0
- package/lib/easy.ts +46 -0
- package/lib/easy.ts.bak +352 -0
- package/lib/erc8004/constants.ts +175 -0
- package/lib/erc8004/discovery.ts +299 -0
- package/lib/erc8004/identity.ts +327 -0
- package/lib/erc8004/index.ts +285 -0
- package/lib/erc8004/reputation.ts +368 -0
- package/lib/escrow-templates.ts +462 -0
- package/lib/escrow.ts +1216 -0
- package/lib/index.ts +13 -0
- package/lib/invoices.ts +588 -0
- package/lib/notifications.ts +484 -0
- package/lib/tips.ts +570 -0
- package/lib/types.ts +108 -0
- package/lib/x402-client.ts +471 -0
- package/lib/x402-server.ts +462 -0
- package/package.json +58 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* x402 Payment-Enabled HTTP Client
|
|
3
|
+
*
|
|
4
|
+
* Wraps fetch() to automatically handle 402 Payment Required responses.
|
|
5
|
+
* Uses Circle Programmable Wallets to pay and retry with signature.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { CircleClient } from './circle-client';
|
|
9
|
+
import crypto from 'crypto';
|
|
10
|
+
import fs from 'fs/promises';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
|
|
13
|
+
// x402 Payment Challenge (from server)
|
|
14
|
+
export interface PaymentChallenge {
|
|
15
|
+
'x-payment-required': {
|
|
16
|
+
version: '1';
|
|
17
|
+
network: string; // e.g., 'ETH-SEPOLIA'
|
|
18
|
+
receiver: string; // Destination wallet address
|
|
19
|
+
asset: string; // 'USDC'
|
|
20
|
+
amount: string; // Amount in USDC
|
|
21
|
+
description: string; // Human-readable description
|
|
22
|
+
expires: number; // Unix timestamp
|
|
23
|
+
nonce: string; // Unique challenge ID
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Payment Receipt (cached)
|
|
28
|
+
export interface PaymentReceipt {
|
|
29
|
+
url: string;
|
|
30
|
+
challenge: PaymentChallenge['x-payment-required'];
|
|
31
|
+
txHash: string;
|
|
32
|
+
signature: string;
|
|
33
|
+
paidAt: string;
|
|
34
|
+
expiresAt: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Configuration
|
|
38
|
+
export interface X402ClientConfig {
|
|
39
|
+
wallet: CircleClient;
|
|
40
|
+
walletId?: string; // Specific wallet to use
|
|
41
|
+
maxAutoPayUSDC?: string; // Max amount to auto-pay (default: none)
|
|
42
|
+
requireConfirmation?: boolean; // Prompt before paying (default: false)
|
|
43
|
+
cacheReceipts?: boolean; // Cache payment receipts (default: true)
|
|
44
|
+
cacheDir?: string; // Receipt cache location
|
|
45
|
+
|
|
46
|
+
// Event hooks
|
|
47
|
+
onPayment?: (amount: string, url: string, txHash: string) => void;
|
|
48
|
+
onChallenge?: (challenge: PaymentChallenge) => void;
|
|
49
|
+
onVerified?: (receipt: PaymentReceipt) => void;
|
|
50
|
+
onError?: (error: Error, url: string) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* x402-enabled fetch function
|
|
55
|
+
*/
|
|
56
|
+
export type X402Fetch = (
|
|
57
|
+
url: string | URL | Request,
|
|
58
|
+
init?: RequestInit
|
|
59
|
+
) => Promise<Response>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a payment-enabled fetch wrapper
|
|
63
|
+
*/
|
|
64
|
+
export function createX402Fetch(config: X402ClientConfig): X402Fetch {
|
|
65
|
+
const client = new X402Client(config);
|
|
66
|
+
|
|
67
|
+
return async (url: string | URL | Request, init?: RequestInit) => {
|
|
68
|
+
return client.fetch(url, init);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* X402 HTTP Client
|
|
74
|
+
*/
|
|
75
|
+
export class X402Client {
|
|
76
|
+
private wallet: CircleClient;
|
|
77
|
+
private walletId?: string;
|
|
78
|
+
private maxAutoPayUSDC?: number;
|
|
79
|
+
private requireConfirmation: boolean;
|
|
80
|
+
private cacheReceipts: boolean;
|
|
81
|
+
private cacheDir: string;
|
|
82
|
+
|
|
83
|
+
private onPayment?: (amount: string, url: string, txHash: string) => void;
|
|
84
|
+
private onChallenge?: (challenge: PaymentChallenge) => void;
|
|
85
|
+
private onVerified?: (receipt: PaymentReceipt) => void;
|
|
86
|
+
private onError?: (error: Error, url: string) => void;
|
|
87
|
+
|
|
88
|
+
// In-memory receipt cache
|
|
89
|
+
private receiptCache: Map<string, PaymentReceipt> = new Map();
|
|
90
|
+
|
|
91
|
+
constructor(config: X402ClientConfig) {
|
|
92
|
+
this.wallet = config.wallet;
|
|
93
|
+
this.walletId = config.walletId;
|
|
94
|
+
this.maxAutoPayUSDC = config.maxAutoPayUSDC ? parseFloat(config.maxAutoPayUSDC) : undefined;
|
|
95
|
+
this.requireConfirmation = config.requireConfirmation ?? false;
|
|
96
|
+
this.cacheReceipts = config.cacheReceipts ?? true;
|
|
97
|
+
this.cacheDir = config.cacheDir || './data/x402-receipts';
|
|
98
|
+
|
|
99
|
+
this.onPayment = config.onPayment;
|
|
100
|
+
this.onChallenge = config.onChallenge;
|
|
101
|
+
this.onVerified = config.onVerified;
|
|
102
|
+
this.onError = config.onError;
|
|
103
|
+
|
|
104
|
+
// Load cached receipts on init
|
|
105
|
+
if (this.cacheReceipts) {
|
|
106
|
+
this.loadReceiptCache().catch(() => {
|
|
107
|
+
// Ignore cache load errors
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Main fetch method - handles 402 automatically
|
|
114
|
+
*/
|
|
115
|
+
async fetch(url: string | URL | Request, init?: RequestInit): Promise<Response> {
|
|
116
|
+
const urlString = this.getUrlString(url);
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
// Check for cached receipt
|
|
120
|
+
const cachedReceipt = this.getCachedReceipt(urlString);
|
|
121
|
+
if (cachedReceipt) {
|
|
122
|
+
// Use cached payment signature
|
|
123
|
+
return this.fetchWithPayment(url, init, cachedReceipt.signature);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// First attempt - no payment
|
|
127
|
+
const response = await fetch(url, init);
|
|
128
|
+
|
|
129
|
+
// Success or non-402 error - return as-is
|
|
130
|
+
if (response.status !== 402) {
|
|
131
|
+
return response;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 402 Payment Required - handle payment
|
|
135
|
+
const paymentResponse = await this.handlePaymentRequired(response, urlString, url, init);
|
|
136
|
+
return paymentResponse;
|
|
137
|
+
|
|
138
|
+
} catch (error) {
|
|
139
|
+
const err = error as Error;
|
|
140
|
+
this.onError?.(err, urlString);
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Handle 402 Payment Required response
|
|
147
|
+
*/
|
|
148
|
+
private async handlePaymentRequired(
|
|
149
|
+
response: Response,
|
|
150
|
+
urlString: string,
|
|
151
|
+
originalUrl: string | URL | Request,
|
|
152
|
+
originalInit?: RequestInit
|
|
153
|
+
): Promise<Response> {
|
|
154
|
+
// Parse payment challenge from response
|
|
155
|
+
const challenge = await this.parsePaymentChallenge(response);
|
|
156
|
+
|
|
157
|
+
if (!challenge) {
|
|
158
|
+
throw new Error('Invalid 402 response: missing x-payment-required');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Trigger challenge hook
|
|
162
|
+
this.onChallenge?.(challenge);
|
|
163
|
+
|
|
164
|
+
// Check if challenge is expired
|
|
165
|
+
if (challenge['x-payment-required'].expires < Math.floor(Date.now() / 1000)) {
|
|
166
|
+
throw new Error('Payment challenge expired');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check max auto-pay limit
|
|
170
|
+
const amount = parseFloat(challenge['x-payment-required'].amount);
|
|
171
|
+
if (this.maxAutoPayUSDC !== undefined && amount > this.maxAutoPayUSDC) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Payment amount ${amount} USDC exceeds max auto-pay limit ${this.maxAutoPayUSDC} USDC`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Confirm payment if required
|
|
178
|
+
if (this.requireConfirmation) {
|
|
179
|
+
const confirmed = await this.confirmPayment(challenge);
|
|
180
|
+
if (!confirmed) {
|
|
181
|
+
throw new Error('Payment cancelled by user');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Execute payment
|
|
186
|
+
const txHash = await this.executePayment(challenge);
|
|
187
|
+
|
|
188
|
+
// Trigger payment hook
|
|
189
|
+
this.onPayment?.(
|
|
190
|
+
challenge['x-payment-required'].amount,
|
|
191
|
+
urlString,
|
|
192
|
+
txHash
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Generate payment signature
|
|
196
|
+
const signature = this.generatePaymentSignature(challenge, txHash);
|
|
197
|
+
|
|
198
|
+
// Cache receipt
|
|
199
|
+
const receipt: PaymentReceipt = {
|
|
200
|
+
url: urlString,
|
|
201
|
+
challenge: challenge['x-payment-required'],
|
|
202
|
+
txHash,
|
|
203
|
+
signature,
|
|
204
|
+
paidAt: new Date().toISOString(),
|
|
205
|
+
expiresAt: challenge['x-payment-required'].expires,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
await this.cacheReceipt(receipt);
|
|
209
|
+
this.onVerified?.(receipt);
|
|
210
|
+
|
|
211
|
+
// Retry request with payment signature
|
|
212
|
+
return this.fetchWithPayment(originalUrl, originalInit, signature);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Execute USDC payment via Circle
|
|
217
|
+
*/
|
|
218
|
+
private async executePayment(challenge: PaymentChallenge): Promise<string> {
|
|
219
|
+
const { amount, receiver, network } = challenge['x-payment-required'];
|
|
220
|
+
|
|
221
|
+
// Determine wallet to use
|
|
222
|
+
let walletId = this.walletId;
|
|
223
|
+
|
|
224
|
+
if (!walletId) {
|
|
225
|
+
// Auto-select wallet for the network
|
|
226
|
+
const wallets = await this.wallet.listWallets();
|
|
227
|
+
const wallet = wallets.find(w => w.blockchain === network);
|
|
228
|
+
|
|
229
|
+
if (!wallet) {
|
|
230
|
+
throw new Error(`No wallet found for network ${network}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
walletId = wallet.id;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Send USDC
|
|
237
|
+
const tx = await this.wallet.sendUSDC({
|
|
238
|
+
fromWalletId: walletId,
|
|
239
|
+
toAddress: receiver,
|
|
240
|
+
amount,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Wait for transaction to be confirmed
|
|
244
|
+
// In production, you might want to poll for confirmation
|
|
245
|
+
// For now, return transaction ID
|
|
246
|
+
return tx.txHash || tx.id;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Fetch with payment signature header
|
|
251
|
+
*/
|
|
252
|
+
private async fetchWithPayment(
|
|
253
|
+
url: string | URL | Request,
|
|
254
|
+
init: RequestInit | undefined,
|
|
255
|
+
signature: string
|
|
256
|
+
): Promise<Response> {
|
|
257
|
+
const headers = new Headers(init?.headers);
|
|
258
|
+
headers.set('x-payment-signature', signature);
|
|
259
|
+
|
|
260
|
+
return fetch(url, {
|
|
261
|
+
...init,
|
|
262
|
+
headers,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Parse payment challenge from 402 response
|
|
268
|
+
*/
|
|
269
|
+
private async parsePaymentChallenge(response: Response): Promise<PaymentChallenge | null> {
|
|
270
|
+
try {
|
|
271
|
+
const body = await response.json();
|
|
272
|
+
|
|
273
|
+
if (body['x-payment-required']) {
|
|
274
|
+
return body as PaymentChallenge;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return null;
|
|
278
|
+
} catch {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Generate payment signature from challenge and tx hash
|
|
285
|
+
*/
|
|
286
|
+
private generatePaymentSignature(challenge: PaymentChallenge, txHash: string): string {
|
|
287
|
+
// In production, this would involve the x402 facilitator
|
|
288
|
+
// For now, create a deterministic signature
|
|
289
|
+
const data = JSON.stringify({
|
|
290
|
+
nonce: challenge['x-payment-required'].nonce,
|
|
291
|
+
amount: challenge['x-payment-required'].amount,
|
|
292
|
+
receiver: challenge['x-payment-required'].receiver,
|
|
293
|
+
txHash,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Simple hash-based signature (production would use proper signing)
|
|
297
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Confirm payment with user (if required)
|
|
302
|
+
*/
|
|
303
|
+
private async confirmPayment(challenge: PaymentChallenge): Promise<boolean> {
|
|
304
|
+
// In a real implementation, this would prompt the user
|
|
305
|
+
// For CLI/automated contexts, we can skip confirmation
|
|
306
|
+
// For UI contexts, show a dialog
|
|
307
|
+
|
|
308
|
+
const { amount, description } = challenge['x-payment-required'];
|
|
309
|
+
|
|
310
|
+
console.log(`\n💳 Payment Required`);
|
|
311
|
+
console.log(` Amount: ${amount} USDC`);
|
|
312
|
+
console.log(` Description: ${description}`);
|
|
313
|
+
console.log(` Confirm? (auto-approved)\n`);
|
|
314
|
+
|
|
315
|
+
// Auto-approve for now
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get cached receipt for URL
|
|
321
|
+
*/
|
|
322
|
+
private getCachedReceipt(url: string): PaymentReceipt | null {
|
|
323
|
+
if (!this.cacheReceipts) return null;
|
|
324
|
+
|
|
325
|
+
const receipt = this.receiptCache.get(url);
|
|
326
|
+
|
|
327
|
+
// Check if receipt is still valid
|
|
328
|
+
if (receipt && receipt.expiresAt > Math.floor(Date.now() / 1000)) {
|
|
329
|
+
return receipt;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Expired - remove from cache
|
|
333
|
+
if (receipt) {
|
|
334
|
+
this.receiptCache.delete(url);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Cache payment receipt
|
|
342
|
+
*/
|
|
343
|
+
private async cacheReceipt(receipt: PaymentReceipt): Promise<void> {
|
|
344
|
+
if (!this.cacheReceipts) return;
|
|
345
|
+
|
|
346
|
+
// Add to in-memory cache
|
|
347
|
+
this.receiptCache.set(receipt.url, receipt);
|
|
348
|
+
|
|
349
|
+
// Persist to disk
|
|
350
|
+
try {
|
|
351
|
+
await fs.mkdir(this.cacheDir, { recursive: true });
|
|
352
|
+
|
|
353
|
+
const filename = crypto.createHash('md5').update(receipt.url).digest('hex') + '.json';
|
|
354
|
+
const filepath = path.join(this.cacheDir, filename);
|
|
355
|
+
|
|
356
|
+
await fs.writeFile(filepath, JSON.stringify(receipt, null, 2));
|
|
357
|
+
} catch (error) {
|
|
358
|
+
// Ignore cache write errors
|
|
359
|
+
console.error('Failed to cache receipt:', error);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Load cached receipts from disk
|
|
365
|
+
*/
|
|
366
|
+
private async loadReceiptCache(): Promise<void> {
|
|
367
|
+
try {
|
|
368
|
+
await fs.mkdir(this.cacheDir, { recursive: true });
|
|
369
|
+
const files = await fs.readdir(this.cacheDir);
|
|
370
|
+
|
|
371
|
+
for (const file of files) {
|
|
372
|
+
if (!file.endsWith('.json')) continue;
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
const filepath = path.join(this.cacheDir, file);
|
|
376
|
+
const data = await fs.readFile(filepath, 'utf-8');
|
|
377
|
+
const receipt: PaymentReceipt = JSON.parse(data);
|
|
378
|
+
|
|
379
|
+
// Only cache if not expired
|
|
380
|
+
if (receipt.expiresAt > Math.floor(Date.now() / 1000)) {
|
|
381
|
+
this.receiptCache.set(receipt.url, receipt);
|
|
382
|
+
} else {
|
|
383
|
+
// Delete expired receipt
|
|
384
|
+
await fs.unlink(filepath);
|
|
385
|
+
}
|
|
386
|
+
} catch {
|
|
387
|
+
// Ignore individual file errors
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} catch {
|
|
391
|
+
// Ignore cache directory errors
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Get receipt history
|
|
397
|
+
*/
|
|
398
|
+
async getReceiptHistory(): Promise<PaymentReceipt[]> {
|
|
399
|
+
const receipts: PaymentReceipt[] = [];
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const files = await fs.readdir(this.cacheDir);
|
|
403
|
+
|
|
404
|
+
for (const file of files) {
|
|
405
|
+
if (!file.endsWith('.json')) continue;
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
const filepath = path.join(this.cacheDir, file);
|
|
409
|
+
const data = await fs.readFile(filepath, 'utf-8');
|
|
410
|
+
const receipt: PaymentReceipt = JSON.parse(data);
|
|
411
|
+
receipts.push(receipt);
|
|
412
|
+
} catch {
|
|
413
|
+
// Ignore individual file errors
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
} catch {
|
|
417
|
+
// Ignore directory errors
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Sort by payment date (newest first)
|
|
421
|
+
return receipts.sort((a, b) =>
|
|
422
|
+
new Date(b.paidAt).getTime() - new Date(a.paidAt).getTime()
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Clear receipt cache
|
|
428
|
+
*/
|
|
429
|
+
async clearCache(): Promise<void> {
|
|
430
|
+
this.receiptCache.clear();
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
const files = await fs.readdir(this.cacheDir);
|
|
434
|
+
|
|
435
|
+
for (const file of files) {
|
|
436
|
+
const filepath = path.join(this.cacheDir, file);
|
|
437
|
+
await fs.unlink(filepath);
|
|
438
|
+
}
|
|
439
|
+
} catch {
|
|
440
|
+
// Ignore errors
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Extract URL string from various input types
|
|
446
|
+
*/
|
|
447
|
+
private getUrlString(url: string | URL | Request): string {
|
|
448
|
+
if (typeof url === 'string') {
|
|
449
|
+
return url;
|
|
450
|
+
} else if (url instanceof URL) {
|
|
451
|
+
return url.toString();
|
|
452
|
+
} else {
|
|
453
|
+
return url.url;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Helper: Create simple x402 client with minimal config
|
|
460
|
+
*/
|
|
461
|
+
export function simpleX402Fetch(wallet: CircleClient, maxAutoPayUSDC?: string): X402Fetch {
|
|
462
|
+
return createX402Fetch({
|
|
463
|
+
wallet,
|
|
464
|
+
maxAutoPayUSDC,
|
|
465
|
+
onPayment: (amount, url) => {
|
|
466
|
+
console.log(`💸 Paid ${amount} USDC for ${url}`);
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export default { createX402Fetch, simpleX402Fetch, X402Client };
|