uvd-x402-sdk 2.5.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.
Files changed (75) hide show
  1. package/README.md +380 -3
  2. package/dist/adapters/index.d.mts +1 -1
  3. package/dist/adapters/index.d.ts +1 -1
  4. package/dist/adapters/index.js +82 -1
  5. package/dist/adapters/index.js.map +1 -1
  6. package/dist/adapters/index.mjs +82 -1
  7. package/dist/adapters/index.mjs.map +1 -1
  8. package/dist/backend/index.d.mts +1036 -0
  9. package/dist/backend/index.d.ts +1036 -0
  10. package/dist/backend/index.js +1722 -0
  11. package/dist/backend/index.js.map +1 -0
  12. package/dist/backend/index.mjs +1704 -0
  13. package/dist/backend/index.mjs.map +1 -0
  14. package/dist/{index-BrFeSWKm.d.mts → index-C60c_e5z.d.mts} +13 -4
  15. package/dist/{index-DR2vXt-c.d.mts → index-D-dO_FoP.d.mts} +70 -4
  16. package/dist/{index-DR2vXt-c.d.ts → index-D-dO_FoP.d.ts} +70 -4
  17. package/dist/{index-BYX9BU79.d.ts → index-VIOUicmO.d.ts} +13 -4
  18. package/dist/index.d.mts +3 -3
  19. package/dist/index.d.ts +3 -3
  20. package/dist/index.js +115 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +110 -2
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/providers/algorand/index.d.mts +86 -0
  25. package/dist/providers/algorand/index.d.ts +86 -0
  26. package/dist/providers/algorand/index.js +903 -0
  27. package/dist/providers/algorand/index.js.map +1 -0
  28. package/dist/providers/algorand/index.mjs +898 -0
  29. package/dist/providers/algorand/index.mjs.map +1 -0
  30. package/dist/providers/evm/index.d.mts +1 -1
  31. package/dist/providers/evm/index.d.ts +1 -1
  32. package/dist/providers/evm/index.js +78 -1
  33. package/dist/providers/evm/index.js.map +1 -1
  34. package/dist/providers/evm/index.mjs +78 -1
  35. package/dist/providers/evm/index.mjs.map +1 -1
  36. package/dist/providers/near/index.d.mts +1 -1
  37. package/dist/providers/near/index.d.ts +1 -1
  38. package/dist/providers/near/index.js +78 -1
  39. package/dist/providers/near/index.js.map +1 -1
  40. package/dist/providers/near/index.mjs +78 -1
  41. package/dist/providers/near/index.mjs.map +1 -1
  42. package/dist/providers/solana/index.d.mts +1 -1
  43. package/dist/providers/solana/index.d.ts +1 -1
  44. package/dist/providers/solana/index.js +78 -1
  45. package/dist/providers/solana/index.js.map +1 -1
  46. package/dist/providers/solana/index.mjs +78 -1
  47. package/dist/providers/solana/index.mjs.map +1 -1
  48. package/dist/providers/stellar/index.d.mts +1 -1
  49. package/dist/providers/stellar/index.d.ts +1 -1
  50. package/dist/providers/stellar/index.js +78 -1
  51. package/dist/providers/stellar/index.js.map +1 -1
  52. package/dist/providers/stellar/index.mjs +78 -1
  53. package/dist/providers/stellar/index.mjs.map +1 -1
  54. package/dist/react/index.d.mts +3 -3
  55. package/dist/react/index.d.ts +3 -3
  56. package/dist/react/index.js +82 -1
  57. package/dist/react/index.js.map +1 -1
  58. package/dist/react/index.mjs +82 -1
  59. package/dist/react/index.mjs.map +1 -1
  60. package/dist/utils/index.d.mts +57 -5
  61. package/dist/utils/index.d.ts +57 -5
  62. package/dist/utils/index.js +96 -1
  63. package/dist/utils/index.js.map +1 -1
  64. package/dist/utils/index.mjs +93 -2
  65. package/dist/utils/index.mjs.map +1 -1
  66. package/package.json +24 -3
  67. package/src/adapters/wagmi.ts +4 -0
  68. package/src/backend/index.ts +2131 -0
  69. package/src/chains/index.ts +94 -2
  70. package/src/client/X402Client.ts +4 -0
  71. package/src/index.ts +26 -1
  72. package/src/providers/algorand/index.ts +356 -0
  73. package/src/types/index.ts +78 -3
  74. package/src/utils/index.ts +4 -0
  75. package/src/utils/validation.ts +76 -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
+ }