UNPKG

@ledgerhq/coin-cardano

Version:
265 lines (243 loc) 9.57 kB
import expect from "expect"; import type { AppSpec } from "@ledgerhq/coin-framework/bot/types"; import type { CardanoAccount, CardanoOperationExtra, CardanoResources, Transaction } from "./types"; import { botTest, genericTestDestination, pickSiblings } from "@ledgerhq/coin-framework/bot/specs"; import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets"; import { DeviceModelId } from "@ledgerhq/devices"; import BigNumber from "bignumber.js"; import invariant from "invariant"; import { utils as TyphonUtils } from "@stricahq/typhonjs"; import { mergeTokens } from "./logic"; import { formatCurrencyUnit, parseCurrencyUnit } from "@ledgerhq/coin-framework/currencies/index"; import { TokenAccount } from "@ledgerhq/types-live"; import { acceptTransaction } from "./speculos-deviceActions"; import { CARDANO_MAX_SUPPLY } from "./constants"; const maxAccounts = 5; const currency = getCryptoCurrencyById("cardano"); const minBalanceRequired = parseCurrencyUnit(currency.units[0], "2.2"); const minBalanceRequiredForMaxSend = parseCurrencyUnit(currency.units[0], "1"); const minBalanceRequiredForDelegate = parseCurrencyUnit(currency.units[0], "3"); const minSpendableRequiredForTokenTx = parseCurrencyUnit(currency.units[0], "1"); const cardano: AppSpec<Transaction> = { name: "cardano", currency: getCryptoCurrencyById("cardano"), appQuery: { model: DeviceModelId.nanoSP, appName: "CardanoADA", // FIXME latest (v7) app version requires to update cardano libs appVersion: "6.1.2", }, minViableAmount: minBalanceRequired, genericDeviceAction: acceptTransaction, testTimeout: 5 * 60 * 1000, mutations: [ { name: "move ~10% token", feature: "tokens", maxRun: 1, transaction: ({ account, siblings, bridge, maxSpendable }) => { invariant(maxSpendable.gte(minSpendableRequiredForTokenTx), "balance is too low"); const sibling = pickSiblings(siblings, maxAccounts); const recipient = sibling.freshAddress; const transaction = bridge.createTransaction(account); const subAccount = account.subAccounts?.find(subAccount => subAccount.balance.gt(1), ) as TokenAccount; invariant(subAccount, "No token account with balance"); const updates = [ { subAccountId: subAccount.id }, { recipient }, { amount: new BigNumber(subAccount.balance.dividedBy(10)).dp(0, BigNumber.ROUND_CEIL), }, ]; return { transaction, updates, }; }, test: ({ operation, transaction }): void => { botTest("subOperations is defined", () => expect(operation.subOperations).toBeTruthy()); botTest("there's only one subOperation", () => expect(operation.subOperations?.length).toEqual(1), ); const subOperation = operation.subOperations && operation.subOperations[0]; botTest("subOperation have correct tx amount", () => expect(subOperation?.value).toEqual(transaction.amount), ); }, }, { testDestination: genericTestDestination, name: "move ~50%", feature: "send", maxRun: 1, transaction: ({ account, siblings, bridge, maxSpendable }) => { invariant(maxSpendable.gt(minBalanceRequired), "balance is too low"); const sibling = pickSiblings(siblings, maxAccounts); const recipient = sibling.freshAddress; const transaction = bridge.createTransaction(account); const updates = [ { recipient }, { amount: new BigNumber(account.balance.dividedBy(2)).dp(0, BigNumber.ROUND_CEIL), }, { memo: "LedgerLiveBot" }, ]; return { transaction, updates, }; }, test: ({ accountBeforeTransaction, operation, transaction }): void => { const cardanoResources = (accountBeforeTransaction as CardanoAccount) .cardanoResources as CardanoResources; const extra: CardanoOperationExtra = { memo: transaction.memo, }; if (cardanoResources.delegation?.rewards.gt(0)) { extra.rewards = formatCurrencyUnit( accountBeforeTransaction.currency.units[0], new BigNumber(cardanoResources.delegation.rewards), { showCode: true, disableRounding: true, }, ); } botTest("operation extra matches memo", () => expect(operation.extra).toEqual(extra)); botTest("optimistic value matches transaction amount", () => expect(transaction.amount).toEqual(operation.value.minus(operation.fee)), ); }, }, { name: "send max", feature: "sendMax", maxRun: 1, testDestination: genericTestDestination, transaction: ({ account, siblings, bridge, maxSpendable }) => { invariant(maxSpendable.gt(minBalanceRequiredForMaxSend), "balance is too low"); const sibling = pickSiblings(siblings, maxAccounts); const recipient = sibling.freshAddress; const transaction = bridge.createTransaction(account); const updates = [{ recipient }, { useAllAmount: true }]; return { transaction, updates, }; }, test: ({ account }): void => { const cardanoResources = (account as CardanoAccount).cardanoResources as CardanoResources; const utxoTokens = cardanoResources.utxos.map(u => u.tokens).flat(); const tokenBalance = mergeTokens(utxoTokens); const changeAddress = TyphonUtils.getAddressFromString(account.freshAddress); const requiredAdaForChangeTokens = tokenBalance.length ? TyphonUtils.calculateMinUtxoAmountBabbage( { address: changeAddress, amount: new BigNumber(CARDANO_MAX_SUPPLY), tokens: tokenBalance, }, new BigNumber(cardanoResources.protocolParams.utxoCostPerByte), ) : new BigNumber(0); botTest("remaining balance equals requiredAdaForTokens)", () => expect(account.balance).toEqual(requiredAdaForChangeTokens), ); }, }, { name: "delegate to pool", feature: "staking", maxRun: 1, transaction: ({ account, bridge, maxSpendable }) => { invariant(maxSpendable.gte(minBalanceRequiredForDelegate), "balance is too low"); const transaction = bridge.createTransaction(account); return { transaction, updates: [ { mode: "delegate", poolId: "7df262feae9201d1b2e32d4c825ca91b29fbafb2b8e556f6efb7f549", }, ], }; }, test: ({ operation, transaction, accountBeforeTransaction }): void => { botTest("check delegate operation type", () => { expect(operation.type).toEqual("DELEGATE"); }); botTest("check operation value", () => { const cardanoResources = (accountBeforeTransaction as CardanoAccount).cardanoResources; const isStakeKeyRegistered = cardanoResources.delegation?.status ?? false; let opValue = transaction.fees as BigNumber; if (!isStakeKeyRegistered) { opValue = opValue.plus(cardanoResources.protocolParams.stakeKeyDeposit); } expect(operation.value.toString()).toEqual(opValue.toString()); }); }, }, { name: "redelegate to pool", feature: "staking", maxRun: 1, transaction: ({ account, bridge, maxSpendable }) => { invariant(maxSpendable.gte(minBalanceRequiredForDelegate), "balance is too low"); invariant( (account as CardanoAccount).cardanoResources.delegation?.poolId, "account should already be delegated to redelegate", ); const transaction = bridge.createTransaction(account); return { transaction, updates: [ { mode: "delegate", poolId: "da50099e7aa1d926e1888990b1c404caf554dd6f68a1cb0322999d1d", }, ], }; }, test: ({ operation, transaction }): void => { botTest("check delegate operation type", () => { expect(operation.type).toEqual("DELEGATE"); }); botTest("op value should be equal to fees", () => { expect(operation.value.toString()).toEqual(transaction.fees?.toString()); }); }, }, { name: "undelegate", feature: "staking", maxRun: 1, transaction: ({ account, bridge, maxSpendable }) => { invariant(maxSpendable.gte(minBalanceRequiredForDelegate), "balance is too low"); invariant( (account as CardanoAccount).cardanoResources.delegation?.poolId, "account should already be delegated to undelegate", ); const transaction = bridge.createTransaction(account); return { transaction, updates: [ { mode: "undelegate", }, ], }; }, test: ({ operation, transaction }): void => { botTest("check undelegate operation type", () => { expect(operation.type).toEqual("UNDELEGATE"); }); botTest("op value should be equal to fees", () => { expect(operation.value.toString()).toEqual(transaction.fees?.toString()); }); }, }, ], }; export default { cardano };