UNPKG

@ledgerhq/coin-multiversx

Version:
388 lines (353 loc) 12.5 kB
import type { MultiversXAccount, MultiversXOperation, MultiversXOperationRaw, Transaction, } from "./types"; import invariant from "invariant"; import { parseCurrencyUnit } from "@ledgerhq/coin-framework/currencies"; import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/currencies"; import { botTest, pickSiblings, genericTestDestination } from "@ledgerhq/coin-framework/bot/specs"; import type { AppSpec, TransactionTestInput } from "@ledgerhq/coin-framework/bot/types"; import { toOperationRaw } from "@ledgerhq/coin-framework/serialization"; import { DeviceModelId } from "@ledgerhq/devices"; import expect from "expect"; import { acceptDelegateTransaction, acceptEsdtTransferTransaction, acceptMoveBalanceTransaction, acceptUndelegateTransaction, acceptWithdrawTransaction, } from "./speculos-deviceActions"; import BigNumber from "bignumber.js"; import { MIN_DELEGATION_AMOUNT } from "./constants"; import { TokenAccount } from "@ledgerhq/types-live"; import sample from "lodash/sample"; const currency = getCryptoCurrencyById("elrond"); const minimalAmount = parseCurrencyUnit(currency.units[0], "0.001"); const maxAccounts = 6; const MULTIVERSX_MIN_ACTIVATION_SAFE = new BigNumber(10000); const UNCAPPED_PROVIDER = "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqlhllllsr0pd0j"; function expectCorrectBalanceChange(input: TransactionTestInput<Transaction>) { const { account, operation, accountBeforeTransaction } = input; botTest("EGLD balance change is correct", () => expect(account.balance.toFixed()).toStrictEqual( accountBeforeTransaction.balance.minus(operation.value).toFixed(), ), ); } function expectCorrectEsdtBalanceChange(input: TransactionTestInput<Transaction>) { const { account, operation, accountBeforeTransaction, transaction } = input; const { subAccountId } = transaction; const subAccounts = account.subAccounts ?? []; const subAccountsBefore = accountBeforeTransaction.subAccounts ?? []; const tokenAccount = subAccounts.find(ta => ta.id === subAccountId); const tokenAccountBefore = subAccountsBefore.find(ta => ta.id === subAccountId); const subOperation = operation.subOperations?.find(sa => sa.id === operation.id); if (tokenAccount && tokenAccountBefore && subOperation) { botTest("ESDT balance change is correct", () => expect(tokenAccount.balance.toFixed()).toStrictEqual( tokenAccountBefore.balance.minus(subOperation.value).toFixed(), ), ); } } function expectCorrectOptimisticOperation(input: TransactionTestInput<Transaction>) { const { operation, optimisticOperation, transaction } = input; const opExpected: Partial<MultiversXOperationRaw> = toOperationRaw({ ...optimisticOperation, }) as MultiversXOperationRaw; delete opExpected.value; delete opExpected.fee; delete opExpected.date; delete opExpected.blockHash; delete opExpected.blockHeight; if (operation.type !== "OUT") { delete opExpected.senders; delete opExpected.recipients; delete opExpected.contract; } botTest("optimistic operation matches id", () => expect(operation.id).toStrictEqual(optimisticOperation.id), ); botTest("optimistic operation matches hash", () => expect(operation.hash).toStrictEqual(optimisticOperation.hash), ); botTest("optimistic operation matches accountId", () => expect(operation.accountId).toStrictEqual(optimisticOperation.accountId), ); // On ESDT transactions the fee can decrease when the transaction is executed if (!transaction.subAccountId) { botTest("optimistic operation matches fee", () => expect(operation.fee.toFixed()).toStrictEqual(optimisticOperation.fee.toFixed()), ); } botTest("optimistic operation matches type", () => expect(operation.type).toStrictEqual(optimisticOperation.type), ); if (operation.type === "OUT") { botTest("optimistic operation matches contract", () => expect(operation.contract).toStrictEqual(optimisticOperation.contract), ); botTest("optimistic operation matches senders", () => expect(operation.senders).toStrictEqual(optimisticOperation.senders), ); botTest("optimistic operation matches recipients", () => expect(operation.recipients).toStrictEqual(optimisticOperation.recipients), ); if (!transaction.subAccountId) { botTest("optimistic operation matches value", () => expect(operation.value.toFixed()).toStrictEqual(optimisticOperation.value.toFixed()), ); } } botTest("optimistic operation matches transactionSequenceNumber", () => expect(operation.transactionSequenceNumber).toStrictEqual( optimisticOperation.transactionSequenceNumber, ), ); botTest("raw optimistic operation matches", () => expect(toOperationRaw(operation)).toMatchObject(opExpected), ); } function expectCorrectSpendableBalanceChange(input: TransactionTestInput<Transaction>) { const { account, accountBeforeTransaction } = input; const operation = input.operation as MultiversXOperation; let value = operation.value; if (operation.extra.amount) { if (operation.type === "DELEGATE") { value = value.plus(operation.extra.amount); } else if (operation.type === "WITHDRAW_UNBONDED") { value = value.minus(operation.extra.amount); } } botTest("EGLD spendable balance change is correct", () => expect(account.spendableBalance.toFixed()).toStrictEqual( accountBeforeTransaction.spendableBalance.minus(value).toFixed(), ), ); } function expectCorrectBalanceFeeChange(input: TransactionTestInput<Transaction>) { const { account, operation, accountBeforeTransaction } = input; botTest("Only change on balance is fees", () => expect(account.balance.toFixed()).toStrictEqual( accountBeforeTransaction.balance.minus(operation.fee).toFixed(), ), ); } const multiversx: AppSpec<Transaction> = { name: "MultiversX", currency: getCryptoCurrencyById("elrond"), appQuery: { model: DeviceModelId.nanoS, appName: "MultiversX", }, genericDeviceAction: acceptMoveBalanceTransaction, genericDeviceActionForSubAccountTransfers: acceptEsdtTransferTransaction, testTimeout: 2 * 60 * 1000, minViableAmount: minimalAmount, transactionCheck: ({ maxSpendable }) => { invariant(maxSpendable.gt(minimalAmount), "balance is too low"); }, test: input => { expectCorrectOptimisticOperation(input); }, mutations: [ { name: "send 50%~", feature: "send", maxRun: 1, deviceAction: acceptMoveBalanceTransaction, transaction: ({ account, siblings, bridge }) => { invariant(account.spendableBalance.gt(0), "balance is 0"); const sibling = pickSiblings(siblings, maxAccounts); let amount = account.spendableBalance.div(1.9 + 0.2 * Math.random()).integerValue(); if (!sibling.used && amount.lt(MULTIVERSX_MIN_ACTIVATION_SAFE)) { invariant( account.spendableBalance.gt(MULTIVERSX_MIN_ACTIVATION_SAFE), "send is too low to activate account", ); amount = MULTIVERSX_MIN_ACTIVATION_SAFE; } return { transaction: bridge.createTransaction(account), updates: [ { recipient: sibling.freshAddress, }, { amount, }, ], }; }, testDestination: genericTestDestination, test: input => { expectCorrectBalanceChange(input); }, }, { name: "send max", feature: "sendMax", maxRun: 1, deviceAction: acceptMoveBalanceTransaction, transaction: ({ account, siblings, bridge }) => { invariant(account.spendableBalance.gt(0), "balance is 0"); const sibling = pickSiblings(siblings, maxAccounts); if (!sibling.used) { invariant( account.spendableBalance.gt(MULTIVERSX_MIN_ACTIVATION_SAFE), "send is too low to activate account", ); } return { transaction: bridge.createTransaction(account), updates: [ { recipient: sibling.freshAddress, }, { useAllAmount: true, }, ], }; }, testDestination: genericTestDestination, test: input => { expectCorrectBalanceChange(input); }, }, { name: "move some ESDT", feature: "tokens", maxRun: 1, deviceAction: acceptEsdtTransferTransaction, transaction: ({ account, siblings, bridge }) => { const esdtAccount: TokenAccount | undefined = sample( (account.subAccounts || []).filter(a => a.balance.gt(0)), ); invariant(esdtAccount, "no esdt account"); invariant(esdtAccount?.balance.gt(0), "esdt balance is 0"); const sibling = pickSiblings(siblings, 2); const recipient = sibling.freshAddress; const amount = esdtAccount?.balance.times(Math.random()).integerValue(); return { transaction: bridge.createTransaction(account), updates: [ { subAccountId: esdtAccount?.id, }, { recipient, }, { amount, }, ], }; }, test: input => { expectCorrectEsdtBalanceChange(input); expectCorrectBalanceFeeChange(input); }, }, { name: "delegate 1 EGLD", feature: "staking", maxRun: 1, deviceAction: acceptDelegateTransaction, transaction: ({ account, bridge }) => { invariant( account.spendableBalance.gt(MIN_DELEGATION_AMOUNT), `spendable balance is less than minimum delegation amount`, ); const amount = MIN_DELEGATION_AMOUNT; return { transaction: bridge.createTransaction(account), updates: [ { recipient: UNCAPPED_PROVIDER, mode: "delegate", amount, }, ], }; }, test: input => { expectCorrectSpendableBalanceChange(input); expectCorrectBalanceFeeChange(input); }, }, { name: "unDelegate 1 EGLD", feature: "staking", maxRun: 1, deviceAction: acceptUndelegateTransaction, transaction: ({ account, bridge }) => { const delegations = (account as MultiversXAccount)?.multiversxResources?.delegations; invariant(delegations?.length, "account doesn't have any delegations"); invariant( delegations.some(d => new BigNumber(d.userActiveStake).gt(0)), "no active stake for account", ); const amount = MIN_DELEGATION_AMOUNT; return { transaction: bridge.createTransaction(account), updates: [ { recipient: UNCAPPED_PROVIDER, mode: "unDelegate", amount, }, ], }; }, test: input => { expectCorrectSpendableBalanceChange(input); expectCorrectBalanceFeeChange(input); }, }, { name: "withdraw all EGLD", feature: "staking", maxRun: 1, deviceAction: acceptWithdrawTransaction, transaction: ({ account, bridge }) => { const delegations = (account as MultiversXAccount)?.multiversxResources?.delegations; invariant(delegations?.length, "account doesn't have any delegations"); invariant( // among all delegations delegations.some(d => // among all undelegating amounts d.userUndelegatedList?.some( u => new BigNumber(u.amount).gt(0) && // the undelegation has a positive amount new BigNumber(u.seconds).eq(0), // the undelegation period has ended ), ), "no withdrawable stake for account", ); return { transaction: bridge.createTransaction(account), updates: [ { recipient: UNCAPPED_PROVIDER, mode: "withdraw", amount: new BigNumber(0), }, ], }; }, test: input => { expectCorrectSpendableBalanceChange(input); expectCorrectBalanceFeeChange(input); }, }, // TODO // "reDelegateRewards" // "claimRewards" ], }; export default { multiversx, };