@ledgerhq/coin-canton 0.12.0-nightly.20251210120335 → 0.12.0-nightly.20251212024049
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/CHANGELOG.md +15 -9
- package/lib/bridge/buildSubAccounts.d.ts +23 -0
- package/lib/bridge/buildSubAccounts.d.ts.map +1 -0
- package/lib/bridge/buildSubAccounts.js +80 -0
- package/lib/bridge/buildSubAccounts.js.map +1 -0
- package/lib/bridge/getTransactionStatus.d.ts.map +1 -1
- package/lib/bridge/getTransactionStatus.js +45 -24
- package/lib/bridge/getTransactionStatus.js.map +1 -1
- package/lib/bridge/index.d.ts.map +1 -1
- package/lib/bridge/index.js +2 -0
- package/lib/bridge/index.js.map +1 -1
- package/lib/bridge/prepareTransaction.d.ts.map +1 -1
- package/lib/bridge/prepareTransaction.js +19 -3
- package/lib/bridge/prepareTransaction.js.map +1 -1
- package/lib/bridge/signOperation.d.ts.map +1 -1
- package/lib/bridge/signOperation.js +3 -0
- package/lib/bridge/signOperation.js.map +1 -1
- package/lib/bridge/sync.d.ts +3 -0
- package/lib/bridge/sync.d.ts.map +1 -1
- package/lib/bridge/sync.js +115 -19
- package/lib/bridge/sync.js.map +1 -1
- package/lib/bridge/validateAddress.d.ts +3 -0
- package/lib/bridge/validateAddress.d.ts.map +1 -0
- package/lib/bridge/validateAddress.js +8 -0
- package/lib/bridge/validateAddress.js.map +1 -0
- package/lib/common-logic/account/getBalance.d.ts +1 -0
- package/lib/common-logic/account/getBalance.d.ts.map +1 -1
- package/lib/common-logic/account/getBalance.js +1 -0
- package/lib/common-logic/account/getBalance.js.map +1 -1
- package/lib/common-logic/transaction/craftTransaction.d.ts +1 -0
- package/lib/common-logic/transaction/craftTransaction.d.ts.map +1 -1
- package/lib/common-logic/transaction/craftTransaction.js +3 -0
- package/lib/common-logic/transaction/craftTransaction.js.map +1 -1
- package/lib/network/gateway.d.ts +30 -12
- package/lib/network/gateway.d.ts.map +1 -1
- package/lib/network/gateway.js +44 -1
- package/lib/network/gateway.js.map +1 -1
- package/lib/types/bridge.d.ts +2 -0
- package/lib/types/bridge.d.ts.map +1 -1
- package/lib-es/bridge/buildSubAccounts.d.ts +23 -0
- package/lib-es/bridge/buildSubAccounts.d.ts.map +1 -0
- package/lib-es/bridge/buildSubAccounts.js +74 -0
- package/lib-es/bridge/buildSubAccounts.js.map +1 -0
- package/lib-es/bridge/getTransactionStatus.d.ts.map +1 -1
- package/lib-es/bridge/getTransactionStatus.js +46 -25
- package/lib-es/bridge/getTransactionStatus.js.map +1 -1
- package/lib-es/bridge/index.d.ts.map +1 -1
- package/lib-es/bridge/index.js +2 -0
- package/lib-es/bridge/index.js.map +1 -1
- package/lib-es/bridge/prepareTransaction.d.ts.map +1 -1
- package/lib-es/bridge/prepareTransaction.js +19 -3
- package/lib-es/bridge/prepareTransaction.js.map +1 -1
- package/lib-es/bridge/signOperation.d.ts.map +1 -1
- package/lib-es/bridge/signOperation.js +3 -0
- package/lib-es/bridge/signOperation.js.map +1 -1
- package/lib-es/bridge/sync.d.ts +3 -0
- package/lib-es/bridge/sync.d.ts.map +1 -1
- package/lib-es/bridge/sync.js +115 -20
- package/lib-es/bridge/sync.js.map +1 -1
- package/lib-es/bridge/validateAddress.d.ts +3 -0
- package/lib-es/bridge/validateAddress.d.ts.map +1 -0
- package/lib-es/bridge/validateAddress.js +5 -0
- package/lib-es/bridge/validateAddress.js.map +1 -0
- package/lib-es/common-logic/account/getBalance.d.ts +1 -0
- package/lib-es/common-logic/account/getBalance.d.ts.map +1 -1
- package/lib-es/common-logic/account/getBalance.js +1 -0
- package/lib-es/common-logic/account/getBalance.js.map +1 -1
- package/lib-es/common-logic/transaction/craftTransaction.d.ts +1 -0
- package/lib-es/common-logic/transaction/craftTransaction.d.ts.map +1 -1
- package/lib-es/common-logic/transaction/craftTransaction.js +3 -0
- package/lib-es/common-logic/transaction/craftTransaction.js.map +1 -1
- package/lib-es/network/gateway.d.ts +30 -12
- package/lib-es/network/gateway.d.ts.map +1 -1
- package/lib-es/network/gateway.js +41 -0
- package/lib-es/network/gateway.js.map +1 -1
- package/lib-es/types/bridge.d.ts +2 -0
- package/lib-es/types/bridge.d.ts.map +1 -1
- package/package.json +9 -9
- package/src/bridge/buildSubAccounts.test.ts +120 -0
- package/src/bridge/buildSubAccounts.ts +132 -0
- package/src/bridge/getTransactionStatus.ts +53 -22
- package/src/bridge/index.ts +2 -0
- package/src/bridge/prepareTransaction.ts +29 -4
- package/src/bridge/signOperation.ts +4 -0
- package/src/bridge/sync.test.ts +237 -191
- package/src/bridge/sync.ts +154 -24
- package/src/bridge/validateAddress.test.ts +25 -0
- package/src/bridge/validateAddress.ts +9 -0
- package/src/common-logic/account/getBalance.ts +2 -0
- package/src/common-logic/transaction/craftTransaction.ts +5 -0
- package/src/network/gateway.test.ts +169 -0
- package/src/network/gateway.ts +83 -12
- package/src/types/bridge.ts +2 -0
- package/tsconfig.json +7 -2
package/src/bridge/sync.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import BigNumber from "bignumber.js";
|
|
2
|
-
import { Operation, OperationType } from "@ledgerhq/types-live";
|
|
2
|
+
import { Operation, OperationType, TokenAccount } from "@ledgerhq/types-live";
|
|
3
|
+
import type { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets";
|
|
3
4
|
import { encodeAccountId } from "@ledgerhq/coin-framework/account/index";
|
|
4
5
|
import { GetAccountShape, mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers";
|
|
5
6
|
import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
|
|
@@ -9,18 +10,27 @@ import {
|
|
|
9
10
|
getOperations,
|
|
10
11
|
type OperationInfo,
|
|
11
12
|
getPendingTransferProposals,
|
|
13
|
+
getEnabledInstrumentsCached,
|
|
14
|
+
getCalTokensCached,
|
|
12
15
|
} from "../network/gateway";
|
|
13
|
-
import { getBalance
|
|
16
|
+
import { getBalance } from "../common-logic/account/getBalance";
|
|
14
17
|
import coinConfig from "../config";
|
|
15
18
|
import resolver from "../signer";
|
|
16
19
|
import { CantonAccount, CantonResources, CantonSigner } from "../types";
|
|
17
20
|
import { isAccountOnboarded } from "./onboard";
|
|
18
21
|
import { isCantonAccountEmpty } from "../helpers";
|
|
22
|
+
import { getCryptoAssetsStore } from "@ledgerhq/cryptoassets/state";
|
|
23
|
+
import { buildSubAccounts } from "./buildSubAccounts";
|
|
24
|
+
|
|
25
|
+
const SEPARATOR = "____";
|
|
26
|
+
|
|
27
|
+
const getKey = (id: string, adminId: string) => `${id}${SEPARATOR}${adminId}`;
|
|
19
28
|
|
|
20
29
|
const txInfoToOperationAdapter =
|
|
21
30
|
(accountId: string, partyId: string) =>
|
|
22
31
|
(txInfo: OperationInfo): Operation => {
|
|
23
32
|
const {
|
|
33
|
+
asset: { instrumentId, instrumentAdmin },
|
|
24
34
|
transaction_hash,
|
|
25
35
|
uid,
|
|
26
36
|
block: { height, hash },
|
|
@@ -73,6 +83,8 @@ const txInfoToOperationAdapter =
|
|
|
73
83
|
extra: {
|
|
74
84
|
uid,
|
|
75
85
|
memo,
|
|
86
|
+
instrumentId,
|
|
87
|
+
instrumentAdmin,
|
|
76
88
|
},
|
|
77
89
|
};
|
|
78
90
|
|
|
@@ -87,6 +99,35 @@ const filterOperations = (
|
|
|
87
99
|
return transactions.map(txInfoToOperationAdapter(accountId, partyId));
|
|
88
100
|
};
|
|
89
101
|
|
|
102
|
+
export async function filterDisabledTokenAccounts(
|
|
103
|
+
currency: CryptoCurrency,
|
|
104
|
+
subAccounts: TokenAccount[] | undefined,
|
|
105
|
+
): Promise<TokenAccount[]> {
|
|
106
|
+
if (!subAccounts || subAccounts.length === 0) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let enabledInstruments: Set<string>;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const enabledList = await getEnabledInstrumentsCached(currency);
|
|
114
|
+
enabledInstruments = new Set(enabledList);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
// If API fails, hide all token accounts
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return subAccounts.filter(subAccount => {
|
|
121
|
+
const instrumentId = subAccount.token.contractAddress;
|
|
122
|
+
|
|
123
|
+
if (!instrumentId) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return enabledInstruments.has(instrumentId);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
90
131
|
export function makeGetAccountShape(
|
|
91
132
|
signerContext: SignerContext<CantonSigner>,
|
|
92
133
|
): GetAccountShape<CantonAccount> {
|
|
@@ -129,31 +170,96 @@ export function makeGetAccountShape(
|
|
|
129
170
|
? await getPendingTransferProposals(currency, xpubOrAddress)
|
|
130
171
|
: [];
|
|
131
172
|
|
|
132
|
-
|
|
133
|
-
|
|
173
|
+
// Aggregate all balances by instrument (unlocked + locked)
|
|
174
|
+
const aggregatedBalances = new Map<
|
|
175
|
+
string,
|
|
176
|
+
{
|
|
177
|
+
unlockedBalance: bigint;
|
|
178
|
+
lockedBalance: bigint;
|
|
179
|
+
utxoCount: number;
|
|
180
|
+
token: TokenCurrency | null;
|
|
181
|
+
adminId: string;
|
|
182
|
+
}
|
|
183
|
+
>();
|
|
184
|
+
|
|
185
|
+
const proposalInstrumentKeys = new Set(
|
|
186
|
+
pendingTransferProposals.map(proposal =>
|
|
187
|
+
getKey(proposal.instrument_id, proposal.instrument_admin),
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
for (const key of proposalInstrumentKeys) {
|
|
191
|
+
if (aggregatedBalances.has(key)) continue;
|
|
192
|
+
|
|
193
|
+
const [instrumentId, adminId] = key.split(SEPARATOR);
|
|
194
|
+
balances.push({
|
|
195
|
+
value: 0n,
|
|
196
|
+
locked: 0n,
|
|
197
|
+
utxoCount: 0,
|
|
198
|
+
instrumentId,
|
|
199
|
+
adminId,
|
|
200
|
+
asset: { type: "token", assetReference: instrumentId },
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const tokensByKey = new Map<string, TokenCurrency>();
|
|
205
|
+
for await (const balance of balances) {
|
|
206
|
+
const token = await getCryptoAssetsStore().findTokenByAddressInCurrency(
|
|
207
|
+
balance.adminId,
|
|
208
|
+
currency.id,
|
|
209
|
+
);
|
|
210
|
+
if (!token) continue;
|
|
211
|
+
tokensByKey.set(getKey(balance.instrumentId, balance.adminId), token);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for await (const balance of balances) {
|
|
215
|
+
const isNative = balance.instrumentId === nativeInstrumentId;
|
|
216
|
+
// Use just instrumentId for native (no admin), composite key for tokens
|
|
217
|
+
const balanceKey = isNative
|
|
218
|
+
? nativeInstrumentId
|
|
219
|
+
: getKey(balance.instrumentId, balance.adminId);
|
|
220
|
+
const token: TokenCurrency | null = isNative ? null : tokensByKey.get(balanceKey) ?? null;
|
|
221
|
+
|
|
222
|
+
const existing = aggregatedBalances.get(balanceKey);
|
|
223
|
+
if (existing) {
|
|
134
224
|
if (balance.locked) {
|
|
135
|
-
|
|
225
|
+
existing.lockedBalance += balance.value;
|
|
136
226
|
} else {
|
|
137
|
-
|
|
227
|
+
existing.unlockedBalance += balance.value;
|
|
138
228
|
}
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
229
|
+
existing.utxoCount += balance.utxoCount;
|
|
230
|
+
} else {
|
|
231
|
+
aggregatedBalances.set(balanceKey, {
|
|
232
|
+
unlockedBalance: balance.locked ? 0n : balance.value,
|
|
233
|
+
lockedBalance: balance.locked ? balance.value : 0n,
|
|
234
|
+
utxoCount: balance.utxoCount,
|
|
235
|
+
token,
|
|
236
|
+
adminId: balance.adminId,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
143
240
|
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
);
|
|
241
|
+
// Find native balance (token is null for native)
|
|
242
|
+
const nativeBalance = Array.from(aggregatedBalances.values()).find(data => data.token === null);
|
|
243
|
+
const unlockedAmount = new BigNumber((nativeBalance?.unlockedBalance ?? 0n).toString());
|
|
244
|
+
const lockedAmount = new BigNumber((nativeBalance?.lockedBalance ?? 0n).toString());
|
|
148
245
|
const totalBalance = unlockedAmount.plus(lockedAmount);
|
|
149
246
|
const reserveMin = new BigNumber(coinConfig.getCoinConfig(currency).minReserve || 0);
|
|
150
|
-
const spendableBalance = BigNumber.max(0,
|
|
247
|
+
const spendableBalance = BigNumber.max(0, unlockedAmount.minus(reserveMin));
|
|
151
248
|
|
|
152
249
|
const instrumentUtxoCounts: Record<string, number> = {};
|
|
153
|
-
for (const [
|
|
154
|
-
instrumentUtxoCounts[
|
|
250
|
+
for (const [key, data] of aggregatedBalances) {
|
|
251
|
+
instrumentUtxoCounts[key] = data.utxoCount;
|
|
155
252
|
}
|
|
156
253
|
|
|
254
|
+
const tokenBalances = Array.from(aggregatedBalances.entries())
|
|
255
|
+
.filter(([, data]) => data.token !== null)
|
|
256
|
+
.map(([, { unlockedBalance, lockedBalance, token, adminId }]) => ({
|
|
257
|
+
totalBalance: unlockedBalance + lockedBalance,
|
|
258
|
+
spendableBalance: unlockedBalance,
|
|
259
|
+
token: token!,
|
|
260
|
+
adminId,
|
|
261
|
+
}));
|
|
262
|
+
|
|
157
263
|
let operations: Operation[] = [];
|
|
158
264
|
if (xpubOrAddress) {
|
|
159
265
|
const oldOperations = initialAccount?.operations || [];
|
|
@@ -166,26 +272,49 @@ export function makeGetAccountShape(
|
|
|
166
272
|
operations = mergeOps(oldOperations, newOperations);
|
|
167
273
|
}
|
|
168
274
|
|
|
275
|
+
// Filter main account operations (native instrument only)
|
|
276
|
+
const mainAccountOperations = operations.filter(op => {
|
|
277
|
+
const extra = op.extra as { instrumentId?: string };
|
|
278
|
+
return extra?.instrumentId === nativeInstrumentId;
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Build sub-accounts for tokens with their filtered operations
|
|
282
|
+
|
|
283
|
+
const calTokens = await getCalTokensCached(currency);
|
|
284
|
+
|
|
285
|
+
const subAccounts = buildSubAccounts({
|
|
286
|
+
accountId,
|
|
287
|
+
tokenBalances,
|
|
288
|
+
existingSubAccounts: initialAccount?.subAccounts ?? [],
|
|
289
|
+
allOperations: operations,
|
|
290
|
+
pendingTransferProposals,
|
|
291
|
+
calTokens,
|
|
292
|
+
});
|
|
293
|
+
|
|
169
294
|
const cantonResources: CantonResources = {
|
|
170
295
|
isOnboarded,
|
|
171
296
|
instrumentUtxoCounts,
|
|
172
|
-
pendingTransferProposals
|
|
297
|
+
pendingTransferProposals: pendingTransferProposals.filter(
|
|
298
|
+
proposal => proposal.instrument_id === nativeInstrumentId,
|
|
299
|
+
),
|
|
173
300
|
...(publicKey && { publicKey }),
|
|
174
301
|
xpub: xpubOrAddress,
|
|
175
302
|
};
|
|
176
303
|
|
|
304
|
+
const filteredSubAccounts = await filterDisabledTokenAccounts(currency, subAccounts);
|
|
305
|
+
|
|
177
306
|
const used = !isCantonAccountEmpty({
|
|
178
|
-
operationsCount:
|
|
307
|
+
operationsCount: mainAccountOperations.length,
|
|
179
308
|
balance: totalBalance,
|
|
180
|
-
subAccounts:
|
|
309
|
+
subAccounts: filteredSubAccounts,
|
|
181
310
|
cantonResources,
|
|
182
311
|
});
|
|
183
312
|
|
|
184
313
|
const blockHeight = await getLedgerEnd(currency);
|
|
185
314
|
|
|
186
315
|
const creationDate =
|
|
187
|
-
|
|
188
|
-
? new Date(Math.min(...
|
|
316
|
+
mainAccountOperations.length > 0
|
|
317
|
+
? new Date(Math.min(...mainAccountOperations.map(op => op.date.getTime())))
|
|
189
318
|
: new Date();
|
|
190
319
|
|
|
191
320
|
const shape = {
|
|
@@ -197,9 +326,10 @@ export function makeGetAccountShape(
|
|
|
197
326
|
lastSyncDate: new Date(),
|
|
198
327
|
freshAddress: address,
|
|
199
328
|
seedIdentifier: address,
|
|
200
|
-
operations,
|
|
201
|
-
operationsCount:
|
|
329
|
+
operations: mainAccountOperations,
|
|
330
|
+
operationsCount: mainAccountOperations.length,
|
|
202
331
|
spendableBalance,
|
|
332
|
+
subAccounts: filteredSubAccounts,
|
|
203
333
|
xpub: xpubOrAddress,
|
|
204
334
|
used,
|
|
205
335
|
cantonResources,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { isRecipientValid } from "../common-logic";
|
|
2
|
+
import { validateAddress } from "./validateAddress";
|
|
3
|
+
|
|
4
|
+
jest.mock("../common-logic");
|
|
5
|
+
|
|
6
|
+
describe("validateAddress", () => {
|
|
7
|
+
const mockedIsRecipientValid = jest.mocked(isRecipientValid);
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
mockedIsRecipientValid.mockClear();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it.each([true, false])(
|
|
14
|
+
"should call correctly isRecipientValid and return the correct value",
|
|
15
|
+
async (expectedValue: boolean) => {
|
|
16
|
+
mockedIsRecipientValid.mockReturnValueOnce(expectedValue);
|
|
17
|
+
|
|
18
|
+
const result = await validateAddress("some random address", {});
|
|
19
|
+
expect(result).toEqual(expectedValue);
|
|
20
|
+
|
|
21
|
+
expect(mockedIsRecipientValid).toHaveBeenCalledTimes(1);
|
|
22
|
+
expect(mockedIsRecipientValid).toHaveBeenCalledWith("some random address");
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { isRecipientValid } from "../common-logic";
|
|
2
|
+
import { AddressValidationCurrencyParameters } from "@ledgerhq/types-live";
|
|
3
|
+
|
|
4
|
+
export async function validateAddress(
|
|
5
|
+
address: string,
|
|
6
|
+
_parameters: Partial<AddressValidationCurrencyParameters>,
|
|
7
|
+
): Promise<boolean> {
|
|
8
|
+
return isRecipientValid(address);
|
|
9
|
+
}
|
|
@@ -6,6 +6,7 @@ import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
|
|
|
6
6
|
export type CantonBalance = Balance & {
|
|
7
7
|
utxoCount: number;
|
|
8
8
|
instrumentId: string;
|
|
9
|
+
adminId: string;
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
const useGateway = (currency: CryptoCurrency) =>
|
|
@@ -23,6 +24,7 @@ function adaptInstrument(currency: CryptoCurrency, instrument: InstrumentBalance
|
|
|
23
24
|
: { type: "token", assetReference: instrument.instrument_id },
|
|
24
25
|
utxoCount: instrument.utxo_count,
|
|
25
26
|
instrumentId: instrument.instrument_id,
|
|
27
|
+
adminId: instrument.admin_id,
|
|
26
28
|
};
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -15,6 +15,7 @@ export async function craftTransaction(
|
|
|
15
15
|
},
|
|
16
16
|
transaction: {
|
|
17
17
|
recipient?: string;
|
|
18
|
+
instrumentAdmin?: string;
|
|
18
19
|
amount: BigNumber;
|
|
19
20
|
tokenId: string;
|
|
20
21
|
expireInSeconds: number;
|
|
@@ -33,6 +34,10 @@ export async function craftTransaction(
|
|
|
33
34
|
instrument_id: transaction.tokenId,
|
|
34
35
|
};
|
|
35
36
|
|
|
37
|
+
if (transaction.instrumentAdmin) {
|
|
38
|
+
params.instrument_admin = transaction.instrumentAdmin;
|
|
39
|
+
}
|
|
40
|
+
|
|
36
41
|
if (transaction.memo) {
|
|
37
42
|
params.reason = transaction.memo;
|
|
38
43
|
}
|
|
@@ -2,8 +2,12 @@ import {
|
|
|
2
2
|
getBalance,
|
|
3
3
|
isPartyAlreadyExists,
|
|
4
4
|
submitOnboarding,
|
|
5
|
+
getEnabledInstruments,
|
|
6
|
+
getEnabledInstrumentsCached,
|
|
7
|
+
clearEnabledInstrumentsCache,
|
|
5
8
|
type GetBalanceResponse,
|
|
6
9
|
type InstrumentBalance,
|
|
10
|
+
type InstrumentsResponse,
|
|
7
11
|
type OnboardingPrepareResponse,
|
|
8
12
|
} from "./gateway";
|
|
9
13
|
import type { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
|
|
@@ -126,3 +130,168 @@ describe("getBalance", () => {
|
|
|
126
130
|
await expect(getBalance(mockCurrency, "test-party-id")).rejects.toThrow(TopologyChangeError);
|
|
127
131
|
});
|
|
128
132
|
});
|
|
133
|
+
|
|
134
|
+
describe("getEnabledInstruments", () => {
|
|
135
|
+
const mockCurrency = {
|
|
136
|
+
id: "canton_network",
|
|
137
|
+
} as unknown as CryptoCurrency;
|
|
138
|
+
|
|
139
|
+
const mockNetwork = network as jest.MockedFunction<typeof network>;
|
|
140
|
+
|
|
141
|
+
beforeAll(() => {
|
|
142
|
+
coinConfig.setCoinConfig(() => ({
|
|
143
|
+
gatewayUrl: "https://canton-gateway-devnet.api.live.ledger-test.com",
|
|
144
|
+
nodeId: "test-node-id",
|
|
145
|
+
useGateway: true,
|
|
146
|
+
networkType: "devnet",
|
|
147
|
+
nativeInstrumentId: "Amulet",
|
|
148
|
+
status: {
|
|
149
|
+
type: "active",
|
|
150
|
+
},
|
|
151
|
+
}));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
beforeEach(() => {
|
|
155
|
+
jest.clearAllMocks();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should fetch and return enabled instruments from API", async () => {
|
|
159
|
+
const mockInstrumentsResponse: InstrumentsResponse = {
|
|
160
|
+
instruments: [
|
|
161
|
+
{ instrument_id: "Amulet", display_name: "Canton Coin" },
|
|
162
|
+
{ instrument_id: "0x1234567890abcdef", display_name: "Test Token 1" },
|
|
163
|
+
{ instrument_id: "0xabcdef1234567890", display_name: "Test Token 2" },
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
mockNetwork.mockResolvedValue({ data: mockInstrumentsResponse, status: 200 });
|
|
168
|
+
|
|
169
|
+
const result = await getEnabledInstruments(mockCurrency);
|
|
170
|
+
|
|
171
|
+
expect(result).toEqual(["Amulet", "0x1234567890abcdef", "0xabcdef1234567890"]);
|
|
172
|
+
expect(mockNetwork).toHaveBeenCalledWith(
|
|
173
|
+
expect.objectContaining({
|
|
174
|
+
method: "GET",
|
|
175
|
+
url: expect.stringContaining("/v1/node/test-node-id/instruments"),
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should return empty array on API failure (fail-safe)", async () => {
|
|
181
|
+
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation();
|
|
182
|
+
mockNetwork.mockRejectedValue(new Error("Network error"));
|
|
183
|
+
|
|
184
|
+
const result = await getEnabledInstruments(mockCurrency);
|
|
185
|
+
|
|
186
|
+
expect(result).toEqual([]);
|
|
187
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
188
|
+
"Failed to fetch enabled instruments:",
|
|
189
|
+
expect.any(Error),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
consoleErrorSpy.mockRestore();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("should return empty array when API returns empty instruments list", async () => {
|
|
196
|
+
const mockEmptyResponse: InstrumentsResponse = {
|
|
197
|
+
instruments: [],
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
mockNetwork.mockResolvedValue({ data: mockEmptyResponse, status: 200 });
|
|
201
|
+
|
|
202
|
+
const result = await getEnabledInstruments(mockCurrency);
|
|
203
|
+
|
|
204
|
+
expect(result).toEqual([]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("should handle malformed API response gracefully", async () => {
|
|
208
|
+
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation();
|
|
209
|
+
// API returns data without instruments property
|
|
210
|
+
mockNetwork.mockResolvedValue({ data: { invalid: "response" }, status: 200 });
|
|
211
|
+
|
|
212
|
+
const result = await getEnabledInstruments(mockCurrency);
|
|
213
|
+
|
|
214
|
+
expect(result).toEqual([]);
|
|
215
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
216
|
+
|
|
217
|
+
consoleErrorSpy.mockRestore();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("getEnabledInstrumentsCached", () => {
|
|
222
|
+
const mockCurrency = {
|
|
223
|
+
id: "canton_network",
|
|
224
|
+
} as unknown as CryptoCurrency;
|
|
225
|
+
|
|
226
|
+
const mockNetwork = network as jest.MockedFunction<typeof network>;
|
|
227
|
+
|
|
228
|
+
beforeAll(() => {
|
|
229
|
+
coinConfig.setCoinConfig(() => ({
|
|
230
|
+
gatewayUrl: "https://canton-gateway-devnet.api.live.ledger-test.com",
|
|
231
|
+
nodeId: "test-node-id",
|
|
232
|
+
useGateway: true,
|
|
233
|
+
networkType: "devnet",
|
|
234
|
+
nativeInstrumentId: "Amulet",
|
|
235
|
+
status: {
|
|
236
|
+
type: "active",
|
|
237
|
+
},
|
|
238
|
+
}));
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
beforeEach(() => {
|
|
242
|
+
jest.clearAllMocks();
|
|
243
|
+
// Clear cache before each test
|
|
244
|
+
clearEnabledInstrumentsCache(mockCurrency);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should cache results and return cached value on subsequent calls", async () => {
|
|
248
|
+
const mockInstrumentsResponse: InstrumentsResponse = {
|
|
249
|
+
instruments: [{ instrument_id: "Amulet" }, { instrument_id: "0x1234567890abcdef" }],
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
mockNetwork.mockResolvedValue({ data: mockInstrumentsResponse, status: 200 });
|
|
253
|
+
|
|
254
|
+
// First call - should fetch from API
|
|
255
|
+
const result1 = await getEnabledInstrumentsCached(mockCurrency);
|
|
256
|
+
expect(result1).toEqual(["Amulet", "0x1234567890abcdef"]);
|
|
257
|
+
expect(mockNetwork).toHaveBeenCalledTimes(1);
|
|
258
|
+
|
|
259
|
+
// Second call - should return cached value
|
|
260
|
+
const result2 = await getEnabledInstrumentsCached(mockCurrency);
|
|
261
|
+
expect(result2).toEqual(["Amulet", "0x1234567890abcdef"]);
|
|
262
|
+
expect(mockNetwork).toHaveBeenCalledTimes(1); // Still only 1 call
|
|
263
|
+
|
|
264
|
+
// Third call - should still return cached value
|
|
265
|
+
const result3 = await getEnabledInstrumentsCached(mockCurrency);
|
|
266
|
+
expect(result3).toEqual(["Amulet", "0x1234567890abcdef"]);
|
|
267
|
+
expect(mockNetwork).toHaveBeenCalledTimes(1); // Still only 1 call
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("should fetch fresh data after cache is cleared", async () => {
|
|
271
|
+
const mockResponse1: InstrumentsResponse = {
|
|
272
|
+
instruments: [{ instrument_id: "Amulet" }],
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const mockResponse2: InstrumentsResponse = {
|
|
276
|
+
instruments: [{ instrument_id: "Amulet" }, { instrument_id: "0xnewtoken" }],
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
mockNetwork.mockResolvedValueOnce({ data: mockResponse1, status: 200 });
|
|
280
|
+
|
|
281
|
+
// First call
|
|
282
|
+
const result1 = await getEnabledInstrumentsCached(mockCurrency);
|
|
283
|
+
expect(result1).toEqual(["Amulet"]);
|
|
284
|
+
expect(mockNetwork).toHaveBeenCalledTimes(1);
|
|
285
|
+
|
|
286
|
+
// Clear cache
|
|
287
|
+
clearEnabledInstrumentsCache(mockCurrency);
|
|
288
|
+
|
|
289
|
+
// Set up new response
|
|
290
|
+
mockNetwork.mockResolvedValueOnce({ data: mockResponse2, status: 200 });
|
|
291
|
+
|
|
292
|
+
// Second call after clear - should fetch fresh data
|
|
293
|
+
const result2 = await getEnabledInstrumentsCached(mockCurrency);
|
|
294
|
+
expect(result2).toEqual(["Amulet", "0xnewtoken"]);
|
|
295
|
+
expect(mockNetwork).toHaveBeenCalledTimes(2);
|
|
296
|
+
});
|
|
297
|
+
});
|
package/src/network/gateway.ts
CHANGED
|
@@ -69,6 +69,7 @@ export type PrepareTransferRequest = {
|
|
|
69
69
|
recipient: string;
|
|
70
70
|
execute_before_secs: number;
|
|
71
71
|
instrument_id: string;
|
|
72
|
+
instrument_admin?: string;
|
|
72
73
|
reason?: string;
|
|
73
74
|
};
|
|
74
75
|
|
|
@@ -87,6 +88,7 @@ export type TransferProposal = {
|
|
|
87
88
|
receiver: string;
|
|
88
89
|
amount: string;
|
|
89
90
|
instrument_id: string;
|
|
91
|
+
instrument_admin: string;
|
|
90
92
|
memo: string;
|
|
91
93
|
expires_at_micros: number;
|
|
92
94
|
};
|
|
@@ -112,6 +114,13 @@ type TransactionSubmitRequest = {
|
|
|
112
114
|
|
|
113
115
|
type TransactionSubmitResponse = { update_id: string };
|
|
114
116
|
|
|
117
|
+
type Asset = {
|
|
118
|
+
instrumentAdmin: string;
|
|
119
|
+
instrumentId: string;
|
|
120
|
+
issuer: string | null;
|
|
121
|
+
type: "token" | "native";
|
|
122
|
+
};
|
|
123
|
+
|
|
115
124
|
export type GetBalanceResponse =
|
|
116
125
|
| {
|
|
117
126
|
at_round: number;
|
|
@@ -228,10 +237,7 @@ export type OperationInfo =
|
|
|
228
237
|
type: string;
|
|
229
238
|
};
|
|
230
239
|
};
|
|
231
|
-
asset:
|
|
232
|
-
type: "token";
|
|
233
|
-
issuer: string;
|
|
234
|
-
};
|
|
240
|
+
asset: Asset;
|
|
235
241
|
details: {
|
|
236
242
|
operationType: OperationType;
|
|
237
243
|
};
|
|
@@ -273,10 +279,7 @@ export type OperationInfo =
|
|
|
273
279
|
type: string;
|
|
274
280
|
};
|
|
275
281
|
};
|
|
276
|
-
asset:
|
|
277
|
-
type: "native";
|
|
278
|
-
issuer: null;
|
|
279
|
-
};
|
|
282
|
+
asset: Asset;
|
|
280
283
|
details: {
|
|
281
284
|
operationType: OperationType;
|
|
282
285
|
};
|
|
@@ -318,10 +321,7 @@ export type OperationInfo =
|
|
|
318
321
|
type: string;
|
|
319
322
|
};
|
|
320
323
|
};
|
|
321
|
-
asset:
|
|
322
|
-
type: "native";
|
|
323
|
-
issuer: null;
|
|
324
|
-
};
|
|
324
|
+
asset: Asset;
|
|
325
325
|
details: {
|
|
326
326
|
operationType: OperationType;
|
|
327
327
|
};
|
|
@@ -426,6 +426,45 @@ export function clearIsTopologyChangeRequiredCache(currency: CryptoCurrency, pub
|
|
|
426
426
|
isTopologyChangeRequiredCached.clear(cacheKey);
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
+
export type InstrumentInfo = {
|
|
430
|
+
instrument_id: string;
|
|
431
|
+
display_name?: string;
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
export type InstrumentsResponse = {
|
|
435
|
+
instruments: InstrumentInfo[];
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
export async function getEnabledInstruments(currency: CryptoCurrency): Promise<string[]> {
|
|
439
|
+
try {
|
|
440
|
+
const { data } = await gatewayNetwork<InstrumentsResponse>({
|
|
441
|
+
method: "GET",
|
|
442
|
+
url: `${getGatewayUrl(currency)}/v1/node/${getNodeId(currency)}/instruments`,
|
|
443
|
+
});
|
|
444
|
+
return data.instruments.map(instrument => instrument.instrument_id);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
// If API fails, return empty array (fail-safe: only native instrument will work)
|
|
447
|
+
console.error("Failed to fetch enabled instruments:", error);
|
|
448
|
+
return [];
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const getEnabledInstrumentsCacheKey = (currency: CryptoCurrency): string => {
|
|
453
|
+
const nodeId = getNodeId(currency);
|
|
454
|
+
return `instruments_${nodeId}`;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
export const getEnabledInstrumentsCached = makeLRUCache(
|
|
458
|
+
getEnabledInstruments,
|
|
459
|
+
getEnabledInstrumentsCacheKey,
|
|
460
|
+
minutes(15),
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
export function clearEnabledInstrumentsCache(currency: CryptoCurrency): void {
|
|
464
|
+
const cacheKey = getEnabledInstrumentsCacheKey(currency);
|
|
465
|
+
getEnabledInstrumentsCached.clear(cacheKey);
|
|
466
|
+
}
|
|
467
|
+
|
|
429
468
|
export async function submitOnboarding(
|
|
430
469
|
currency: CryptoCurrency,
|
|
431
470
|
publicKey: string,
|
|
@@ -704,3 +743,35 @@ export async function getPendingTransferProposals(currency: CryptoCurrency, part
|
|
|
704
743
|
});
|
|
705
744
|
return data;
|
|
706
745
|
}
|
|
746
|
+
|
|
747
|
+
// CAL API types
|
|
748
|
+
export type CalToken = {
|
|
749
|
+
id: string;
|
|
750
|
+
name: string;
|
|
751
|
+
ticker: string;
|
|
752
|
+
network: string;
|
|
753
|
+
contract_address: string;
|
|
754
|
+
token_identifier: string;
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Fetch Canton tokens from CAL service and create a map of id -> token_identifier
|
|
759
|
+
*/
|
|
760
|
+
async function getCalTokens(currency: CryptoCurrency): Promise<Map<string, string>> {
|
|
761
|
+
const calUrl = getEnv("CAL_SERVICE_URL");
|
|
762
|
+
const { data: calTokens } = await gatewayNetwork<CalToken[]>({
|
|
763
|
+
method: "GET",
|
|
764
|
+
url: `${calUrl}/v1/tokens?network=${currency.id}&output=id,name,ticker,network,contract_address,token_identifier,units,standard`,
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// Map id -> token_identifier
|
|
768
|
+
const tokenIdentifierMap = new Map<string, string>();
|
|
769
|
+
for (const token of calTokens) {
|
|
770
|
+
tokenIdentifierMap.set(token.id, token.token_identifier);
|
|
771
|
+
}
|
|
772
|
+
return tokenIdentifierMap;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const getCalTokensCacheKey = (currency: CryptoCurrency): string => currency.id;
|
|
776
|
+
|
|
777
|
+
export const getCalTokensCached = makeLRUCache(getCalTokens, getCalTokensCacheKey, minutes(30));
|
package/src/types/bridge.ts
CHANGED
|
@@ -62,6 +62,7 @@ export type Transaction = TransactionCommon & {
|
|
|
62
62
|
memo?: string;
|
|
63
63
|
tokenId: string;
|
|
64
64
|
expireInSeconds?: number;
|
|
65
|
+
instrumentAdmin?: string;
|
|
65
66
|
};
|
|
66
67
|
|
|
67
68
|
export type TransactionRaw = TransactionCommonRaw & {
|
|
@@ -70,6 +71,7 @@ export type TransactionRaw = TransactionCommonRaw & {
|
|
|
70
71
|
memo?: string;
|
|
71
72
|
tokenId: string;
|
|
72
73
|
expireInSeconds?: number;
|
|
74
|
+
instrumentAdmin?: string;
|
|
73
75
|
};
|
|
74
76
|
|
|
75
77
|
export type TransactionStatus = TransactionStatusCommon;
|
package/tsconfig.json
CHANGED
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
"declaration": true,
|
|
5
5
|
"declarationMap": true,
|
|
6
6
|
"downlevelIteration": true,
|
|
7
|
-
"lib": [
|
|
7
|
+
"lib": [
|
|
8
|
+
"es2020",
|
|
9
|
+
"dom"
|
|
10
|
+
],
|
|
8
11
|
"outDir": "lib",
|
|
9
12
|
"exactOptionalPropertyTypes": true
|
|
10
13
|
},
|
|
11
|
-
"include": [
|
|
14
|
+
"include": [
|
|
15
|
+
"src/**/*"
|
|
16
|
+
]
|
|
12
17
|
}
|