@ledgerhq/coin-algorand
Version:
Ledger Algorand Coin integration
229 lines • 10.7 kB
JavaScript
;
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