@ledgerhq/coin-stellar 0.2.0-nightly.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +20 -0
- package/.turbo/turbo-build.log +4 -0
- package/.unimportedrc.json +35 -0
- package/CHANGELOG.md +13 -0
- package/LICENSE.txt +21 -0
- package/jest.config.js +8 -0
- package/lib/bridge/bridge.integration.test.d.ts +4 -0
- package/lib/bridge/bridge.integration.test.d.ts.map +1 -0
- package/lib/bridge/bridge.integration.test.js +317 -0
- package/lib/bridge/bridge.integration.test.js.map +1 -0
- package/lib/bridge/index.d.ts +10 -0
- package/lib/bridge/index.d.ts.map +1 -0
- package/lib/bridge/index.js +72 -0
- package/lib/bridge/index.js.map +1 -0
- package/lib/broadcast.d.ts +9 -0
- package/lib/broadcast.d.ts.map +1 -0
- package/lib/broadcast.js +26 -0
- package/lib/broadcast.js.map +1 -0
- package/lib/buildOptimisticOperation.d.ts +4 -0
- package/lib/buildOptimisticOperation.d.ts.map +1 -0
- package/lib/buildOptimisticOperation.js +72 -0
- package/lib/buildOptimisticOperation.js.map +1 -0
- package/lib/buildTransaction.d.ts +10 -0
- package/lib/buildTransaction.d.ts.map +1 -0
- package/lib/buildTransaction.js +97 -0
- package/lib/buildTransaction.js.map +1 -0
- package/lib/cli.d.ts +38 -0
- package/lib/cli.d.ts.map +1 -0
- package/lib/cli.js +83 -0
- package/lib/cli.js.map +1 -0
- package/lib/config.d.ts +5 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +17 -0
- package/lib/config.js.map +1 -0
- package/lib/createTransaction.d.ts +10 -0
- package/lib/createTransaction.d.ts.map +1 -0
- package/lib/createTransaction.js +26 -0
- package/lib/createTransaction.js.map +1 -0
- package/lib/deviceTransactionConfig.d.ts +24 -0
- package/lib/deviceTransactionConfig.d.ts.map +1 -0
- package/lib/deviceTransactionConfig.js +41 -0
- package/lib/deviceTransactionConfig.js.map +1 -0
- package/lib/errors.d.ts +37 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +17 -0
- package/lib/errors.js.map +1 -0
- package/lib/estimateMaxSpendable.d.ts +5 -0
- package/lib/estimateMaxSpendable.d.ts.map +1 -0
- package/lib/estimateMaxSpendable.js +32 -0
- package/lib/estimateMaxSpendable.js.map +1 -0
- package/lib/getTransactionStatus.d.ts +5 -0
- package/lib/getTransactionStatus.d.ts.map +1 -0
- package/lib/getTransactionStatus.js +170 -0
- package/lib/getTransactionStatus.js.map +1 -0
- package/lib/hw-getAddress.d.ts +6 -0
- package/lib/hw-getAddress.d.ts.map +1 -0
- package/lib/hw-getAddress.js +28 -0
- package/lib/hw-getAddress.js.map +1 -0
- package/lib/logic.d.ts +36 -0
- package/lib/logic.d.ts.map +1 -0
- package/lib/logic.js +289 -0
- package/lib/logic.js.map +1 -0
- package/lib/network/horizon.d.ts +71 -0
- package/lib/network/horizon.d.ts.map +1 -0
- package/lib/network/horizon.js +300 -0
- package/lib/network/horizon.js.map +1 -0
- package/lib/network/index.d.ts +2 -0
- package/lib/network/index.d.ts.map +1 -0
- package/lib/network/index.js +20 -0
- package/lib/network/index.js.map +1 -0
- package/lib/prepareTransaction.d.ts +5 -0
- package/lib/prepareTransaction.d.ts.map +1 -0
- package/lib/prepareTransaction.js +41 -0
- package/lib/prepareTransaction.js.map +1 -0
- package/lib/signOperation.d.ts +6 -0
- package/lib/signOperation.d.ts.map +1 -0
- package/lib/signOperation.js +56 -0
- package/lib/signOperation.js.map +1 -0
- package/lib/specs.d.ts +7 -0
- package/lib/specs.d.ts.map +1 -0
- package/lib/specs.js +234 -0
- package/lib/specs.js.map +1 -0
- package/lib/speculos-deviceActions.d.ts +4 -0
- package/lib/speculos-deviceActions.d.ts.map +1 -0
- package/lib/speculos-deviceActions.js +99 -0
- package/lib/speculos-deviceActions.js.map +1 -0
- package/lib/synchronization.d.ts +4 -0
- package/lib/synchronization.d.ts.map +1 -0
- package/lib/synchronization.js +73 -0
- package/lib/synchronization.js.map +1 -0
- package/lib/tokens.d.ts +12 -0
- package/lib/tokens.d.ts.map +1 -0
- package/lib/tokens.js +58 -0
- package/lib/tokens.js.map +1 -0
- package/lib/transaction.d.ts +15 -0
- package/lib/transaction.d.ts.map +1 -0
- package/lib/transaction.js +63 -0
- package/lib/transaction.js.map +1 -0
- package/lib/types/index.d.ts +89 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +26 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/signer.d.ts +10 -0
- package/lib/types/signer.d.ts.map +1 -0
- package/lib/types/signer.js +3 -0
- package/lib/types/signer.js.map +1 -0
- package/lib-es/bridge/bridge.integration.test.d.ts +4 -0
- package/lib-es/bridge/bridge.integration.test.d.ts.map +1 -0
- package/lib-es/bridge/bridge.integration.test.js +311 -0
- package/lib-es/bridge/bridge.integration.test.js.map +1 -0
- package/lib-es/bridge/index.d.ts +10 -0
- package/lib-es/bridge/index.d.ts.map +1 -0
- package/lib-es/bridge/index.js +65 -0
- package/lib-es/bridge/index.js.map +1 -0
- package/lib-es/broadcast.d.ts +9 -0
- package/lib-es/broadcast.d.ts.map +1 -0
- package/lib-es/broadcast.js +22 -0
- package/lib-es/broadcast.js.map +1 -0
- package/lib-es/buildOptimisticOperation.d.ts +4 -0
- package/lib-es/buildOptimisticOperation.d.ts.map +1 -0
- package/lib-es/buildOptimisticOperation.js +65 -0
- package/lib-es/buildOptimisticOperation.js.map +1 -0
- package/lib-es/buildTransaction.d.ts +10 -0
- package/lib-es/buildTransaction.d.ts.map +1 -0
- package/lib-es/buildTransaction.js +90 -0
- package/lib-es/buildTransaction.js.map +1 -0
- package/lib-es/cli.d.ts +38 -0
- package/lib-es/cli.d.ts.map +1 -0
- package/lib-es/cli.js +77 -0
- package/lib-es/cli.js.map +1 -0
- package/lib-es/config.d.ts +5 -0
- package/lib-es/config.d.ts.map +1 -0
- package/lib-es/config.js +12 -0
- package/lib-es/config.js.map +1 -0
- package/lib-es/createTransaction.d.ts +10 -0
- package/lib-es/createTransaction.d.ts.map +1 -0
- package/lib-es/createTransaction.js +22 -0
- package/lib-es/createTransaction.js.map +1 -0
- package/lib-es/deviceTransactionConfig.d.ts +24 -0
- package/lib-es/deviceTransactionConfig.d.ts.map +1 -0
- package/lib-es/deviceTransactionConfig.js +39 -0
- package/lib-es/deviceTransactionConfig.js.map +1 -0
- package/lib-es/errors.d.ts +37 -0
- package/lib-es/errors.d.ts.map +1 -0
- package/lib-es/errors.js +14 -0
- package/lib-es/errors.js.map +1 -0
- package/lib-es/estimateMaxSpendable.d.ts +5 -0
- package/lib-es/estimateMaxSpendable.d.ts.map +1 -0
- package/lib-es/estimateMaxSpendable.js +25 -0
- package/lib-es/estimateMaxSpendable.js.map +1 -0
- package/lib-es/getTransactionStatus.d.ts +5 -0
- package/lib-es/getTransactionStatus.d.ts.map +1 -0
- package/lib-es/getTransactionStatus.js +166 -0
- package/lib-es/getTransactionStatus.js.map +1 -0
- package/lib-es/hw-getAddress.d.ts +6 -0
- package/lib-es/hw-getAddress.d.ts.map +1 -0
- package/lib-es/hw-getAddress.js +26 -0
- package/lib-es/hw-getAddress.js.map +1 -0
- package/lib-es/logic.d.ts +36 -0
- package/lib-es/logic.d.ts.map +1 -0
- package/lib-es/logic.js +274 -0
- package/lib-es/logic.js.map +1 -0
- package/lib-es/network/horizon.d.ts +71 -0
- package/lib-es/network/horizon.d.ts.map +1 -0
- package/lib-es/network/horizon.js +285 -0
- package/lib-es/network/horizon.js.map +1 -0
- package/lib-es/network/index.d.ts +2 -0
- package/lib-es/network/index.d.ts.map +1 -0
- package/lib-es/network/index.js +2 -0
- package/lib-es/network/index.js.map +1 -0
- package/lib-es/prepareTransaction.d.ts +5 -0
- package/lib-es/prepareTransaction.d.ts.map +1 -0
- package/lib-es/prepareTransaction.js +34 -0
- package/lib-es/prepareTransaction.js.map +1 -0
- package/lib-es/signOperation.d.ts +6 -0
- package/lib-es/signOperation.d.ts.map +1 -0
- package/lib-es/signOperation.js +52 -0
- package/lib-es/signOperation.js.map +1 -0
- package/lib-es/specs.d.ts +7 -0
- package/lib-es/specs.d.ts.map +1 -0
- package/lib-es/specs.js +229 -0
- package/lib-es/specs.js.map +1 -0
- package/lib-es/speculos-deviceActions.d.ts +4 -0
- package/lib-es/speculos-deviceActions.d.ts.map +1 -0
- package/lib-es/speculos-deviceActions.js +96 -0
- package/lib-es/speculos-deviceActions.js.map +1 -0
- package/lib-es/synchronization.d.ts +4 -0
- package/lib-es/synchronization.d.ts.map +1 -0
- package/lib-es/synchronization.js +69 -0
- package/lib-es/synchronization.js.map +1 -0
- package/lib-es/tokens.d.ts +12 -0
- package/lib-es/tokens.d.ts.map +1 -0
- package/lib-es/tokens.js +50 -0
- package/lib-es/tokens.js.map +1 -0
- package/lib-es/transaction.d.ts +15 -0
- package/lib-es/transaction.d.ts.map +1 -0
- package/lib-es/transaction.js +59 -0
- package/lib-es/transaction.js.map +1 -0
- package/lib-es/types/index.d.ts +89 -0
- package/lib-es/types/index.d.ts.map +1 -0
- package/lib-es/types/index.js +9 -0
- package/lib-es/types/index.js.map +1 -0
- package/lib-es/types/signer.d.ts +10 -0
- package/lib-es/types/signer.d.ts.map +1 -0
- package/lib-es/types/signer.js +2 -0
- package/lib-es/types/signer.js.map +1 -0
- package/package.json +80 -0
- package/src/bridge/bridge.integration.test.ts +373 -0
- package/src/bridge/index.ts +77 -0
- package/src/broadcast.ts +20 -0
- package/src/buildOptimisticOperation.ts +63 -0
- package/src/buildTransaction.ts +106 -0
- package/src/cli.ts +107 -0
- package/src/config.ts +18 -0
- package/src/createTransaction.ts +25 -0
- package/src/deviceTransactionConfig.ts +75 -0
- package/src/errors.ts +20 -0
- package/src/estimateMaxSpendable.ts +29 -0
- package/src/getTransactionStatus.ts +207 -0
- package/src/hw-getAddress.ts +24 -0
- package/src/logic.ts +371 -0
- package/src/network/horizon.ts +352 -0
- package/src/network/index.ts +17 -0
- package/src/prepareTransaction.ts +35 -0
- package/src/signOperation.ts +58 -0
- package/src/specs.ts +290 -0
- package/src/speculos-deviceActions.ts +117 -0
- package/src/synchronization.ts +80 -0
- package/src/tokens.ts +98 -0
- package/src/transaction.ts +99 -0
- package/src/types/index.ts +112 -0
- package/src/types/signer.ts +9 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export {
|
|
2
|
+
fetchAccount,
|
|
3
|
+
fetchOperations,
|
|
4
|
+
fetchBaseFee,
|
|
5
|
+
fetchSequence,
|
|
6
|
+
fetchSigners,
|
|
7
|
+
fetchAccountNetworkInfo,
|
|
8
|
+
broadcastTransaction,
|
|
9
|
+
buildPaymentOperation,
|
|
10
|
+
buildCreateAccountOperation,
|
|
11
|
+
buildChangeTrustOperation,
|
|
12
|
+
buildTransactionBuilder,
|
|
13
|
+
loadAccount,
|
|
14
|
+
BASE_RESERVE,
|
|
15
|
+
BASE_RESERVE_MIN_COUNT,
|
|
16
|
+
MIN_BALANCE,
|
|
17
|
+
} from "./horizon";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import invariant from "invariant";
|
|
2
|
+
import type { AccountBridge } from "@ledgerhq/types-live";
|
|
3
|
+
import { defaultUpdateTransaction } from "@ledgerhq/coin-framework/bridge/jsHelpers";
|
|
4
|
+
import { fetchAccountNetworkInfo } from "./network";
|
|
5
|
+
import { getAssetCodeIssuer } from "./logic";
|
|
6
|
+
import type { Transaction } from "./types";
|
|
7
|
+
|
|
8
|
+
export const prepareTransaction: AccountBridge<Transaction>["prepareTransaction"] = async (
|
|
9
|
+
account,
|
|
10
|
+
transaction,
|
|
11
|
+
) => {
|
|
12
|
+
const networkInfo = transaction.networkInfo || (await fetchAccountNetworkInfo(account));
|
|
13
|
+
invariant(networkInfo.family === "stellar", "stellar networkInfo expected");
|
|
14
|
+
const fees = transaction.fees || networkInfo.fees;
|
|
15
|
+
const baseReserve = transaction.baseReserve || networkInfo.baseReserve;
|
|
16
|
+
const [assetCode, assetIssuer] = getAssetCodeIssuer(transaction);
|
|
17
|
+
|
|
18
|
+
if (
|
|
19
|
+
transaction.networkInfo !== networkInfo ||
|
|
20
|
+
transaction.fees !== fees ||
|
|
21
|
+
transaction.baseReserve !== baseReserve
|
|
22
|
+
) {
|
|
23
|
+
return defaultUpdateTransaction(transaction, {
|
|
24
|
+
networkInfo,
|
|
25
|
+
fees,
|
|
26
|
+
baseReserve,
|
|
27
|
+
assetCode,
|
|
28
|
+
assetIssuer,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return defaultUpdateTransaction(transaction, { assetCode, assetIssuer });
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default prepareTransaction;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Observable } from "rxjs";
|
|
2
|
+
import { FeeNotLoaded } from "@ledgerhq/errors";
|
|
3
|
+
import type { Account, AccountBridge } from "@ledgerhq/types-live";
|
|
4
|
+
import { SignerContext } from "@ledgerhq/coin-framework/signer";
|
|
5
|
+
import type { Transaction } from "./types";
|
|
6
|
+
import { buildTransaction } from "./buildTransaction";
|
|
7
|
+
import { buildOptimisticOperation } from "./buildOptimisticOperation";
|
|
8
|
+
import { StellarSigner } from "./types/signer";
|
|
9
|
+
|
|
10
|
+
export function buildSignOperation(
|
|
11
|
+
signerContext: SignerContext<StellarSigner>,
|
|
12
|
+
): AccountBridge<Transaction, Account>["signOperation"] {
|
|
13
|
+
return function signOperation({ account, deviceId, transaction }) {
|
|
14
|
+
return new Observable(obs => {
|
|
15
|
+
let cancelled = false;
|
|
16
|
+
|
|
17
|
+
async function main() {
|
|
18
|
+
obs.next({ type: "device-signature-requested" });
|
|
19
|
+
|
|
20
|
+
if (cancelled) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!transaction.fees) {
|
|
25
|
+
throw new FeeNotLoaded();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const unsigned = await buildTransaction(account, transaction);
|
|
29
|
+
const unsignedPayload: Buffer = Buffer.from(unsigned.signatureBase());
|
|
30
|
+
// Sign by device
|
|
31
|
+
const { signature } = await signerContext(deviceId, signer =>
|
|
32
|
+
signer.signTransaction(account.freshAddressPath, unsignedPayload),
|
|
33
|
+
);
|
|
34
|
+
unsigned.addSignature(account.freshAddress, signature.toString("base64"));
|
|
35
|
+
obs.next({
|
|
36
|
+
type: "device-signature-granted",
|
|
37
|
+
});
|
|
38
|
+
const operation = await buildOptimisticOperation(account, transaction);
|
|
39
|
+
obs.next({
|
|
40
|
+
type: "signed",
|
|
41
|
+
signedOperation: {
|
|
42
|
+
operation,
|
|
43
|
+
signature: unsigned.toXDR(),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
main().then(
|
|
49
|
+
() => obs.complete(),
|
|
50
|
+
err => obs.error(err),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return () => {
|
|
54
|
+
cancelled = true;
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
}
|
package/src/specs.ts
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import expect from "expect";
|
|
2
|
+
import invariant from "invariant";
|
|
3
|
+
import BigNumber from "bignumber.js";
|
|
4
|
+
import { DeviceModelId } from "@ledgerhq/devices";
|
|
5
|
+
import type { SubAccount } from "@ledgerhq/types-live";
|
|
6
|
+
import type { TokenCurrency } from "@ledgerhq/types-cryptoassets";
|
|
7
|
+
import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/currencies";
|
|
8
|
+
import { parseCurrencyUnit } from "@ledgerhq/coin-framework/currencies";
|
|
9
|
+
import { AppSpec } from "@ledgerhq/coin-framework/bot/types";
|
|
10
|
+
import { botTest, pickSiblings } from "@ledgerhq/coin-framework/bot/specs";
|
|
11
|
+
import { listTokensForCryptoCurrency } from "@ledgerhq/cryptoassets/tokens";
|
|
12
|
+
import { acceptTransaction } from "./speculos-deviceActions";
|
|
13
|
+
import type { Transaction } from "./types";
|
|
14
|
+
|
|
15
|
+
const currency = getCryptoCurrencyById("stellar");
|
|
16
|
+
const minAmountCutoff = parseCurrencyUnit(currency.units[0], "0.1");
|
|
17
|
+
const reserve = parseCurrencyUnit(currency.units[0], "1.5");
|
|
18
|
+
|
|
19
|
+
const MAX_FEE = 5000;
|
|
20
|
+
const USDC_CODE = "USDC";
|
|
21
|
+
const USDC_ISSUER = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
|
|
22
|
+
const USDC_ASSET_ID = `${USDC_CODE}:${USDC_ISSUER}`;
|
|
23
|
+
const MIN_ASSET_BALANCE = parseCurrencyUnit(currency.units[0], "0.01");
|
|
24
|
+
|
|
25
|
+
const findAssetUSDC = <T extends { id: string }>(subAccounts?: T[]) =>
|
|
26
|
+
(subAccounts || []).find(s => s.id.endsWith(USDC_ASSET_ID));
|
|
27
|
+
|
|
28
|
+
const stellar: AppSpec<Transaction> = {
|
|
29
|
+
name: "Stellar",
|
|
30
|
+
currency,
|
|
31
|
+
appQuery: {
|
|
32
|
+
model: DeviceModelId.nanoS,
|
|
33
|
+
appName: "Stellar",
|
|
34
|
+
},
|
|
35
|
+
genericDeviceAction: acceptTransaction,
|
|
36
|
+
testTimeout: 2 * 60 * 1000,
|
|
37
|
+
minViableAmount: minAmountCutoff,
|
|
38
|
+
mutations: [
|
|
39
|
+
{
|
|
40
|
+
name: "move ~50% XLM",
|
|
41
|
+
maxRun: 2,
|
|
42
|
+
transaction: ({ account, siblings, bridge, maxSpendable }) => {
|
|
43
|
+
invariant(maxSpendable.gt(minAmountCutoff), "XLM balance is too low");
|
|
44
|
+
|
|
45
|
+
const transaction = bridge.createTransaction(account);
|
|
46
|
+
const sibling = pickSiblings(siblings, 4);
|
|
47
|
+
const recipient = sibling.freshAddress;
|
|
48
|
+
let amount = maxSpendable.div(1.9 + 0.2 * Math.random()).integerValue();
|
|
49
|
+
|
|
50
|
+
if (!sibling.used && amount.lt(reserve)) {
|
|
51
|
+
invariant(
|
|
52
|
+
maxSpendable.gt(reserve.plus(minAmountCutoff)),
|
|
53
|
+
"not enough XLM funds to send to new account",
|
|
54
|
+
);
|
|
55
|
+
amount = reserve;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const updates: Array<Partial<Transaction>> = [
|
|
59
|
+
{
|
|
60
|
+
recipient,
|
|
61
|
+
// Setting higher max fee here to make sure transaction doesn't
|
|
62
|
+
// time out.
|
|
63
|
+
fees: new BigNumber(MAX_FEE),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
amount,
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
if (Math.random() < 0.5) {
|
|
71
|
+
updates.push({
|
|
72
|
+
memoType: "MEMO_TEXT",
|
|
73
|
+
memoValue: "Ledger Live",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
transaction,
|
|
79
|
+
updates,
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
test: ({ account, accountBeforeTransaction, operation, transaction }) => {
|
|
83
|
+
// We don't know what the final fee will be until after the tx is
|
|
84
|
+
// submitted. Using higher max fee to make sure tx doesn't time out.
|
|
85
|
+
botTest("account balance decreased with operation", () =>
|
|
86
|
+
expect(account.balance.toNumber()).toBeLessThanOrEqual(
|
|
87
|
+
accountBeforeTransaction.balance.minus(operation.value).toNumber(),
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (transaction.memoValue) {
|
|
92
|
+
botTest("operation memo", () =>
|
|
93
|
+
expect(operation.extra).toMatchObject({
|
|
94
|
+
memo: transaction.memoValue,
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const getType = () => {
|
|
100
|
+
switch (transaction.mode) {
|
|
101
|
+
case "send":
|
|
102
|
+
return "send";
|
|
103
|
+
case "changeTrust":
|
|
104
|
+
return /change_trust/;
|
|
105
|
+
default:
|
|
106
|
+
return "";
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
botTest("transaction mode", () => expect(transaction.mode).toMatch(getType()));
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "Send max XLM",
|
|
115
|
+
maxRun: 2,
|
|
116
|
+
transaction: ({ account, siblings, bridge, maxSpendable }) => {
|
|
117
|
+
invariant(maxSpendable.gt(minAmountCutoff), "XLM balance is too low");
|
|
118
|
+
|
|
119
|
+
const transaction = bridge.createTransaction(account);
|
|
120
|
+
const sibling = pickSiblings(siblings, 4);
|
|
121
|
+
const recipient = sibling.freshAddress;
|
|
122
|
+
|
|
123
|
+
const updates: Array<Partial<Transaction>> = [
|
|
124
|
+
{
|
|
125
|
+
recipient,
|
|
126
|
+
// Setting higher max fee here to make sure transaction doesn't
|
|
127
|
+
// time out.
|
|
128
|
+
fees: new BigNumber(MAX_FEE),
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
useAllAmount: true,
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
if (Math.random() < 0.5) {
|
|
136
|
+
updates.push({
|
|
137
|
+
memoType: "MEMO_TEXT",
|
|
138
|
+
memoValue: "Ledger Live",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
transaction,
|
|
144
|
+
updates,
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
test: ({ account, accountBeforeTransaction, operation, transaction }) => {
|
|
148
|
+
// We don't know what the final fee will be until after the tx is
|
|
149
|
+
// submitted. Using higher max fee to make sure tx doesn't time out.
|
|
150
|
+
botTest("balance decreased with operation", () =>
|
|
151
|
+
expect(account.balance.toNumber()).toBeLessThanOrEqual(
|
|
152
|
+
accountBeforeTransaction.balance.minus(operation.value).toNumber(),
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (transaction.memoValue) {
|
|
157
|
+
botTest("operation memo", () =>
|
|
158
|
+
expect(operation.extra).toMatchObject({
|
|
159
|
+
memo: transaction.memoValue,
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const getType = () => {
|
|
165
|
+
switch (transaction.mode) {
|
|
166
|
+
case "send":
|
|
167
|
+
return "send";
|
|
168
|
+
case "changeTrust":
|
|
169
|
+
return /change_trust/;
|
|
170
|
+
default:
|
|
171
|
+
return "";
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
botTest("transaction mode", () => expect(transaction.mode).toMatch(getType()));
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "add USDC asset",
|
|
180
|
+
maxRun: 1,
|
|
181
|
+
transaction: ({ account, bridge, maxSpendable }) => {
|
|
182
|
+
invariant(maxSpendable.gt(reserve), "XLM balance is too low 1");
|
|
183
|
+
invariant(
|
|
184
|
+
account.subAccounts && !findAssetUSDC(account.subAccounts),
|
|
185
|
+
"already have subaccounts",
|
|
186
|
+
);
|
|
187
|
+
const assetUSDC = findAssetUSDC<TokenCurrency>(
|
|
188
|
+
listTokensForCryptoCurrency(account.currency),
|
|
189
|
+
);
|
|
190
|
+
invariant(assetUSDC, "USDC asset not found");
|
|
191
|
+
|
|
192
|
+
const transaction = bridge.createTransaction(account);
|
|
193
|
+
|
|
194
|
+
const updates: Array<Partial<Transaction>> = [
|
|
195
|
+
{
|
|
196
|
+
mode: "changeTrust",
|
|
197
|
+
// Setting higher max fee here to make sure transaction doesn't
|
|
198
|
+
// time out.
|
|
199
|
+
fees: new BigNumber(MAX_FEE),
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
assetCode: USDC_CODE,
|
|
203
|
+
assetIssuer: USDC_ISSUER,
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
transaction,
|
|
209
|
+
updates,
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
test: ({ account }) => {
|
|
213
|
+
const assetId = `${USDC_CODE}:${USDC_ISSUER}`;
|
|
214
|
+
const hasAsset = account.subAccounts?.find(a => a.id.endsWith(assetId));
|
|
215
|
+
botTest("has asset", () => expect(hasAsset).toBeTruthy());
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: "move ~50% USDC asset",
|
|
220
|
+
maxRun: 1,
|
|
221
|
+
transaction: ({ account, siblings, bridge, maxSpendable }) => {
|
|
222
|
+
invariant(maxSpendable.gt(minAmountCutoff), "XLM balance is too low");
|
|
223
|
+
|
|
224
|
+
const usdcSubAccount = findAssetUSDC<SubAccount>(account?.subAccounts);
|
|
225
|
+
|
|
226
|
+
invariant(usdcSubAccount, "USDC asset not found");
|
|
227
|
+
invariant(usdcSubAccount?.balance.gt(MIN_ASSET_BALANCE), "USDC balance is too low");
|
|
228
|
+
|
|
229
|
+
const siblingWithAssetUSDC = siblings.find(s => findAssetUSDC(s.subAccounts));
|
|
230
|
+
invariant(siblingWithAssetUSDC, "No siblings with USDC asset");
|
|
231
|
+
|
|
232
|
+
if (!usdcSubAccount || !siblingWithAssetUSDC) {
|
|
233
|
+
throw new Error("No USDC asset or sibling with USDC asset");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const transaction = bridge.createTransaction(account);
|
|
237
|
+
const recipient = siblingWithAssetUSDC.freshAddress;
|
|
238
|
+
const amount = usdcSubAccount.balance.div(1.9 + 0.2 * Math.random()).integerValue();
|
|
239
|
+
|
|
240
|
+
const updates: Array<Partial<Transaction>> = [
|
|
241
|
+
{
|
|
242
|
+
recipient,
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
subAccountId: usdcSubAccount.id,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
amount,
|
|
249
|
+
// Setting higher max fee here to make sure transaction doesn't
|
|
250
|
+
// time out.
|
|
251
|
+
fees: new BigNumber(MAX_FEE),
|
|
252
|
+
},
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
if (Math.random() < 0.5) {
|
|
256
|
+
updates.push({
|
|
257
|
+
memoType: "MEMO_TEXT",
|
|
258
|
+
memoValue: "Ledger Live",
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
transaction,
|
|
264
|
+
updates,
|
|
265
|
+
};
|
|
266
|
+
},
|
|
267
|
+
test: ({ account, accountBeforeTransaction, operation, transaction, status }) => {
|
|
268
|
+
const asset = findAssetUSDC<SubAccount>(account?.subAccounts);
|
|
269
|
+
const assetBeforeTx = findAssetUSDC<SubAccount>(accountBeforeTransaction?.subAccounts);
|
|
270
|
+
|
|
271
|
+
botTest("asset balance decreased with operation", () =>
|
|
272
|
+
expect(asset?.balance.toString()).toBe(
|
|
273
|
+
assetBeforeTx?.balance.minus(status.amount).toString(),
|
|
274
|
+
),
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
if (transaction.memoValue) {
|
|
278
|
+
botTest("operation memo", () =>
|
|
279
|
+
expect(operation.extra).toMatchObject({
|
|
280
|
+
memo: transaction.memoValue,
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
};
|
|
288
|
+
export default {
|
|
289
|
+
stellar,
|
|
290
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { DeviceAction } from "@ledgerhq/coin-framework/bot/types";
|
|
2
|
+
import { getAccountCurrency } from "@ledgerhq/coin-framework/account/index";
|
|
3
|
+
import { formatCurrencyUnit } from "@ledgerhq/coin-framework/currencies/formatCurrencyUnit";
|
|
4
|
+
import {
|
|
5
|
+
deviceActionFlow,
|
|
6
|
+
formatDeviceAmount,
|
|
7
|
+
SpeculosButton,
|
|
8
|
+
} from "@ledgerhq/coin-framework/bot/specs";
|
|
9
|
+
import { Account } from "@ledgerhq/types-live";
|
|
10
|
+
import type { Transaction, TransactionStatus } from "./types";
|
|
11
|
+
|
|
12
|
+
function expectedAmount({
|
|
13
|
+
account,
|
|
14
|
+
status,
|
|
15
|
+
transaction,
|
|
16
|
+
}: {
|
|
17
|
+
account: Account;
|
|
18
|
+
status: TransactionStatus;
|
|
19
|
+
transaction: Transaction;
|
|
20
|
+
}) {
|
|
21
|
+
if (transaction.assetCode && transaction.assetIssuer) {
|
|
22
|
+
const amount = formatDeviceAmount(account.currency, status.amount, {
|
|
23
|
+
hideCode: true,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return `${amount} ${transaction.assetCode}@${truncateAddress(transaction.assetIssuer, 3, 4)}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return formatDeviceAmount(account.currency, status.amount, {
|
|
30
|
+
postfixCode: true,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function truncateAddress(stellarAddress: string, start = 6, end = 6) {
|
|
35
|
+
return `${stellarAddress.slice(0, start)}..${stellarAddress.slice(-end)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const acceptTransaction: DeviceAction<Transaction, any> = deviceActionFlow({
|
|
39
|
+
steps: [
|
|
40
|
+
{
|
|
41
|
+
title: "Review",
|
|
42
|
+
button: SpeculosButton.RIGHT,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
title: "Memo Text",
|
|
46
|
+
button: SpeculosButton.RIGHT,
|
|
47
|
+
expectedValue: ({ transaction }) => transaction.memoValue || "",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
title: "Max Fee",
|
|
51
|
+
button: SpeculosButton.RIGHT,
|
|
52
|
+
expectedValue: ({ account, status }) =>
|
|
53
|
+
formatCurrencyUnit(getAccountCurrency(account).units[0], status.estimatedFees, {
|
|
54
|
+
disableRounding: true,
|
|
55
|
+
}) + " XLM",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
title: "Sequence Num",
|
|
59
|
+
button: SpeculosButton.RIGHT,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
title: "Tx Source",
|
|
63
|
+
button: SpeculosButton.RIGHT,
|
|
64
|
+
expectedValue: ({ account }) => truncateAddress(account.freshAddress),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
title: "Operation Type",
|
|
68
|
+
button: SpeculosButton.RIGHT,
|
|
69
|
+
// Operation type can be Payment (or Create Account) or Change Trust.
|
|
70
|
+
// Create Account type is coming from operation, not transaction.
|
|
71
|
+
// Testing in `specs.ts`.
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
title: "Change Trust",
|
|
75
|
+
button: SpeculosButton.RIGHT,
|
|
76
|
+
expectedValue: ({ transaction }) =>
|
|
77
|
+
`${transaction.assetCode || ""}@${truncateAddress(transaction.assetIssuer || "", 3, 4)}`,
|
|
78
|
+
maxY: 5,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
title: "Create Account",
|
|
82
|
+
button: SpeculosButton.RIGHT,
|
|
83
|
+
expectedValue: ({ transaction }) => transaction.recipient,
|
|
84
|
+
maxY: 5,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
title: "Send",
|
|
88
|
+
button: SpeculosButton.RIGHT,
|
|
89
|
+
expectedValue: expectedAmount,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
title: "Destination",
|
|
93
|
+
button: SpeculosButton.RIGHT,
|
|
94
|
+
expectedValue: ({ transaction }) => transaction.recipient,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
title: "Finalize",
|
|
98
|
+
button: SpeculosButton.BOTH,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
title: "Starting Balance",
|
|
102
|
+
button: SpeculosButton.RIGHT,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
title: "Network",
|
|
106
|
+
button: SpeculosButton.RIGHT,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
title: "Valid Before (UTC)",
|
|
110
|
+
button: SpeculosButton.RIGHT,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
title: "Valid After (UTC)",
|
|
114
|
+
button: SpeculosButton.RIGHT,
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { encodeAccountId } from "@ledgerhq/coin-framework/account/index";
|
|
2
|
+
import { inferSubOperations } from "@ledgerhq/coin-framework/serialization/index";
|
|
3
|
+
import { Account } from "@ledgerhq/types-live";
|
|
4
|
+
import { GetAccountShape, mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers";
|
|
5
|
+
import { fetchAccount, fetchOperations } from "./network";
|
|
6
|
+
import { buildSubAccounts } from "./tokens";
|
|
7
|
+
import { StellarOperation } from "./types";
|
|
8
|
+
import { STELLAR_BURN_ADDRESS } from "./logic";
|
|
9
|
+
import { StellarBurnAddressError } from "./errors";
|
|
10
|
+
|
|
11
|
+
export const getAccountShape: GetAccountShape<Account> = async (info, syncConfig) => {
|
|
12
|
+
const { address, currency, initialAccount, derivationMode } = info;
|
|
13
|
+
|
|
14
|
+
// FIXME Workaround for burn address, see https://ledgerhq.atlassian.net/browse/LIVE-4014
|
|
15
|
+
if (address === STELLAR_BURN_ADDRESS) throw new StellarBurnAddressError();
|
|
16
|
+
|
|
17
|
+
const accountId = encodeAccountId({
|
|
18
|
+
type: "js",
|
|
19
|
+
version: "2",
|
|
20
|
+
currencyId: currency.id,
|
|
21
|
+
xpubOrAddress: address,
|
|
22
|
+
derivationMode,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const { blockHeight, balance, spendableBalance, assets } = await fetchAccount(address);
|
|
26
|
+
|
|
27
|
+
const oldOperations = (initialAccount?.operations || []) as StellarOperation[];
|
|
28
|
+
const lastPagingToken = oldOperations[0]?.extra.pagingToken || "0";
|
|
29
|
+
|
|
30
|
+
const newOperations =
|
|
31
|
+
(await fetchOperations({
|
|
32
|
+
accountId,
|
|
33
|
+
addr: address,
|
|
34
|
+
order: "asc",
|
|
35
|
+
cursor: lastPagingToken,
|
|
36
|
+
})) || [];
|
|
37
|
+
|
|
38
|
+
const allOperations = mergeOps(oldOperations, newOperations) as StellarOperation[];
|
|
39
|
+
const assetOperations: StellarOperation[] = [];
|
|
40
|
+
|
|
41
|
+
allOperations.forEach(operation => {
|
|
42
|
+
if (
|
|
43
|
+
operation?.extra?.assetCode &&
|
|
44
|
+
operation?.extra?.assetIssuer &&
|
|
45
|
+
!["OPT_IN", "OPT_OUT"].includes(operation.type)
|
|
46
|
+
) {
|
|
47
|
+
assetOperations.push(operation);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const subAccounts =
|
|
52
|
+
buildSubAccounts({
|
|
53
|
+
currency,
|
|
54
|
+
accountId,
|
|
55
|
+
assets,
|
|
56
|
+
syncConfig,
|
|
57
|
+
operations: assetOperations,
|
|
58
|
+
}) || [];
|
|
59
|
+
|
|
60
|
+
const shape = {
|
|
61
|
+
id: accountId,
|
|
62
|
+
balance,
|
|
63
|
+
spendableBalance,
|
|
64
|
+
operationsCount: allOperations.length,
|
|
65
|
+
blockHeight,
|
|
66
|
+
subAccounts,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
...shape,
|
|
71
|
+
operations: allOperations.map(op => {
|
|
72
|
+
const subOperations = inferSubOperations(op.hash, subAccounts);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
...op,
|
|
76
|
+
subOperations,
|
|
77
|
+
};
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
};
|
package/src/tokens.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import BigNumber from "bignumber.js";
|
|
2
|
+
import { emptyHistoryCache } from "@ledgerhq/coin-framework/account/index";
|
|
3
|
+
import type { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets";
|
|
4
|
+
import type { SyncConfig, TokenAccount } from "@ledgerhq/types-live";
|
|
5
|
+
import { parseCurrencyUnit } from "@ledgerhq/coin-framework/currencies/parseCurrencyUnit";
|
|
6
|
+
import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
|
|
7
|
+
import { findTokenById, listTokensForCryptoCurrency } from "@ledgerhq/cryptoassets";
|
|
8
|
+
import type { BalanceAsset, StellarOperation } from "./types";
|
|
9
|
+
|
|
10
|
+
export const getAssetIdFromTokenId = (tokenId: string): string => tokenId.split("/")[2];
|
|
11
|
+
|
|
12
|
+
const getAssetIdFromAsset = (asset: BalanceAsset) => `${asset.asset_code}:${asset.asset_issuer}`;
|
|
13
|
+
|
|
14
|
+
function buildStellarTokenAccount({
|
|
15
|
+
parentAccountId,
|
|
16
|
+
stellarAsset,
|
|
17
|
+
token,
|
|
18
|
+
operations,
|
|
19
|
+
}: {
|
|
20
|
+
parentAccountId: string;
|
|
21
|
+
stellarAsset: BalanceAsset;
|
|
22
|
+
token: TokenCurrency;
|
|
23
|
+
operations: StellarOperation[];
|
|
24
|
+
}): TokenAccount {
|
|
25
|
+
const assetId = getAssetIdFromTokenId(token.id);
|
|
26
|
+
const id = `${parentAccountId}+${assetId}`;
|
|
27
|
+
const balance = parseCurrencyUnit(token.units[0], stellarAsset.balance || "0");
|
|
28
|
+
|
|
29
|
+
const reservedBalance = new BigNumber(stellarAsset.balance).minus(
|
|
30
|
+
stellarAsset.selling_liabilities || 0,
|
|
31
|
+
);
|
|
32
|
+
const spendableBalance = parseCurrencyUnit(token.units[0], reservedBalance.toString());
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
type: "TokenAccount",
|
|
36
|
+
id,
|
|
37
|
+
parentId: parentAccountId,
|
|
38
|
+
token,
|
|
39
|
+
operationsCount: operations.length,
|
|
40
|
+
operations: operations.map(op => ({
|
|
41
|
+
...op,
|
|
42
|
+
id: encodeOperationId(id, op.hash, op.extra.ledgerOpType),
|
|
43
|
+
accountId: id,
|
|
44
|
+
type: op.extra.ledgerOpType,
|
|
45
|
+
value: op.extra.assetAmount ? new BigNumber(op.extra.assetAmount) : op.value,
|
|
46
|
+
})),
|
|
47
|
+
pendingOperations: [],
|
|
48
|
+
balance,
|
|
49
|
+
spendableBalance,
|
|
50
|
+
swapHistory: [],
|
|
51
|
+
creationDate: operations.length > 0 ? operations[operations.length - 1].date : new Date(),
|
|
52
|
+
balanceHistoryCache: emptyHistoryCache, // calculated in the jsHelpers
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildSubAccounts({
|
|
57
|
+
currency,
|
|
58
|
+
accountId,
|
|
59
|
+
assets,
|
|
60
|
+
syncConfig,
|
|
61
|
+
operations,
|
|
62
|
+
}: {
|
|
63
|
+
currency: CryptoCurrency;
|
|
64
|
+
accountId: string;
|
|
65
|
+
assets: BalanceAsset[];
|
|
66
|
+
syncConfig: SyncConfig;
|
|
67
|
+
operations: StellarOperation[];
|
|
68
|
+
}): TokenAccount[] | undefined {
|
|
69
|
+
const { blacklistedTokenIds = [] } = syncConfig;
|
|
70
|
+
const allTokens = listTokensForCryptoCurrency(currency);
|
|
71
|
+
|
|
72
|
+
if (allTokens.length === 0 || assets.length === 0) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const tokenAccounts: TokenAccount[] = [];
|
|
77
|
+
|
|
78
|
+
assets.map(asset => {
|
|
79
|
+
const token = findTokenById(`stellar/asset/${getAssetIdFromAsset(asset)}`);
|
|
80
|
+
|
|
81
|
+
if (token && !blacklistedTokenIds.includes(token.id)) {
|
|
82
|
+
tokenAccounts.push(
|
|
83
|
+
buildStellarTokenAccount({
|
|
84
|
+
parentAccountId: accountId,
|
|
85
|
+
stellarAsset: asset,
|
|
86
|
+
token,
|
|
87
|
+
operations: operations.filter(
|
|
88
|
+
op =>
|
|
89
|
+
op.extra.assetCode === asset.asset_code &&
|
|
90
|
+
op.extra.assetIssuer === asset.asset_issuer,
|
|
91
|
+
),
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return tokenAccounts;
|
|
98
|
+
}
|