@ledgerhq/coin-tron
Version:
Ledger Tron Coin integration
460 lines (444 loc) • 17 kB
text/typescript
import { BigNumber } from "bignumber.js";
import invariant from "invariant";
import expect from "expect";
import type { Transaction } from "../types";
import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/index";
import { parseCurrencyUnit } from "@ledgerhq/coin-framework/currencies";
import { botTest, pickSiblings } from "@ledgerhq/coin-framework/bot/specs";
import type { AppSpec, TransactionDestinationTestInput } from "@ledgerhq/coin-framework/bot/types";
// import { getUnfreezeData, getNextRewardDate } from "./react";
import { DeviceModelId } from "@ledgerhq/devices";
import { acceptTransaction } from "./bot-deviceActions";
const currency = getCryptoCurrencyById("tron");
const minimalAmount = parseCurrencyUnit(currency.units[0], "1");
const maxAccount = 10;
/*const getDecimalPart = (value: BigNumber, magnitude: number) =>
value.minus(value.modulo(10 ** magnitude));*/
// FIXME TRON have a bug where the amounts from the API have imprecisions
const expectedApproximate = (value: BigNumber, expected: BigNumber, delta = 50) => {
if (value.minus(expected).abs().gt(delta)) {
expect(value.toString()).toEqual(value.toString());
}
};
const testDestination = <T>({
destination,
operation,
destinationBeforeTransaction,
sendingOperation,
}: TransactionDestinationTestInput<T>): void => {
const amount = sendingOperation.value.minus(sendingOperation.fee);
botTest("account balance increased with transaction amount", () =>
expectedApproximate(destination.balance, destinationBeforeTransaction.balance.plus(amount)),
);
botTest("operation amount is consistent with sendingOperation", () =>
expectedApproximate(operation.value, amount),
);
};
const tron: AppSpec<Transaction> = {
name: "Tron",
currency,
appQuery: {
model: DeviceModelId.nanoSP,
appName: "Tron",
firmware: "1.1.1",
appVersion: "0.5.0",
},
genericDeviceAction: acceptTransaction,
testTimeout: 2 * 60 * 1000,
minViableAmount: minimalAmount,
mutations: [
{
name: "move 50% to another account",
feature: "send",
maxRun: 1,
testDestination,
transaction: ({ account, siblings, bridge, maxSpendable }) => {
invariant(maxSpendable.gt(minimalAmount), "balance is too low");
const sibling = pickSiblings(siblings, maxAccount);
const recipient = sibling.freshAddress;
const amount = maxSpendable.div(2).integerValue();
return {
transaction: bridge.createTransaction(account),
updates: [
{
recipient,
},
{
amount,
},
],
};
},
test: ({ accountBeforeTransaction, operation, account }) => {
botTest("account spendable balance decreased with operation", () =>
expectedApproximate(
account.spendableBalance,
accountBeforeTransaction.spendableBalance.minus(operation.value),
),
);
},
},
{
name: "send max to another account",
feature: "sendMax",
maxRun: 1,
testDestination,
transaction: ({ account, siblings, bridge, maxSpendable }) => {
invariant(maxSpendable.gt(minimalAmount), "balance is too low");
const sibling = pickSiblings(siblings, maxAccount);
const recipient = sibling.freshAddress;
return {
transaction: bridge.createTransaction(account),
updates: [
{
recipient,
},
{
useAllAmount: true,
},
],
};
},
test: ({ account }) => {
botTest("account spendable balance is zero", () =>
expectedApproximate(account.spendableBalance, new BigNumber(0)),
);
},
},
/*
We do not manage staking anymore
{
name: "freeze 25% to bandwidth | energy",
maxRun: 1,
transaction: ({ siblings, account, bridge, maxSpendable }) => {
expectSiblingsHaveSpendablePartGreaterThan(siblings, 0.5);
invariant(maxSpendable.gt(minimalAmount), "balance is too low");
let amount = getDecimalPart(
maxSpendable.div(4),
currency.units[0].magnitude,
).integerValue();
if (amount.lt(minimalAmount)) {
amount = minimalAmount;
}
const energy = get(account, `tronResources.energy`, new BigNumber(0));
return {
transaction: bridge.createTransaction(account),
updates: [
{
mode: "freeze",
},
{
resource: energy.eq(0) ? "ENERGY" : "BANDWIDTH",
},
{
amount,
},
],
};
},
test: ({ account, accountBeforeTransaction, transaction }) => {
const resourceType = (transaction.resource || "").toLocaleLowerCase();
const resourceBeforeTransaction = get(
accountBeforeTransaction,
`tronResources.frozen.${resourceType}.amount`,
new BigNumber(0),
);
const expectedAmount = new BigNumber(transaction.amount).plus(resourceBeforeTransaction);
const currentRessourceAmount = get(
account,
`tronResources.frozen.${resourceType}.amount`,
new BigNumber(0),
);
botTest("frozen amount is accumulated in resources", () =>
expect(expectedAmount.toString()).toBe(currentRessourceAmount.toString()),
);
},
},
{
name: "unfreeze bandwith / energy",
maxRun: 1,
transaction: ({ account, bridge }) => {
const TP = new BigNumber(get(account, "tronResources.tronPower", "0"));
invariant(TP.gt(0), "no frozen assets");
const { canUnfreezeBandwidth, canUnfreezeEnergy } = getUnfreezeData(account as TronAccount);
invariant(canUnfreezeBandwidth || canUnfreezeEnergy, "freeze period not expired yet");
const resourceToUnfreeze = canUnfreezeBandwidth ? "BANDWIDTH" : "ENERGY";
return {
transaction: bridge.createTransaction(account),
updates: [
{
mode: "unfreeze",
},
{
resource: resourceToUnfreeze,
},
],
};
},
test: ({ account, accountBeforeTransaction, transaction }) => {
const TxResource = (transaction.resource || "").toLocaleLowerCase();
const currentFrozen = get(account, `tronResources.frozen.${TxResource}`, undefined);
botTest("no current frozen", () => expect(currentFrozen).toBeUndefined());
const TPBeforeTx = new BigNumber(
get(accountBeforeTransaction, "tronResources.tronPower", 0),
);
const currentTP = new BigNumber(get(account, "tronResources.tronPower", 0));
const expectedTronPower = TPBeforeTx.minus(transaction.amount);
botTest("tron power", () => expectedApproximate(currentTP, expectedTronPower));
},
},
{
name: "submit vote",
maxRun: 1,
transaction: ({ account, bridge, preloadedData }) => {
const TP = new BigNumber(get(account, "tronResources.tronPower", "0"));
invariant(TP.gt(0), "no tron power to vote");
const currentTPVoted = get(account, "tronResources.votes", []).reduce(
(acc, curr) => acc.plus(new BigNumber(get(curr, "voteCount", 0))),
new BigNumber(0),
);
invariant(TP.gt(currentTPVoted), "you have no tron power left");
const { superRepresentatives } = preloadedData;
invariant(
superRepresentatives && superRepresentatives.length,
"there are no super representatives to vote for, or the list has not been loaded yet",
);
const count = 1 + Math.floor(5 * Math.random());
const candidates = sampleSize(superRepresentatives.slice(0, 40), count);
let remaining = TP;
const votes = candidates
.map(c => {
if (!remaining.gt(0)) return null;
const voteCount = remaining.eq(1)
? remaining.integerValue().toNumber()
: remaining.times(Math.random()).integerValue().toNumber();
if (voteCount === 0) return null;
remaining = remaining.minus(voteCount);
return {
address: c.address,
voteCount,
};
})
.filter(Boolean);
return {
transaction: bridge.createTransaction(account) as Transaction,
updates: [
{
mode: "vote",
},
{
votes,
},
] as Array<Partial<Transaction>>,
};
},
test: ({ account, transaction }) => {
const votes = sortBy(transaction.votes, ["address"]);
const currentVotes = sortBy(get(account, "tronResources.votes", []), ["address"]);
botTest("current votes", () => expect(currentVotes).toEqual(votes));
},
},*/
/**
* FIXME
*
* Our bad implementation of TRC10/TRC20 operations and sync make those tests impossible to
* work properly.
*
* To make them work we need to rework the link between Operation and SubOperations which is wrong as of now and
* rework our fee estimation (TRC20 do have fees which are ignored today until the next sync creates an OUT tx to represent it).
*/
// {
// name: "move some TRC10",
// maxRun: 1,
// transaction: ({ account, siblings, bridge }) => {
// const trc10Account = sample(
// (account.subAccounts || []).filter(
// (a) => a.type === "TokenAccount" && a.token.tokenType === "trc10"
// )
// );
// invariant(trc10Account, "no trc10 account");
// if (!trc10Account) throw new Error("no trc10 account");
// invariant(trc10Account?.balance.gt(0), "trc10 account has no balance");
// const sibling = pickSiblings(siblings, maxAccount);
// const recipient = sibling.freshAddress;
// return {
// transaction: bridge.createTransaction(account),
// updates: [
// {
// recipient,
// subAccountId: trc10Account.id,
// },
// Math.random() < 0.5
// ? {
// useAllAmount: true,
// }
// : {
// amount: trc10Account.balance
// .times(Math.random())
// .integerValue(),
// },
// ],
// };
// },
// test: ({ accountBeforeTransaction, account, transaction }) => {
// invariant(accountBeforeTransaction.subAccounts, "sub accounts before");
// const trc10accountBefore = (
// accountBeforeTransaction.subAccounts as SubAccount[]
// ).find((s) => s.id === transaction.subAccountId);
// invariant(trc10accountBefore, "trc10 acc was here before");
// if (!trc10accountBefore) throw new Error("no trc10before account");
// invariant(account.subAccounts, "sub accounts");
// const trc10account = (account.subAccounts as SubAccount[]).find(
// (s) => s.id === transaction.subAccountId
// );
// invariant(trc10account, "trc10 acc is still here");
// if (!trc10account) throw new Error("no trc10 account");
// if (transaction.useAllAmount) {
// botTest("trc10 balance became zero", () =>
// expect(trc10account.balance.toString()).toBe("0")
// );
// } else {
// botTest("trc10 balance decreased with operation", () =>
// expect(trc10account.balance.toString()).toBe(
// trc10accountBefore.balance.minus(transaction.amount).toString()
// )
// );
// }
// },
// },
// {
// name: "move some TRC20",
// maxRun: 1,
// transaction: ({ account, siblings, bridge }) => {
// const balance = account.spendableBalance;
// const energy = get(account, "tronResources.energy", new BigNumber(0));
// invariant(
// energy.gt(0) || balance.gt(minimalAmount),
// "trx and energy too low"
// );
// const trc20Account = sample(
// (account.subAccounts || []).filter(
// (a) => a.type === "TokenAccount" && a.token.tokenType === "trc20"
// )
// );
// invariant(trc20Account, "no trc20 account");
// invariant(trc20Account?.balance.gt(0), "trc20 account has no balance");
// if (!trc20Account) throw new Error("no trc20 account");
// const sibling = pickSiblings(siblings, maxAccount);
// invariant(
// sibling.balance.gt(0),
// "recipient cannot receive trc20 because it has no balance"
// );
// const recipient = sibling.freshAddress;
// return {
// transaction: bridge.createTransaction(account),
// updates: [
// {
// recipient,
// subAccountId: trc20Account.id,
// },
// Math.random() < 0.5
// ? {
// useAllAmount: true,
// }
// : {
// amount: trc20Account.balance
// .times(Math.random())
// .integerValue(),
// },
// ],
// };
// },
// test: ({ accountBeforeTransaction, account, transaction }) => {
// invariant(accountBeforeTransaction.subAccounts, "sub accounts before");
// const trc20accountBefore = (
// accountBeforeTransaction.subAccounts as SubAccount[]
// ).find((s) => s.id === transaction.subAccountId);
// invariant(trc20accountBefore, "trc20 acc was here before");
// if (!trc20accountBefore) throw new Error("no trc20 before account");
// invariant(account.subAccounts, "sub accounts");
// const trc20account = (account.subAccounts as SubAccount[]).find(
// (s) => s.id === transaction.subAccountId
// );
// invariant(trc20account, "trc20 acc is still here");
// if (!trc20account) throw new Error("no trc20 account");
// if (transaction.useAllAmount) {
// botTest("trc10 balance became zero", () =>
// expect(trc20account.balance.toString()).toBe("0")
// );
// } else {
// botTest("trc10 balance decreased with operation", () =>
// expect(trc20account.balance.toString()).toBe(
// trc20accountBefore.balance.minus(transaction.amount).toString()
// )
// );
// }
// if (
// get(trc20accountBefore, "tronResources.energy", new BigNumber(0)).eq(
// 0
// )
// ) {
// botTest("account balance decreased", () =>
// expect(account.balance.lt(accountBeforeTransaction.balance)).toBe(
// true
// )
// );
// } else {
// botTest("energy decreased", () =>
// expect(
// get(account, "tronResources.energy", new BigNumber(0)).lt(
// get(
// accountBeforeTransaction,
// "tronResources.energy",
// new BigNumber(0)
// )
// )
// ).toBe(true)
// );
// botTest("balance didn't change", () =>
// expect(account.balance.eq(accountBeforeTransaction.balance)).toBe(
// true
// )
// );
// }
// },
// },
/*
We do not manage staking anymore
{
name: "claim rewards",
maxRun: 1,
transaction: ({ account, bridge }) => {
const nextRewardDate = getNextRewardDate(account as TronAccount);
const today = Date.now();
const unwithdrawnReward = new BigNumber(
get(account, "tronResources.unwithdrawnReward", "0"),
);
invariant(unwithdrawnReward.gt(0), "no rewards to claim");
invariant(
nextRewardDate && nextRewardDate <= today,
"you can't claim twice in less than 24 hours",
);
return {
transaction: bridge.createTransaction(account),
updates: [
{
mode: "claimReward",
},
],
};
},
test: ({ account }) => {
const rewards = new BigNumber(get(account, "tronResources.unwithdrawnReward", "0"));
const nextRewardDate = getNextRewardDate(account as TronAccount);
botTest("rewards is zero", () => expect(rewards.eq(0)).toBe(true));
botTest("next reward date settled", () =>
expect(nextRewardDate && nextRewardDate > Date.now()).toBe(true),
);
},
},*/
],
};
export default {
tron,
};