@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 +37 -39
- package/dist/canton/withdrawals.d.ts +20 -0
- package/dist/canton/withdrawals.js +265 -0
- package/package.json +1 -1
- package/src/canton/index.js +0 -179
- package/src/canton/withdrawals.ts +299 -0
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
|
|
318
|
-
|
|
|
319
|
-
| `subscribeUserOrders(cb)`
|
|
320
|
-
| `subscribeUserTrades(cb)`
|
|
321
|
-
| `subscribeUserBalances(cb)
|
|
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
|
-
| `
|
|
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
|
|
422
|
-
|
|
|
423
|
-
| `createOrderRequest(opts)`
|
|
424
|
-
| `cancelOrder(orderId)`
|
|
425
|
-
| `cancelAllOrders(options?)`
|
|
426
|
-
| `getTradingBalance()`
|
|
427
|
-
| `getActiveOrders(options?)`
|
|
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
|
|
439
|
-
|
|
|
440
|
-
| `getDisclosures(
|
|
441
|
-
| `getDelegation()`
|
|
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
package/src/canton/index.js
CHANGED
|
@@ -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.
|