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