@rotateprotocol/sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +453 -0
  2. package/dist/catalog.d.ts +112 -0
  3. package/dist/catalog.d.ts.map +1 -0
  4. package/dist/catalog.js +210 -0
  5. package/dist/catalog.js.map +1 -0
  6. package/dist/components/CheckoutForm.d.ts +86 -0
  7. package/dist/components/CheckoutForm.d.ts.map +1 -0
  8. package/dist/components/CheckoutForm.js +332 -0
  9. package/dist/components/CheckoutForm.js.map +1 -0
  10. package/dist/components/HostedCheckout.d.ts +57 -0
  11. package/dist/components/HostedCheckout.d.ts.map +1 -0
  12. package/dist/components/HostedCheckout.js +414 -0
  13. package/dist/components/HostedCheckout.js.map +1 -0
  14. package/dist/components/PaymentButton.d.ts +80 -0
  15. package/dist/components/PaymentButton.d.ts.map +1 -0
  16. package/dist/components/PaymentButton.js +210 -0
  17. package/dist/components/PaymentButton.js.map +1 -0
  18. package/dist/components/RotateProvider.d.ts +115 -0
  19. package/dist/components/RotateProvider.d.ts.map +1 -0
  20. package/dist/components/RotateProvider.js +264 -0
  21. package/dist/components/RotateProvider.js.map +1 -0
  22. package/dist/components/index.d.ts +17 -0
  23. package/dist/components/index.d.ts.map +1 -0
  24. package/dist/components/index.js +27 -0
  25. package/dist/components/index.js.map +1 -0
  26. package/dist/embed.d.ts +85 -0
  27. package/dist/embed.d.ts.map +1 -0
  28. package/dist/embed.js +313 -0
  29. package/dist/embed.js.map +1 -0
  30. package/dist/hooks.d.ts +156 -0
  31. package/dist/hooks.d.ts.map +1 -0
  32. package/dist/hooks.js +280 -0
  33. package/dist/hooks.js.map +1 -0
  34. package/dist/idl/rotate_connect.json +2572 -0
  35. package/dist/index.d.ts +505 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +1197 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/marketplace.d.ts +257 -0
  40. package/dist/marketplace.d.ts.map +1 -0
  41. package/dist/marketplace.js +433 -0
  42. package/dist/marketplace.js.map +1 -0
  43. package/dist/platform.d.ts +234 -0
  44. package/dist/platform.d.ts.map +1 -0
  45. package/dist/platform.js +268 -0
  46. package/dist/platform.js.map +1 -0
  47. package/dist/react.d.ts +140 -0
  48. package/dist/react.d.ts.map +1 -0
  49. package/dist/react.js +429 -0
  50. package/dist/react.js.map +1 -0
  51. package/dist/store.d.ts +213 -0
  52. package/dist/store.d.ts.map +1 -0
  53. package/dist/store.js +404 -0
  54. package/dist/store.js.map +1 -0
  55. package/dist/webhooks.d.ts +149 -0
  56. package/dist/webhooks.d.ts.map +1 -0
  57. package/dist/webhooks.js +371 -0
  58. package/dist/webhooks.js.map +1 -0
  59. package/package.json +114 -0
  60. package/src/catalog.ts +299 -0
  61. package/src/components/CheckoutForm.tsx +608 -0
  62. package/src/components/HostedCheckout.tsx +675 -0
  63. package/src/components/PaymentButton.tsx +348 -0
  64. package/src/components/RotateProvider.tsx +370 -0
  65. package/src/components/index.ts +26 -0
  66. package/src/embed.ts +408 -0
  67. package/src/hooks.ts +518 -0
  68. package/src/idl/rotate_connect.json +2572 -0
  69. package/src/index.ts +1538 -0
  70. package/src/marketplace.ts +642 -0
  71. package/src/platform.ts +403 -0
  72. package/src/react.ts +459 -0
  73. package/src/store.ts +577 -0
  74. package/src/webhooks.ts +506 -0
@@ -0,0 +1,642 @@
1
+ /**
2
+ * Rotate Marketplace — Multi-Vendor Cart & Split Payments
3
+ *
4
+ * Multi-vendor split payments for decentralized commerce.
5
+ * Manages vendors (merchants), routes payments, and splits multi-vendor carts
6
+ * into separate on-chain transactions per merchant.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const marketplace = new RotateMarketplace(sdk, {
11
+ * platformId: 1000000,
12
+ * platformFeeBps: 200, // 2% marketplace fee
13
+ * });
14
+ *
15
+ * // Register vendors
16
+ * marketplace.addVendor({ id: 'vendor-a', merchantId: 1000001, name: 'Shirt Co.' });
17
+ * marketplace.addVendor({ id: 'vendor-b', merchantId: 1000002, name: 'Hat Store' });
18
+ *
19
+ * // Add products from different vendors
20
+ * marketplace.addProduct({ id: 'shirt-1', vendorId: 'vendor-a', name: 'Logo Tee', price: 29.99 });
21
+ * marketplace.addProduct({ id: 'hat-1', vendorId: 'vendor-b', name: 'Snapback', price: 24.99 });
22
+ *
23
+ * // Customer shops from multiple vendors
24
+ * const cart = marketplace.createCart();
25
+ * cart.addItem('shirt-1', 2);
26
+ * cart.addItem('hat-1', 1);
27
+ *
28
+ * const result = await cart.checkout();
29
+ * // Creates separate payment links per vendor:
30
+ * // vendor-a: $59.98 (2x shirts)
31
+ * // vendor-b: $24.99 (1x hat)
32
+ * ```
33
+ *
34
+ * @packageDocumentation
35
+ */
36
+
37
+ import RotateSDK, { calculateFees } from './index';
38
+ import { Product, LineItem, Currency } from './store';
39
+ import { Catalog } from './catalog';
40
+
41
+ // ==================== TYPES ====================
42
+
43
+ /** A vendor (merchant) in the marketplace */
44
+ export interface Vendor {
45
+ /** Your internal vendor ID */
46
+ id: string;
47
+ /** On-chain merchant ID (7-digit) */
48
+ merchantId: number;
49
+ /** Display name */
50
+ name: string;
51
+ /** Description */
52
+ description?: string;
53
+ /** Logo URL */
54
+ logo?: string;
55
+ /** Rating (0-5) */
56
+ rating?: number;
57
+ /** Whether the vendor is active */
58
+ active: boolean;
59
+ /** Arbitrary metadata */
60
+ metadata?: Record<string, string>;
61
+ }
62
+
63
+ export interface VendorInput {
64
+ id: string;
65
+ merchantId: number;
66
+ name: string;
67
+ description?: string;
68
+ logo?: string;
69
+ rating?: number;
70
+ active?: boolean;
71
+ metadata?: Record<string, string>;
72
+ }
73
+
74
+ /** A marketplace product extends the base Product with a vendorId */
75
+ export interface MarketplaceProduct extends Product {
76
+ vendorId: string;
77
+ }
78
+
79
+ export interface MarketplaceProductInput {
80
+ id: string;
81
+ vendorId: string;
82
+ name: string;
83
+ description?: string;
84
+ price: number;
85
+ compareAtPrice?: number;
86
+ category?: string;
87
+ images?: string[];
88
+ inventory?: number;
89
+ active?: boolean;
90
+ metadata?: Record<string, string>;
91
+ }
92
+
93
+ /** Line item with vendor context */
94
+ export interface MarketplaceLineItem extends LineItem {
95
+ vendorId: string;
96
+ vendorName: string;
97
+ }
98
+
99
+ /** Per-vendor subtotals in a marketplace cart */
100
+ export interface VendorSubtotal {
101
+ vendorId: string;
102
+ vendorName: string;
103
+ merchantId: number;
104
+ items: Array<{
105
+ productId: string;
106
+ name: string;
107
+ quantity: number;
108
+ unitPrice: number;
109
+ lineTotal: number;
110
+ }>;
111
+ subtotal: number;
112
+ fees: number;
113
+ total: number;
114
+ merchantReceives: number;
115
+ }
116
+
117
+ /** Full marketplace cart totals */
118
+ export interface MarketplaceCartTotals {
119
+ /** Total unique items */
120
+ itemCount: number;
121
+ /** Total quantity */
122
+ totalQuantity: number;
123
+ /** Combined subtotal */
124
+ subtotal: number;
125
+ /** Combined fees */
126
+ fees: number;
127
+ /** Grand total */
128
+ total: number;
129
+ /** Number of vendors in this cart */
130
+ vendorCount: number;
131
+ /** Per-vendor breakdown */
132
+ vendors: VendorSubtotal[];
133
+ }
134
+
135
+ /** Result of a marketplace checkout — one payment link per vendor */
136
+ export interface MarketplaceCheckoutResult {
137
+ /** Per-vendor payment links */
138
+ vendorPayments: Array<{
139
+ vendorId: string;
140
+ vendorName: string;
141
+ merchantId: number;
142
+ linkId: number;
143
+ tx: string;
144
+ paymentUrl: string;
145
+ qrCodeUrl: string;
146
+ subtotal: number;
147
+ fees: number;
148
+ total: number;
149
+ }>;
150
+ /** Combined totals */
151
+ totals: MarketplaceCartTotals;
152
+ /** Master order reference */
153
+ orderRef: string;
154
+ /** Timestamp */
155
+ createdAt: number;
156
+ }
157
+
158
+ export interface MarketplaceConfig {
159
+ platformId: number;
160
+ /** Platform fee BPS charged by the marketplace */
161
+ platformFeeBps?: number;
162
+ /** Default currency */
163
+ currency?: Currency;
164
+ /** Allow tips */
165
+ allowTips?: boolean;
166
+ /** Default link expiration in seconds (0 = never, default: 300 = 5min) */
167
+ defaultExpiresIn?: number;
168
+ }
169
+
170
+ // ==================== ROTATE MARKETPLACE ====================
171
+
172
+ export class RotateMarketplace {
173
+ private sdk: RotateSDK;
174
+ private config: Required<MarketplaceConfig>;
175
+ private vendors: Map<string, Vendor> = new Map();
176
+ /** @internal Shared catalog logic for marketplace products */
177
+ private catalog: Catalog<MarketplaceProduct, MarketplaceProductInput>;
178
+
179
+ constructor(sdk: RotateSDK, config: MarketplaceConfig) {
180
+ this.sdk = sdk;
181
+ this.config = {
182
+ platformId: config.platformId,
183
+ platformFeeBps: config.platformFeeBps ?? 0,
184
+ currency: config.currency || 'USD',
185
+ allowTips: config.allowTips ?? false,
186
+ defaultExpiresIn: config.defaultExpiresIn ?? 300,
187
+ };
188
+ this.catalog = new Catalog<MarketplaceProduct, MarketplaceProductInput>((input) => {
189
+ // Validate vendor exists at creation time
190
+ const vendor = this.vendors.get(input.vendorId);
191
+ if (!vendor) throw new Error(`Vendor '${input.vendorId}' not found. Register the vendor first.`);
192
+ const now = Date.now();
193
+ return { ...input, active: input.active ?? true, createdAt: now, updatedAt: now };
194
+ });
195
+ }
196
+
197
+ // ==================== VENDOR MANAGEMENT ====================
198
+
199
+ /** Register a vendor */
200
+ addVendor(input: VendorInput): Vendor {
201
+ const vendor: Vendor = {
202
+ ...input,
203
+ active: input.active ?? true,
204
+ };
205
+ this.vendors.set(vendor.id, vendor);
206
+ return vendor;
207
+ }
208
+
209
+ /** Bulk register vendors */
210
+ addVendors(inputs: VendorInput[]): Vendor[] {
211
+ return inputs.map((v) => this.addVendor(v));
212
+ }
213
+
214
+ /** Update a vendor */
215
+ updateVendor(id: string, updates: Partial<VendorInput>): Vendor {
216
+ const existing = this.vendors.get(id);
217
+ if (!existing) throw new Error(`Vendor '${id}' not found`);
218
+ const updated: Vendor = { ...existing, ...updates, id: existing.id };
219
+ this.vendors.set(id, updated);
220
+ return updated;
221
+ }
222
+
223
+ /** Get a vendor */
224
+ getVendor(id: string): Vendor | undefined {
225
+ return this.vendors.get(id);
226
+ }
227
+
228
+ /** List all vendors */
229
+ getVendors(filter?: { active?: boolean; search?: string }): Vendor[] {
230
+ let results = Array.from(this.vendors.values());
231
+ if (filter?.active !== undefined) {
232
+ results = results.filter((v) => v.active === filter.active);
233
+ }
234
+ if (filter?.search) {
235
+ const q = filter.search.toLowerCase();
236
+ results = results.filter(
237
+ (v) => v.name.toLowerCase().includes(q) || v.description?.toLowerCase().includes(q)
238
+ );
239
+ }
240
+ return results;
241
+ }
242
+
243
+ /** Remove a vendor and all their products */
244
+ removeVendor(id: string): boolean {
245
+ // Remove all products from this vendor
246
+ this.catalog.removeProductsWhere((p) => p.vendorId === id);
247
+ return this.vendors.delete(id);
248
+ }
249
+
250
+ get vendorCount(): number {
251
+ return this.vendors.size;
252
+ }
253
+
254
+ // ==================== PRODUCT CATALOG (delegated to Catalog) ====================
255
+
256
+ addProduct(input: MarketplaceProductInput): MarketplaceProduct { return this.catalog.addProduct(input); }
257
+ addProducts(inputs: MarketplaceProductInput[]): MarketplaceProduct[] { return this.catalog.addProducts(inputs); }
258
+
259
+ updateProduct(id: string, updates: Partial<MarketplaceProductInput>): MarketplaceProduct {
260
+ // Ensure vendorId remains immutable
261
+ const existing = this.catalog.getProduct(id);
262
+ if (!existing) throw new Error(`Product '${id}' not found`);
263
+ const { vendorId: _ignored, ...safeUpdates } = updates;
264
+ return this.catalog.updateProduct(id, { ...safeUpdates, vendorId: existing.vendorId } as Partial<MarketplaceProductInput>);
265
+ }
266
+
267
+ removeProduct(id: string): boolean { return this.catalog.removeProduct(id); }
268
+ getProduct(id: string): MarketplaceProduct | undefined { return this.catalog.getProduct(id); }
269
+
270
+ /** List products with filters (includes vendorId filter on top of base catalog) */
271
+ getProducts(filter?: {
272
+ vendorId?: string;
273
+ category?: string; active?: boolean; search?: string;
274
+ minPrice?: number; maxPrice?: number;
275
+ sortBy?: 'name' | 'price' | 'createdAt' | 'rating';
276
+ sortOrder?: 'asc' | 'desc';
277
+ limit?: number; offset?: number;
278
+ }): MarketplaceProduct[] {
279
+ if (filter?.vendorId) {
280
+ // When filtering by vendor, strip limit/offset from the catalog query
281
+ // so pagination applies AFTER the vendor filter. Without this, the
282
+ // catalog returns `limit` products of *any* vendor, and the subsequent
283
+ // vendorId filter would silently shrink the result set.
284
+ const { vendorId, limit, offset, ...catalogFilter } = filter;
285
+ let results = this.catalog.getProducts(catalogFilter);
286
+ results = results.filter((p) => p.vendorId === vendorId);
287
+ if (offset) results = results.slice(offset);
288
+ if (limit) results = results.slice(0, limit);
289
+ return results;
290
+ }
291
+ return this.catalog.getProducts(filter);
292
+ }
293
+
294
+ getVendorProducts(vendorId: string): MarketplaceProduct[] {
295
+ return this.getProducts({ vendorId, active: true });
296
+ }
297
+
298
+ getCategories(): string[] { return this.catalog.getCategories(); }
299
+ get productCount(): number { return this.catalog.productCount; }
300
+
301
+ // ==================== INVENTORY (delegated) ====================
302
+
303
+ isInStock(productId: string, quantity: number = 1): boolean { return this.catalog.isInStock(productId, quantity); }
304
+ reserveInventory(productId: string, quantity: number): void { this.catalog.reserveInventory(productId, quantity); }
305
+ releaseInventory(productId: string, quantity: number): void { this.catalog.releaseInventory(productId, quantity); }
306
+
307
+ // ==================== MARKETPLACE CART ====================
308
+
309
+ /** Create a multi-vendor cart */
310
+ createCart(): MarketplaceCart {
311
+ return new MarketplaceCart(this, this.sdk, this.config);
312
+ }
313
+
314
+ // ==================== SERIALIZATION ====================
315
+
316
+ exportCatalog(): { vendors: Vendor[]; products: MarketplaceProduct[] } {
317
+ return {
318
+ vendors: Array.from(this.vendors.values()),
319
+ products: this.catalog.exportProducts(),
320
+ };
321
+ }
322
+
323
+ importCatalog(data: { vendors: Vendor[]; products: MarketplaceProduct[] }): void {
324
+ this.vendors.clear();
325
+ for (const v of data.vendors) this.vendors.set(v.id, v);
326
+ this.catalog.importProducts(data.products);
327
+ }
328
+ }
329
+
330
+ // ==================== MARKETPLACE CART ====================
331
+
332
+ export class MarketplaceCart {
333
+ private marketplace: RotateMarketplace;
334
+ private sdk: RotateSDK;
335
+ private config: Required<MarketplaceConfig>;
336
+ private items: Map<string, { product: MarketplaceProduct; quantity: number }> = new Map();
337
+ private _metadata: Record<string, string> = {};
338
+
339
+ /** @internal */
340
+ constructor(marketplace: RotateMarketplace, sdk: RotateSDK, config: Required<MarketplaceConfig>) {
341
+ this.marketplace = marketplace;
342
+ this.sdk = sdk;
343
+ this.config = config;
344
+ }
345
+
346
+ // ==================== LINE ITEMS ====================
347
+
348
+ addItem(productId: string, quantity: number = 1): void {
349
+ if (quantity <= 0) throw new Error('Quantity must be positive');
350
+
351
+ const product = this.marketplace.getProduct(productId);
352
+ if (!product) throw new Error(`Product '${productId}' not found`);
353
+ if (!product.active) throw new Error(`Product '${productId}' is not active`);
354
+
355
+ const existing = this.items.get(productId);
356
+ const newQty = (existing?.quantity || 0) + quantity;
357
+
358
+ if (!this.marketplace.isInStock(productId, newQty)) {
359
+ throw new Error(`Insufficient stock for '${productId}'`);
360
+ }
361
+
362
+ this.items.set(productId, { product, quantity: newQty });
363
+ }
364
+
365
+ setItemQuantity(productId: string, quantity: number): void {
366
+ if (quantity <= 0) {
367
+ this.items.delete(productId);
368
+ return;
369
+ }
370
+
371
+ const product = this.marketplace.getProduct(productId);
372
+ if (!product) throw new Error(`Product '${productId}' not found`);
373
+
374
+ if (!this.marketplace.isInStock(productId, quantity)) {
375
+ throw new Error(`Insufficient stock for '${productId}'`);
376
+ }
377
+
378
+ this.items.set(productId, { product, quantity });
379
+ }
380
+
381
+ removeItem(productId: string): boolean {
382
+ return this.items.delete(productId);
383
+ }
384
+
385
+ clear(): void {
386
+ this.items.clear();
387
+ this._metadata = {};
388
+ }
389
+
390
+ getItems(): MarketplaceLineItem[] {
391
+ const result: MarketplaceLineItem[] = [];
392
+ for (const { product, quantity } of this.items.values()) {
393
+ const vendor = this.marketplace.getVendor(product.vendorId);
394
+ result.push({
395
+ product,
396
+ quantity,
397
+ unitPrice: product.price,
398
+ vendorId: product.vendorId,
399
+ vendorName: vendor?.name || product.vendorId,
400
+ });
401
+ }
402
+ return result;
403
+ }
404
+
405
+ get isEmpty(): boolean {
406
+ return this.items.size === 0;
407
+ }
408
+
409
+ setMetadata(key: string, value: string): void {
410
+ this._metadata[key] = value;
411
+ }
412
+
413
+ // ==================== TOTALS ====================
414
+
415
+ /** Group items by vendor and calculate per-vendor + combined totals */
416
+ getTotals(): MarketplaceCartTotals {
417
+ // Group by vendor
418
+ const vendorGroups: Map<string, { vendor: Vendor; items: Array<{ product: MarketplaceProduct; quantity: number }> }> = new Map();
419
+
420
+ for (const { product, quantity } of this.items.values()) {
421
+ const vendor = this.marketplace.getVendor(product.vendorId);
422
+ if (!vendor) continue;
423
+
424
+ let group = vendorGroups.get(product.vendorId);
425
+ if (!group) {
426
+ group = { vendor, items: [] };
427
+ vendorGroups.set(product.vendorId, group);
428
+ }
429
+ group.items.push({ product, quantity });
430
+ }
431
+
432
+ const vendorSubtotals: VendorSubtotal[] = [];
433
+ let grandSubtotal = 0;
434
+ let grandFees = 0;
435
+ let grandTotal = 0;
436
+ let totalItems = 0;
437
+ let totalQuantity = 0;
438
+
439
+ for (const [vendorId, group] of vendorGroups.entries()) {
440
+ const items: VendorSubtotal['items'] = [];
441
+ let vendorSubtotal = 0;
442
+
443
+ for (const { product, quantity } of group.items) {
444
+ const lineTotal = Math.round(product.price * quantity * 100) / 100;
445
+ vendorSubtotal += lineTotal;
446
+ totalQuantity += quantity;
447
+ items.push({
448
+ productId: product.id,
449
+ name: product.name,
450
+ quantity,
451
+ unitPrice: product.price,
452
+ lineTotal,
453
+ });
454
+ }
455
+
456
+ vendorSubtotal = Math.round(vendorSubtotal * 100) / 100;
457
+
458
+
459
+ // Calculate fees
460
+ const amountMicro = Math.floor(vendorSubtotal * 1_000_000);
461
+ const feesResult = calculateFees(amountMicro, this.config.platformFeeBps);
462
+ const fees = Math.round(feesResult.buyerFeeShare / 1_000_000 * 100) / 100;
463
+ const vendorTotal = Math.round((vendorSubtotal + fees) * 100) / 100;
464
+ const merchantReceives = Math.round(feesResult.merchantReceives / 1_000_000 * 100) / 100;
465
+
466
+ totalItems += items.length;
467
+
468
+ vendorSubtotals.push({
469
+ vendorId,
470
+ vendorName: group.vendor.name,
471
+ merchantId: group.vendor.merchantId,
472
+ items,
473
+ subtotal: vendorSubtotal,
474
+ fees,
475
+ total: vendorTotal,
476
+ merchantReceives,
477
+ });
478
+
479
+ grandSubtotal += vendorSubtotal;
480
+ grandFees += fees;
481
+ grandTotal += vendorTotal;
482
+ }
483
+
484
+ return {
485
+ itemCount: totalItems,
486
+ totalQuantity,
487
+ subtotal: Math.round(grandSubtotal * 100) / 100,
488
+ fees: Math.round(grandFees * 100) / 100,
489
+ total: Math.round(grandTotal * 100) / 100,
490
+ vendorCount: vendorGroups.size,
491
+ vendors: vendorSubtotals,
492
+ };
493
+ }
494
+
495
+ // ==================== CHECKOUT ====================
496
+
497
+ /**
498
+ * Checkout the marketplace cart.
499
+ * Creates one on-chain payment link **per vendor** so each merchant
500
+ * receives funds directly into their own wallet — true decentralized
501
+ * marketplace payments with no intermediary holding funds.
502
+ */
503
+ async checkout(options?: {
504
+ expiresIn?: number;
505
+ currency?: Currency;
506
+ allowTips?: boolean;
507
+ orderRef?: string;
508
+ }): Promise<MarketplaceCheckoutResult> {
509
+ if (this.isEmpty) throw new Error('Cart is empty');
510
+
511
+ const totals = this.getTotals();
512
+ const masterOrderRef = options?.orderRef || `mkt_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
513
+ const currency = options?.currency || this.config.currency;
514
+ const expiresAt = (options?.expiresIn ?? this.config.defaultExpiresIn) > 0
515
+ ? Math.floor(Date.now() / 1000) + (options?.expiresIn ?? this.config.defaultExpiresIn)
516
+ : 0;
517
+ const allowTips = options?.allowTips ?? this.config.allowTips;
518
+
519
+ // Validate stock
520
+ for (const { product, quantity } of this.items.values()) {
521
+ if (!this.marketplace.isInStock(product.id, quantity)) {
522
+ throw new Error(`'${product.name}' is out of stock`);
523
+ }
524
+ }
525
+
526
+ // Create one payment link per vendor
527
+ const vendorPayments: MarketplaceCheckoutResult['vendorPayments'] = [];
528
+
529
+ for (const vendorTotals of totals.vendors) {
530
+ const vendorOrderRef = `${masterOrderRef}_v${vendorTotals.vendorId}`;
531
+ const amountMicro = BigInt(Math.floor(vendorTotals.subtotal * 1_000_000));
532
+
533
+ // Auto-generate description from vendor items
534
+ const description = vendorTotals.items.map(i => `${i.quantity}x ${i.name}`).join(', ');
535
+
536
+ let result: { tx: string; linkId: number };
537
+
538
+ if (currency === 'SOL') {
539
+ const solPrice = await this.sdk.getSolPrice();
540
+ const lamports = BigInt(Math.floor((vendorTotals.subtotal / solPrice) * 1_000_000_000));
541
+ result = await this.sdk.createLinkSol({
542
+ merchantId: vendorTotals.merchantId,
543
+ platformId: this.config.platformId,
544
+ amount: lamports,
545
+ expiresAt,
546
+ allowTips,
547
+ allowPartial: false,
548
+ orderRef: vendorOrderRef,
549
+ description,
550
+ });
551
+ } else if (currency === 'USDC' || currency === 'USDT') {
552
+ result = await this.sdk.createLinkToken({
553
+ merchantId: vendorTotals.merchantId,
554
+ platformId: this.config.platformId,
555
+ amount: amountMicro,
556
+ expiresAt,
557
+ allowTips,
558
+ allowPartial: false,
559
+ orderRef: vendorOrderRef,
560
+ currency,
561
+ description,
562
+ });
563
+ } else {
564
+ result = await this.sdk.createLinkUsd({
565
+ merchantId: vendorTotals.merchantId,
566
+ platformId: this.config.platformId,
567
+ amount: amountMicro,
568
+ expiresAt,
569
+ allowTips,
570
+ allowPartial: false,
571
+ orderRef: vendorOrderRef,
572
+ description,
573
+ });
574
+ }
575
+
576
+ vendorPayments.push({
577
+ vendorId: vendorTotals.vendorId,
578
+ vendorName: vendorTotals.vendorName,
579
+ merchantId: vendorTotals.merchantId,
580
+ linkId: result.linkId,
581
+ tx: result.tx,
582
+ paymentUrl: this.sdk.getPaymentUrl(result.linkId),
583
+ qrCodeUrl: this.sdk.getQRCodeUrl(result.linkId),
584
+ subtotal: vendorTotals.subtotal,
585
+ fees: vendorTotals.fees,
586
+ total: vendorTotals.total,
587
+ });
588
+ }
589
+
590
+ // Reserve inventory
591
+ for (const { product, quantity } of this.items.values()) {
592
+ this.marketplace.reserveInventory(product.id, quantity);
593
+ }
594
+
595
+ return {
596
+ vendorPayments,
597
+ totals,
598
+ orderRef: masterOrderRef,
599
+ createdAt: Date.now(),
600
+ };
601
+ }
602
+
603
+ /**
604
+ * Wait for ALL vendor payments to complete.
605
+ * Returns once every link is paid or the timeout is reached.
606
+ */
607
+ async waitForAllPayments(
608
+ vendorPayments: MarketplaceCheckoutResult['vendorPayments'],
609
+ timeoutMs: number = 600000
610
+ ): Promise<{ allPaid: boolean; results: Array<{ vendorId: string; linkId: number; paid: boolean }> }> {
611
+ const startTime = Date.now();
612
+ const results = vendorPayments.map((vp) => ({
613
+ vendorId: vp.vendorId,
614
+ linkId: vp.linkId,
615
+ paid: false,
616
+ }));
617
+
618
+ while (Date.now() - startTime < timeoutMs) {
619
+ let allPaid = true;
620
+
621
+ for (const r of results) {
622
+ if (r.paid) continue;
623
+ const paid = await this.sdk.isLinkPaid(r.linkId);
624
+ if (paid) {
625
+ r.paid = true;
626
+ } else {
627
+ allPaid = false;
628
+ }
629
+ }
630
+
631
+ if (allPaid) return { allPaid: true, results };
632
+
633
+ await new Promise((resolve) => setTimeout(resolve, 3000));
634
+ }
635
+
636
+ return { allPaid: false, results };
637
+ }
638
+ }
639
+
640
+ // ==================== EXPORTS ====================
641
+
642
+ export default RotateMarketplace;