@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.
- package/dist/canton/deposits.d.ts +52 -0
- package/dist/canton/deposits.js +466 -0
- package/dist/canton/withdrawals.js +2 -1
- package/package.json +1 -1
- package/src/canton/index.js +4 -4
|
@@ -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
package/src/canton/index.js
CHANGED
|
@@ -2915,8 +2915,8 @@ export async function mergeAmuletHoldingsForParty(party, returnCommand = false,
|
|
|
2915
2915
|
return null;
|
|
2916
2916
|
}
|
|
2917
2917
|
|
|
2918
|
-
// Sort by quantity
|
|
2919
|
-
contracts.sort((a, b) =>
|
|
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
|
|
3220
|
+
// Sort descending by quantity so largest UTXOs are merged first
|
|
3221
3221
|
if (maxUtxos) {
|
|
3222
|
-
contracts.sort((a, b) =>
|
|
3222
|
+
contracts.sort((a, b) => b.quantity - a.quantity);
|
|
3223
3223
|
if (contracts.length > maxUtxos) {
|
|
3224
3224
|
contracts.length = maxUtxos;
|
|
3225
3225
|
}
|