@temple-digital-group/temple-canton-js 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -109,6 +109,7 @@ const result = await deposit(10, "CC");
109
109
  ```
110
110
 
111
111
  `deposit()` requires the wallet adapter to be connected. It:
112
+
112
113
  1. Checks your CC balance to ensure at least 10 CC is reserved for transaction fees
113
114
  2. For utility deposits (USDCx, CBTC), verifies you have enough of the token **and** 10 CC for fees
114
115
  3. Selects the right UTXOs from your wallet
@@ -301,10 +302,10 @@ These require an explicit subscribe message. The SDK handles this automatically.
301
302
 
302
303
  | Function | Channel | Example |
303
304
  | ------------------------------------------- | -------------------------------- | ------------------------- |
304
- | `subscribeOrderbook(symbol, cb)` | `orderbook:{symbol}` | `orderbook:Amulet/USDCx` |
305
- | `subscribeTrades(symbol, cb)` | `trades:{symbol}` | `trades:Amulet/USDCx` |
306
- | `subscribeTicker(symbol, cb)` | `ticker:{symbol}` | `ticker:CBTC/USDCx` |
307
- | `subscribeCandles(symbol, granularity, cb)` | `candles:{symbol}:{granularity}` | `candles:Amulet/USDCx:60`|
305
+ | `subscribeOrderbook(symbol, cb)` | `orderbook:{symbol}` | `orderbook:Amulet/USDCx` |
306
+ | `subscribeTrades(symbol, cb)` | `trades:{symbol}` | `trades:Amulet/USDCx` |
307
+ | `subscribeTicker(symbol, cb)` | `ticker:{symbol}` | `ticker:CBTC/USDCx` |
308
+ | `subscribeCandles(symbol, granularity, cb)` | `candles:{symbol}:{granularity}` | `candles:Amulet/USDCx:60` |
308
309
  | `subscribeOracle(symbol, cb)` | `oracle:{symbol}` | `oracle:cc` |
309
310
  | `subscribeOracleVolume(symbol, cb)` | `oracle_volume:{symbol}` | `oracle_volume:cc` |
310
311
 
@@ -314,11 +315,11 @@ These require an explicit subscribe message. The SDK handles this automatically.
314
315
 
315
316
  Pushed automatically by the server after authentication. No subscribe message is sent — you just register a local handler. Requires `API_KEY` (Node.js) or cookie auth (browser).
316
317
 
317
- | Function | Server Event | Description |
318
- | -------------------------- | -------------- | -------------------------------------------------- |
319
- | `subscribeUserOrders(cb)` | `user_order` | Order lifecycle updates (created, filled, cancelled)|
320
- | `subscribeUserTrades(cb)` | `user_trade` | Trade fill confirmations |
321
- | `subscribeUserBalances(cb)`| `user_balance` | Balance changes |
318
+ | Function | Server Event | Description |
319
+ | --------------------------- | -------------- | ---------------------------------------------------- |
320
+ | `subscribeUserOrders(cb)` | `user_order` | Order lifecycle updates (created, filled, cancelled) |
321
+ | `subscribeUserTrades(cb)` | `user_trade` | Trade fill confirmations |
322
+ | `subscribeUserBalances(cb)` | `user_balance` | Balance changes |
322
323
 
323
324
  ### Advanced Usage
324
325
 
@@ -370,13 +371,13 @@ const unsubOrder = ws.onUserEvent("user_order", (data) => { ... });
370
371
 
371
372
  ### Deposits & Withdrawals
372
373
 
373
- | Function | Provider | Description |
374
- | ----------------------------------------- | -------- | ----------------------------------------------------- |
374
+ | Function | Provider | Description |
375
+ | ----------------------------------------- | -------- | ---------------------------------------------------------- |
375
376
  | `deposit(amount, symbol)` | **W** | Deposit funds (validates balance, reserves 10 CC for fees) |
376
- | `prepareDepositHoldings(amount, assetId)` | **W** | Resolve holdings for a deposit amount (low-level) |
377
- | `depositFunds(opts)` | **W** | Submit deposit allocation (low-level) |
378
- | `withdrawFunds({ asset_id, amount })` | **W** | Withdraw available trading balance back to wallet |
379
- | `emergencyWithdrawFunds(opts)` | **W** | Cancel all orders and withdraw everything immediately |
377
+ | `prepareDepositHoldings(amount, assetId)` | **W** | Resolve holdings for a deposit amount (low-level) |
378
+ | `depositFunds(opts)` | **W** | Submit deposit allocation (low-level) |
379
+ | `withdrawFunds({ asset_id, amount })` | **W** | Withdraw available trading balance back to wallet |
380
+ | `bulkWithdrawFunds(opts)` | **W** | Cancel all orders and withdraw everything immediately |
380
381
 
381
382
  ### Holdings
382
383
 
@@ -418,13 +419,13 @@ const unsubOrder = ws.onUserEvent("user_order", (data) => { ... });
418
419
 
419
420
  #### Trading
420
421
 
421
- | Function | Description |
422
- | ----------------------------------- | ------------------------------------------------------ |
423
- | `createOrderRequest(opts)` | Place a buy/sell order via the trading backend |
424
- | `cancelOrder(orderId)` | Cancel a specific order |
425
- | `cancelAllOrders(options?)` | Cancel all orders (options: `symbol` filter) |
426
- | `getTradingBalance()` | Get user's trading balance (unlocked/locked/in-flight) |
427
- | `getActiveOrders(options?)` | Get active orders (options: `symbol`, `limit`) |
422
+ | Function | Description |
423
+ | --------------------------- | ------------------------------------------------------ |
424
+ | `createOrderRequest(opts)` | Place a buy/sell order via the trading backend |
425
+ | `cancelOrder(orderId)` | Cancel a specific order |
426
+ | `cancelAllOrders(options?)` | Cancel all orders (options: `symbol` filter) |
427
+ | `getTradingBalance()` | Get user's trading balance (unlocked/locked/in-flight) |
428
+ | `getActiveOrders(options?)` | Get active orders (options: `symbol`, `limit`) |
428
429
 
429
430
  #### Withdrawals
430
431
 
@@ -435,23 +436,20 @@ const unsubOrder = ws.onUserEvent("user_order", (data) => { ... });
435
436
 
436
437
  #### Disclosures & Delegation
437
438
 
438
- | Function | Description |
439
- | ------------------------- | ---------------------------------------------------------------------------- |
440
- | `getDisclosures(partyId)` | Get Amulet disclosure data (factory ID, choice context, disclosed contracts) |
441
- | `getDelegation()` | Get the user's delegation contract from the API |
439
+ | Function | Description |
440
+ | ------------------ | ---------------------------------------------------------------------------- |
441
+ | `getDisclosures()` | Get Amulet disclosure data (factory ID, choice context, disclosed contracts) |
442
+ | `getDelegation()` | Get the user's delegation contract from the API |
442
443
 
443
444
  ### WebSocket
444
445
 
445
- | Function | Description |
446
- | ------------------------------------------- | -------------------------------------------------------------- |
447
- | `createWebSocket()` | Get or create the shared WS instance (auto-connects) |
448
- | `disconnectWebSocket()` | Disconnect and destroy the shared WS instance |
449
- | `subscribeOrderbook(symbol, cb)` | Subscribe to orderbook updates |
450
- | `subscribeTrades(symbol, cb)` | Subscribe to trade updates |
451
- | `subscribeTicker(symbol, cb)` | Subscribe to ticker updates |
452
- | `subscribeCandles(symbol, granularity, cb)` | Subscribe to candle updates |
453
- | `subscribeOracle(symbol, cb)` | Subscribe to oracle price updates |
454
- | `subscribeOracleVolume(symbol, cb)` | Subscribe to oracle volume updates |
455
- | `subscribeUserOrders(cb)` | Listen to user order events (auto-pushed, no subscribe needed) |
456
- | `subscribeUserTrades(cb)` | Listen to user trade events (auto-pushed, no subscribe needed) |
457
- | `subscribeUserBalances(cb)` | Listen to user balance events (auto-pushed, no subscribe needed)|
446
+ | Function | Description |
447
+ | ------------------------------------------- | ---------------------------------------------------- |
448
+ | `createWebSocket()` | Get or create the shared WS instance (auto-connects) |
449
+ | `disconnectWebSocket()` | Disconnect and destroy the shared WS instance |
450
+ | `subscribeOrderbook(symbol, cb)` | Subscribe to orderbook updates |
451
+ | `subscribeTrades(symbol, cb)` | Subscribe to trade updates |
452
+ | `subscribeTicker(symbol, cb)` | Subscribe to ticker updates |
453
+ | `subscribeCandles(symbol, granularity, cb)` | Subscribe to candle updates |
454
+ | `subscribeOracle(symbol, cb)` | Subscribe to oracle price updates |
455
+ | `subscribeOracleVolume(symbol, cb)` | Subscribe to oracle volume updates |
@@ -17,6 +17,13 @@ export interface FinalizeWithdrawOpts {
17
17
  };
18
18
  userId?: string;
19
19
  }
20
+ export interface BulkWithdrawFundsOpts {
21
+ allocationIds: string[];
22
+ sender?: string;
23
+ assetId: string;
24
+ disclosures?: FinalizeWithdrawOpts["disclosures"];
25
+ userId?: string;
26
+ }
20
27
  export interface WithdrawFundsOpts {
21
28
  asset_id: string;
22
29
  amount: string | number;
@@ -32,6 +39,19 @@ export interface WithdrawFundsOpts {
32
39
  * 3. Remote: Scan API / Registry API
33
40
  */
34
41
  export declare function finalizeWithdrawFunds(opts: FinalizeWithdrawOpts, returnCommand?: boolean): Promise<Record<string, unknown>>;
42
+ /**
43
+ * Finalize multiple withdrawals in a single signed transaction.
44
+ *
45
+ * Bundles N Allocation_Withdraw exercises into one ledger submission so the user
46
+ * signs once for the whole batch. All allocations must belong to the same sender
47
+ * and the same asset (caller's responsibility — pass one assetId).
48
+ *
49
+ * Choice-context fetching:
50
+ * - Localhost ledger / Amulet via /api/amulet/disclosures: one shared context for all N.
51
+ * - Scan API (Amulet) / Registry API (utility): one fetch per allocation, then disclosed
52
+ * contracts merged and deduped.
53
+ */
54
+ export declare function bulkWithdrawFunds(opts: BulkWithdrawFundsOpts, returnCommand?: boolean): Promise<Record<string, unknown>>;
35
55
  /**
36
56
  * High-level withdrawal flow:
37
57
  * 1. Creates a withdrawal request via the backend API.
@@ -258,6 +258,271 @@ export async function finalizeWithdrawFunds(opts, returnCommand = false) {
258
258
  return { error: msg };
259
259
  }
260
260
  }
261
+ /**
262
+ * Finalize multiple withdrawals in a single signed transaction.
263
+ *
264
+ * Bundles N Allocation_Withdraw exercises into one ledger submission so the user
265
+ * signs once for the whole batch. All allocations must belong to the same sender
266
+ * and the same asset (caller's responsibility — pass one assetId).
267
+ *
268
+ * Choice-context fetching:
269
+ * - Localhost ledger / Amulet via /api/amulet/disclosures: one shared context for all N.
270
+ * - Scan API (Amulet) / Registry API (utility): one fetch per allocation, then disclosed
271
+ * contracts merged and deduped.
272
+ */
273
+ export async function bulkWithdrawFunds(opts, returnCommand = false) {
274
+ const { allocationIds, sender: senderOpt, assetId: rawAssetId, disclosures, userId } = opts;
275
+ const assetId = normalizeAssetId(rawAssetId);
276
+ const sender = getAdapterPartyId() ?? senderOpt ?? config.VALIDATOR_USER_PARTY_ID;
277
+ if (!Array.isArray(allocationIds) || allocationIds.length === 0) {
278
+ const msg = "bulkWithdrawFunds: allocationIds must be a non-empty array";
279
+ console.error(msg);
280
+ return { error: msg };
281
+ }
282
+ if (!sender || !assetId) {
283
+ const msg = "bulkWithdrawFunds: sender and assetId are required";
284
+ console.error(msg);
285
+ return { error: msg };
286
+ }
287
+ const isAmulet = assetId === "Amulet";
288
+ const allocationCids = allocationIds.map((cid) => normalizeContractId(cid));
289
+ // Per-allocation choice context (one entry per allocation, in the same order).
290
+ // Shared-context paths fill all N entries with the same object.
291
+ let perAllocContexts = [];
292
+ let mergedDisclosedContracts = [];
293
+ if (shouldUseLedgerForMetadata()) {
294
+ // Localhost: resolve once, reuse for all allocations.
295
+ let sharedContext = {};
296
+ const sharedDisclosed = [];
297
+ if (isAmulet) {
298
+ const amuletCtx = await resolveAmuletContext({ investor: sender, holdingIds: [], transferAmount: 0 });
299
+ if (!amuletCtx) {
300
+ const msg = "bulkWithdrawFunds: failed to resolve Amulet context from ledger";
301
+ console.error(msg);
302
+ return { error: msg };
303
+ }
304
+ const ctx = amuletCtx;
305
+ const contextKeys = ctx.contextKeys;
306
+ const amuletRules = ctx.amuletRules;
307
+ const openMiningRound = ctx.openMiningRound;
308
+ const featuredAppRight = ctx.featuredAppRight;
309
+ const externalAmuletRules = ctx.externalAmuletRules;
310
+ sharedContext = {
311
+ values: {
312
+ [contextKeys.amuletRules]: { tag: "AV_ContractId", value: amuletRules.contractCid },
313
+ [contextKeys.openRound]: { tag: "AV_ContractId", value: openMiningRound.contractCid },
314
+ [contextKeys.featuredAppRight]: { tag: "AV_ContractId", value: featuredAppRight.contractCid },
315
+ [contextKeys.expireLock]: { tag: "AV_Bool", value: true },
316
+ },
317
+ };
318
+ sharedDisclosed.push({
319
+ templateId: amuletRules.templateId ?? null,
320
+ contractId: amuletRules.contractCid,
321
+ createdEventBlob: amuletRules.disclosureCid,
322
+ synchronizerId: amuletRules.synchronizerId,
323
+ }, {
324
+ templateId: openMiningRound.templateId ?? null,
325
+ contractId: openMiningRound.contractCid,
326
+ createdEventBlob: openMiningRound.disclosureCid,
327
+ synchronizerId: openMiningRound.synchronizerId,
328
+ }, {
329
+ templateId: externalAmuletRules.templateId ?? null,
330
+ contractId: externalAmuletRules.contractCid,
331
+ createdEventBlob: externalAmuletRules.disclosureCid,
332
+ synchronizerId: externalAmuletRules.synchronizerId,
333
+ });
334
+ if (featuredAppRight?.contractCid && featuredAppRight?.disclosureCid) {
335
+ sharedDisclosed.push({
336
+ templateId: featuredAppRight.templateId ?? null,
337
+ contractId: featuredAppRight.contractCid,
338
+ createdEventBlob: featuredAppRight.disclosureCid,
339
+ synchronizerId: featuredAppRight.synchronizerId,
340
+ });
341
+ }
342
+ }
343
+ else {
344
+ const instrumentDef = resolveInstrumentDefinition(assetId);
345
+ const networkDef = instrumentDef?.[config.NETWORK];
346
+ const registrar = networkDef?.registrar || getInstrumentRegistrar(assetId) || config.VALIDATOR_REGISTRAR_PARTY_ID;
347
+ const [allocFactory, instConfig] = await Promise.all([
348
+ resolveUtilityAllocationFactory(registrar, sender),
349
+ resolveUtilityInstrumentConfiguration(assetId, registrar),
350
+ ]);
351
+ if (instConfig) {
352
+ sharedContext = {
353
+ values: {
354
+ [DEFAULT_UTILITY_CONTEXT_KEYS.instrumentConfiguration]: {
355
+ tag: "AV_ContractId",
356
+ value: instConfig.contractCid,
357
+ },
358
+ [DEFAULT_UTILITY_CONTEXT_KEYS.instrumentConfigurationPrefixed]: {
359
+ tag: "AV_ContractId",
360
+ value: instConfig.contractCid,
361
+ },
362
+ [DEFAULT_UTILITY_CONTEXT_KEYS.senderCredentials]: { tag: "AV_List", value: [] },
363
+ [DEFAULT_UTILITY_CONTEXT_KEYS.senderCredentialsPrefixed]: { tag: "AV_List", value: [] },
364
+ [DEFAULT_UTILITY_CONTEXT_KEYS.receiverCredentials]: { tag: "AV_List", value: [] },
365
+ [DEFAULT_UTILITY_CONTEXT_KEYS.receiverCredentialsPrefixed]: { tag: "AV_List", value: [] },
366
+ [DEFAULT_AMULET_CONTEXT_KEYS.expireLock]: { tag: "AV_Bool", value: true },
367
+ },
368
+ };
369
+ sharedDisclosed.push({
370
+ templateId: null,
371
+ contractId: instConfig.contractCid,
372
+ createdEventBlob: instConfig.disclosureCid,
373
+ synchronizerId: instConfig.synchronizerId,
374
+ });
375
+ }
376
+ if (allocFactory) {
377
+ sharedDisclosed.push({
378
+ templateId: null,
379
+ contractId: allocFactory.contractCid,
380
+ createdEventBlob: allocFactory.disclosureCid,
381
+ synchronizerId: allocFactory.synchronizerId,
382
+ });
383
+ }
384
+ }
385
+ perAllocContexts = allocationCids.map(() => sharedContext);
386
+ mergedDisclosedContracts = sharedDisclosed;
387
+ }
388
+ else if (isAmulet && !config.VALIDATOR_SCAN_API_URL) {
389
+ // Amulet via /api/amulet/disclosures: one fetch shared across all allocations.
390
+ const factoryData = disclosures?.disclosures || disclosures || null;
391
+ let resolvedData = factoryData;
392
+ if (!resolvedData?.choiceContext) {
393
+ const disclosuresResult = (await getDisclosures(sender));
394
+ resolvedData = disclosuresResult?.disclosures;
395
+ if (disclosuresResult?.error || !resolvedData?.choiceContext) {
396
+ const detail = disclosuresResult?.message ||
397
+ disclosuresResult?.error ||
398
+ "missing choiceContext in response";
399
+ const msg = `bulkWithdrawFunds: failed to resolve Amulet disclosures: ${detail}`;
400
+ console.error(msg);
401
+ return { error: msg };
402
+ }
403
+ }
404
+ const choiceContext = resolvedData.choiceContext;
405
+ const sharedContext = choiceContext.choiceContextData || {};
406
+ const values = sharedContext.values;
407
+ if (values && !values[DEFAULT_AMULET_CONTEXT_KEYS.expireLock]) {
408
+ values[DEFAULT_AMULET_CONTEXT_KEYS.expireLock] = { tag: "AV_Bool", value: true };
409
+ }
410
+ const sharedDisclosed = (choiceContext.disclosedContracts || []).map((dc) => ({
411
+ templateId: dc.templateId,
412
+ contractId: dc.contractId,
413
+ createdEventBlob: dc.createdEventBlob,
414
+ synchronizerId: dc.synchronizerId,
415
+ }));
416
+ perAllocContexts = allocationCids.map(() => sharedContext);
417
+ mergedDisclosedContracts = sharedDisclosed;
418
+ }
419
+ else {
420
+ // Per-allocation context: Scan API (Amulet) or Registry API (utility).
421
+ let baseUrl;
422
+ let contextHeaders = {};
423
+ if (isAmulet) {
424
+ if (!config.VALIDATOR_SCAN_API_URL) {
425
+ const msg = "bulkWithdrawFunds: VALIDATOR_SCAN_API_URL is required for Amulet allocations";
426
+ console.error(msg);
427
+ return { error: msg };
428
+ }
429
+ baseUrl = `${config.VALIDATOR_SCAN_API_URL}/registry`;
430
+ contextHeaders = await buildHeaders();
431
+ }
432
+ else {
433
+ const instrumentDef = resolveInstrumentDefinition(assetId);
434
+ const networkContracts = instrumentDef?.[config.NETWORK];
435
+ const registryAPI = networkContracts?.registryAPI;
436
+ if (!registryAPI) {
437
+ const msg = `bulkWithdrawFunds: no registryAPI defined for ${assetId} on ${config.NETWORK}`;
438
+ console.error(msg);
439
+ return { error: msg };
440
+ }
441
+ baseUrl = registryAPI;
442
+ }
443
+ const requestConfig = Object.keys(contextHeaders).length > 0 ? { headers: contextHeaders } : undefined;
444
+ try {
445
+ const contextResults = await Promise.all(allocationCids.map(async (cid) => {
446
+ const url = `${baseUrl}/allocations/v1/${encodeURIComponent(cid)}/choice-contexts/withdraw`;
447
+ try {
448
+ const resp = await axios.post(url, { meta: {}, excludeDebugFields: true }, requestConfig);
449
+ return resp.data;
450
+ }
451
+ catch (error) {
452
+ const axiosErr = error;
453
+ const detail = axiosErr.response?.data ? JSON.stringify(axiosErr.response.data) : axiosErr.message;
454
+ throw new Error(`bulkWithdrawFunds: error fetching withdraw context for ${cid} from ${isAmulet ? "Scan API" : "Registry API"}: ${detail}`);
455
+ }
456
+ }));
457
+ for (const data of contextResults) {
458
+ perAllocContexts.push(data?.choiceContextData || {});
459
+ const disclosed = (data?.disclosedContracts || []).map((dc) => ({
460
+ templateId: dc.templateId,
461
+ contractId: dc.contractId,
462
+ createdEventBlob: dc.createdEventBlob,
463
+ synchronizerId: dc.synchronizerId,
464
+ }));
465
+ mergedDisclosedContracts.push(...disclosed);
466
+ }
467
+ }
468
+ catch (error) {
469
+ const msg = error.message;
470
+ console.error(msg);
471
+ return { error: msg };
472
+ }
473
+ }
474
+ // Build one ExerciseCommand per allocation; Canton signs the whole submission atomically.
475
+ const exerciseCommands = allocationCids.map((cid, idx) => ({
476
+ ExerciseCommand: {
477
+ templateId: "#splice-api-token-allocation-v1:Splice.Api.Token.AllocationV1:Allocation",
478
+ contractId: cid,
479
+ choice: "Allocation_Withdraw",
480
+ choiceArgument: {
481
+ extraArgs: {
482
+ context: perAllocContexts[idx],
483
+ meta: { values: {} },
484
+ },
485
+ },
486
+ },
487
+ }));
488
+ const command = {
489
+ commands: exerciseCommands,
490
+ commandId: randomUUID(),
491
+ userId: userId || getUserId() || config.AUTH0_USER_ID || "temple",
492
+ applicationId: "temple",
493
+ actAs: [sender],
494
+ disclosedContracts: mergedDisclosedContracts,
495
+ };
496
+ dedupeDisclosedContracts(command);
497
+ const endpoint = `${config.VALIDATOR_API_URL}/v2/commands/submit-and-wait`;
498
+ if (returnCommand) {
499
+ return { command, endpoint };
500
+ }
501
+ if (getWalletAdapter()) {
502
+ try {
503
+ await payDueGasIfAny();
504
+ return (await submitCommand(command));
505
+ }
506
+ catch (error) {
507
+ const msg = `bulkWithdrawFunds: wallet adapter submission failed: ${error.message}`;
508
+ console.error(msg);
509
+ return { error: msg };
510
+ }
511
+ }
512
+ const headers = await buildHeaders();
513
+ try {
514
+ const response = await axios.post(endpoint, command, { headers });
515
+ return response.data;
516
+ }
517
+ catch (error) {
518
+ const axiosErr = error;
519
+ const errorData = axiosErr?.response?.data;
520
+ const errorDetail = errorData ? JSON.stringify(errorData, null, 2) : axiosErr.message;
521
+ const msg = `bulkWithdrawFunds: error submitting command: ${errorDetail}`;
522
+ console.error(msg);
523
+ return { error: msg };
524
+ }
525
+ }
261
526
  /**
262
527
  * High-level withdrawal flow:
263
528
  * 1. Creates a withdrawal request via the backend API.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@temple-digital-group/temple-canton-js",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "JavaScript library for interacting with Temple Canton blockchain",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -2645,185 +2645,6 @@ export { deposit, prepareDepositHoldings, depositFunds } from "../../dist/canton
2645
2645
  // Withdrawal functions moved to src/canton/withdrawals.ts
2646
2646
  export { finalizeWithdrawFunds, withdrawFunds, withdrawDelegation } from "../../dist/canton/withdrawals.js";
2647
2647
 
2648
- // ─── Emergency Allocation Withdraw ──────────────────────────────────────────
2649
-
2650
- export async function buildAllocationWithdrawCommand(opts) {
2651
- const { disclosures, userId } = opts || {};
2652
- const assetId = normalizeAssetId(opts.assetId);
2653
- const sender = getAdapterPartyId() || opts.sender || config.VALIDATOR_USER_PARTY_ID;
2654
-
2655
- if (!opts.allocationCids || !sender || !assetId) {
2656
- return { error: "buildAllocationWithdrawCommand: allocationCids, sender, and assetId are required" };
2657
- }
2658
-
2659
- const cids = Array.isArray(opts.allocationCids) ? opts.allocationCids : [opts.allocationCids];
2660
- if (cids.length === 0) {
2661
- return { error: "buildAllocationWithdrawCommand: allocationCids must not be empty" };
2662
- }
2663
-
2664
- const isAmulet = assetId === "Amulet";
2665
-
2666
- // --- Resolve context and disclosures (once for all CIDs) ---
2667
- let choiceContextData = {};
2668
- let disclosedContracts = [];
2669
-
2670
- if (shouldUseLedgerForMetadata()) {
2671
- if (isAmulet) {
2672
- const amuletCtx = await resolveAmuletContext({ investor: sender, holdingIds: [], transferAmount: 0 });
2673
- if (!amuletCtx) {
2674
- return { error: "buildAllocationWithdrawCommand: failed to resolve Amulet context from ledger" };
2675
- }
2676
- const contextKeys = amuletCtx.contextKeys;
2677
- choiceContextData = {
2678
- values: {
2679
- [contextKeys.amuletRules]: { tag: "AV_ContractId", value: amuletCtx.amuletRules.contractCid },
2680
- [contextKeys.openRound]: { tag: "AV_ContractId", value: amuletCtx.openMiningRound.contractCid },
2681
- [contextKeys.featuredAppRight]: { tag: "AV_ContractId", value: amuletCtx.featuredAppRight.contractCid },
2682
- [contextKeys.expireLock]: { tag: "AV_Bool", value: true },
2683
- },
2684
- };
2685
- disclosedContracts = [
2686
- { templateId: amuletCtx.amuletRules.templateId || null, contractId: amuletCtx.amuletRules.contractCid, createdEventBlob: amuletCtx.amuletRules.disclosureCid, synchronizerId: amuletCtx.amuletRules.synchronizerId },
2687
- { templateId: amuletCtx.openMiningRound.templateId || null, contractId: amuletCtx.openMiningRound.contractCid, createdEventBlob: amuletCtx.openMiningRound.disclosureCid, synchronizerId: amuletCtx.openMiningRound.synchronizerId },
2688
- { templateId: amuletCtx.externalAmuletRules.templateId || null, contractId: amuletCtx.externalAmuletRules.contractCid, createdEventBlob: amuletCtx.externalAmuletRules.disclosureCid, synchronizerId: amuletCtx.externalAmuletRules.synchronizerId },
2689
- ];
2690
- if (amuletCtx.featuredAppRight?.contractCid && amuletCtx.featuredAppRight?.disclosureCid) {
2691
- disclosedContracts.push({ templateId: amuletCtx.featuredAppRight.templateId || null, contractId: amuletCtx.featuredAppRight.contractCid, createdEventBlob: amuletCtx.featuredAppRight.disclosureCid, synchronizerId: amuletCtx.featuredAppRight.synchronizerId });
2692
- }
2693
- } else {
2694
- const instrumentDef = resolveInstrumentDefinition(assetId);
2695
- const networkDef = instrumentDef?.[config.NETWORK];
2696
- const registrar = networkDef?.registrar || getInstrumentRegistrar(assetId) || config.VALIDATOR_REGISTRAR_PARTY_ID;
2697
- const [allocFactory, instConfig] = await Promise.all([
2698
- resolveUtilityAllocationFactory(registrar, sender),
2699
- resolveUtilityInstrumentConfiguration(assetId, registrar),
2700
- ]);
2701
- if (instConfig) {
2702
- choiceContextData = {
2703
- values: {
2704
- [DEFAULT_UTILITY_CONTEXT_KEYS.instrumentConfiguration]: { tag: "AV_ContractId", value: instConfig.contractCid },
2705
- [DEFAULT_UTILITY_CONTEXT_KEYS.instrumentConfigurationPrefixed]: { tag: "AV_ContractId", value: instConfig.contractCid },
2706
- [DEFAULT_UTILITY_CONTEXT_KEYS.senderCredentials]: { tag: "AV_List", value: [] },
2707
- [DEFAULT_UTILITY_CONTEXT_KEYS.senderCredentialsPrefixed]: { tag: "AV_List", value: [] },
2708
- [DEFAULT_UTILITY_CONTEXT_KEYS.receiverCredentials]: { tag: "AV_List", value: [] },
2709
- [DEFAULT_UTILITY_CONTEXT_KEYS.receiverCredentialsPrefixed]: { tag: "AV_List", value: [] },
2710
- [DEFAULT_AMULET_CONTEXT_KEYS.expireLock]: { tag: "AV_Bool", value: true },
2711
- },
2712
- };
2713
- disclosedContracts.push({ templateId: null, contractId: instConfig.contractCid, createdEventBlob: instConfig.disclosureCid, synchronizerId: instConfig.synchronizerId });
2714
- }
2715
- if (allocFactory) {
2716
- disclosedContracts.push({ templateId: null, contractId: allocFactory.contractCid, createdEventBlob: allocFactory.disclosureCid, synchronizerId: allocFactory.synchronizerId });
2717
- }
2718
- }
2719
- } else if (isAmulet) {
2720
- // Resolve via disclosures API
2721
- let resolvedData = disclosures?.disclosures || disclosures || null;
2722
- if (!resolvedData?.choiceContext) {
2723
- const disclosuresResult = await getDisclosures(sender);
2724
- resolvedData = disclosuresResult?.disclosures;
2725
- if (disclosuresResult?.error || !resolvedData?.choiceContext) {
2726
- return { error: `buildAllocationWithdrawCommand: failed to resolve Amulet disclosures: ${disclosuresResult?.message || disclosuresResult?.error || "missing choiceContext"}` };
2727
- }
2728
- }
2729
- choiceContextData = resolvedData.choiceContext.choiceContextData || {};
2730
- const values = choiceContextData.values;
2731
- if (values && !values[DEFAULT_AMULET_CONTEXT_KEYS.expireLock]) {
2732
- values[DEFAULT_AMULET_CONTEXT_KEYS.expireLock] = { tag: "AV_Bool", value: true };
2733
- }
2734
- disclosedContracts = (resolvedData.choiceContext.disclosedContracts || []).map(dc => ({
2735
- templateId: dc.templateId, contractId: dc.contractId, createdEventBlob: dc.createdEventBlob, synchronizerId: dc.synchronizerId,
2736
- }));
2737
- } else {
2738
- // Utility: build context from catalog data (no ledger/validator access needed)
2739
- const instrumentDef = resolveInstrumentDefinition(assetId);
2740
- const networkContracts = instrumentDef?.[config.NETWORK];
2741
- if (!networkContracts) {
2742
- return { error: `buildAllocationWithdrawCommand: no ${config.NETWORK} config for ${assetId} in instrument catalog` };
2743
- }
2744
- const instConfig = networkContracts.instrumentConfiguration;
2745
- const allocFactory = networkContracts.allocationFactory;
2746
- const synchronizerId = networkContracts.synchronizerId;
2747
-
2748
- if (!instConfig?.contractId || !instConfig?.eventBlob) {
2749
- return { error: `buildAllocationWithdrawCommand: instrumentConfiguration missing for ${assetId} on ${config.NETWORK}` };
2750
- }
2751
-
2752
- choiceContextData = {
2753
- values: {
2754
- [DEFAULT_UTILITY_CONTEXT_KEYS.instrumentConfiguration]: { tag: "AV_ContractId", value: instConfig.contractId },
2755
- [DEFAULT_UTILITY_CONTEXT_KEYS.instrumentConfigurationPrefixed]: { tag: "AV_ContractId", value: instConfig.contractId },
2756
- [DEFAULT_UTILITY_CONTEXT_KEYS.senderCredentials]: { tag: "AV_List", value: [] },
2757
- [DEFAULT_UTILITY_CONTEXT_KEYS.senderCredentialsPrefixed]: { tag: "AV_List", value: [] },
2758
- [DEFAULT_UTILITY_CONTEXT_KEYS.receiverCredentials]: { tag: "AV_List", value: [] },
2759
- [DEFAULT_UTILITY_CONTEXT_KEYS.receiverCredentialsPrefixed]: { tag: "AV_List", value: [] },
2760
- [DEFAULT_AMULET_CONTEXT_KEYS.expireLock]: { tag: "AV_Bool", value: true },
2761
- },
2762
- };
2763
- disclosedContracts.push({
2764
- templateId: instConfig.templateId || null,
2765
- contractId: instConfig.contractId,
2766
- createdEventBlob: instConfig.eventBlob,
2767
- synchronizerId: synchronizerId,
2768
- });
2769
- if (allocFactory?.contractId && allocFactory?.eventBlob) {
2770
- disclosedContracts.push({
2771
- templateId: allocFactory.templateId || null,
2772
- contractId: allocFactory.contractId,
2773
- createdEventBlob: allocFactory.eventBlob,
2774
- synchronizerId: synchronizerId,
2775
- });
2776
- }
2777
- }
2778
-
2779
- // --- Build one command per allocation CID ---
2780
- const endpoint = `${config.VALIDATOR_API_URL}/v2/commands/submit-and-wait`;
2781
- const commands = cids.map(cid => {
2782
- const allocationCid = normalizeContractId(cid);
2783
- const command = {
2784
- commands: [{
2785
- ExerciseCommand: {
2786
- templateId: "#splice-api-token-allocation-v1:Splice.Api.Token.AllocationV1:Allocation",
2787
- contractId: allocationCid,
2788
- choice: "Allocation_Withdraw",
2789
- choiceArgument: {
2790
- extraArgs: {
2791
- context: choiceContextData,
2792
- meta: { values: {} },
2793
- },
2794
- },
2795
- },
2796
- }],
2797
- commandId: randomUUID(),
2798
- userId: userId || getUserId() || config.AUTH0_USER_ID || "temple",
2799
- applicationId: "temple",
2800
- actAs: [sender],
2801
- disclosedContracts: [...disclosedContracts],
2802
- };
2803
- dedupeDisclosedContracts(command);
2804
- return { command, endpoint };
2805
- });
2806
-
2807
- // Auto-submit via wallet adapter if submit option is true
2808
- if (opts.submit && getWalletAdapter()) {
2809
- const results = [];
2810
- for (const { command } of commands) {
2811
- try {
2812
- const txResult = await submitCommand(command);
2813
- results.push({ success: true, commandId: command.commandId, result: txResult });
2814
- } catch (error) {
2815
- results.push({ success: false, commandId: command.commandId, error: error.message });
2816
- }
2817
- }
2818
- return { commands, results };
2819
- }
2820
-
2821
- if (opts.submit && !getWalletAdapter()) {
2822
- return { error: "buildAllocationWithdrawCommand: submit requires a wallet adapter. Call setWalletAdapter() first." };
2823
- }
2824
-
2825
- return { commands };
2826
- }
2827
2648
 
2828
2649
  // ─── Onboarding & Delegation ─────────────────────────────────────────────────
2829
2650
 
@@ -54,6 +54,14 @@ export interface FinalizeWithdrawOpts {
54
54
  userId?: string;
55
55
  }
56
56
 
57
+ export interface BulkWithdrawFundsOpts {
58
+ allocationIds: string[];
59
+ sender?: string;
60
+ assetId: string;
61
+ disclosures?: FinalizeWithdrawOpts["disclosures"];
62
+ userId?: string;
63
+ }
64
+
57
65
  export interface WithdrawFundsOpts {
58
66
  asset_id: string;
59
67
  amount: string | number;
@@ -332,6 +340,297 @@ export async function finalizeWithdrawFunds(
332
340
  }
333
341
  }
334
342
 
343
+ /**
344
+ * Finalize multiple withdrawals in a single signed transaction.
345
+ *
346
+ * Bundles N Allocation_Withdraw exercises into one ledger submission so the user
347
+ * signs once for the whole batch. All allocations must belong to the same sender
348
+ * and the same asset (caller's responsibility — pass one assetId).
349
+ *
350
+ * Choice-context fetching:
351
+ * - Localhost ledger / Amulet via /api/amulet/disclosures: one shared context for all N.
352
+ * - Scan API (Amulet) / Registry API (utility): one fetch per allocation, then disclosed
353
+ * contracts merged and deduped.
354
+ */
355
+ export async function bulkWithdrawFunds(
356
+ opts: BulkWithdrawFundsOpts,
357
+ returnCommand = false,
358
+ ): Promise<Record<string, unknown>> {
359
+ const { allocationIds, sender: senderOpt, assetId: rawAssetId, disclosures, userId } = opts;
360
+ const assetId = normalizeAssetId(rawAssetId);
361
+ const sender = getAdapterPartyId() ?? senderOpt ?? config.VALIDATOR_USER_PARTY_ID;
362
+
363
+ if (!Array.isArray(allocationIds) || allocationIds.length === 0) {
364
+ const msg = "bulkWithdrawFunds: allocationIds must be a non-empty array";
365
+ console.error(msg);
366
+ return { error: msg };
367
+ }
368
+ if (!sender || !assetId) {
369
+ const msg = "bulkWithdrawFunds: sender and assetId are required";
370
+ console.error(msg);
371
+ return { error: msg };
372
+ }
373
+
374
+ const isAmulet = assetId === "Amulet";
375
+ const allocationCids = allocationIds.map((cid) => normalizeContractId(cid) as string);
376
+
377
+ // Per-allocation choice context (one entry per allocation, in the same order).
378
+ // Shared-context paths fill all N entries with the same object.
379
+ let perAllocContexts: Record<string, unknown>[] = [];
380
+ let mergedDisclosedContracts: DisclosedContract[] = [];
381
+
382
+ if (shouldUseLedgerForMetadata()) {
383
+ // Localhost: resolve once, reuse for all allocations.
384
+ let sharedContext: Record<string, unknown> = {};
385
+ const sharedDisclosed: DisclosedContract[] = [];
386
+
387
+ if (isAmulet) {
388
+ const amuletCtx = await resolveAmuletContext({ investor: sender, holdingIds: [], transferAmount: 0 });
389
+ if (!amuletCtx) {
390
+ const msg = "bulkWithdrawFunds: failed to resolve Amulet context from ledger";
391
+ console.error(msg);
392
+ return { error: msg };
393
+ }
394
+ const ctx = amuletCtx as Record<string, unknown>;
395
+ const contextKeys = ctx.contextKeys as Record<string, string>;
396
+ const amuletRules = ctx.amuletRules as ContractMetadata;
397
+ const openMiningRound = ctx.openMiningRound as ContractMetadata;
398
+ const featuredAppRight = ctx.featuredAppRight as ContractMetadata;
399
+ const externalAmuletRules = ctx.externalAmuletRules as ContractMetadata;
400
+
401
+ sharedContext = {
402
+ values: {
403
+ [contextKeys.amuletRules]: { tag: "AV_ContractId", value: amuletRules.contractCid },
404
+ [contextKeys.openRound]: { tag: "AV_ContractId", value: openMiningRound.contractCid },
405
+ [contextKeys.featuredAppRight]: { tag: "AV_ContractId", value: featuredAppRight.contractCid },
406
+ [contextKeys.expireLock]: { tag: "AV_Bool", value: true },
407
+ },
408
+ };
409
+ sharedDisclosed.push(
410
+ {
411
+ templateId: amuletRules.templateId ?? null,
412
+ contractId: amuletRules.contractCid,
413
+ createdEventBlob: amuletRules.disclosureCid,
414
+ synchronizerId: amuletRules.synchronizerId,
415
+ },
416
+ {
417
+ templateId: openMiningRound.templateId ?? null,
418
+ contractId: openMiningRound.contractCid,
419
+ createdEventBlob: openMiningRound.disclosureCid,
420
+ synchronizerId: openMiningRound.synchronizerId,
421
+ },
422
+ {
423
+ templateId: externalAmuletRules.templateId ?? null,
424
+ contractId: externalAmuletRules.contractCid,
425
+ createdEventBlob: externalAmuletRules.disclosureCid,
426
+ synchronizerId: externalAmuletRules.synchronizerId,
427
+ },
428
+ );
429
+ if (featuredAppRight?.contractCid && featuredAppRight?.disclosureCid) {
430
+ sharedDisclosed.push({
431
+ templateId: featuredAppRight.templateId ?? null,
432
+ contractId: featuredAppRight.contractCid,
433
+ createdEventBlob: featuredAppRight.disclosureCid,
434
+ synchronizerId: featuredAppRight.synchronizerId,
435
+ });
436
+ }
437
+ } else {
438
+ const instrumentDef = resolveInstrumentDefinition(assetId);
439
+ const networkDef = instrumentDef?.[config.NETWORK] as Record<string, unknown> | undefined;
440
+ const registrar =
441
+ (networkDef?.registrar as string) || getInstrumentRegistrar(assetId) || config.VALIDATOR_REGISTRAR_PARTY_ID;
442
+ const [allocFactory, instConfig] = await Promise.all([
443
+ resolveUtilityAllocationFactory(registrar, sender),
444
+ resolveUtilityInstrumentConfiguration(assetId, registrar),
445
+ ]);
446
+ if (instConfig) {
447
+ sharedContext = {
448
+ values: {
449
+ [DEFAULT_UTILITY_CONTEXT_KEYS.instrumentConfiguration]: {
450
+ tag: "AV_ContractId",
451
+ value: instConfig.contractCid,
452
+ },
453
+ [DEFAULT_UTILITY_CONTEXT_KEYS.instrumentConfigurationPrefixed]: {
454
+ tag: "AV_ContractId",
455
+ value: instConfig.contractCid,
456
+ },
457
+ [DEFAULT_UTILITY_CONTEXT_KEYS.senderCredentials]: { tag: "AV_List", value: [] },
458
+ [DEFAULT_UTILITY_CONTEXT_KEYS.senderCredentialsPrefixed]: { tag: "AV_List", value: [] },
459
+ [DEFAULT_UTILITY_CONTEXT_KEYS.receiverCredentials]: { tag: "AV_List", value: [] },
460
+ [DEFAULT_UTILITY_CONTEXT_KEYS.receiverCredentialsPrefixed]: { tag: "AV_List", value: [] },
461
+ [DEFAULT_AMULET_CONTEXT_KEYS.expireLock]: { tag: "AV_Bool", value: true },
462
+ },
463
+ };
464
+ sharedDisclosed.push({
465
+ templateId: null,
466
+ contractId: instConfig.contractCid,
467
+ createdEventBlob: instConfig.disclosureCid,
468
+ synchronizerId: instConfig.synchronizerId,
469
+ });
470
+ }
471
+ if (allocFactory) {
472
+ sharedDisclosed.push({
473
+ templateId: null,
474
+ contractId: allocFactory.contractCid,
475
+ createdEventBlob: allocFactory.disclosureCid,
476
+ synchronizerId: allocFactory.synchronizerId,
477
+ });
478
+ }
479
+ }
480
+
481
+ perAllocContexts = allocationCids.map(() => sharedContext);
482
+ mergedDisclosedContracts = sharedDisclosed;
483
+ } else if (isAmulet && !config.VALIDATOR_SCAN_API_URL) {
484
+ // Amulet via /api/amulet/disclosures: one fetch shared across all allocations.
485
+ const factoryData = (disclosures as Record<string, unknown>)?.disclosures || disclosures || null;
486
+ let resolvedData = factoryData as Record<string, unknown> | null;
487
+
488
+ if (!resolvedData?.choiceContext) {
489
+ const disclosuresResult = (await getDisclosures(sender)) as unknown as Record<string, unknown>;
490
+ resolvedData = disclosuresResult?.disclosures as Record<string, unknown>;
491
+ if (disclosuresResult?.error || !resolvedData?.choiceContext) {
492
+ const detail =
493
+ (disclosuresResult?.message as string) ||
494
+ (disclosuresResult?.error as string) ||
495
+ "missing choiceContext in response";
496
+ const msg = `bulkWithdrawFunds: failed to resolve Amulet disclosures: ${detail}`;
497
+ console.error(msg);
498
+ return { error: msg };
499
+ }
500
+ }
501
+
502
+ const choiceContext = resolvedData.choiceContext as Record<string, unknown>;
503
+ const sharedContext = (choiceContext.choiceContextData as Record<string, unknown>) || {};
504
+ const values = sharedContext.values as Record<string, unknown> | undefined;
505
+ if (values && !values[DEFAULT_AMULET_CONTEXT_KEYS.expireLock]) {
506
+ values[DEFAULT_AMULET_CONTEXT_KEYS.expireLock] = { tag: "AV_Bool", value: true };
507
+ }
508
+ const sharedDisclosed = ((choiceContext.disclosedContracts as DisclosedContract[]) || []).map((dc) => ({
509
+ templateId: dc.templateId,
510
+ contractId: dc.contractId,
511
+ createdEventBlob: dc.createdEventBlob,
512
+ synchronizerId: dc.synchronizerId,
513
+ }));
514
+
515
+ perAllocContexts = allocationCids.map(() => sharedContext);
516
+ mergedDisclosedContracts = sharedDisclosed;
517
+ } else {
518
+ // Per-allocation context: Scan API (Amulet) or Registry API (utility).
519
+ let baseUrl: string;
520
+ let contextHeaders: Record<string, string> = {};
521
+
522
+ if (isAmulet) {
523
+ if (!config.VALIDATOR_SCAN_API_URL) {
524
+ const msg = "bulkWithdrawFunds: VALIDATOR_SCAN_API_URL is required for Amulet allocations";
525
+ console.error(msg);
526
+ return { error: msg };
527
+ }
528
+ baseUrl = `${config.VALIDATOR_SCAN_API_URL}/registry`;
529
+ contextHeaders = await buildHeaders();
530
+ } else {
531
+ const instrumentDef = resolveInstrumentDefinition(assetId);
532
+ const networkContracts = instrumentDef?.[config.NETWORK] as Record<string, unknown> | undefined;
533
+ const registryAPI = networkContracts?.registryAPI as string | undefined;
534
+ if (!registryAPI) {
535
+ const msg = `bulkWithdrawFunds: no registryAPI defined for ${assetId} on ${config.NETWORK}`;
536
+ console.error(msg);
537
+ return { error: msg };
538
+ }
539
+ baseUrl = registryAPI;
540
+ }
541
+
542
+ const requestConfig = Object.keys(contextHeaders).length > 0 ? { headers: contextHeaders } : undefined;
543
+ try {
544
+ const contextResults = await Promise.all(
545
+ allocationCids.map(async (cid) => {
546
+ const url = `${baseUrl}/allocations/v1/${encodeURIComponent(cid)}/choice-contexts/withdraw`;
547
+ try {
548
+ const resp = await axios.post(url, { meta: {}, excludeDebugFields: true }, requestConfig);
549
+ return resp.data as Record<string, unknown>;
550
+ } catch (error: unknown) {
551
+ const axiosErr = error as { response?: { data?: unknown }; message?: string };
552
+ const detail = axiosErr.response?.data ? JSON.stringify(axiosErr.response.data) : axiosErr.message;
553
+ throw new Error(
554
+ `bulkWithdrawFunds: error fetching withdraw context for ${cid} from ${isAmulet ? "Scan API" : "Registry API"}: ${detail}`,
555
+ );
556
+ }
557
+ }),
558
+ );
559
+
560
+ for (const data of contextResults) {
561
+ perAllocContexts.push((data?.choiceContextData as Record<string, unknown>) || {});
562
+ const disclosed = ((data?.disclosedContracts as DisclosedContract[]) || []).map((dc) => ({
563
+ templateId: dc.templateId,
564
+ contractId: dc.contractId,
565
+ createdEventBlob: dc.createdEventBlob,
566
+ synchronizerId: dc.synchronizerId,
567
+ }));
568
+ mergedDisclosedContracts.push(...disclosed);
569
+ }
570
+ } catch (error: unknown) {
571
+ const msg = (error as Error).message;
572
+ console.error(msg);
573
+ return { error: msg };
574
+ }
575
+ }
576
+
577
+ // Build one ExerciseCommand per allocation; Canton signs the whole submission atomically.
578
+ const exerciseCommands = allocationCids.map((cid, idx) => ({
579
+ ExerciseCommand: {
580
+ templateId: "#splice-api-token-allocation-v1:Splice.Api.Token.AllocationV1:Allocation",
581
+ contractId: cid,
582
+ choice: "Allocation_Withdraw",
583
+ choiceArgument: {
584
+ extraArgs: {
585
+ context: perAllocContexts[idx],
586
+ meta: { values: {} },
587
+ },
588
+ },
589
+ },
590
+ }));
591
+
592
+ const command = {
593
+ commands: exerciseCommands,
594
+ commandId: randomUUID(),
595
+ userId: userId || getUserId() || config.AUTH0_USER_ID || "temple",
596
+ applicationId: "temple",
597
+ actAs: [sender],
598
+ disclosedContracts: mergedDisclosedContracts,
599
+ };
600
+
601
+ dedupeDisclosedContracts(command);
602
+
603
+ const endpoint = `${config.VALIDATOR_API_URL}/v2/commands/submit-and-wait`;
604
+
605
+ if (returnCommand) {
606
+ return { command, endpoint };
607
+ }
608
+
609
+ if (getWalletAdapter()) {
610
+ try {
611
+ await payDueGasIfAny();
612
+ return (await submitCommand(command)) as Record<string, unknown>;
613
+ } catch (error: unknown) {
614
+ const msg = `bulkWithdrawFunds: wallet adapter submission failed: ${(error as Error).message}`;
615
+ console.error(msg);
616
+ return { error: msg };
617
+ }
618
+ }
619
+
620
+ const headers = await buildHeaders();
621
+ try {
622
+ const response = await axios.post(endpoint, command, { headers });
623
+ return response.data;
624
+ } catch (error: unknown) {
625
+ const axiosErr = error as { response?: { data?: unknown }; message?: string };
626
+ const errorData = axiosErr?.response?.data;
627
+ const errorDetail = errorData ? JSON.stringify(errorData, null, 2) : axiosErr.message;
628
+ const msg = `bulkWithdrawFunds: error submitting command: ${errorDetail}`;
629
+ console.error(msg);
630
+ return { error: msg };
631
+ }
632
+ }
633
+
335
634
  /**
336
635
  * High-level withdrawal flow:
337
636
  * 1. Creates a withdrawal request via the backend API.