@temple-digital-group/temple-canton-js 1.0.39 → 1.0.40

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.
@@ -0,0 +1,52 @@
1
+ export interface DepositFundsOpts {
2
+ sender: string;
3
+ receiver?: string;
4
+ assetId: string;
5
+ amount: string | number;
6
+ holdingCids?: string[];
7
+ settlementId?: string;
8
+ transferLegId?: string;
9
+ allocateBefore?: string;
10
+ settleBefore?: string;
11
+ disclosures?: Record<string, unknown>;
12
+ userId?: string;
13
+ }
14
+ export interface PreparedDeposit {
15
+ sender: string;
16
+ receiver: string;
17
+ assetId: string;
18
+ amount: string;
19
+ holdingCids: string[];
20
+ }
21
+ /**
22
+ * Prepare holdings for a deposit by selecting UTXOs from the wallet provider.
23
+ *
24
+ * Queries the provider for holdings, filters and sorts them, then greedily
25
+ * selects until the requested amount is covered.
26
+ */
27
+ export declare function prepareDepositHoldings(amount: number | string, symbol: string): Promise<PreparedDeposit | {
28
+ error: string;
29
+ data?: Record<string, unknown>;
30
+ }>;
31
+ /**
32
+ * High-level deposit helper.
33
+ *
34
+ * Wraps `prepareDepositHoldings` and `depositFunds` into a single call.
35
+ * Validates the user has sufficient balance and reserves 10 CC for transaction fees.
36
+ *
37
+ * Requires:
38
+ * - Wallet adapter (for reading balances and submitting)
39
+ * - API connection (for disclosures on Amulet deposits)
40
+ */
41
+ export declare function deposit(amount: number | string, symbol: string): Promise<Record<string, unknown>>;
42
+ /**
43
+ * Create a CIP-56 Allocation.
44
+ * Fetches the AllocationFactory, then exercises AllocationFactory_Allocate
45
+ * to lock holdings for a settlement leg.
46
+ *
47
+ * Three resolution paths:
48
+ * 1. Localhost: resolve factory and context from ledger directly
49
+ * 2. Amulet via disclosures (FE/proxy path — no Scan API access)
50
+ * 3. Remote: Scan API / Registry API
51
+ */
52
+ export declare function depositFunds(opts: DepositFundsOpts, returnCommand?: boolean): Promise<Record<string, unknown>>;
@@ -0,0 +1,466 @@
1
+ import config from "../../src/config/index.js";
2
+ import axios from "axios";
3
+ import { getDisclosures } from "../api/index.js";
4
+ import { getUserId } from "../api/tokenStore.js";
5
+ import { getAdapterPartyId, getWalletAdapter, submitCommand } from "../../src/canton/walletAdapter.js";
6
+ import { normalizeAssetId, instrumentCatalog } from "../../src/canton/instrumentCatalog.js";
7
+ import { randomUUID, shouldUseLedgerForMetadata, normalizeContractId, resolveInstrumentDefinition, getInstrumentRegistrar, resolveProvider, dedupeDisclosedContracts, buildHeaders, DEFAULT_UTILITY_CONTEXT_KEYS, } from "./helpers.js";
8
+ import { resolveAmuletContext, resolveUtilityInstrumentConfiguration, resolveUtilityAllocationFactory, getAmuletHoldingsForParty, getUtilityHoldingsForParty, getUtxoCount, } from "../../src/canton/index.js";
9
+ // ─── Constants ───────────────────────────────────────────────────────────────
10
+ const CC_FEE_RESERVE = 10;
11
+ // ─── Deposit Functions ───────────────────────────────────────────────────────
12
+ /**
13
+ * Prepare holdings for a deposit by selecting UTXOs from the wallet provider.
14
+ *
15
+ * Queries the provider for holdings, filters and sorts them, then greedily
16
+ * selects until the requested amount is covered.
17
+ */
18
+ export async function prepareDepositHoldings(amount, symbol) {
19
+ const provider = resolveProvider(null);
20
+ if (!provider && !config.VALIDATOR_SCAN_API_URL) {
21
+ return { error: "prepareDepositHoldings: wallet provider is required to prepare holdings. Connect a wallet adapter." };
22
+ }
23
+ const party = getAdapterPartyId() ?? config.VALIDATOR_USER_PARTY_ID;
24
+ if (!party) {
25
+ return { error: "prepareDepositHoldings: party ID is required. Connect a wallet adapter or configure VALIDATOR_USER_PARTY_ID." };
26
+ }
27
+ const assetId = normalizeAssetId(symbol);
28
+ if (!instrumentCatalog[assetId]) {
29
+ return { error: `prepareDepositHoldings: unsupported symbol "${symbol}"` };
30
+ }
31
+ const requiredAmount = parseFloat(String(amount));
32
+ if (isNaN(requiredAmount) || requiredAmount <= 0) {
33
+ return { error: "prepareDepositHoldings: amount must be a positive number" };
34
+ }
35
+ const isAmulet = assetId === "Amulet";
36
+ const holdings = isAmulet
37
+ ? await getAmuletHoldingsForParty(party, false, provider)
38
+ : await getUtilityHoldingsForParty(party, false, provider);
39
+ if (!holdings || holdings.length === 0) {
40
+ return { error: `prepareDepositHoldings: no holdings found for party ${party}` };
41
+ }
42
+ // Parse holdings into { contractId, quantity } and filter out locked
43
+ const parsed = [];
44
+ for (const h of holdings) {
45
+ const contractEntry = h.contractEntry;
46
+ const jsActive = contractEntry?.JsActiveContract;
47
+ const createdEvent = jsActive?.createdEvent;
48
+ if (!createdEvent)
49
+ continue;
50
+ const createArg = createdEvent.createArgument;
51
+ // Skip asset types that don't match the requested symbol
52
+ const instrument = createArg?.instrument;
53
+ const createdAssetId = instrument?.id || (isAmulet ? "Amulet" : null);
54
+ if (createdAssetId !== assetId)
55
+ continue;
56
+ // Skip locked utility holdings
57
+ if (!isAmulet && createArg?.lock)
58
+ continue;
59
+ const amountField = createArg?.amount;
60
+ const quantity = isAmulet
61
+ ? parseFloat(amountField?.initialAmount || "0")
62
+ : parseFloat(amountField || "0");
63
+ if (quantity <= 0)
64
+ continue;
65
+ parsed.push({
66
+ contractId: normalizeContractId(createdEvent.contractId),
67
+ quantity,
68
+ });
69
+ }
70
+ if (parsed.length === 0) {
71
+ return { error: `prepareDepositHoldings: no unlocked holdings found for ${assetId}` };
72
+ }
73
+ // Sort descending — largest first
74
+ parsed.sort((a, b) => b.quantity - a.quantity);
75
+ // Greedily select until we cover the required amount
76
+ const selected = [];
77
+ let cumulative = 0;
78
+ for (const holding of parsed) {
79
+ selected.push(holding.contractId);
80
+ cumulative += holding.quantity;
81
+ if (cumulative >= requiredAmount)
82
+ break;
83
+ }
84
+ if (cumulative < requiredAmount) {
85
+ return {
86
+ error: `prepareDepositHoldings: insufficient balance. Have ${cumulative}, need ${requiredAmount}`,
87
+ data: { totalBalance: cumulative, requiredAmount },
88
+ };
89
+ }
90
+ const formattedAmount = parseFloat(parseFloat(String(amount)).toFixed(10)).toString();
91
+ return {
92
+ sender: party,
93
+ receiver: party,
94
+ assetId,
95
+ amount: formattedAmount,
96
+ holdingCids: selected,
97
+ };
98
+ }
99
+ /**
100
+ * High-level deposit helper.
101
+ *
102
+ * Wraps `prepareDepositHoldings` and `depositFunds` into a single call.
103
+ * Validates the user has sufficient balance and reserves 10 CC for transaction fees.
104
+ *
105
+ * Requires:
106
+ * - Wallet adapter (for reading balances and submitting)
107
+ * - API connection (for disclosures on Amulet deposits)
108
+ */
109
+ export async function deposit(amount, symbol) {
110
+ if (!getWalletAdapter()) {
111
+ return { error: "deposit: wallet adapter is required. Call setWalletAdapter() first." };
112
+ }
113
+ const party = getAdapterPartyId();
114
+ if (!party) {
115
+ return { error: "deposit: could not resolve party ID from wallet adapter." };
116
+ }
117
+ const assetId = normalizeAssetId(symbol);
118
+ if (!instrumentCatalog[assetId]) {
119
+ return { error: `deposit: unsupported symbol "${symbol}"` };
120
+ }
121
+ const depositAmount = parseFloat(String(amount));
122
+ if (isNaN(depositAmount) || depositAmount <= 0) {
123
+ return { error: "deposit: amount must be a positive number" };
124
+ }
125
+ const isAmulet = assetId === "Amulet";
126
+ const provider = resolveProvider(null);
127
+ // Check CC balance — required for all deposits (fees)
128
+ const ccBalance = await getUtxoCount(party, "Amulet", provider);
129
+ const availableCC = ccBalance.unlockedBalance || 0;
130
+ if (isAmulet) {
131
+ // CC deposit: user needs depositAmount + 10 CC fee reserve
132
+ const maxDeposit = availableCC - CC_FEE_RESERVE;
133
+ if (maxDeposit <= 0) {
134
+ return {
135
+ error: `deposit: insufficient CC balance. You have ${availableCC} CC but need at least ${CC_FEE_RESERVE} CC reserved for fees.`,
136
+ data: { balance: availableCC, feeReserve: CC_FEE_RESERVE, maxDeposit: 0 },
137
+ };
138
+ }
139
+ if (depositAmount > maxDeposit) {
140
+ return {
141
+ error: `deposit: amount exceeds maximum. You have ${availableCC} CC, ${CC_FEE_RESERVE} CC is reserved for fees, max deposit is ${maxDeposit} CC.`,
142
+ data: { balance: availableCC, feeReserve: CC_FEE_RESERVE, maxDeposit },
143
+ };
144
+ }
145
+ }
146
+ else {
147
+ // Utility deposit: user needs 10 CC for fees + enough of the utility token
148
+ if (availableCC < CC_FEE_RESERVE) {
149
+ return {
150
+ error: `deposit: insufficient CC for fees. You have ${availableCC} CC but need at least ${CC_FEE_RESERVE} CC to cover transaction fees.`,
151
+ data: { ccBalance: availableCC, feeReserve: CC_FEE_RESERVE },
152
+ };
153
+ }
154
+ const utilityBalance = await getUtxoCount(party, assetId, provider);
155
+ const availableUtility = utilityBalance.unlockedBalance || 0;
156
+ if (depositAmount > availableUtility) {
157
+ return {
158
+ error: `deposit: insufficient ${assetId} balance. You have ${availableUtility} ${assetId}, need ${depositAmount}.`,
159
+ data: { balance: availableUtility, requested: depositAmount },
160
+ };
161
+ }
162
+ }
163
+ // Step 1: Prepare holdings (select UTXOs)
164
+ console.log(`deposit: preparing ${depositAmount} ${assetId} for deposit...`);
165
+ const prepared = await prepareDepositHoldings(depositAmount, assetId);
166
+ if ("error" in prepared) {
167
+ return prepared;
168
+ }
169
+ // Step 2: Execute deposit (allocate holdings)
170
+ console.log(`deposit: submitting deposit with ${prepared.holdingCids.length} holding(s)...`);
171
+ const result = await depositFunds(prepared);
172
+ if (result?.error) {
173
+ return result;
174
+ }
175
+ console.log(`deposit: ${depositAmount} ${assetId} deposited successfully`);
176
+ return result;
177
+ }
178
+ /**
179
+ * Create a CIP-56 Allocation.
180
+ * Fetches the AllocationFactory, then exercises AllocationFactory_Allocate
181
+ * to lock holdings for a settlement leg.
182
+ *
183
+ * Three resolution paths:
184
+ * 1. Localhost: resolve factory and context from ledger directly
185
+ * 2. Amulet via disclosures (FE/proxy path — no Scan API access)
186
+ * 3. Remote: Scan API / Registry API
187
+ */
188
+ export async function depositFunds(opts, returnCommand = false) {
189
+ const { sender, receiver, amount, holdingCids = [], settlementId, transferLegId, allocateBefore, settleBefore, disclosures, userId } = opts;
190
+ const assetId = normalizeAssetId(opts.assetId);
191
+ if (!sender || !assetId || !amount) {
192
+ const msg = "depositFunds: sender, assetId, and amount are required";
193
+ console.error(msg);
194
+ return { error: msg };
195
+ }
196
+ const isAmulet = assetId === "Amulet";
197
+ // --- 1. Resolve admin and factory ---
198
+ let admin;
199
+ let factoryContractId;
200
+ let choiceContextData = {};
201
+ let disclosedContracts = [];
202
+ if (isAmulet) {
203
+ admin = config.VALIDATOR_DSO_PARTY_ID;
204
+ }
205
+ else {
206
+ const instrumentDef = resolveInstrumentDefinition(assetId);
207
+ const networkContracts = instrumentDef?.[config.NETWORK];
208
+ admin = networkContracts?.registrar || getInstrumentRegistrar(assetId) || config.VALIDATOR_REGISTRAR_PARTY_ID;
209
+ }
210
+ // --- 2. Build the choice arguments upfront ---
211
+ const now = new Date();
212
+ const allocateDeadline = allocateBefore || new Date(now.getTime() + 60 * 60 * 1000).toISOString();
213
+ const settleDeadline = settleBefore || new Date(now.getTime() + 2 * 60 * 60 * 1000).toISOString();
214
+ const executor = config.TEMPLE_PARTY_ID;
215
+ const resolvedTransferLegId = transferLegId || randomUUID();
216
+ const resolvedSettlementId = settlementId || `SR-${randomUUID()}`;
217
+ const choiceArgument = {
218
+ expectedAdmin: admin,
219
+ allocation: {
220
+ settlement: {
221
+ executor: executor,
222
+ settlementRef: {
223
+ id: resolvedSettlementId,
224
+ cid: null,
225
+ },
226
+ requestedAt: now.toISOString(),
227
+ allocateBefore: allocateDeadline,
228
+ settleBefore: settleDeadline,
229
+ meta: { values: config.ALLOCATION_META_TAG ? { tag: config.ALLOCATION_META_TAG } : {} },
230
+ },
231
+ transferLegId: resolvedTransferLegId,
232
+ transferLeg: {
233
+ sender: sender,
234
+ receiver: receiver || sender,
235
+ amount: parseFloat(parseFloat(String(amount)).toFixed(10)).toString(),
236
+ instrumentId: {
237
+ admin: admin,
238
+ id: assetId,
239
+ },
240
+ meta: { values: {} },
241
+ },
242
+ },
243
+ requestedAt: now.toISOString(),
244
+ inputHoldingCids: holdingCids.map(normalizeContractId),
245
+ extraArgs: {
246
+ context: { values: {} },
247
+ meta: { values: {} },
248
+ },
249
+ };
250
+ // --- 3. Fetch AllocationFactory ---
251
+ if (shouldUseLedgerForMetadata()) {
252
+ // Localhost: resolve factory and context from ledger directly
253
+ if (isAmulet) {
254
+ const amuletCtx = await resolveAmuletContext({ investor: sender, holdingIds: holdingCids, transferAmount: Number(amount) });
255
+ if (!amuletCtx) {
256
+ const msg = "depositFunds: failed to resolve Amulet context from ledger";
257
+ console.error(msg);
258
+ return { error: msg };
259
+ }
260
+ const ctx = amuletCtx;
261
+ const contextKeys = ctx.contextKeys;
262
+ const amuletRules = ctx.amuletRules;
263
+ const openMiningRound = ctx.openMiningRound;
264
+ const featuredAppRight = ctx.featuredAppRight;
265
+ const externalAmuletRules = ctx.externalAmuletRules;
266
+ factoryContractId = normalizeContractId(externalAmuletRules.contractCid);
267
+ choiceContextData = {
268
+ values: {
269
+ [contextKeys.amuletRules]: { tag: "AV_ContractId", value: amuletRules.contractCid },
270
+ [contextKeys.openRound]: { tag: "AV_ContractId", value: openMiningRound.contractCid },
271
+ [contextKeys.featuredAppRight]: { tag: "AV_ContractId", value: featuredAppRight.contractCid },
272
+ },
273
+ };
274
+ disclosedContracts = [
275
+ {
276
+ templateId: amuletRules.templateId ?? null,
277
+ contractId: amuletRules.contractCid,
278
+ createdEventBlob: amuletRules.disclosureCid,
279
+ synchronizerId: amuletRules.synchronizerId,
280
+ },
281
+ {
282
+ templateId: openMiningRound.templateId ?? null,
283
+ contractId: openMiningRound.contractCid,
284
+ createdEventBlob: openMiningRound.disclosureCid,
285
+ synchronizerId: openMiningRound.synchronizerId,
286
+ },
287
+ {
288
+ templateId: externalAmuletRules.templateId ?? null,
289
+ contractId: externalAmuletRules.contractCid,
290
+ createdEventBlob: externalAmuletRules.disclosureCid,
291
+ synchronizerId: externalAmuletRules.synchronizerId,
292
+ },
293
+ ];
294
+ if (featuredAppRight?.contractCid && featuredAppRight?.disclosureCid) {
295
+ disclosedContracts.push({
296
+ templateId: featuredAppRight.templateId ?? null,
297
+ contractId: featuredAppRight.contractCid,
298
+ createdEventBlob: featuredAppRight.disclosureCid,
299
+ synchronizerId: featuredAppRight.synchronizerId,
300
+ });
301
+ }
302
+ }
303
+ else {
304
+ // Utility token: resolve from ledger
305
+ const instrumentDef = resolveInstrumentDefinition(assetId);
306
+ const networkDef = instrumentDef?.[config.NETWORK];
307
+ const registrar = networkDef?.registrar || getInstrumentRegistrar(assetId) || config.VALIDATOR_REGISTRAR_PARTY_ID;
308
+ const [allocFactory, instConfig] = await Promise.all([
309
+ resolveUtilityAllocationFactory(registrar, sender),
310
+ resolveUtilityInstrumentConfiguration(assetId, registrar),
311
+ ]);
312
+ if (!allocFactory) {
313
+ const msg = `depositFunds: no AllocationFactory found on ledger for ${assetId}`;
314
+ console.error(msg);
315
+ return { error: msg };
316
+ }
317
+ factoryContractId = normalizeContractId(allocFactory.contractCid);
318
+ if (instConfig) {
319
+ choiceContextData = {
320
+ values: {
321
+ [DEFAULT_UTILITY_CONTEXT_KEYS.instrumentConfiguration]: { tag: "AV_ContractId", value: instConfig.contractCid },
322
+ [DEFAULT_UTILITY_CONTEXT_KEYS.instrumentConfigurationPrefixed]: { tag: "AV_ContractId", value: instConfig.contractCid },
323
+ [DEFAULT_UTILITY_CONTEXT_KEYS.senderCredentials]: { tag: "AV_List", value: [] },
324
+ [DEFAULT_UTILITY_CONTEXT_KEYS.senderCredentialsPrefixed]: { tag: "AV_List", value: [] },
325
+ [DEFAULT_UTILITY_CONTEXT_KEYS.receiverCredentials]: { tag: "AV_List", value: [] },
326
+ [DEFAULT_UTILITY_CONTEXT_KEYS.receiverCredentialsPrefixed]: { tag: "AV_List", value: [] },
327
+ },
328
+ };
329
+ disclosedContracts.push({
330
+ templateId: null,
331
+ contractId: instConfig.contractCid,
332
+ createdEventBlob: instConfig.disclosureCid,
333
+ synchronizerId: instConfig.synchronizerId,
334
+ });
335
+ }
336
+ disclosedContracts.push({
337
+ templateId: null,
338
+ contractId: allocFactory.contractCid,
339
+ createdEventBlob: allocFactory.disclosureCid,
340
+ synchronizerId: allocFactory.synchronizerId,
341
+ });
342
+ }
343
+ }
344
+ else if (isAmulet && !config.VALIDATOR_SCAN_API_URL) {
345
+ // Amulet via disclosures (FE/proxy path — no Scan API access)
346
+ const factoryData = disclosures?.disclosures || disclosures || null;
347
+ let resolvedFactoryData = factoryData;
348
+ if (!resolvedFactoryData?.factoryId) {
349
+ const disclosuresResult = (await getDisclosures(sender));
350
+ resolvedFactoryData = disclosuresResult?.disclosures;
351
+ if (disclosuresResult?.error || !resolvedFactoryData?.factoryId) {
352
+ const detail = disclosuresResult?.message || disclosuresResult?.error || "missing factoryId in response";
353
+ const msg = `depositFunds: failed to resolve Amulet disclosures. Pass disclosures directly or ensure API access: ${detail}`;
354
+ console.error(msg);
355
+ return { error: msg };
356
+ }
357
+ }
358
+ factoryContractId = normalizeContractId(resolvedFactoryData.factoryId);
359
+ const choiceContext = resolvedFactoryData.choiceContext;
360
+ choiceContextData = choiceContext?.choiceContextData || {};
361
+ disclosedContracts = (choiceContext?.disclosedContracts || []).map((dc) => ({
362
+ templateId: dc.templateId,
363
+ contractId: dc.contractId,
364
+ createdEventBlob: dc.createdEventBlob,
365
+ synchronizerId: dc.synchronizerId,
366
+ }));
367
+ }
368
+ else {
369
+ // Remote: use Scan API (Amulet) or Registry API (utility)
370
+ let factoryUrl;
371
+ let factoryHeaders = {};
372
+ if (isAmulet) {
373
+ factoryUrl = `${config.VALIDATOR_SCAN_API_URL}/registry/allocation-instruction/v1/allocation-factory`;
374
+ factoryHeaders = await buildHeaders();
375
+ }
376
+ else {
377
+ const instrumentDef = resolveInstrumentDefinition(assetId);
378
+ const networkContracts = instrumentDef?.[config.NETWORK];
379
+ const registryAPI = networkContracts?.registryAPI;
380
+ if (!registryAPI) {
381
+ const msg = `depositFunds: no registryAPI defined for ${assetId} on ${config.NETWORK}`;
382
+ console.error(msg);
383
+ return { error: msg };
384
+ }
385
+ factoryUrl = `${registryAPI}/allocation-instruction/v1/allocation-factory`;
386
+ }
387
+ const factoryBody = {
388
+ choiceArguments: isAmulet ? {} : choiceArgument,
389
+ excludeDebugFields: true,
390
+ };
391
+ let factoryData;
392
+ try {
393
+ const factoryResponse = await axios.post(factoryUrl, factoryBody, Object.keys(factoryHeaders).length > 0 ? { headers: factoryHeaders } : undefined);
394
+ factoryData = factoryResponse.data;
395
+ }
396
+ catch (error) {
397
+ const axiosErr = error;
398
+ const detail = axiosErr.response?.data ? JSON.stringify(axiosErr.response.data) : axiosErr.message;
399
+ const msg = `depositFunds: error fetching allocation factory from ${isAmulet ? "Scan API" : "Registry API"}: ${detail}`;
400
+ console.error(msg);
401
+ return { error: msg };
402
+ }
403
+ factoryContractId = normalizeContractId(factoryData?.factoryId);
404
+ const choiceContext = factoryData?.choiceContext;
405
+ choiceContextData = choiceContext?.choiceContextData || {};
406
+ disclosedContracts = (choiceContext?.disclosedContracts || []).map((dc) => ({
407
+ templateId: dc.templateId,
408
+ contractId: dc.contractId,
409
+ createdEventBlob: dc.createdEventBlob,
410
+ synchronizerId: dc.synchronizerId,
411
+ }));
412
+ }
413
+ if (!factoryContractId) {
414
+ const msg = "depositFunds: allocation factory response missing factoryId";
415
+ console.error(msg);
416
+ return { error: msg };
417
+ }
418
+ // --- 4. Build the ExerciseCommand for AllocationFactory_Allocate ---
419
+ choiceArgument.extraArgs.context = choiceContextData;
420
+ const command = {
421
+ commands: [
422
+ {
423
+ ExerciseCommand: {
424
+ templateId: "#splice-api-token-allocation-instruction-v1:Splice.Api.Token.AllocationInstructionV1:AllocationFactory",
425
+ contractId: factoryContractId,
426
+ choice: "AllocationFactory_Allocate",
427
+ choiceArgument: choiceArgument,
428
+ },
429
+ },
430
+ ],
431
+ commandId: randomUUID(),
432
+ userId: userId || getUserId() || config.AUTH0_USER_ID || "temple",
433
+ applicationId: "temple",
434
+ actAs: [sender],
435
+ disclosedContracts: disclosedContracts,
436
+ };
437
+ dedupeDisclosedContracts(command);
438
+ const endpoint = `${config.VALIDATOR_API_URL}/v2/commands/submit-and-wait`;
439
+ if (returnCommand) {
440
+ return { command, endpoint };
441
+ }
442
+ // Auto-submit via wallet adapter if available
443
+ if (getWalletAdapter()) {
444
+ try {
445
+ return (await submitCommand(command));
446
+ }
447
+ catch (error) {
448
+ const msg = `depositFunds: wallet adapter submission failed: ${error.message}`;
449
+ console.error(msg);
450
+ return { error: msg };
451
+ }
452
+ }
453
+ const headers = await buildHeaders();
454
+ try {
455
+ const response = await axios.post(endpoint, command, { headers });
456
+ return response.data;
457
+ }
458
+ catch (error) {
459
+ const axiosErr = error;
460
+ const errorData = axiosErr?.response?.data;
461
+ const errorDetail = errorData ? JSON.stringify(errorData, null, 2) : axiosErr.message;
462
+ const msg = `depositFunds: error submitting command: ${errorDetail}`;
463
+ console.error(msg);
464
+ return { error: msg };
465
+ }
466
+ }
@@ -16,7 +16,8 @@ import { normalizeAssetId } from "../../src/canton/instrumentCatalog.js";
16
16
  * 3. Remote: Scan API / Registry API
17
17
  */
18
18
  export async function finalizeWithdrawFunds(opts, returnCommand = false) {
19
- const { allocationId, sender: senderOpt, assetId, disclosures, userId } = opts;
19
+ const { allocationId, sender: senderOpt, assetId: rawAssetId, disclosures, userId } = opts;
20
+ const assetId = normalizeAssetId(rawAssetId);
20
21
  const sender = getAdapterPartyId() ?? senderOpt ?? config.VALIDATOR_USER_PARTY_ID;
21
22
  if (!allocationId || !sender || !assetId) {
22
23
  const msg = "finalizeWithdrawFunds: allocationId, sender, and assetId are required";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@temple-digital-group/temple-canton-js",
3
- "version": "1.0.39",
3
+ "version": "1.0.40",
4
4
  "description": "JavaScript library for interacting with Temple Canton blockchain",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -2915,8 +2915,8 @@ export async function mergeAmuletHoldingsForParty(party, returnCommand = false,
2915
2915
  return null;
2916
2916
  }
2917
2917
 
2918
- // Sort by quantity ascending (smallest first) and apply maxUtxos limit
2919
- contracts.sort((a, b) => a.quantity - b.quantity);
2918
+ // Sort by quantity descending (largest first) and apply maxUtxos limit
2919
+ contracts.sort((a, b) => b.quantity - a.quantity);
2920
2920
  const selectedContracts = maxUtxos && maxUtxos < contracts.length ? contracts.slice(0, maxUtxos) : contracts;
2921
2921
 
2922
2922
  if (selectedContracts.length < 2) {
@@ -3217,9 +3217,9 @@ export async function mergeUtilityHoldingsForParty(party, utilityAsset, returnCo
3217
3217
  const finalHolders = [...new Set(contracts.map(c => c.holder).filter(Boolean))];
3218
3218
  const holder = finalHolders[0] || party; // Use the holder from filtered holdings
3219
3219
 
3220
- // Sort ascending by quantity so smallest UTXOs are merged first
3220
+ // Sort descending by quantity so largest UTXOs are merged first
3221
3221
  if (maxUtxos) {
3222
- contracts.sort((a, b) => a.quantity - b.quantity);
3222
+ contracts.sort((a, b) => b.quantity - a.quantity);
3223
3223
  if (contracts.length > maxUtxos) {
3224
3224
  contracts.length = maxUtxos;
3225
3225
  }