@ledgerhq/coin-multiversx
Version:
Ledger MultiversX Coin integration
281 lines • 13.1 kB
JavaScript
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 { 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 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) {
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) {
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) {
const { operation, optimisticOperation, transaction } = input;
const opExpected = toOperationRaw({
...optimisticOperation,
});
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) {
const { account, accountBeforeTransaction } = input;
const operation = input.operation;
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) {
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 = {
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 = 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?.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?.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))), "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,
};
//# sourceMappingURL=specs.js.map