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

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.
@@ -54,14 +54,6 @@ 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
-
65
57
  export interface WithdrawFundsOpts {
66
58
  asset_id: string;
67
59
  amount: string | number;
@@ -340,297 +332,6 @@ export async function finalizeWithdrawFunds(
340
332
  }
341
333
  }
342
334
 
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
-
634
335
  /**
635
336
  * High-level withdrawal flow:
636
337
  * 1. Creates a withdrawal request via the backend API.