uvd-x402-sdk 2.6.0 → 2.10.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 +380 -3
- package/dist/adapters/index.d.mts +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +78 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/index.mjs +78 -1
- package/dist/adapters/index.mjs.map +1 -1
- package/dist/backend/index.d.mts +1036 -0
- package/dist/backend/index.d.ts +1036 -0
- package/dist/backend/index.js +1722 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/index.mjs +1704 -0
- package/dist/backend/index.mjs.map +1 -0
- package/dist/{index-fwbSkart.d.ts → index-C60c_e5z.d.mts} +13 -4
- package/dist/{index-BR1o8JZQ.d.mts → index-D-dO_FoP.d.mts} +38 -4
- package/dist/{index-BR1o8JZQ.d.ts → index-D-dO_FoP.d.ts} +38 -4
- package/dist/{index-DKbWiaJ9.d.mts → index-VIOUicmO.d.ts} +13 -4
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +93 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +92 -2
- package/dist/index.mjs.map +1 -1
- package/dist/providers/algorand/index.d.mts +86 -0
- package/dist/providers/algorand/index.d.ts +86 -0
- package/dist/providers/algorand/index.js +903 -0
- package/dist/providers/algorand/index.js.map +1 -0
- package/dist/providers/algorand/index.mjs +898 -0
- package/dist/providers/algorand/index.mjs.map +1 -0
- package/dist/providers/evm/index.d.mts +1 -1
- package/dist/providers/evm/index.d.ts +1 -1
- package/dist/providers/evm/index.js +78 -1
- package/dist/providers/evm/index.js.map +1 -1
- package/dist/providers/evm/index.mjs +78 -1
- package/dist/providers/evm/index.mjs.map +1 -1
- package/dist/providers/near/index.d.mts +1 -1
- package/dist/providers/near/index.d.ts +1 -1
- package/dist/providers/near/index.js +78 -1
- package/dist/providers/near/index.js.map +1 -1
- package/dist/providers/near/index.mjs +78 -1
- package/dist/providers/near/index.mjs.map +1 -1
- package/dist/providers/solana/index.d.mts +1 -1
- package/dist/providers/solana/index.d.ts +1 -1
- package/dist/providers/solana/index.js +78 -1
- package/dist/providers/solana/index.js.map +1 -1
- package/dist/providers/solana/index.mjs +78 -1
- package/dist/providers/solana/index.mjs.map +1 -1
- package/dist/providers/stellar/index.d.mts +1 -1
- package/dist/providers/stellar/index.d.ts +1 -1
- package/dist/providers/stellar/index.js +78 -1
- package/dist/providers/stellar/index.js.map +1 -1
- package/dist/providers/stellar/index.mjs +78 -1
- package/dist/providers/stellar/index.mjs.map +1 -1
- package/dist/react/index.d.mts +3 -3
- package/dist/react/index.d.ts +3 -3
- package/dist/react/index.js +78 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +78 -1
- package/dist/react/index.mjs.map +1 -1
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +78 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/index.mjs +78 -1
- package/dist/utils/index.mjs.map +1 -1
- package/package.json +24 -3
- package/src/backend/index.ts +2131 -0
- package/src/chains/index.ts +94 -2
- package/src/index.ts +19 -1
- package/src/providers/algorand/index.ts +356 -0
- package/src/types/index.ts +44 -3
|
@@ -0,0 +1,2131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* uvd-x402-sdk - Backend Utilities
|
|
3
|
+
*
|
|
4
|
+
* Server-side utilities for building x402 payment APIs.
|
|
5
|
+
* These utilities help backend developers:
|
|
6
|
+
* - Build verify/settle requests for the facilitator
|
|
7
|
+
* - Parse X-PAYMENT headers from incoming requests
|
|
8
|
+
* - Configure CORS for x402 payment flows
|
|
9
|
+
* - Create atomic payment handlers
|
|
10
|
+
* - Discover and register resources via Bazaar Discovery API
|
|
11
|
+
* - Manage escrow payments with refund and dispute resolution
|
|
12
|
+
*
|
|
13
|
+
* @example Basic payment flow
|
|
14
|
+
* ```ts
|
|
15
|
+
* import {
|
|
16
|
+
* parsePaymentHeader,
|
|
17
|
+
* FacilitatorClient,
|
|
18
|
+
* X402_CORS_HEADERS,
|
|
19
|
+
* } from 'uvd-x402-sdk/backend';
|
|
20
|
+
*
|
|
21
|
+
* // Parse payment from request header
|
|
22
|
+
* const payment = parsePaymentHeader(req.headers['x-payment']);
|
|
23
|
+
*
|
|
24
|
+
* // Verify with facilitator
|
|
25
|
+
* const client = new FacilitatorClient();
|
|
26
|
+
* const verifyResult = await client.verify(payment, paymentRequirements);
|
|
27
|
+
*
|
|
28
|
+
* // If valid, provide service then settle
|
|
29
|
+
* const settleResult = await client.settle(payment, paymentRequirements);
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @example Escrow payment with refund support
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { EscrowClient } from 'uvd-x402-sdk/backend';
|
|
35
|
+
*
|
|
36
|
+
* const escrow = new EscrowClient();
|
|
37
|
+
*
|
|
38
|
+
* // Hold payment in escrow
|
|
39
|
+
* const escrowPayment = await escrow.createEscrow({
|
|
40
|
+
* paymentHeader: req.headers['x-payment'],
|
|
41
|
+
* requirements: paymentRequirements,
|
|
42
|
+
* escrowDuration: 86400, // 24 hours
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* // After service delivered, release to recipient
|
|
46
|
+
* await escrow.release(escrowPayment.id);
|
|
47
|
+
*
|
|
48
|
+
* // Or if service failed, request refund
|
|
49
|
+
* await escrow.requestRefund({
|
|
50
|
+
* escrowId: escrowPayment.id,
|
|
51
|
+
* reason: 'Service not delivered',
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @example Resource discovery
|
|
56
|
+
* ```ts
|
|
57
|
+
* import { BazaarClient } from 'uvd-x402-sdk/backend';
|
|
58
|
+
*
|
|
59
|
+
* const bazaar = new BazaarClient();
|
|
60
|
+
* const resources = await bazaar.discover({
|
|
61
|
+
* category: 'ai',
|
|
62
|
+
* network: 'base',
|
|
63
|
+
* maxPrice: '0.10',
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
import type {
|
|
69
|
+
X402Header,
|
|
70
|
+
X402Version,
|
|
71
|
+
} from '../types';
|
|
72
|
+
import { decodeX402Header, chainToCAIP2 } from '../utils';
|
|
73
|
+
import { getChainByName } from '../chains';
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// TYPES
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Payment requirements sent to the facilitator
|
|
81
|
+
*/
|
|
82
|
+
export interface PaymentRequirements {
|
|
83
|
+
/** Payment scheme (always "exact") */
|
|
84
|
+
scheme: 'exact';
|
|
85
|
+
/** Network name (v1) or CAIP-2 identifier (v2) */
|
|
86
|
+
network: string;
|
|
87
|
+
/** Maximum amount required in atomic units (e.g., "1000000" for 1 USDC) */
|
|
88
|
+
maxAmountRequired: string;
|
|
89
|
+
/** Resource URL being paid for */
|
|
90
|
+
resource: string;
|
|
91
|
+
/** Description of what's being paid for */
|
|
92
|
+
description: string;
|
|
93
|
+
/** MIME type of the resource */
|
|
94
|
+
mimeType: string;
|
|
95
|
+
/** Recipient address for payment */
|
|
96
|
+
payTo: string;
|
|
97
|
+
/** Maximum timeout in seconds */
|
|
98
|
+
maxTimeoutSeconds: number;
|
|
99
|
+
/** Token contract address */
|
|
100
|
+
asset: string;
|
|
101
|
+
/** Optional output schema for the resource */
|
|
102
|
+
outputSchema?: unknown;
|
|
103
|
+
/** Optional extra data */
|
|
104
|
+
extra?: unknown;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Verify request body for the facilitator /verify endpoint
|
|
109
|
+
*/
|
|
110
|
+
export interface VerifyRequest {
|
|
111
|
+
x402Version: X402Version;
|
|
112
|
+
paymentPayload: X402Header;
|
|
113
|
+
paymentRequirements: PaymentRequirements;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Settle request body for the facilitator /settle endpoint
|
|
118
|
+
*/
|
|
119
|
+
export interface SettleRequest {
|
|
120
|
+
x402Version: X402Version;
|
|
121
|
+
paymentPayload: X402Header;
|
|
122
|
+
paymentRequirements: PaymentRequirements;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Verify response from the facilitator
|
|
127
|
+
*/
|
|
128
|
+
export interface VerifyResponse {
|
|
129
|
+
isValid: boolean;
|
|
130
|
+
invalidReason?: string;
|
|
131
|
+
payer?: string;
|
|
132
|
+
network?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Settle response from the facilitator
|
|
137
|
+
*/
|
|
138
|
+
export interface SettleResponse {
|
|
139
|
+
success: boolean;
|
|
140
|
+
transactionHash?: string;
|
|
141
|
+
network?: string;
|
|
142
|
+
error?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Options for building payment requirements
|
|
147
|
+
*/
|
|
148
|
+
export interface PaymentRequirementsOptions {
|
|
149
|
+
/** Amount in human-readable format (e.g., "1.00") */
|
|
150
|
+
amount: string;
|
|
151
|
+
/** Recipient address */
|
|
152
|
+
recipient: string;
|
|
153
|
+
/** Resource URL being protected */
|
|
154
|
+
resource: string;
|
|
155
|
+
/** Chain name (e.g., "base") */
|
|
156
|
+
chainName?: string;
|
|
157
|
+
/** Description of the resource */
|
|
158
|
+
description?: string;
|
|
159
|
+
/** MIME type of the resource */
|
|
160
|
+
mimeType?: string;
|
|
161
|
+
/** Timeout in seconds (default: 300) */
|
|
162
|
+
timeoutSeconds?: number;
|
|
163
|
+
/** x402 version to use */
|
|
164
|
+
x402Version?: X402Version;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ============================================================================
|
|
168
|
+
// HEADER PARSING
|
|
169
|
+
// ============================================================================
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Parse X-PAYMENT or PAYMENT-SIGNATURE header value
|
|
173
|
+
*
|
|
174
|
+
* @param headerValue - Base64-encoded header value (or undefined/null)
|
|
175
|
+
* @returns Parsed x402 header object, or null if invalid
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* // Express.js
|
|
180
|
+
* const payment = parsePaymentHeader(req.headers['x-payment']);
|
|
181
|
+
* if (!payment) {
|
|
182
|
+
* return res.status(400).json({ error: 'Invalid payment header' });
|
|
183
|
+
* }
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
export function parsePaymentHeader(
|
|
187
|
+
headerValue: string | undefined | null
|
|
188
|
+
): X402Header | null {
|
|
189
|
+
if (!headerValue) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
return decodeX402Header(headerValue);
|
|
195
|
+
} catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Extract payment header from request headers object
|
|
202
|
+
*
|
|
203
|
+
* Checks both X-PAYMENT and PAYMENT-SIGNATURE headers.
|
|
204
|
+
*
|
|
205
|
+
* @param headers - Request headers object (case-insensitive)
|
|
206
|
+
* @returns Parsed x402 header object, or null if not found/invalid
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```ts
|
|
210
|
+
* const payment = extractPaymentFromHeaders(req.headers);
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
export function extractPaymentFromHeaders(
|
|
214
|
+
headers: Record<string, string | string[] | undefined>
|
|
215
|
+
): X402Header | null {
|
|
216
|
+
// Normalize header keys to lowercase
|
|
217
|
+
const normalizedHeaders: Record<string, string> = {};
|
|
218
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
219
|
+
if (typeof value === 'string') {
|
|
220
|
+
normalizedHeaders[key.toLowerCase()] = value;
|
|
221
|
+
} else if (Array.isArray(value) && value.length > 0) {
|
|
222
|
+
normalizedHeaders[key.toLowerCase()] = value[0];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Try X-PAYMENT first, then PAYMENT-SIGNATURE
|
|
227
|
+
const headerValue =
|
|
228
|
+
normalizedHeaders['x-payment'] ||
|
|
229
|
+
normalizedHeaders['payment-signature'];
|
|
230
|
+
|
|
231
|
+
return parsePaymentHeader(headerValue);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ============================================================================
|
|
235
|
+
// REQUEST BUILDERS
|
|
236
|
+
// ============================================================================
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Build payment requirements for the facilitator
|
|
240
|
+
*
|
|
241
|
+
* @param options - Payment requirements options
|
|
242
|
+
* @returns PaymentRequirements object ready for verify/settle
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```ts
|
|
246
|
+
* const requirements = buildPaymentRequirements({
|
|
247
|
+
* amount: '1.00',
|
|
248
|
+
* recipient: '0x1234...',
|
|
249
|
+
* resource: 'https://api.example.com/premium-data',
|
|
250
|
+
* chainName: 'base',
|
|
251
|
+
* });
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
export function buildPaymentRequirements(
|
|
255
|
+
options: PaymentRequirementsOptions
|
|
256
|
+
): PaymentRequirements {
|
|
257
|
+
const {
|
|
258
|
+
amount,
|
|
259
|
+
recipient,
|
|
260
|
+
resource,
|
|
261
|
+
chainName = 'base',
|
|
262
|
+
description = 'Payment for resource access',
|
|
263
|
+
mimeType = 'application/json',
|
|
264
|
+
timeoutSeconds = 300,
|
|
265
|
+
x402Version = 1,
|
|
266
|
+
} = options;
|
|
267
|
+
|
|
268
|
+
const chain = getChainByName(chainName);
|
|
269
|
+
if (!chain) {
|
|
270
|
+
throw new Error(`Unsupported chain: ${chainName}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Convert amount to atomic units
|
|
274
|
+
const atomicAmount = Math.floor(
|
|
275
|
+
parseFloat(amount) * Math.pow(10, chain.usdc.decimals)
|
|
276
|
+
).toString();
|
|
277
|
+
|
|
278
|
+
// Use CAIP-2 for v2, chain name for v1
|
|
279
|
+
const network = x402Version === 2 ? chainToCAIP2(chainName) : chainName;
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
scheme: 'exact',
|
|
283
|
+
network,
|
|
284
|
+
maxAmountRequired: atomicAmount,
|
|
285
|
+
resource,
|
|
286
|
+
description,
|
|
287
|
+
mimeType,
|
|
288
|
+
payTo: recipient,
|
|
289
|
+
maxTimeoutSeconds: timeoutSeconds,
|
|
290
|
+
asset: chain.usdc.address,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Build a verify request for the facilitator /verify endpoint
|
|
296
|
+
*
|
|
297
|
+
* @param paymentHeader - Parsed x402 payment header
|
|
298
|
+
* @param requirements - Payment requirements
|
|
299
|
+
* @returns VerifyRequest body ready for fetch/axios
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* ```ts
|
|
303
|
+
* const payment = parsePaymentHeader(req.headers['x-payment']);
|
|
304
|
+
* const verifyBody = buildVerifyRequest(payment, requirements);
|
|
305
|
+
*
|
|
306
|
+
* const response = await fetch('https://facilitator.uvd.xyz/verify', {
|
|
307
|
+
* method: 'POST',
|
|
308
|
+
* headers: { 'Content-Type': 'application/json' },
|
|
309
|
+
* body: JSON.stringify(verifyBody),
|
|
310
|
+
* });
|
|
311
|
+
* ```
|
|
312
|
+
*/
|
|
313
|
+
export function buildVerifyRequest(
|
|
314
|
+
paymentHeader: X402Header,
|
|
315
|
+
requirements: PaymentRequirements
|
|
316
|
+
): VerifyRequest {
|
|
317
|
+
return {
|
|
318
|
+
x402Version: paymentHeader.x402Version,
|
|
319
|
+
paymentPayload: paymentHeader,
|
|
320
|
+
paymentRequirements: requirements,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Build a settle request for the facilitator /settle endpoint
|
|
326
|
+
*
|
|
327
|
+
* @param paymentHeader - Parsed x402 payment header
|
|
328
|
+
* @param requirements - Payment requirements
|
|
329
|
+
* @returns SettleRequest body ready for fetch/axios
|
|
330
|
+
*/
|
|
331
|
+
export function buildSettleRequest(
|
|
332
|
+
paymentHeader: X402Header,
|
|
333
|
+
requirements: PaymentRequirements
|
|
334
|
+
): SettleRequest {
|
|
335
|
+
return {
|
|
336
|
+
x402Version: paymentHeader.x402Version,
|
|
337
|
+
paymentPayload: paymentHeader,
|
|
338
|
+
paymentRequirements: requirements,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ============================================================================
|
|
343
|
+
// CORS CONFIGURATION
|
|
344
|
+
// ============================================================================
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Recommended CORS headers for x402 payment APIs
|
|
348
|
+
*
|
|
349
|
+
* These headers allow browsers to send payment headers in cross-origin requests.
|
|
350
|
+
*/
|
|
351
|
+
export const X402_CORS_HEADERS = {
|
|
352
|
+
'Access-Control-Allow-Headers':
|
|
353
|
+
'Content-Type, X-PAYMENT, PAYMENT-SIGNATURE, Authorization',
|
|
354
|
+
'Access-Control-Expose-Headers':
|
|
355
|
+
'X-PAYMENT-RESPONSE, PAYMENT-RESPONSE, PAYMENT-REQUIRED',
|
|
356
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
357
|
+
} as const;
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* All x402 custom header names that should be allowed in CORS
|
|
361
|
+
*/
|
|
362
|
+
export const X402_HEADER_NAMES = [
|
|
363
|
+
'X-PAYMENT',
|
|
364
|
+
'PAYMENT-SIGNATURE',
|
|
365
|
+
'X-PAYMENT-RESPONSE',
|
|
366
|
+
'PAYMENT-RESPONSE',
|
|
367
|
+
'PAYMENT-REQUIRED',
|
|
368
|
+
] as const;
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Get CORS headers with custom origin
|
|
372
|
+
*
|
|
373
|
+
* @param origin - Allowed origin (use '*' for any, or specific domain)
|
|
374
|
+
* @returns Complete CORS headers object
|
|
375
|
+
*
|
|
376
|
+
* @example
|
|
377
|
+
* ```ts
|
|
378
|
+
* // Express.js middleware
|
|
379
|
+
* app.use((req, res, next) => {
|
|
380
|
+
* const corsHeaders = getCorsHeaders('https://myapp.com');
|
|
381
|
+
* Object.entries(corsHeaders).forEach(([key, value]) => {
|
|
382
|
+
* res.setHeader(key, value);
|
|
383
|
+
* });
|
|
384
|
+
* if (req.method === 'OPTIONS') {
|
|
385
|
+
* return res.status(204).end();
|
|
386
|
+
* }
|
|
387
|
+
* next();
|
|
388
|
+
* });
|
|
389
|
+
* ```
|
|
390
|
+
*/
|
|
391
|
+
export function getCorsHeaders(origin: string = '*'): Record<string, string> {
|
|
392
|
+
return {
|
|
393
|
+
'Access-Control-Allow-Origin': origin,
|
|
394
|
+
...X402_CORS_HEADERS,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ============================================================================
|
|
399
|
+
// FACILITATOR CLIENT
|
|
400
|
+
// ============================================================================
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Options for the FacilitatorClient
|
|
404
|
+
*/
|
|
405
|
+
export interface FacilitatorClientOptions {
|
|
406
|
+
/** Base URL of the facilitator (default: https://facilitator.ultravioletadao.xyz) */
|
|
407
|
+
baseUrl?: string;
|
|
408
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
409
|
+
timeout?: number;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Client for interacting with the x402 facilitator API
|
|
414
|
+
*
|
|
415
|
+
* @example
|
|
416
|
+
* ```ts
|
|
417
|
+
* const client = new FacilitatorClient();
|
|
418
|
+
*
|
|
419
|
+
* // Verify a payment
|
|
420
|
+
* const verifyResult = await client.verify(paymentHeader, requirements);
|
|
421
|
+
* if (!verifyResult.isValid) {
|
|
422
|
+
* return res.status(402).json({ error: verifyResult.invalidReason });
|
|
423
|
+
* }
|
|
424
|
+
*
|
|
425
|
+
* // Provide the service, then settle
|
|
426
|
+
* const settleResult = await client.settle(paymentHeader, requirements);
|
|
427
|
+
* if (!settleResult.success) {
|
|
428
|
+
* // Handle settlement failure (maybe refund or retry)
|
|
429
|
+
* }
|
|
430
|
+
* ```
|
|
431
|
+
*/
|
|
432
|
+
export class FacilitatorClient {
|
|
433
|
+
private readonly baseUrl: string;
|
|
434
|
+
private readonly timeout: number;
|
|
435
|
+
|
|
436
|
+
constructor(options: FacilitatorClientOptions = {}) {
|
|
437
|
+
this.baseUrl = options.baseUrl || 'https://facilitator.ultravioletadao.xyz';
|
|
438
|
+
this.timeout = options.timeout || 30000;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Verify a payment with the facilitator
|
|
443
|
+
*
|
|
444
|
+
* Call this before providing the paid resource to validate the payment.
|
|
445
|
+
*
|
|
446
|
+
* @param paymentHeader - Parsed x402 payment header
|
|
447
|
+
* @param requirements - Payment requirements
|
|
448
|
+
* @returns Verification result
|
|
449
|
+
*/
|
|
450
|
+
async verify(
|
|
451
|
+
paymentHeader: X402Header,
|
|
452
|
+
requirements: PaymentRequirements
|
|
453
|
+
): Promise<VerifyResponse> {
|
|
454
|
+
const body = buildVerifyRequest(paymentHeader, requirements);
|
|
455
|
+
|
|
456
|
+
const controller = new AbortController();
|
|
457
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
const response = await fetch(`${this.baseUrl}/verify`, {
|
|
461
|
+
method: 'POST',
|
|
462
|
+
headers: { 'Content-Type': 'application/json' },
|
|
463
|
+
body: JSON.stringify(body),
|
|
464
|
+
signal: controller.signal,
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
clearTimeout(timeoutId);
|
|
468
|
+
|
|
469
|
+
if (!response.ok) {
|
|
470
|
+
const errorText = await response.text();
|
|
471
|
+
return {
|
|
472
|
+
isValid: false,
|
|
473
|
+
invalidReason: `Facilitator error: ${response.status} - ${errorText}`,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return await response.json();
|
|
478
|
+
} catch (error) {
|
|
479
|
+
clearTimeout(timeoutId);
|
|
480
|
+
return {
|
|
481
|
+
isValid: false,
|
|
482
|
+
invalidReason: error instanceof Error ? error.message : 'Unknown error',
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Settle a payment with the facilitator
|
|
489
|
+
*
|
|
490
|
+
* Call this after providing the paid resource to execute the on-chain transfer.
|
|
491
|
+
*
|
|
492
|
+
* @param paymentHeader - Parsed x402 payment header
|
|
493
|
+
* @param requirements - Payment requirements
|
|
494
|
+
* @returns Settlement result with transaction hash
|
|
495
|
+
*/
|
|
496
|
+
async settle(
|
|
497
|
+
paymentHeader: X402Header,
|
|
498
|
+
requirements: PaymentRequirements
|
|
499
|
+
): Promise<SettleResponse> {
|
|
500
|
+
const body = buildSettleRequest(paymentHeader, requirements);
|
|
501
|
+
|
|
502
|
+
const controller = new AbortController();
|
|
503
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
const response = await fetch(`${this.baseUrl}/settle`, {
|
|
507
|
+
method: 'POST',
|
|
508
|
+
headers: { 'Content-Type': 'application/json' },
|
|
509
|
+
body: JSON.stringify(body),
|
|
510
|
+
signal: controller.signal,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
clearTimeout(timeoutId);
|
|
514
|
+
|
|
515
|
+
if (!response.ok) {
|
|
516
|
+
const errorText = await response.text();
|
|
517
|
+
return {
|
|
518
|
+
success: false,
|
|
519
|
+
error: `Facilitator error: ${response.status} - ${errorText}`,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const result = await response.json();
|
|
524
|
+
return {
|
|
525
|
+
success: true,
|
|
526
|
+
transactionHash: result.transactionHash || result.transaction_hash,
|
|
527
|
+
network: result.network,
|
|
528
|
+
};
|
|
529
|
+
} catch (error) {
|
|
530
|
+
clearTimeout(timeoutId);
|
|
531
|
+
return {
|
|
532
|
+
success: false,
|
|
533
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Verify and settle atomically
|
|
540
|
+
*
|
|
541
|
+
* Convenience method that verifies first, then settles if valid.
|
|
542
|
+
* Use this for simple payment flows where you don't need custom logic between verify and settle.
|
|
543
|
+
*
|
|
544
|
+
* @param paymentHeader - Parsed x402 payment header
|
|
545
|
+
* @param requirements - Payment requirements
|
|
546
|
+
* @returns Combined result with verify and settle status
|
|
547
|
+
*/
|
|
548
|
+
async verifyAndSettle(
|
|
549
|
+
paymentHeader: X402Header,
|
|
550
|
+
requirements: PaymentRequirements
|
|
551
|
+
): Promise<{
|
|
552
|
+
verified: boolean;
|
|
553
|
+
settled: boolean;
|
|
554
|
+
transactionHash?: string;
|
|
555
|
+
error?: string;
|
|
556
|
+
}> {
|
|
557
|
+
// Verify first
|
|
558
|
+
const verifyResult = await this.verify(paymentHeader, requirements);
|
|
559
|
+
if (!verifyResult.isValid) {
|
|
560
|
+
return {
|
|
561
|
+
verified: false,
|
|
562
|
+
settled: false,
|
|
563
|
+
error: verifyResult.invalidReason,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Settle
|
|
568
|
+
const settleResult = await this.settle(paymentHeader, requirements);
|
|
569
|
+
return {
|
|
570
|
+
verified: true,
|
|
571
|
+
settled: settleResult.success,
|
|
572
|
+
transactionHash: settleResult.transactionHash,
|
|
573
|
+
error: settleResult.error,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Check if the facilitator is healthy
|
|
579
|
+
*
|
|
580
|
+
* @returns True if the facilitator is responding
|
|
581
|
+
*/
|
|
582
|
+
async healthCheck(): Promise<boolean> {
|
|
583
|
+
try {
|
|
584
|
+
const response = await fetch(`${this.baseUrl}/health`, {
|
|
585
|
+
method: 'GET',
|
|
586
|
+
});
|
|
587
|
+
return response.ok;
|
|
588
|
+
} catch {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ============================================================================
|
|
595
|
+
// ATOMIC PAYMENT HELPERS
|
|
596
|
+
// ============================================================================
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Create a 402 Payment Required response
|
|
600
|
+
*
|
|
601
|
+
* @param requirements - Payment requirements
|
|
602
|
+
* @param options - Additional response options
|
|
603
|
+
* @returns Object with status code, headers, and body for the 402 response
|
|
604
|
+
*
|
|
605
|
+
* @example
|
|
606
|
+
* ```ts
|
|
607
|
+
* // Express.js
|
|
608
|
+
* app.get('/premium-data', (req, res) => {
|
|
609
|
+
* const payment = extractPaymentFromHeaders(req.headers);
|
|
610
|
+
*
|
|
611
|
+
* if (!payment) {
|
|
612
|
+
* const { status, headers, body } = create402Response({
|
|
613
|
+
* amount: '1.00',
|
|
614
|
+
* recipient: '0x...',
|
|
615
|
+
* resource: 'https://api.example.com/premium-data',
|
|
616
|
+
* });
|
|
617
|
+
* return res.status(status).set(headers).json(body);
|
|
618
|
+
* }
|
|
619
|
+
*
|
|
620
|
+
* // Verify and serve...
|
|
621
|
+
* });
|
|
622
|
+
* ```
|
|
623
|
+
*/
|
|
624
|
+
export function create402Response(
|
|
625
|
+
requirements: PaymentRequirementsOptions,
|
|
626
|
+
options: {
|
|
627
|
+
accepts?: Array<{ network: string; asset: string; amount: string }>;
|
|
628
|
+
} = {}
|
|
629
|
+
): {
|
|
630
|
+
status: 402;
|
|
631
|
+
headers: Record<string, string>;
|
|
632
|
+
body: Record<string, unknown>;
|
|
633
|
+
} {
|
|
634
|
+
const reqs = buildPaymentRequirements(requirements);
|
|
635
|
+
|
|
636
|
+
const body: Record<string, unknown> = {
|
|
637
|
+
x402Version: requirements.x402Version || 1,
|
|
638
|
+
...reqs,
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
if (options.accepts) {
|
|
642
|
+
body.accepts = options.accepts;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return {
|
|
646
|
+
status: 402,
|
|
647
|
+
headers: {
|
|
648
|
+
'Content-Type': 'application/json',
|
|
649
|
+
...X402_CORS_HEADERS,
|
|
650
|
+
},
|
|
651
|
+
body,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Create an Express-compatible middleware for x402 payments
|
|
657
|
+
*
|
|
658
|
+
* @param getRequirements - Function to get payment requirements for a request
|
|
659
|
+
* @param options - Middleware options
|
|
660
|
+
* @returns Express middleware function
|
|
661
|
+
*
|
|
662
|
+
* @example
|
|
663
|
+
* ```ts
|
|
664
|
+
* const paymentMiddleware = createPaymentMiddleware(
|
|
665
|
+
* (req) => ({
|
|
666
|
+
* amount: '1.00',
|
|
667
|
+
* recipient: process.env.PAYMENT_RECIPIENT,
|
|
668
|
+
* resource: `${req.protocol}://${req.get('host')}${req.originalUrl}`,
|
|
669
|
+
* }),
|
|
670
|
+
* { facilitatorUrl: 'https://facilitator.uvd.xyz' }
|
|
671
|
+
* );
|
|
672
|
+
*
|
|
673
|
+
* app.get('/premium/*', paymentMiddleware, (req, res) => {
|
|
674
|
+
* res.json({ premium: 'data' });
|
|
675
|
+
* });
|
|
676
|
+
* ```
|
|
677
|
+
*/
|
|
678
|
+
export function createPaymentMiddleware(
|
|
679
|
+
getRequirements: (req: { headers: Record<string, string | string[] | undefined> }) => PaymentRequirementsOptions,
|
|
680
|
+
options: FacilitatorClientOptions = {}
|
|
681
|
+
): (
|
|
682
|
+
req: { headers: Record<string, string | string[] | undefined> },
|
|
683
|
+
res: { status: (code: number) => { json: (body: unknown) => void; set: (headers: Record<string, string>) => { json: (body: unknown) => void } } },
|
|
684
|
+
next: () => void
|
|
685
|
+
) => Promise<void> {
|
|
686
|
+
const client = new FacilitatorClient(options);
|
|
687
|
+
|
|
688
|
+
return async (req, res, next) => {
|
|
689
|
+
// Extract payment header
|
|
690
|
+
const payment = extractPaymentFromHeaders(req.headers);
|
|
691
|
+
|
|
692
|
+
// If no payment, return 402
|
|
693
|
+
if (!payment) {
|
|
694
|
+
const reqOptions = getRequirements(req);
|
|
695
|
+
const { status, headers, body } = create402Response(reqOptions);
|
|
696
|
+
res.status(status).set(headers).json(body);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Build requirements and verify
|
|
701
|
+
const reqOptions = getRequirements(req);
|
|
702
|
+
const requirements = buildPaymentRequirements(reqOptions);
|
|
703
|
+
const verifyResult = await client.verify(payment, requirements);
|
|
704
|
+
|
|
705
|
+
if (!verifyResult.isValid) {
|
|
706
|
+
res.status(402).json({
|
|
707
|
+
error: 'Payment verification failed',
|
|
708
|
+
reason: verifyResult.invalidReason,
|
|
709
|
+
});
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Payment is valid, continue to handler
|
|
714
|
+
// Note: Settlement should be done after the response is sent
|
|
715
|
+
next();
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// ============================================================================
|
|
720
|
+
// BAZAAR DISCOVERY API
|
|
721
|
+
// ============================================================================
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Resource category for discovery
|
|
725
|
+
*/
|
|
726
|
+
export type BazaarCategory =
|
|
727
|
+
| 'api'
|
|
728
|
+
| 'data'
|
|
729
|
+
| 'ai'
|
|
730
|
+
| 'media'
|
|
731
|
+
| 'compute'
|
|
732
|
+
| 'storage'
|
|
733
|
+
| 'other';
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Network/chain filter for discovery
|
|
737
|
+
*/
|
|
738
|
+
export type BazaarNetwork =
|
|
739
|
+
| 'base'
|
|
740
|
+
| 'ethereum'
|
|
741
|
+
| 'polygon'
|
|
742
|
+
| 'arbitrum'
|
|
743
|
+
| 'optimism'
|
|
744
|
+
| 'avalanche'
|
|
745
|
+
| 'celo'
|
|
746
|
+
| 'hyperevm'
|
|
747
|
+
| 'unichain'
|
|
748
|
+
| 'monad'
|
|
749
|
+
| 'solana'
|
|
750
|
+
| 'fogo'
|
|
751
|
+
| 'stellar'
|
|
752
|
+
| 'near';
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Token/asset filter for discovery
|
|
756
|
+
*/
|
|
757
|
+
export type BazaarToken = 'USDC' | 'EURC' | 'AUSD' | 'PYUSD' | 'USDT';
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Resource registered in the Bazaar
|
|
761
|
+
*/
|
|
762
|
+
export interface BazaarResource {
|
|
763
|
+
/** Unique resource ID */
|
|
764
|
+
id: string;
|
|
765
|
+
/** Resource URL */
|
|
766
|
+
url: string;
|
|
767
|
+
/** Human-readable name */
|
|
768
|
+
name: string;
|
|
769
|
+
/** Description of the resource */
|
|
770
|
+
description: string;
|
|
771
|
+
/** Category of the resource */
|
|
772
|
+
category: BazaarCategory;
|
|
773
|
+
/** Supported networks for payment */
|
|
774
|
+
networks: BazaarNetwork[];
|
|
775
|
+
/** Supported tokens for payment */
|
|
776
|
+
tokens: BazaarToken[];
|
|
777
|
+
/** Price per request in atomic units */
|
|
778
|
+
pricePerRequest: string;
|
|
779
|
+
/** Price currency (e.g., "USDC") */
|
|
780
|
+
priceCurrency: BazaarToken;
|
|
781
|
+
/** Recipient address for payments */
|
|
782
|
+
payTo: string;
|
|
783
|
+
/** MIME type of the resource */
|
|
784
|
+
mimeType: string;
|
|
785
|
+
/** Optional output schema */
|
|
786
|
+
outputSchema?: unknown;
|
|
787
|
+
/** Resource owner/provider */
|
|
788
|
+
provider?: string;
|
|
789
|
+
/** Resource tags for search */
|
|
790
|
+
tags?: string[];
|
|
791
|
+
/** Whether the resource is active */
|
|
792
|
+
isActive: boolean;
|
|
793
|
+
/** ISO timestamp of creation */
|
|
794
|
+
createdAt: string;
|
|
795
|
+
/** ISO timestamp of last update */
|
|
796
|
+
updatedAt: string;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Options for registering a resource
|
|
801
|
+
*/
|
|
802
|
+
export interface BazaarRegisterOptions {
|
|
803
|
+
/** Resource URL (must be unique) */
|
|
804
|
+
url: string;
|
|
805
|
+
/** Human-readable name */
|
|
806
|
+
name: string;
|
|
807
|
+
/** Description of the resource */
|
|
808
|
+
description: string;
|
|
809
|
+
/** Category of the resource */
|
|
810
|
+
category: BazaarCategory;
|
|
811
|
+
/** Supported networks for payment */
|
|
812
|
+
networks: BazaarNetwork[];
|
|
813
|
+
/** Supported tokens for payment */
|
|
814
|
+
tokens?: BazaarToken[];
|
|
815
|
+
/** Price per request (e.g., "0.01") */
|
|
816
|
+
price: string;
|
|
817
|
+
/** Price currency (default: USDC) */
|
|
818
|
+
priceCurrency?: BazaarToken;
|
|
819
|
+
/** Recipient address for payments */
|
|
820
|
+
payTo: string;
|
|
821
|
+
/** MIME type of the resource (default: application/json) */
|
|
822
|
+
mimeType?: string;
|
|
823
|
+
/** Optional output schema */
|
|
824
|
+
outputSchema?: unknown;
|
|
825
|
+
/** Resource tags for search */
|
|
826
|
+
tags?: string[];
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Options for discovering resources
|
|
831
|
+
*/
|
|
832
|
+
export interface BazaarDiscoverOptions {
|
|
833
|
+
/** Filter by category */
|
|
834
|
+
category?: BazaarCategory;
|
|
835
|
+
/** Filter by network */
|
|
836
|
+
network?: BazaarNetwork;
|
|
837
|
+
/** Filter by token */
|
|
838
|
+
token?: BazaarToken;
|
|
839
|
+
/** Filter by provider address */
|
|
840
|
+
provider?: string;
|
|
841
|
+
/** Filter by tags (match any) */
|
|
842
|
+
tags?: string[];
|
|
843
|
+
/** Search query (name, description) */
|
|
844
|
+
query?: string;
|
|
845
|
+
/** Maximum price filter (e.g., "0.10") */
|
|
846
|
+
maxPrice?: string;
|
|
847
|
+
/** Page number (1-indexed) */
|
|
848
|
+
page?: number;
|
|
849
|
+
/** Results per page (default: 20, max: 100) */
|
|
850
|
+
limit?: number;
|
|
851
|
+
/** Sort order */
|
|
852
|
+
sortBy?: 'price' | 'createdAt' | 'name';
|
|
853
|
+
/** Sort direction */
|
|
854
|
+
sortOrder?: 'asc' | 'desc';
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Paginated discovery response
|
|
859
|
+
*/
|
|
860
|
+
export interface BazaarDiscoverResponse {
|
|
861
|
+
/** List of resources matching the query */
|
|
862
|
+
resources: BazaarResource[];
|
|
863
|
+
/** Total number of matching resources */
|
|
864
|
+
total: number;
|
|
865
|
+
/** Current page number */
|
|
866
|
+
page: number;
|
|
867
|
+
/** Results per page */
|
|
868
|
+
limit: number;
|
|
869
|
+
/** Total number of pages */
|
|
870
|
+
totalPages: number;
|
|
871
|
+
/** Whether there are more pages */
|
|
872
|
+
hasMore: boolean;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Options for the BazaarClient
|
|
877
|
+
*/
|
|
878
|
+
export interface BazaarClientOptions {
|
|
879
|
+
/** Base URL of the Bazaar API (default: https://bazaar.ultravioletadao.xyz) */
|
|
880
|
+
baseUrl?: string;
|
|
881
|
+
/** API key for authenticated operations (required for register/update/delete) */
|
|
882
|
+
apiKey?: string;
|
|
883
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
884
|
+
timeout?: number;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Client for interacting with the x402 Bazaar Discovery API
|
|
889
|
+
*
|
|
890
|
+
* The Bazaar is a discovery service for x402-enabled resources.
|
|
891
|
+
* Providers can register their APIs and consumers can discover them.
|
|
892
|
+
*
|
|
893
|
+
* @example
|
|
894
|
+
* ```ts
|
|
895
|
+
* // Discover resources (no auth required)
|
|
896
|
+
* const bazaar = new BazaarClient();
|
|
897
|
+
* const results = await bazaar.discover({
|
|
898
|
+
* category: 'ai',
|
|
899
|
+
* network: 'base',
|
|
900
|
+
* maxPrice: '0.10',
|
|
901
|
+
* });
|
|
902
|
+
*
|
|
903
|
+
* // Register a resource (requires API key)
|
|
904
|
+
* const authBazaar = new BazaarClient({ apiKey: 'your-api-key' });
|
|
905
|
+
* const resource = await authBazaar.register({
|
|
906
|
+
* url: 'https://api.example.com/v1/chat',
|
|
907
|
+
* name: 'AI Chat API',
|
|
908
|
+
* description: 'Pay-per-message AI chat',
|
|
909
|
+
* category: 'ai',
|
|
910
|
+
* networks: ['base', 'ethereum'],
|
|
911
|
+
* price: '0.01',
|
|
912
|
+
* payTo: '0x...',
|
|
913
|
+
* });
|
|
914
|
+
* ```
|
|
915
|
+
*/
|
|
916
|
+
export class BazaarClient {
|
|
917
|
+
private readonly baseUrl: string;
|
|
918
|
+
private readonly apiKey?: string;
|
|
919
|
+
private readonly timeout: number;
|
|
920
|
+
|
|
921
|
+
constructor(options: BazaarClientOptions = {}) {
|
|
922
|
+
this.baseUrl = options.baseUrl || 'https://bazaar.ultravioletadao.xyz';
|
|
923
|
+
this.apiKey = options.apiKey;
|
|
924
|
+
this.timeout = options.timeout || 30000;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Discover x402-enabled resources
|
|
929
|
+
*
|
|
930
|
+
* @param options - Discovery filters
|
|
931
|
+
* @returns Paginated list of matching resources
|
|
932
|
+
*
|
|
933
|
+
* @example
|
|
934
|
+
* ```ts
|
|
935
|
+
* // Find AI APIs on Base with USDC under $0.10
|
|
936
|
+
* const results = await bazaar.discover({
|
|
937
|
+
* category: 'ai',
|
|
938
|
+
* network: 'base',
|
|
939
|
+
* token: 'USDC',
|
|
940
|
+
* maxPrice: '0.10',
|
|
941
|
+
* });
|
|
942
|
+
*
|
|
943
|
+
* for (const resource of results.resources) {
|
|
944
|
+
* console.log(`${resource.name}: ${resource.url}`);
|
|
945
|
+
* }
|
|
946
|
+
* ```
|
|
947
|
+
*/
|
|
948
|
+
async discover(
|
|
949
|
+
options: BazaarDiscoverOptions = {}
|
|
950
|
+
): Promise<BazaarDiscoverResponse> {
|
|
951
|
+
const params = new URLSearchParams();
|
|
952
|
+
|
|
953
|
+
if (options.category) params.set('category', options.category);
|
|
954
|
+
if (options.network) params.set('network', options.network);
|
|
955
|
+
if (options.token) params.set('token', options.token);
|
|
956
|
+
if (options.provider) params.set('provider', options.provider);
|
|
957
|
+
if (options.tags?.length) params.set('tags', options.tags.join(','));
|
|
958
|
+
if (options.query) params.set('query', options.query);
|
|
959
|
+
if (options.maxPrice) params.set('maxPrice', options.maxPrice);
|
|
960
|
+
if (options.page) params.set('page', options.page.toString());
|
|
961
|
+
if (options.limit) params.set('limit', options.limit.toString());
|
|
962
|
+
if (options.sortBy) params.set('sortBy', options.sortBy);
|
|
963
|
+
if (options.sortOrder) params.set('sortOrder', options.sortOrder);
|
|
964
|
+
|
|
965
|
+
const url = `${this.baseUrl}/resources${params.toString() ? `?${params}` : ''}`;
|
|
966
|
+
|
|
967
|
+
const controller = new AbortController();
|
|
968
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
969
|
+
|
|
970
|
+
try {
|
|
971
|
+
const response = await fetch(url, {
|
|
972
|
+
method: 'GET',
|
|
973
|
+
headers: { 'Accept': 'application/json' },
|
|
974
|
+
signal: controller.signal,
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
clearTimeout(timeoutId);
|
|
978
|
+
|
|
979
|
+
if (!response.ok) {
|
|
980
|
+
const errorText = await response.text();
|
|
981
|
+
throw new Error(`Bazaar API error: ${response.status} - ${errorText}`);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
return await response.json();
|
|
985
|
+
} catch (error) {
|
|
986
|
+
clearTimeout(timeoutId);
|
|
987
|
+
throw error;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Get a specific resource by ID
|
|
993
|
+
*
|
|
994
|
+
* @param resourceId - Resource ID
|
|
995
|
+
* @returns Resource details
|
|
996
|
+
*/
|
|
997
|
+
async getResource(resourceId: string): Promise<BazaarResource> {
|
|
998
|
+
const url = `${this.baseUrl}/resources/${encodeURIComponent(resourceId)}`;
|
|
999
|
+
|
|
1000
|
+
const controller = new AbortController();
|
|
1001
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1002
|
+
|
|
1003
|
+
try {
|
|
1004
|
+
const response = await fetch(url, {
|
|
1005
|
+
method: 'GET',
|
|
1006
|
+
headers: { 'Accept': 'application/json' },
|
|
1007
|
+
signal: controller.signal,
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
clearTimeout(timeoutId);
|
|
1011
|
+
|
|
1012
|
+
if (!response.ok) {
|
|
1013
|
+
const errorText = await response.text();
|
|
1014
|
+
throw new Error(`Bazaar API error: ${response.status} - ${errorText}`);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
return await response.json();
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
clearTimeout(timeoutId);
|
|
1020
|
+
throw error;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Get a resource by its URL
|
|
1026
|
+
*
|
|
1027
|
+
* @param resourceUrl - Resource URL
|
|
1028
|
+
* @returns Resource details
|
|
1029
|
+
*/
|
|
1030
|
+
async getResourceByUrl(resourceUrl: string): Promise<BazaarResource> {
|
|
1031
|
+
const url = `${this.baseUrl}/resources/by-url?url=${encodeURIComponent(resourceUrl)}`;
|
|
1032
|
+
|
|
1033
|
+
const controller = new AbortController();
|
|
1034
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1035
|
+
|
|
1036
|
+
try {
|
|
1037
|
+
const response = await fetch(url, {
|
|
1038
|
+
method: 'GET',
|
|
1039
|
+
headers: { 'Accept': 'application/json' },
|
|
1040
|
+
signal: controller.signal,
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
clearTimeout(timeoutId);
|
|
1044
|
+
|
|
1045
|
+
if (!response.ok) {
|
|
1046
|
+
const errorText = await response.text();
|
|
1047
|
+
throw new Error(`Bazaar API error: ${response.status} - ${errorText}`);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
return await response.json();
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
clearTimeout(timeoutId);
|
|
1053
|
+
throw error;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Register a new resource in the Bazaar
|
|
1059
|
+
*
|
|
1060
|
+
* Requires API key authentication.
|
|
1061
|
+
*
|
|
1062
|
+
* @param options - Resource registration options
|
|
1063
|
+
* @returns Registered resource
|
|
1064
|
+
*
|
|
1065
|
+
* @example
|
|
1066
|
+
* ```ts
|
|
1067
|
+
* const resource = await bazaar.register({
|
|
1068
|
+
* url: 'https://api.example.com/v1/generate',
|
|
1069
|
+
* name: 'Image Generator API',
|
|
1070
|
+
* description: 'Generate images with AI',
|
|
1071
|
+
* category: 'ai',
|
|
1072
|
+
* networks: ['base', 'ethereum', 'polygon'],
|
|
1073
|
+
* price: '0.05',
|
|
1074
|
+
* payTo: '0x1234...',
|
|
1075
|
+
* tags: ['ai', 'image', 'generator'],
|
|
1076
|
+
* });
|
|
1077
|
+
* ```
|
|
1078
|
+
*/
|
|
1079
|
+
async register(options: BazaarRegisterOptions): Promise<BazaarResource> {
|
|
1080
|
+
if (!this.apiKey) {
|
|
1081
|
+
throw new Error('API key required for resource registration');
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const url = `${this.baseUrl}/resources`;
|
|
1085
|
+
|
|
1086
|
+
const controller = new AbortController();
|
|
1087
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1088
|
+
|
|
1089
|
+
try {
|
|
1090
|
+
const response = await fetch(url, {
|
|
1091
|
+
method: 'POST',
|
|
1092
|
+
headers: {
|
|
1093
|
+
'Content-Type': 'application/json',
|
|
1094
|
+
'Accept': 'application/json',
|
|
1095
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
1096
|
+
},
|
|
1097
|
+
body: JSON.stringify({
|
|
1098
|
+
url: options.url,
|
|
1099
|
+
name: options.name,
|
|
1100
|
+
description: options.description,
|
|
1101
|
+
category: options.category,
|
|
1102
|
+
networks: options.networks,
|
|
1103
|
+
tokens: options.tokens || ['USDC'],
|
|
1104
|
+
price: options.price,
|
|
1105
|
+
priceCurrency: options.priceCurrency || 'USDC',
|
|
1106
|
+
payTo: options.payTo,
|
|
1107
|
+
mimeType: options.mimeType || 'application/json',
|
|
1108
|
+
outputSchema: options.outputSchema,
|
|
1109
|
+
tags: options.tags,
|
|
1110
|
+
}),
|
|
1111
|
+
signal: controller.signal,
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
clearTimeout(timeoutId);
|
|
1115
|
+
|
|
1116
|
+
if (!response.ok) {
|
|
1117
|
+
const errorText = await response.text();
|
|
1118
|
+
throw new Error(`Bazaar API error: ${response.status} - ${errorText}`);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
return await response.json();
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
clearTimeout(timeoutId);
|
|
1124
|
+
throw error;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Update an existing resource
|
|
1130
|
+
*
|
|
1131
|
+
* Requires API key authentication. Only the owner can update.
|
|
1132
|
+
*
|
|
1133
|
+
* @param resourceId - Resource ID to update
|
|
1134
|
+
* @param updates - Partial update options
|
|
1135
|
+
* @returns Updated resource
|
|
1136
|
+
*/
|
|
1137
|
+
async update(
|
|
1138
|
+
resourceId: string,
|
|
1139
|
+
updates: Partial<BazaarRegisterOptions>
|
|
1140
|
+
): Promise<BazaarResource> {
|
|
1141
|
+
if (!this.apiKey) {
|
|
1142
|
+
throw new Error('API key required for resource update');
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const url = `${this.baseUrl}/resources/${encodeURIComponent(resourceId)}`;
|
|
1146
|
+
|
|
1147
|
+
const controller = new AbortController();
|
|
1148
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1149
|
+
|
|
1150
|
+
try {
|
|
1151
|
+
const response = await fetch(url, {
|
|
1152
|
+
method: 'PATCH',
|
|
1153
|
+
headers: {
|
|
1154
|
+
'Content-Type': 'application/json',
|
|
1155
|
+
'Accept': 'application/json',
|
|
1156
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
1157
|
+
},
|
|
1158
|
+
body: JSON.stringify(updates),
|
|
1159
|
+
signal: controller.signal,
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
clearTimeout(timeoutId);
|
|
1163
|
+
|
|
1164
|
+
if (!response.ok) {
|
|
1165
|
+
const errorText = await response.text();
|
|
1166
|
+
throw new Error(`Bazaar API error: ${response.status} - ${errorText}`);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
return await response.json();
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
clearTimeout(timeoutId);
|
|
1172
|
+
throw error;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* Delete a resource from the Bazaar
|
|
1178
|
+
*
|
|
1179
|
+
* Requires API key authentication. Only the owner can delete.
|
|
1180
|
+
*
|
|
1181
|
+
* @param resourceId - Resource ID to delete
|
|
1182
|
+
*/
|
|
1183
|
+
async delete(resourceId: string): Promise<void> {
|
|
1184
|
+
if (!this.apiKey) {
|
|
1185
|
+
throw new Error('API key required for resource deletion');
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const url = `${this.baseUrl}/resources/${encodeURIComponent(resourceId)}`;
|
|
1189
|
+
|
|
1190
|
+
const controller = new AbortController();
|
|
1191
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1192
|
+
|
|
1193
|
+
try {
|
|
1194
|
+
const response = await fetch(url, {
|
|
1195
|
+
method: 'DELETE',
|
|
1196
|
+
headers: {
|
|
1197
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
1198
|
+
},
|
|
1199
|
+
signal: controller.signal,
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
clearTimeout(timeoutId);
|
|
1203
|
+
|
|
1204
|
+
if (!response.ok) {
|
|
1205
|
+
const errorText = await response.text();
|
|
1206
|
+
throw new Error(`Bazaar API error: ${response.status} - ${errorText}`);
|
|
1207
|
+
}
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
clearTimeout(timeoutId);
|
|
1210
|
+
throw error;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Deactivate a resource (soft delete)
|
|
1216
|
+
*
|
|
1217
|
+
* Requires API key authentication. Only the owner can deactivate.
|
|
1218
|
+
*
|
|
1219
|
+
* @param resourceId - Resource ID to deactivate
|
|
1220
|
+
* @returns Updated resource with isActive: false
|
|
1221
|
+
*/
|
|
1222
|
+
async deactivate(resourceId: string): Promise<BazaarResource> {
|
|
1223
|
+
if (!this.apiKey) {
|
|
1224
|
+
throw new Error('API key required for resource deactivation');
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
const url = `${this.baseUrl}/resources/${encodeURIComponent(resourceId)}/deactivate`;
|
|
1228
|
+
|
|
1229
|
+
const controller = new AbortController();
|
|
1230
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1231
|
+
|
|
1232
|
+
try {
|
|
1233
|
+
const response = await fetch(url, {
|
|
1234
|
+
method: 'POST',
|
|
1235
|
+
headers: {
|
|
1236
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
1237
|
+
},
|
|
1238
|
+
signal: controller.signal,
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
clearTimeout(timeoutId);
|
|
1242
|
+
|
|
1243
|
+
if (!response.ok) {
|
|
1244
|
+
const errorText = await response.text();
|
|
1245
|
+
throw new Error(`Bazaar API error: ${response.status} - ${errorText}`);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
return await response.json();
|
|
1249
|
+
} catch (error) {
|
|
1250
|
+
clearTimeout(timeoutId);
|
|
1251
|
+
throw error;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Reactivate a deactivated resource
|
|
1257
|
+
*
|
|
1258
|
+
* Requires API key authentication. Only the owner can reactivate.
|
|
1259
|
+
*
|
|
1260
|
+
* @param resourceId - Resource ID to reactivate
|
|
1261
|
+
* @returns Updated resource with isActive: true
|
|
1262
|
+
*/
|
|
1263
|
+
async reactivate(resourceId: string): Promise<BazaarResource> {
|
|
1264
|
+
if (!this.apiKey) {
|
|
1265
|
+
throw new Error('API key required for resource reactivation');
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
const url = `${this.baseUrl}/resources/${encodeURIComponent(resourceId)}/reactivate`;
|
|
1269
|
+
|
|
1270
|
+
const controller = new AbortController();
|
|
1271
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1272
|
+
|
|
1273
|
+
try {
|
|
1274
|
+
const response = await fetch(url, {
|
|
1275
|
+
method: 'POST',
|
|
1276
|
+
headers: {
|
|
1277
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
1278
|
+
},
|
|
1279
|
+
signal: controller.signal,
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
clearTimeout(timeoutId);
|
|
1283
|
+
|
|
1284
|
+
if (!response.ok) {
|
|
1285
|
+
const errorText = await response.text();
|
|
1286
|
+
throw new Error(`Bazaar API error: ${response.status} - ${errorText}`);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
return await response.json();
|
|
1290
|
+
} catch (error) {
|
|
1291
|
+
clearTimeout(timeoutId);
|
|
1292
|
+
throw error;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* List all resources owned by the authenticated user
|
|
1298
|
+
*
|
|
1299
|
+
* Requires API key authentication.
|
|
1300
|
+
*
|
|
1301
|
+
* @param options - Pagination options
|
|
1302
|
+
* @returns Paginated list of owned resources
|
|
1303
|
+
*/
|
|
1304
|
+
async listMyResources(options: {
|
|
1305
|
+
page?: number;
|
|
1306
|
+
limit?: number;
|
|
1307
|
+
includeInactive?: boolean;
|
|
1308
|
+
} = {}): Promise<BazaarDiscoverResponse> {
|
|
1309
|
+
if (!this.apiKey) {
|
|
1310
|
+
throw new Error('API key required to list owned resources');
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
const params = new URLSearchParams();
|
|
1314
|
+
if (options.page) params.set('page', options.page.toString());
|
|
1315
|
+
if (options.limit) params.set('limit', options.limit.toString());
|
|
1316
|
+
if (options.includeInactive) params.set('includeInactive', 'true');
|
|
1317
|
+
|
|
1318
|
+
const url = `${this.baseUrl}/resources/mine${params.toString() ? `?${params}` : ''}`;
|
|
1319
|
+
|
|
1320
|
+
const controller = new AbortController();
|
|
1321
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1322
|
+
|
|
1323
|
+
try {
|
|
1324
|
+
const response = await fetch(url, {
|
|
1325
|
+
method: 'GET',
|
|
1326
|
+
headers: {
|
|
1327
|
+
'Accept': 'application/json',
|
|
1328
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
1329
|
+
},
|
|
1330
|
+
signal: controller.signal,
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
clearTimeout(timeoutId);
|
|
1334
|
+
|
|
1335
|
+
if (!response.ok) {
|
|
1336
|
+
const errorText = await response.text();
|
|
1337
|
+
throw new Error(`Bazaar API error: ${response.status} - ${errorText}`);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
return await response.json();
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
clearTimeout(timeoutId);
|
|
1343
|
+
throw error;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* Get Bazaar API health status
|
|
1349
|
+
*
|
|
1350
|
+
* @returns True if the Bazaar API is healthy
|
|
1351
|
+
*/
|
|
1352
|
+
async healthCheck(): Promise<boolean> {
|
|
1353
|
+
try {
|
|
1354
|
+
const response = await fetch(`${this.baseUrl}/health`, {
|
|
1355
|
+
method: 'GET',
|
|
1356
|
+
});
|
|
1357
|
+
return response.ok;
|
|
1358
|
+
} catch {
|
|
1359
|
+
return false;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/**
|
|
1364
|
+
* Get Bazaar statistics
|
|
1365
|
+
*
|
|
1366
|
+
* @returns Global statistics about the Bazaar
|
|
1367
|
+
*/
|
|
1368
|
+
async getStats(): Promise<{
|
|
1369
|
+
totalResources: number;
|
|
1370
|
+
activeResources: number;
|
|
1371
|
+
totalProviders: number;
|
|
1372
|
+
categoryCounts: Record<BazaarCategory, number>;
|
|
1373
|
+
networkCounts: Record<BazaarNetwork, number>;
|
|
1374
|
+
}> {
|
|
1375
|
+
const url = `${this.baseUrl}/stats`;
|
|
1376
|
+
|
|
1377
|
+
const controller = new AbortController();
|
|
1378
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1379
|
+
|
|
1380
|
+
try {
|
|
1381
|
+
const response = await fetch(url, {
|
|
1382
|
+
method: 'GET',
|
|
1383
|
+
headers: { 'Accept': 'application/json' },
|
|
1384
|
+
signal: controller.signal,
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
clearTimeout(timeoutId);
|
|
1388
|
+
|
|
1389
|
+
if (!response.ok) {
|
|
1390
|
+
const errorText = await response.text();
|
|
1391
|
+
throw new Error(`Bazaar API error: ${response.status} - ${errorText}`);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
return await response.json();
|
|
1395
|
+
} catch (error) {
|
|
1396
|
+
clearTimeout(timeoutId);
|
|
1397
|
+
throw error;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// ============================================================================
|
|
1403
|
+
// ESCROW & REFUND EXTENSION
|
|
1404
|
+
// ============================================================================
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* Escrow payment status
|
|
1408
|
+
*/
|
|
1409
|
+
export type EscrowStatus =
|
|
1410
|
+
| 'pending' // Payment initiated, awaiting confirmation
|
|
1411
|
+
| 'held' // Funds held in escrow
|
|
1412
|
+
| 'released' // Funds released to recipient
|
|
1413
|
+
| 'refunded' // Funds returned to payer
|
|
1414
|
+
| 'disputed' // Dispute in progress
|
|
1415
|
+
| 'expired'; // Escrow expired without resolution
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* Refund request status
|
|
1419
|
+
*/
|
|
1420
|
+
export type RefundStatus =
|
|
1421
|
+
| 'pending' // Refund requested, awaiting processing
|
|
1422
|
+
| 'approved' // Refund approved
|
|
1423
|
+
| 'rejected' // Refund rejected
|
|
1424
|
+
| 'processed' // Refund completed on-chain
|
|
1425
|
+
| 'disputed'; // Under dispute review
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* Dispute resolution outcome
|
|
1429
|
+
*/
|
|
1430
|
+
export type DisputeOutcome =
|
|
1431
|
+
| 'pending' // Dispute under review
|
|
1432
|
+
| 'payer_wins' // Payer gets refund
|
|
1433
|
+
| 'recipient_wins' // Recipient keeps funds
|
|
1434
|
+
| 'split'; // Funds split between parties
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* Escrow payment record
|
|
1438
|
+
*/
|
|
1439
|
+
export interface EscrowPayment {
|
|
1440
|
+
/** Unique escrow ID */
|
|
1441
|
+
id: string;
|
|
1442
|
+
/** Original payment header (base64 encoded) */
|
|
1443
|
+
paymentHeader: string;
|
|
1444
|
+
/** Current status */
|
|
1445
|
+
status: EscrowStatus;
|
|
1446
|
+
/** Network where payment was made */
|
|
1447
|
+
network: string;
|
|
1448
|
+
/** Payer address */
|
|
1449
|
+
payer: string;
|
|
1450
|
+
/** Recipient address */
|
|
1451
|
+
recipient: string;
|
|
1452
|
+
/** Amount in atomic units */
|
|
1453
|
+
amount: string;
|
|
1454
|
+
/** Token/asset contract */
|
|
1455
|
+
asset: string;
|
|
1456
|
+
/** Resource URL being paid for */
|
|
1457
|
+
resource: string;
|
|
1458
|
+
/** Escrow expiration timestamp (ISO) */
|
|
1459
|
+
expiresAt: string;
|
|
1460
|
+
/** Release conditions (optional) */
|
|
1461
|
+
releaseConditions?: {
|
|
1462
|
+
/** Minimum time before release (seconds) */
|
|
1463
|
+
minHoldTime?: number;
|
|
1464
|
+
/** Required confirmations */
|
|
1465
|
+
confirmations?: number;
|
|
1466
|
+
/** Custom condition metadata */
|
|
1467
|
+
custom?: unknown;
|
|
1468
|
+
};
|
|
1469
|
+
/** Transaction hash if released/refunded */
|
|
1470
|
+
transactionHash?: string;
|
|
1471
|
+
/** Creation timestamp (ISO) */
|
|
1472
|
+
createdAt: string;
|
|
1473
|
+
/** Last update timestamp (ISO) */
|
|
1474
|
+
updatedAt: string;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/**
|
|
1478
|
+
* Refund request record
|
|
1479
|
+
*/
|
|
1480
|
+
export interface RefundRequest {
|
|
1481
|
+
/** Unique refund request ID */
|
|
1482
|
+
id: string;
|
|
1483
|
+
/** Related escrow ID */
|
|
1484
|
+
escrowId: string;
|
|
1485
|
+
/** Current status */
|
|
1486
|
+
status: RefundStatus;
|
|
1487
|
+
/** Reason for refund request */
|
|
1488
|
+
reason: string;
|
|
1489
|
+
/** Additional evidence/details */
|
|
1490
|
+
evidence?: string;
|
|
1491
|
+
/** Amount requested (may be partial) */
|
|
1492
|
+
amountRequested: string;
|
|
1493
|
+
/** Amount approved (if any) */
|
|
1494
|
+
amountApproved?: string;
|
|
1495
|
+
/** Requester (payer) address */
|
|
1496
|
+
requester: string;
|
|
1497
|
+
/** Transaction hash if processed */
|
|
1498
|
+
transactionHash?: string;
|
|
1499
|
+
/** Response from recipient/facilitator */
|
|
1500
|
+
response?: {
|
|
1501
|
+
status: 'approved' | 'rejected';
|
|
1502
|
+
reason?: string;
|
|
1503
|
+
respondedAt: string;
|
|
1504
|
+
};
|
|
1505
|
+
/** Creation timestamp (ISO) */
|
|
1506
|
+
createdAt: string;
|
|
1507
|
+
/** Last update timestamp (ISO) */
|
|
1508
|
+
updatedAt: string;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
/**
|
|
1512
|
+
* Dispute record
|
|
1513
|
+
*/
|
|
1514
|
+
export interface Dispute {
|
|
1515
|
+
/** Unique dispute ID */
|
|
1516
|
+
id: string;
|
|
1517
|
+
/** Related escrow ID */
|
|
1518
|
+
escrowId: string;
|
|
1519
|
+
/** Related refund request ID (if any) */
|
|
1520
|
+
refundRequestId?: string;
|
|
1521
|
+
/** Dispute outcome */
|
|
1522
|
+
outcome: DisputeOutcome;
|
|
1523
|
+
/** Initiator (payer or recipient) */
|
|
1524
|
+
initiator: 'payer' | 'recipient';
|
|
1525
|
+
/** Reason for dispute */
|
|
1526
|
+
reason: string;
|
|
1527
|
+
/** Evidence from payer */
|
|
1528
|
+
payerEvidence?: string;
|
|
1529
|
+
/** Evidence from recipient */
|
|
1530
|
+
recipientEvidence?: string;
|
|
1531
|
+
/** Arbitration notes */
|
|
1532
|
+
arbitrationNotes?: string;
|
|
1533
|
+
/** Amount resolved to payer */
|
|
1534
|
+
payerAmount?: string;
|
|
1535
|
+
/** Amount resolved to recipient */
|
|
1536
|
+
recipientAmount?: string;
|
|
1537
|
+
/** Transaction hash(es) for resolution */
|
|
1538
|
+
transactionHashes?: string[];
|
|
1539
|
+
/** Creation timestamp (ISO) */
|
|
1540
|
+
createdAt: string;
|
|
1541
|
+
/** Resolution timestamp (ISO) */
|
|
1542
|
+
resolvedAt?: string;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
/**
|
|
1546
|
+
* Options for creating an escrow payment
|
|
1547
|
+
*/
|
|
1548
|
+
export interface CreateEscrowOptions {
|
|
1549
|
+
/** Payment header (from client SDK) */
|
|
1550
|
+
paymentHeader: string;
|
|
1551
|
+
/** Payment requirements */
|
|
1552
|
+
requirements: PaymentRequirements;
|
|
1553
|
+
/** Escrow duration in seconds (default: 86400 = 24h) */
|
|
1554
|
+
escrowDuration?: number;
|
|
1555
|
+
/** Release conditions */
|
|
1556
|
+
releaseConditions?: {
|
|
1557
|
+
minHoldTime?: number;
|
|
1558
|
+
confirmations?: number;
|
|
1559
|
+
custom?: unknown;
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/**
|
|
1564
|
+
* Options for requesting a refund
|
|
1565
|
+
*/
|
|
1566
|
+
export interface RequestRefundOptions {
|
|
1567
|
+
/** Escrow ID to refund */
|
|
1568
|
+
escrowId: string;
|
|
1569
|
+
/** Reason for refund */
|
|
1570
|
+
reason: string;
|
|
1571
|
+
/** Amount to refund (full amount if not specified) */
|
|
1572
|
+
amount?: string;
|
|
1573
|
+
/** Supporting evidence */
|
|
1574
|
+
evidence?: string;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* Options for the EscrowClient
|
|
1579
|
+
*/
|
|
1580
|
+
export interface EscrowClientOptions {
|
|
1581
|
+
/** Base URL of the Escrow API (default: https://escrow.ultravioletadao.xyz) */
|
|
1582
|
+
baseUrl?: string;
|
|
1583
|
+
/** API key for authenticated operations */
|
|
1584
|
+
apiKey?: string;
|
|
1585
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
1586
|
+
timeout?: number;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/**
|
|
1590
|
+
* Client for x402 Escrow & Refund operations
|
|
1591
|
+
*
|
|
1592
|
+
* The Escrow system holds payments until service is verified,
|
|
1593
|
+
* enabling refunds and dispute resolution.
|
|
1594
|
+
*
|
|
1595
|
+
* @example
|
|
1596
|
+
* ```ts
|
|
1597
|
+
* // Create escrow payment (backend)
|
|
1598
|
+
* const escrow = new EscrowClient();
|
|
1599
|
+
* const escrowPayment = await escrow.createEscrow({
|
|
1600
|
+
* paymentHeader: req.headers['x-payment'],
|
|
1601
|
+
* requirements: paymentRequirements,
|
|
1602
|
+
* escrowDuration: 86400, // 24 hours
|
|
1603
|
+
* });
|
|
1604
|
+
*
|
|
1605
|
+
* // After service is provided, release the escrow
|
|
1606
|
+
* await escrow.release(escrowPayment.id);
|
|
1607
|
+
*
|
|
1608
|
+
* // If service not provided, payer can request refund
|
|
1609
|
+
* await escrow.requestRefund({
|
|
1610
|
+
* escrowId: escrowPayment.id,
|
|
1611
|
+
* reason: 'Service not delivered within expected timeframe',
|
|
1612
|
+
* });
|
|
1613
|
+
* ```
|
|
1614
|
+
*/
|
|
1615
|
+
export class EscrowClient {
|
|
1616
|
+
private readonly baseUrl: string;
|
|
1617
|
+
private readonly apiKey?: string;
|
|
1618
|
+
private readonly timeout: number;
|
|
1619
|
+
|
|
1620
|
+
constructor(options: EscrowClientOptions = {}) {
|
|
1621
|
+
this.baseUrl = options.baseUrl || 'https://escrow.ultravioletadao.xyz';
|
|
1622
|
+
this.apiKey = options.apiKey;
|
|
1623
|
+
this.timeout = options.timeout || 30000;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
private getHeaders(authenticated: boolean = false): Record<string, string> {
|
|
1627
|
+
const headers: Record<string, string> = {
|
|
1628
|
+
'Content-Type': 'application/json',
|
|
1629
|
+
'Accept': 'application/json',
|
|
1630
|
+
};
|
|
1631
|
+
if (authenticated && this.apiKey) {
|
|
1632
|
+
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
1633
|
+
}
|
|
1634
|
+
return headers;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
/**
|
|
1638
|
+
* Create an escrow payment
|
|
1639
|
+
*
|
|
1640
|
+
* Holds the payment in escrow until released or refunded.
|
|
1641
|
+
*
|
|
1642
|
+
* @param options - Escrow creation options
|
|
1643
|
+
* @returns Created escrow payment
|
|
1644
|
+
*/
|
|
1645
|
+
async createEscrow(options: CreateEscrowOptions): Promise<EscrowPayment> {
|
|
1646
|
+
const url = `${this.baseUrl}/escrow`;
|
|
1647
|
+
|
|
1648
|
+
const controller = new AbortController();
|
|
1649
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1650
|
+
|
|
1651
|
+
try {
|
|
1652
|
+
const response = await fetch(url, {
|
|
1653
|
+
method: 'POST',
|
|
1654
|
+
headers: this.getHeaders(true),
|
|
1655
|
+
body: JSON.stringify({
|
|
1656
|
+
paymentHeader: options.paymentHeader,
|
|
1657
|
+
paymentRequirements: options.requirements,
|
|
1658
|
+
escrowDuration: options.escrowDuration || 86400,
|
|
1659
|
+
releaseConditions: options.releaseConditions,
|
|
1660
|
+
}),
|
|
1661
|
+
signal: controller.signal,
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
clearTimeout(timeoutId);
|
|
1665
|
+
|
|
1666
|
+
if (!response.ok) {
|
|
1667
|
+
const errorText = await response.text();
|
|
1668
|
+
throw new Error(`Escrow API error: ${response.status} - ${errorText}`);
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
return await response.json();
|
|
1672
|
+
} catch (error) {
|
|
1673
|
+
clearTimeout(timeoutId);
|
|
1674
|
+
throw error;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
/**
|
|
1679
|
+
* Get escrow payment by ID
|
|
1680
|
+
*
|
|
1681
|
+
* @param escrowId - Escrow payment ID
|
|
1682
|
+
* @returns Escrow payment details
|
|
1683
|
+
*/
|
|
1684
|
+
async getEscrow(escrowId: string): Promise<EscrowPayment> {
|
|
1685
|
+
const url = `${this.baseUrl}/escrow/${encodeURIComponent(escrowId)}`;
|
|
1686
|
+
|
|
1687
|
+
const controller = new AbortController();
|
|
1688
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1689
|
+
|
|
1690
|
+
try {
|
|
1691
|
+
const response = await fetch(url, {
|
|
1692
|
+
method: 'GET',
|
|
1693
|
+
headers: this.getHeaders(),
|
|
1694
|
+
signal: controller.signal,
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1697
|
+
clearTimeout(timeoutId);
|
|
1698
|
+
|
|
1699
|
+
if (!response.ok) {
|
|
1700
|
+
const errorText = await response.text();
|
|
1701
|
+
throw new Error(`Escrow API error: ${response.status} - ${errorText}`);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
return await response.json();
|
|
1705
|
+
} catch (error) {
|
|
1706
|
+
clearTimeout(timeoutId);
|
|
1707
|
+
throw error;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/**
|
|
1712
|
+
* Release escrow funds to recipient
|
|
1713
|
+
*
|
|
1714
|
+
* Call this after service has been successfully provided.
|
|
1715
|
+
*
|
|
1716
|
+
* @param escrowId - Escrow payment ID
|
|
1717
|
+
* @returns Updated escrow payment with transaction hash
|
|
1718
|
+
*/
|
|
1719
|
+
async release(escrowId: string): Promise<EscrowPayment> {
|
|
1720
|
+
const url = `${this.baseUrl}/escrow/${encodeURIComponent(escrowId)}/release`;
|
|
1721
|
+
|
|
1722
|
+
const controller = new AbortController();
|
|
1723
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1724
|
+
|
|
1725
|
+
try {
|
|
1726
|
+
const response = await fetch(url, {
|
|
1727
|
+
method: 'POST',
|
|
1728
|
+
headers: this.getHeaders(true),
|
|
1729
|
+
signal: controller.signal,
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1732
|
+
clearTimeout(timeoutId);
|
|
1733
|
+
|
|
1734
|
+
if (!response.ok) {
|
|
1735
|
+
const errorText = await response.text();
|
|
1736
|
+
throw new Error(`Escrow API error: ${response.status} - ${errorText}`);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
return await response.json();
|
|
1740
|
+
} catch (error) {
|
|
1741
|
+
clearTimeout(timeoutId);
|
|
1742
|
+
throw error;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
/**
|
|
1747
|
+
* Request a refund for an escrow payment
|
|
1748
|
+
*
|
|
1749
|
+
* Initiates a refund request that must be approved.
|
|
1750
|
+
*
|
|
1751
|
+
* @param options - Refund request options
|
|
1752
|
+
* @returns Created refund request
|
|
1753
|
+
*/
|
|
1754
|
+
async requestRefund(options: RequestRefundOptions): Promise<RefundRequest> {
|
|
1755
|
+
const url = `${this.baseUrl}/escrow/${encodeURIComponent(options.escrowId)}/refund`;
|
|
1756
|
+
|
|
1757
|
+
const controller = new AbortController();
|
|
1758
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1759
|
+
|
|
1760
|
+
try {
|
|
1761
|
+
const response = await fetch(url, {
|
|
1762
|
+
method: 'POST',
|
|
1763
|
+
headers: this.getHeaders(true),
|
|
1764
|
+
body: JSON.stringify({
|
|
1765
|
+
reason: options.reason,
|
|
1766
|
+
amount: options.amount,
|
|
1767
|
+
evidence: options.evidence,
|
|
1768
|
+
}),
|
|
1769
|
+
signal: controller.signal,
|
|
1770
|
+
});
|
|
1771
|
+
|
|
1772
|
+
clearTimeout(timeoutId);
|
|
1773
|
+
|
|
1774
|
+
if (!response.ok) {
|
|
1775
|
+
const errorText = await response.text();
|
|
1776
|
+
throw new Error(`Escrow API error: ${response.status} - ${errorText}`);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
return await response.json();
|
|
1780
|
+
} catch (error) {
|
|
1781
|
+
clearTimeout(timeoutId);
|
|
1782
|
+
throw error;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
/**
|
|
1787
|
+
* Approve a refund request (for recipients)
|
|
1788
|
+
*
|
|
1789
|
+
* @param refundId - Refund request ID
|
|
1790
|
+
* @param amount - Amount to approve (may be less than requested)
|
|
1791
|
+
* @returns Updated refund request
|
|
1792
|
+
*/
|
|
1793
|
+
async approveRefund(refundId: string, amount?: string): Promise<RefundRequest> {
|
|
1794
|
+
const url = `${this.baseUrl}/refund/${encodeURIComponent(refundId)}/approve`;
|
|
1795
|
+
|
|
1796
|
+
const controller = new AbortController();
|
|
1797
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1798
|
+
|
|
1799
|
+
try {
|
|
1800
|
+
const response = await fetch(url, {
|
|
1801
|
+
method: 'POST',
|
|
1802
|
+
headers: this.getHeaders(true),
|
|
1803
|
+
body: JSON.stringify({ amount }),
|
|
1804
|
+
signal: controller.signal,
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
clearTimeout(timeoutId);
|
|
1808
|
+
|
|
1809
|
+
if (!response.ok) {
|
|
1810
|
+
const errorText = await response.text();
|
|
1811
|
+
throw new Error(`Escrow API error: ${response.status} - ${errorText}`);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
return await response.json();
|
|
1815
|
+
} catch (error) {
|
|
1816
|
+
clearTimeout(timeoutId);
|
|
1817
|
+
throw error;
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
/**
|
|
1822
|
+
* Reject a refund request (for recipients)
|
|
1823
|
+
*
|
|
1824
|
+
* @param refundId - Refund request ID
|
|
1825
|
+
* @param reason - Reason for rejection
|
|
1826
|
+
* @returns Updated refund request
|
|
1827
|
+
*/
|
|
1828
|
+
async rejectRefund(refundId: string, reason: string): Promise<RefundRequest> {
|
|
1829
|
+
const url = `${this.baseUrl}/refund/${encodeURIComponent(refundId)}/reject`;
|
|
1830
|
+
|
|
1831
|
+
const controller = new AbortController();
|
|
1832
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1833
|
+
|
|
1834
|
+
try {
|
|
1835
|
+
const response = await fetch(url, {
|
|
1836
|
+
method: 'POST',
|
|
1837
|
+
headers: this.getHeaders(true),
|
|
1838
|
+
body: JSON.stringify({ reason }),
|
|
1839
|
+
signal: controller.signal,
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1842
|
+
clearTimeout(timeoutId);
|
|
1843
|
+
|
|
1844
|
+
if (!response.ok) {
|
|
1845
|
+
const errorText = await response.text();
|
|
1846
|
+
throw new Error(`Escrow API error: ${response.status} - ${errorText}`);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
return await response.json();
|
|
1850
|
+
} catch (error) {
|
|
1851
|
+
clearTimeout(timeoutId);
|
|
1852
|
+
throw error;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
/**
|
|
1857
|
+
* Get refund request by ID
|
|
1858
|
+
*
|
|
1859
|
+
* @param refundId - Refund request ID
|
|
1860
|
+
* @returns Refund request details
|
|
1861
|
+
*/
|
|
1862
|
+
async getRefund(refundId: string): Promise<RefundRequest> {
|
|
1863
|
+
const url = `${this.baseUrl}/refund/${encodeURIComponent(refundId)}`;
|
|
1864
|
+
|
|
1865
|
+
const controller = new AbortController();
|
|
1866
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1867
|
+
|
|
1868
|
+
try {
|
|
1869
|
+
const response = await fetch(url, {
|
|
1870
|
+
method: 'GET',
|
|
1871
|
+
headers: this.getHeaders(),
|
|
1872
|
+
signal: controller.signal,
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1875
|
+
clearTimeout(timeoutId);
|
|
1876
|
+
|
|
1877
|
+
if (!response.ok) {
|
|
1878
|
+
const errorText = await response.text();
|
|
1879
|
+
throw new Error(`Escrow API error: ${response.status} - ${errorText}`);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
return await response.json();
|
|
1883
|
+
} catch (error) {
|
|
1884
|
+
clearTimeout(timeoutId);
|
|
1885
|
+
throw error;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
/**
|
|
1890
|
+
* Open a dispute for an escrow payment
|
|
1891
|
+
*
|
|
1892
|
+
* Initiates arbitration when payer and recipient disagree.
|
|
1893
|
+
*
|
|
1894
|
+
* @param escrowId - Escrow payment ID
|
|
1895
|
+
* @param reason - Reason for dispute
|
|
1896
|
+
* @param evidence - Supporting evidence
|
|
1897
|
+
* @returns Created dispute
|
|
1898
|
+
*/
|
|
1899
|
+
async openDispute(
|
|
1900
|
+
escrowId: string,
|
|
1901
|
+
reason: string,
|
|
1902
|
+
evidence?: string
|
|
1903
|
+
): Promise<Dispute> {
|
|
1904
|
+
const url = `${this.baseUrl}/escrow/${encodeURIComponent(escrowId)}/dispute`;
|
|
1905
|
+
|
|
1906
|
+
const controller = new AbortController();
|
|
1907
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1908
|
+
|
|
1909
|
+
try {
|
|
1910
|
+
const response = await fetch(url, {
|
|
1911
|
+
method: 'POST',
|
|
1912
|
+
headers: this.getHeaders(true),
|
|
1913
|
+
body: JSON.stringify({ reason, evidence }),
|
|
1914
|
+
signal: controller.signal,
|
|
1915
|
+
});
|
|
1916
|
+
|
|
1917
|
+
clearTimeout(timeoutId);
|
|
1918
|
+
|
|
1919
|
+
if (!response.ok) {
|
|
1920
|
+
const errorText = await response.text();
|
|
1921
|
+
throw new Error(`Escrow API error: ${response.status} - ${errorText}`);
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
return await response.json();
|
|
1925
|
+
} catch (error) {
|
|
1926
|
+
clearTimeout(timeoutId);
|
|
1927
|
+
throw error;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
/**
|
|
1932
|
+
* Submit evidence to a dispute
|
|
1933
|
+
*
|
|
1934
|
+
* @param disputeId - Dispute ID
|
|
1935
|
+
* @param evidence - Evidence to submit
|
|
1936
|
+
* @returns Updated dispute
|
|
1937
|
+
*/
|
|
1938
|
+
async submitEvidence(disputeId: string, evidence: string): Promise<Dispute> {
|
|
1939
|
+
const url = `${this.baseUrl}/dispute/${encodeURIComponent(disputeId)}/evidence`;
|
|
1940
|
+
|
|
1941
|
+
const controller = new AbortController();
|
|
1942
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1943
|
+
|
|
1944
|
+
try {
|
|
1945
|
+
const response = await fetch(url, {
|
|
1946
|
+
method: 'POST',
|
|
1947
|
+
headers: this.getHeaders(true),
|
|
1948
|
+
body: JSON.stringify({ evidence }),
|
|
1949
|
+
signal: controller.signal,
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1952
|
+
clearTimeout(timeoutId);
|
|
1953
|
+
|
|
1954
|
+
if (!response.ok) {
|
|
1955
|
+
const errorText = await response.text();
|
|
1956
|
+
throw new Error(`Escrow API error: ${response.status} - ${errorText}`);
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
return await response.json();
|
|
1960
|
+
} catch (error) {
|
|
1961
|
+
clearTimeout(timeoutId);
|
|
1962
|
+
throw error;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
/**
|
|
1967
|
+
* Get dispute by ID
|
|
1968
|
+
*
|
|
1969
|
+
* @param disputeId - Dispute ID
|
|
1970
|
+
* @returns Dispute details
|
|
1971
|
+
*/
|
|
1972
|
+
async getDispute(disputeId: string): Promise<Dispute> {
|
|
1973
|
+
const url = `${this.baseUrl}/dispute/${encodeURIComponent(disputeId)}`;
|
|
1974
|
+
|
|
1975
|
+
const controller = new AbortController();
|
|
1976
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1977
|
+
|
|
1978
|
+
try {
|
|
1979
|
+
const response = await fetch(url, {
|
|
1980
|
+
method: 'GET',
|
|
1981
|
+
headers: this.getHeaders(),
|
|
1982
|
+
signal: controller.signal,
|
|
1983
|
+
});
|
|
1984
|
+
|
|
1985
|
+
clearTimeout(timeoutId);
|
|
1986
|
+
|
|
1987
|
+
if (!response.ok) {
|
|
1988
|
+
const errorText = await response.text();
|
|
1989
|
+
throw new Error(`Escrow API error: ${response.status} - ${errorText}`);
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
return await response.json();
|
|
1993
|
+
} catch (error) {
|
|
1994
|
+
clearTimeout(timeoutId);
|
|
1995
|
+
throw error;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
/**
|
|
2000
|
+
* List escrow payments (with filters)
|
|
2001
|
+
*
|
|
2002
|
+
* @param options - Filter and pagination options
|
|
2003
|
+
* @returns Paginated list of escrow payments
|
|
2004
|
+
*/
|
|
2005
|
+
async listEscrows(options: {
|
|
2006
|
+
status?: EscrowStatus;
|
|
2007
|
+
payer?: string;
|
|
2008
|
+
recipient?: string;
|
|
2009
|
+
page?: number;
|
|
2010
|
+
limit?: number;
|
|
2011
|
+
} = {}): Promise<{
|
|
2012
|
+
escrows: EscrowPayment[];
|
|
2013
|
+
total: number;
|
|
2014
|
+
page: number;
|
|
2015
|
+
limit: number;
|
|
2016
|
+
hasMore: boolean;
|
|
2017
|
+
}> {
|
|
2018
|
+
const params = new URLSearchParams();
|
|
2019
|
+
if (options.status) params.set('status', options.status);
|
|
2020
|
+
if (options.payer) params.set('payer', options.payer);
|
|
2021
|
+
if (options.recipient) params.set('recipient', options.recipient);
|
|
2022
|
+
if (options.page) params.set('page', options.page.toString());
|
|
2023
|
+
if (options.limit) params.set('limit', options.limit.toString());
|
|
2024
|
+
|
|
2025
|
+
const url = `${this.baseUrl}/escrow${params.toString() ? `?${params}` : ''}`;
|
|
2026
|
+
|
|
2027
|
+
const controller = new AbortController();
|
|
2028
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
2029
|
+
|
|
2030
|
+
try {
|
|
2031
|
+
const response = await fetch(url, {
|
|
2032
|
+
method: 'GET',
|
|
2033
|
+
headers: this.getHeaders(true),
|
|
2034
|
+
signal: controller.signal,
|
|
2035
|
+
});
|
|
2036
|
+
|
|
2037
|
+
clearTimeout(timeoutId);
|
|
2038
|
+
|
|
2039
|
+
if (!response.ok) {
|
|
2040
|
+
const errorText = await response.text();
|
|
2041
|
+
throw new Error(`Escrow API error: ${response.status} - ${errorText}`);
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
return await response.json();
|
|
2045
|
+
} catch (error) {
|
|
2046
|
+
clearTimeout(timeoutId);
|
|
2047
|
+
throw error;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
/**
|
|
2052
|
+
* Check Escrow API health
|
|
2053
|
+
*
|
|
2054
|
+
* @returns True if healthy
|
|
2055
|
+
*/
|
|
2056
|
+
async healthCheck(): Promise<boolean> {
|
|
2057
|
+
try {
|
|
2058
|
+
const response = await fetch(`${this.baseUrl}/health`, {
|
|
2059
|
+
method: 'GET',
|
|
2060
|
+
});
|
|
2061
|
+
return response.ok;
|
|
2062
|
+
} catch {
|
|
2063
|
+
return false;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// ============================================================================
|
|
2069
|
+
// ESCROW HELPER FUNCTIONS
|
|
2070
|
+
// ============================================================================
|
|
2071
|
+
|
|
2072
|
+
/**
|
|
2073
|
+
* Check if an escrow can be released
|
|
2074
|
+
*
|
|
2075
|
+
* @param escrow - Escrow payment to check
|
|
2076
|
+
* @returns True if the escrow can be released
|
|
2077
|
+
*/
|
|
2078
|
+
export function canReleaseEscrow(escrow: EscrowPayment): boolean {
|
|
2079
|
+
if (escrow.status !== 'held') {
|
|
2080
|
+
return false;
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
// Check expiration
|
|
2084
|
+
if (new Date(escrow.expiresAt) < new Date()) {
|
|
2085
|
+
return false;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// Check minimum hold time if specified
|
|
2089
|
+
if (escrow.releaseConditions?.minHoldTime) {
|
|
2090
|
+
const createdAt = new Date(escrow.createdAt);
|
|
2091
|
+
const minReleaseTime = new Date(
|
|
2092
|
+
createdAt.getTime() + escrow.releaseConditions.minHoldTime * 1000
|
|
2093
|
+
);
|
|
2094
|
+
if (new Date() < minReleaseTime) {
|
|
2095
|
+
return false;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
return true;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
/**
|
|
2103
|
+
* Check if an escrow can be refunded
|
|
2104
|
+
*
|
|
2105
|
+
* @param escrow - Escrow payment to check
|
|
2106
|
+
* @returns True if the escrow can be refunded
|
|
2107
|
+
*/
|
|
2108
|
+
export function canRefundEscrow(escrow: EscrowPayment): boolean {
|
|
2109
|
+
// Can only refund held or pending escrows
|
|
2110
|
+
return escrow.status === 'held' || escrow.status === 'pending';
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
/**
|
|
2114
|
+
* Check if an escrow is expired
|
|
2115
|
+
*
|
|
2116
|
+
* @param escrow - Escrow payment to check
|
|
2117
|
+
* @returns True if the escrow is expired
|
|
2118
|
+
*/
|
|
2119
|
+
export function isEscrowExpired(escrow: EscrowPayment): boolean {
|
|
2120
|
+
return new Date(escrow.expiresAt) < new Date();
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
/**
|
|
2124
|
+
* Calculate time remaining until escrow expires
|
|
2125
|
+
*
|
|
2126
|
+
* @param escrow - Escrow payment to check
|
|
2127
|
+
* @returns Milliseconds until expiration (negative if expired)
|
|
2128
|
+
*/
|
|
2129
|
+
export function escrowTimeRemaining(escrow: EscrowPayment): number {
|
|
2130
|
+
return new Date(escrow.expiresAt).getTime() - Date.now();
|
|
2131
|
+
}
|