UNPKG

@ledgerhq/coin-algorand

Version:
229 lines 10.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const account_1 = require("@ledgerhq/coin-framework/account"); const specs_1 = require("@ledgerhq/coin-framework/bot/specs"); const index_1 = require("@ledgerhq/cryptoassets/index"); const currencies_1 = require("@ledgerhq/coin-framework/currencies"); const devices_1 = require("@ledgerhq/devices"); const bignumber_js_1 = require("bignumber.js"); const expect_1 = __importDefault(require("expect")); const invariant_1 = __importDefault(require("invariant")); const sample_1 = __importDefault(require("lodash/sample")); const speculos_deviceActions_1 = require("./speculos-deviceActions"); const tokens_1 = require("./tokens"); const currency = (0, index_1.getCryptoCurrencyById)("algorand"); // Minimum balance required for a new non-ASA account const minBalanceNewAccount = (0, currencies_1.parseCurrencyUnit)(currency.units[0], "0.1"); // Ensure that, when the recipient corresponds to an empty account, // the amount to send is greater or equal to the required minimum // balance for such a recipient const checkSendableToEmptyAccount = (amount, recipient) => { if ((0, account_1.isAccountEmpty)(recipient) && amount.lte(minBalanceNewAccount)) { (0, invariant_1.default)(amount.gt(minBalanceNewAccount), "not enough funds to send to new account"); } }; // Get list of ASAs associated with the account const getAssetsWithBalance = (account) => { return account.subAccounts ? account.subAccounts.filter(a => a.type === "TokenAccount" && a.balance.gt(0)) : []; }; const pickSiblingsOptedIn = (siblings, assetId) => { return (0, sample_1.default)(siblings.filter(a => { return a.subAccounts?.some(sa => sa.type === "TokenAccount" && sa.token.id.endsWith(assetId)); })); }; // TODO: rework to perform _difference_ between // array of valid ASAs and array of ASAs currently // being opted-in by an account const getRandomAssetId = (account) => { const optedInASA = account.subAccounts?.reduce((old, current) => { if (current.type === "TokenAccount") { return [...old, current.token.id]; } return old; }, []); const ASAs = (0, index_1.listTokensForCryptoCurrency)(account.currency).map(asa => asa.id); const diff = ASAs?.filter(asa => !optedInASA?.includes(asa)); (0, invariant_1.default)(diff && diff.length > 0, "already got all optin"); return (0, sample_1.default)(diff); }; const algorand = { name: "Algorand", currency, appQuery: { model: devices_1.DeviceModelId.nanoS, appName: "Algorand", }, genericDeviceAction: speculos_deviceActions_1.acceptTransaction, mutations: [ { name: "move ~50%", feature: "send", maxRun: 1, testDestination: specs_1.genericTestDestination, transaction: ({ account, siblings, bridge, maxSpendable }) => { (0, invariant_1.default)(maxSpendable.gt(0), "Spendable balance is too low"); const sibling = (0, specs_1.pickSiblings)(siblings, 4); const recipient = sibling.freshAddress; const transaction = bridge.createTransaction(account); const amount = maxSpendable.div(1.9 + 0.2 * Math.random()).integerValue(); checkSendableToEmptyAccount(amount, sibling); const updates = [ { amount, }, { recipient, }, ]; return { transaction, updates, }; }, test: ({ account, accountBeforeTransaction, operation }) => { const rewards = accountBeforeTransaction.algorandResources?.rewards || 0; (0, specs_1.botTest)("account balance moved with the operation value", () => (0, expect_1.default)(account.balance.plus(rewards).toString()).toBe(accountBeforeTransaction.balance.minus(operation.value).toString())); }, }, { name: "send max", feature: "sendMax", maxRun: 1, testDestination: specs_1.genericTestDestination, transaction: ({ account, siblings, bridge, maxSpendable }) => { (0, invariant_1.default)(maxSpendable.gt(0), "Spendable balance is too low"); const sibling = (0, specs_1.pickSiblings)(siblings, 4); // Send the full spendable balance const amount = maxSpendable; checkSendableToEmptyAccount(amount, sibling); return { transaction: bridge.createTransaction(account), updates: [ { recipient: sibling.freshAddress, }, { useAllAmount: true, }, ], }; }, test: ({ account }) => { // Ensure that there is no more than 20 μALGOs (discretionary value) // between the actual balance and the expected one to take into account // the eventual pending rewards added _after_ the transaction (0, specs_1.botTest)("account spendable balance is very low", () => (0, expect_1.default)(account.spendableBalance.lt(20)).toBe(true)); }, }, { name: "send ASA ~50%", feature: "tokens", maxRun: 1, transaction: ({ account, siblings, bridge, maxSpendable }) => { (0, invariant_1.default)(maxSpendable.gt(0), "Spendable balance is too low"); const subAccount = (0, sample_1.default)(getAssetsWithBalance(account)); (0, invariant_1.default)(subAccount && subAccount.type === "TokenAccount", "no subAccount with ASA"); const assetId = subAccount.token.id; const sibling = pickSiblingsOptedIn(siblings, assetId); const transaction = bridge.createTransaction(account); const recipient = sibling.freshAddress; const mode = "send"; const amount = subAccount.balance.div(1.9 + 0.2 * Math.random()).integerValue(); const updates = [ { mode, subAccountId: subAccount.id, }, { recipient, }, { amount, }, ]; return { transaction, updates, }; }, test: ({ account, accountBeforeTransaction, transaction, status }) => { const subAccountId = transaction.subAccountId; const subAccount = account.subAccounts?.find(sa => sa.id === subAccountId); const subAccountBeforeTransaction = accountBeforeTransaction.subAccounts?.find(sa => sa.id === subAccountId); (0, specs_1.botTest)("subAccount balance moved with the tx status amount", () => (0, expect_1.default)(subAccount?.balance.toString()).toBe(subAccountBeforeTransaction?.balance.minus(status.amount).toString())); }, }, { name: "opt-In ASA available", feature: "tokens", maxRun: 1, transaction: ({ account, bridge, maxSpendable }) => { // maxSpendable is expected to be greater than 100,000 micro-Algos // corresponding to the requirement that the main account will have // one more ASA after the opt-in; its minimum balance is updated accordingly (0, invariant_1.default)(maxSpendable.gt(new bignumber_js_1.BigNumber(100000)), "Spendable balance is too low"); const transaction = bridge.createTransaction(account); const mode = "optIn"; const assetId = getRandomAssetId(account); const subAccount = account.subAccounts ? account.subAccounts.find(a => a.id.includes(assetId)) : null; (0, invariant_1.default)(!subAccount, "already opt-in"); const updates = [ { mode, }, { assetId, }, ]; return { transaction, updates, }; }, // eslint-disable-next-line no-unused-vars test: ({ account, transaction }) => { (0, invariant_1.default)(transaction.assetId, "should have an assetId"); const assetId = (0, tokens_1.extractTokenId)(transaction.assetId); (0, specs_1.botTest)("have sub account with asset id", () => (0, expect_1.default)(account.subAccounts && account.subAccounts.some(a => a.id.endsWith(assetId))).toBe(true)); }, }, { name: "claim rewards", feature: "staking", maxRun: 1, transaction: ({ account, bridge, maxSpendable }) => { const rewards = account.algorandResources?.rewards; (0, invariant_1.default)(rewards && rewards.gt(0), "No pending rewards"); // Ensure that the rewards can effectively be claimed // (fees have to be paid in order to claim the rewards) (0, invariant_1.default)(maxSpendable.gt(0), "Spendable balance is too low"); const transaction = bridge.createTransaction(account); const mode = "claimReward"; const updates = [ { mode, }, ]; return { transaction, updates, }; }, test: ({ account }) => { (0, specs_1.botTest)("algoResources rewards is zero", () => (0, expect_1.default)(account.algorandResources && account.algorandResources.rewards.eq(0)).toBe(true)); }, }, ], }; exports.default = { algorand, }; //# sourceMappingURL=specs.js.map