@ledgerhq/coin-tron
Version:
Ledger Tron Coin integration
845 lines (750 loc) • 26.1 kB
text/typescript
import { stringify } from "querystring";
import { InvalidTransactionError } from "@ledgerhq/errors";
import network from "@ledgerhq/live-network";
import { hours, makeLRUCache } from "@ledgerhq/live-network/cache";
import { log } from "@ledgerhq/logs";
import { Account, TokenAccount } from "@ledgerhq/types-live";
import { BigNumber } from "bignumber.js";
import compact from "lodash/compact";
import drop from "lodash/drop";
import sumBy from "lodash/sumBy";
import take from "lodash/take";
import { TronWeb, providers } from "tronweb";
import coinConfig from "../config";
import type {
FreezeTransactionData,
LegacyUnfreezeTransactionData,
NetworkInfo,
SendTransactionData,
SendTransactionDataSuccess,
SmartContractTransactionData,
SuperRepresentative,
SuperRepresentativeData,
Transaction,
TrongridTxInfo,
TronResource,
UnDelegateResourceTransactionData,
UnFreezeTransactionData,
WithdrawExpireUnfreezeTransactionData,
} from "../types";
import { TronTransactionExpired } from "../types/errors";
import {
decode58Check,
encode58Check,
formatTrongridTrc20TxResponse,
formatTrongridTxResponse,
} from "./format";
import {
AccountTronAPI,
Block,
BlockWithTransactionsAPI,
isTransactionTronAPI,
MalformedTransactionTronAPI,
TransactionInfoByBlockNumAPI,
TransactionResponseTronAPI,
TransactionTronAPI,
Trc20API,
} from "./types";
import { abiEncodeTrc20Transfer, hexToAscii } from "./utils";
const getBaseApiUrl = () => coinConfig.getCoinConfig().explorer.url;
function isValidNativeTx(tx: TransactionTronAPI): boolean {
// tx_id indicates a malformed/duplicated entry from TronGrid — these must be excluded.
// Transactions with internal_transactions are valid and should be included.
return !tx.tx_id;
}
function isSuccessfulTriggerSmartContract(tx: TrongridTxInfo): boolean {
return tx.type === "TriggerSmartContract" && !tx.hasFailed;
}
export async function post<T, U extends object = any>(endPoint: string, body: T): Promise<U> {
const { data } = await network<U, T>({
method: "POST",
url: `${getBaseApiUrl()}${endPoint}`,
data: body,
});
// Ugly but trongrid send a 200 status event if there are errors
if ("Error" in data) {
const error = data.Error as any;
const message = stringify(error);
const nonEmptyMessage = message === "" ? error.toString() : message;
log("tron-error", nonEmptyMessage, { endPoint, body });
throw new Error(nonEmptyMessage);
}
return data;
}
async function fetch<T extends object = any>(endPoint: string): Promise<T> {
return fetchWithBaseUrl<T>(`${getBaseApiUrl()}${endPoint}`);
}
async function fetchWithBaseUrl<T extends object = any>(url: string): Promise<T> {
const { data } = await network<T>({ url });
// Ugly but trongrid send a 200 status event if there are errors
if ("Error" in data) {
log("tron-error", stringify(data.Error as any), {
url,
});
throw new Error(stringify(data.Error as any));
}
return data;
}
export const freezeTronTransaction = async (
account: Account,
transaction: Transaction,
): Promise<SendTransactionDataSuccess> => {
const txData: FreezeTransactionData = {
frozen_balance: transaction.amount.toNumber(),
resource: transaction.resource,
owner_address: decode58Check(account.freshAddress),
};
const url = `/wallet/freezebalancev2`;
const result = await post(url, txData);
return result;
};
export const unfreezeTronTransaction = async (
account: Account,
transaction: Transaction,
): Promise<SendTransactionDataSuccess> => {
const txData: UnFreezeTransactionData = {
owner_address: decode58Check(account.freshAddress),
resource: transaction.resource,
unfreeze_balance: transaction.amount.toNumber(),
};
const url = `/wallet/unfreezebalancev2`;
const result = await post(url, txData);
return result;
};
export const withdrawExpireUnfreezeTronTransaction = async (
account: Account,
_transaction: Transaction,
): Promise<SendTransactionDataSuccess> => {
const txData: WithdrawExpireUnfreezeTransactionData = {
owner_address: decode58Check(account.freshAddress),
};
const url = `/wallet/withdrawexpireunfreeze`;
const result = await post(url, txData);
return result;
};
export const unDelegateResourceTransaction = async (
account: Account,
transaction: Transaction,
): Promise<SendTransactionDataSuccess> => {
const txData: UnDelegateResourceTransactionData = {
balance: transaction.amount.toNumber(),
resource: transaction.resource,
owner_address: decode58Check(account.freshAddress),
receiver_address: decode58Check(transaction.recipient),
};
const url = `/wallet/undelegateresource`;
const result = await post(url, txData);
return result;
};
export const legacyUnfreezeTronTransaction = async (
account: Account,
transaction: Transaction,
): Promise<SendTransactionDataSuccess> => {
const txData: LegacyUnfreezeTransactionData = {
resource: transaction.resource,
owner_address: decode58Check(account.freshAddress),
receiver_address: transaction.recipient ? decode58Check(transaction.recipient) : undefined,
};
const url = `/wallet/unfreezebalance`;
const result = await post(url, txData);
return result;
};
export async function getDelegatedResource(
account: Account,
transaction: Transaction,
resource: TronResource,
): Promise<BigNumber> {
const url = `/wallet/getdelegatedresourcev2`;
const {
delegatedResource = [],
}: {
delegatedResource?: {
frozen_balance_for_bandwidth: number;
frozen_balance_for_energy: number;
}[];
} = await post(url, {
fromAddress: decode58Check(account.freshAddress),
toAddress: decode58Check(transaction.recipient),
});
const { frozen_balance_for_bandwidth, frozen_balance_for_energy } = delegatedResource.reduce(
(accum, cur) => {
if (cur.frozen_balance_for_bandwidth) {
accum.frozen_balance_for_bandwidth += cur.frozen_balance_for_bandwidth;
}
if (cur.frozen_balance_for_energy) {
accum.frozen_balance_for_energy += cur.frozen_balance_for_energy;
}
return accum;
},
{ frozen_balance_for_bandwidth: 0, frozen_balance_for_energy: 0 },
);
const amount =
resource === "BANDWIDTH" ? frozen_balance_for_bandwidth : frozen_balance_for_energy;
return new BigNumber(amount);
}
export const DEFAULT_TRC20_FEES_LIMIT = 50000000;
export async function craftTrc20Transaction(
tokenAddress: string,
recipientAddress: string,
senderAddress: string,
amount: BigNumber,
customFees?: number,
expiration?: number,
): Promise<SendTransactionDataSuccess> {
const txData: SmartContractTransactionData = {
function_selector: "transfer(address,uint256)",
fee_limit: customFees ? customFees : DEFAULT_TRC20_FEES_LIMIT,
call_value: 0,
contract_address: decode58Check(tokenAddress),
parameter: abiEncodeTrc20Transfer(recipientAddress, new BigNumber(amount.toString())),
owner_address: senderAddress,
};
const url = `/wallet/triggersmartcontract`;
const { transaction: preparedTransaction } = await post(url, txData);
return await extendExpiration(preparedTransaction, expiration);
}
export async function craftStandardTransaction(
tokenAddress: string | undefined,
recipientAddress: string,
senderAddress: string,
amount: BigNumber,
isTransferAsset: boolean,
memo?: string,
expiration?: number,
): Promise<SendTransactionDataSuccess> {
const url = isTransferAsset ? `/wallet/transferasset` : `/wallet/createtransaction`;
const txData: SendTransactionData = {
to_address: recipientAddress,
owner_address: senderAddress,
amount: Number(amount),
asset_name: tokenAddress && Buffer.from(tokenAddress).toString("hex"),
extra_data: memo && Buffer.from(memo).toString("hex"),
};
const preparedTransaction = await post(url, txData);
return await extendExpiration(preparedTransaction, expiration);
}
const getTokenInfo = (subAccount: TokenAccount | null | undefined): string[] | undefined[] => {
const tokenInfo =
subAccount && subAccount.type === "TokenAccount"
? drop(subAccount.token.id.split("/"), 1)
: [undefined, undefined];
return tokenInfo;
};
// Send trx or trc10/trc20 tokens
export const createTronTransaction = async (
account: Account,
transaction: Transaction,
subAccount: TokenAccount | null | undefined,
): Promise<SendTransactionDataSuccess> => {
const [tokenType, tokenId] = getTokenInfo(subAccount);
const decodeRecipient = decode58Check(transaction.recipient);
const decodeSender = decode58Check(account.freshAddress);
// trc20
if (tokenType === "trc20" && tokenId) {
const tokenContractAddress = (subAccount as TokenAccount).token.contractAddress;
return craftTrc20Transaction(
tokenContractAddress,
decodeRecipient,
decodeSender,
transaction.amount,
);
} else {
const isTransferAsset = subAccount ? true : false;
return craftStandardTransaction(
tokenId,
decodeRecipient,
decodeSender,
transaction.amount,
isTransferAsset,
);
}
};
/** Default expiration of 10 minutes (in seconds) after crafting time. */
export const DEFAULT_EXPIRATION = 600;
async function extendExpiration(
preparedTransaction: any,
expiration?: number,
): Promise<SendTransactionDataSuccess> {
const extension = expiration ?? DEFAULT_EXPIRATION;
const nodeExpiration: number = preparedTransaction.raw_data.expiration;
const minFinalExpiration = Date.now() + 3000;
// Tron nodes may not be properly synced, returning an expiration date in the past.
// We throw an error that encourages users to drop their transaction and re-create a new one.
// https://github.com/tronprotocol/tronweb/blob/9f8b559377d9215a4f5360e8526c6e7197bf5a5b/src/lib/TransactionBuilder/TransactionBuilder.ts#L2449-L2450
if (nodeExpiration + extension * 1000 <= minFinalExpiration) {
log("tron/extendExpiration", "Invalid extension provided", {
preparedTransaction,
extensionInS: extension,
extensionInMs: extension * 1000,
minFinalExpiration,
});
throw new InvalidTransactionError();
}
const HttpProvider = providers.HttpProvider;
const fullNode = new HttpProvider(getBaseApiUrl());
const solidityNode = new HttpProvider(getBaseApiUrl());
const eventServer = new HttpProvider(getBaseApiUrl());
const tronWeb = new TronWeb(fullNode, solidityNode, eventServer);
return tronWeb.transactionBuilder.extendExpiration(preparedTransaction, extension);
}
type BroadcastSuccessResponseTronAPI = { result: true; txid: string };
type BroadcastErrorResponseTronAPI = {
result?: boolean;
txid: string;
code: string;
message: string;
};
type BroadcastResponseTronAPI = BroadcastSuccessResponseTronAPI | BroadcastErrorResponseTronAPI;
/**
* @see https://github.com/tronprotocol/java-tron/blob/develop/framework/src/main/java/org/tron/core/services/http/BroadcastServlet.java
* @param trxTransaction
* @returns Transaction ID
*/
export const broadcastTron = async (
trxTransaction: SendTransactionDataSuccess & { signature: string[] },
): Promise<string> => {
const result: BroadcastResponseTronAPI = await post(
"/wallet/broadcasttransaction",
trxTransaction,
);
if (result.result !== true) {
if (result.code === "TRANSACTION_EXPIRATION_ERROR") {
throw new TronTransactionExpired();
} else {
throw new Error(`${result.code}: ${result.message}`);
}
}
return result.txid;
};
type TronGridBroadcastResponse = {
result: boolean;
code: string;
txid: string;
message: string;
transaction: {
raw_data: Record<string, unknown>;
signature: string[];
};
};
export const broadcastHexTron = async (rawTransaction: string): Promise<string> => {
const result = await post<{ transaction: string }, TronGridBroadcastResponse>(
`/wallet/broadcasthex`,
{ transaction: rawTransaction },
);
if (!result.result) {
throw Error(`Broadcast failed due to ${result.code}`);
}
return result.txid;
};
/**
* {@link https://github.com/tronprotocol/java-tron/blob/develop/framework/src/main/java/org/tron/core/services/http/GetAccountServlet.java | Tron Framework}
*/
export async function fetchTronAccount(addr: string): Promise<AccountTronAPI[]> {
try {
const data = await fetch(`/v1/accounts/${addr}`);
return data.data;
} catch {
return [];
}
}
export async function getLastBlock(): Promise<Block> {
const data = await fetch(`/wallet/getnowblock`);
return toBlock(data);
}
export async function getBlock(blockNumber: number): Promise<Block> {
const data: BlockWithTransactionsAPI = await post(`/wallet/getblock`, {
id_or_num: String(blockNumber),
detail: false,
});
return toBlock(data);
}
export async function getBlockWithTransactions(
blockNumber: number,
): Promise<BlockWithTransactionsAPI> {
return post(`/wallet/getblock`, { id_or_num: String(blockNumber), detail: true });
}
function toBlock(data: BlockWithTransactionsAPI): Block {
const timestamp = data.block_header.raw_data.timestamp;
const ret: Block = {
height: data.block_header.raw_data.number,
hash: data.blockID,
};
if (timestamp) {
ret.time = new Date(timestamp);
}
return ret;
}
export async function getTransactionInfoByBlockNum(
blockNum: number,
): Promise<TransactionInfoByBlockNumAPI[]> {
return post<{ num: number }, TransactionInfoByBlockNumAPI[]>(
`/wallet/gettransactioninfobyblocknum`,
{ num: blockNum },
);
}
async function getAllTransactions<T>(
initialUrl: string,
shouldFetchMoreTxs: (txs: T[]) => boolean,
getTxs: (url: string) => Promise<{
results: Array<T>;
nextUrl?: string;
}>,
) {
let all: Array<T> = [];
let url: string | undefined = initialUrl;
while (url && shouldFetchMoreTxs(all)) {
const { nextUrl, results } = await getTxs(url);
url = nextUrl;
all = all.concat(results);
}
return all;
}
const getTransactions = async (
url: string,
): Promise<{
results: Array<TransactionTronAPI | MalformedTransactionTronAPI>;
nextUrl?: string;
}> => {
const transactions =
await fetchWithBaseUrl<
TransactionResponseTronAPI<TransactionTronAPI | MalformedTransactionTronAPI>
>(url);
const nextUrl = transactions.meta.links?.next?.replace(
/https:\/\/api(\.[a-z]*)?.trongrid.io/,
getBaseApiUrl(),
);
const results = transactions.data ?? [];
return {
results,
nextUrl,
};
};
const getTrc20 = async (
url: string,
): Promise<{
results: Array<Trc20API>;
nextUrl?: string;
}> => {
const transactions = await fetchWithBaseUrl<TransactionResponseTronAPI<Trc20API>>(url);
return {
results: transactions.data,
nextUrl: transactions.meta.links?.next?.replace(
/https:\/\/api(\.[a-z]*)?.trongrid.io/,
getBaseApiUrl(),
),
};
};
export type FetchTxsStopPredicate = (
txs: Array<TransactionTronAPI | Trc20API | MalformedTransactionTronAPI>,
) => boolean;
export type FetchParams = {
/** The maximum number of transactions to fetch per call. */
limitPerCall: number;
/** Hint about the number of transactions to be fetched in total (hint to optimize `limitPerCall`) */
hintGlobalLimit?: number;
minTimestamp: number;
order: "asc" | "desc";
};
export const defaultFetchParams: FetchParams = {
limitPerCall: 100,
minTimestamp: 0,
order: "desc",
} as const;
export type TxPageResult = {
txs: TrongridTxInfo[];
hasNextPage: boolean;
};
export type FetchTxsPageParams = {
limit: number;
minTimestamp: number;
maxTimestamp?: number;
order: "asc" | "desc";
};
export type FetchTxsPageResult = {
nativeTxs: TxPageResult;
trc20Txs: TxPageResult;
};
async function fetchSinglePage<T>(
url: string,
getTxs: (url: string) => Promise<{ results: Array<T>; nextUrl?: string }>,
): Promise<{ results: Array<T>; hasNextPage: boolean }> {
const { results, nextUrl } = await getTxs(url);
return { results, hasNextPage: !!nextUrl };
}
export async function fetchTronAccountTxsPage(
addr: string,
params: FetchTxsPageParams,
): Promise<FetchTxsPageResult> {
const maxTimestampParam =
params.maxTimestamp !== undefined ? `&max_timestamp=${params.maxTimestamp}` : "";
const queryParams = `limit=${params.limit}&min_timestamp=${params.minTimestamp}${maxTimestampParam}&order_by=block_timestamp,${params.order}`;
const [nativeResult, trc20Result] = await Promise.all([
fetchSinglePage<TransactionTronAPI | MalformedTransactionTronAPI>(
`${getBaseApiUrl()}/v1/accounts/${addr}/transactions?${queryParams}`,
getTransactions,
),
fetchSinglePage<Trc20API>(
`${getBaseApiUrl()}/v1/accounts/${addr}/transactions/trc20?${queryParams}&get_detail=true`,
getTrc20,
),
]);
const nativeTxsFormatted = await Promise.all(
nativeResult.results
.filter(isTransactionTronAPI)
.filter(isValidNativeTx)
.map(tx => formatTrongridTxResponse(tx, accountNamesCache)),
);
const trc20TxsFormatted = compact(trc20Result.results.map(formatTrongridTrc20TxResponse));
const trc20TxIds = new Set(trc20TxsFormatted.map(t => t.txID));
const nativeDeduped = compact(nativeTxsFormatted)
.filter(tx => !trc20TxIds.has(tx.txID))
.filter(tx => !isSuccessfulTriggerSmartContract(tx));
return {
nativeTxs: { txs: nativeDeduped, hasNextPage: nativeResult.hasNextPage },
trc20Txs: { txs: trc20TxsFormatted, hasNextPage: trc20Result.hasNextPage },
};
}
export async function fetchTronAccountTxs(
addr: string,
shouldFetchMoreTxs: FetchTxsStopPredicate,
params: FetchParams,
): Promise<TrongridTxInfo[]> {
const adjustedLimitPerCall = params.hintGlobalLimit
? Math.min(params.limitPerCall, params.hintGlobalLimit)
: params.limitPerCall;
const queryParams = `limit=${adjustedLimitPerCall}&min_timestamp=${params.minTimestamp}&order_by=block_timestamp,${params.order}`;
const nativeTxs = await Promise.all(
(
await getAllTransactions<TransactionTronAPI | MalformedTransactionTronAPI>(
`${getBaseApiUrl()}/v1/accounts/${addr}/transactions?${queryParams}`,
shouldFetchMoreTxs,
getTransactions,
)
)
.filter(isTransactionTronAPI)
.filter(isValidNativeTx)
.map(tx => formatTrongridTxResponse(tx, accountNamesCache)),
);
// we need to fetch and filter trc20 transactions from another endpoint
// doc https://developers.tron.network/reference/get-trc20-transaction-info-by-account-address
const callTrc20Endpoint = async () =>
await getAllTransactions<Trc20API>(
`${getBaseApiUrl()}/v1/accounts/${addr}/transactions/trc20?${queryParams}&get_detail=true`,
shouldFetchMoreTxs,
getTrc20,
);
type Acc = {
txs: Trc20API[];
invalids: number[];
};
function isValid(tx: Trc20API): boolean {
const ret = tx?.detail?.ret;
return Array.isArray(ret) && ret.length > 0;
}
function getInvalidTxIndexes(txs: Trc20API[]): number[] {
const invalids: number[] = [];
for (let i = 0; i < txs.length; i++) {
if (!isValid(txs[i])) {
invalids.push(i);
}
}
txs.filter(tx => !isValid(tx)).map((_tx, index) => index);
return invalids;
}
function assert(predicate: boolean, message: string) {
if (!predicate) {
throw new Error(message);
}
}
// Merge the two results
function mergeAccs(acc1: Acc, acc2: Acc): Acc {
assert(acc1.txs.length === acc2.txs.length, "accs should have the same length");
const accRet: Acc = { txs: acc1.txs, invalids: [] };
acc1.invalids.forEach(invalidIndex => {
acc2.invalids.includes(invalidIndex)
? accRet.invalids.push(invalidIndex)
: (accRet.txs[invalidIndex] = acc2.txs[invalidIndex]);
});
return accRet;
}
// see LIVE-18992 for an explanation to why we need this
async function getTrc20TxsWithRetry(acc: Acc | null, times: number): Promise<Trc20API[]> {
assert(
times > 0,
"getTrc20TxsWithRetry: couldn't fetch trc20 transactions after several attempts",
);
const ret = await callTrc20Endpoint();
const thisAcc: Acc = {
txs: ret,
invalids: getInvalidTxIndexes(ret),
};
const newAcc = acc ? mergeAccs(acc, thisAcc) : thisAcc;
if (newAcc.invalids.length === 0) {
return newAcc.txs;
} else {
log(
"coin-tron",
`getTrc20TxsWithRetry: got ${newAcc.invalids.length} invalid trc20 transactions, retrying...`,
);
return await getTrc20TxsWithRetry(newAcc, times - 1);
}
}
const trc20Txs = compact(
(await getTrc20TxsWithRetry(null, 3)).map(formatTrongridTrc20TxResponse),
);
const trc20TxIds = new Set(trc20Txs.map(t => t.txID));
const nativeDeduped = compact(nativeTxs)
.filter(tx => !trc20TxIds.has(tx.txID))
.filter(tx => !isSuccessfulTriggerSmartContract(tx));
const txInfos: TrongridTxInfo[] = nativeDeduped
.concat(trc20Txs)
.sort((a, b) => b.date.getTime() - a.date.getTime());
return txInfos;
}
export const getContractUserEnergyRatioConsumption = async (address: string): Promise<number> => {
const result = await fetchTronContract(address);
if (result) {
const { consume_user_resource_percent } = result;
return consume_user_resource_percent;
}
return 0;
};
export const fetchTronContract = async (addr: string): Promise<Record<string, any> | undefined> => {
try {
const data = await post(`/wallet/getcontract`, {
value: decode58Check(addr),
});
return Object.keys(data).length !== 0 ? data : undefined;
} catch {
return undefined;
}
};
export const getTronAccountNetwork = async (address: string): Promise<NetworkInfo> => {
const result = await fetch(
`/wallet/getaccountresource?address=${encodeURIComponent(decode58Check(address))}`,
);
const {
freeNetUsed = 0,
freeNetLimit = 0,
NetUsed = 0,
NetLimit = 0,
EnergyUsed = 0,
EnergyLimit = 0,
} = result;
return {
family: "tron",
freeNetUsed: new BigNumber(freeNetUsed),
freeNetLimit: new BigNumber(freeNetLimit),
netUsed: new BigNumber(NetUsed),
netLimit: new BigNumber(NetLimit),
energyUsed: new BigNumber(EnergyUsed),
energyLimit: new BigNumber(EnergyLimit),
};
};
export const validateAddress = async (address: string): Promise<boolean> => {
try {
const result = await post(`/wallet/validateaddress`, {
address: decode58Check(address),
});
return result.result || false;
} catch (e: any) {
// FIXME we should not silent errors!
log("tron-error", "validateAddress fails with " + e.message, {
address,
});
return false;
}
};
// cache for account names (name is unchanged over time)
export const accountNamesCache = makeLRUCache(
async (addr: string): Promise<string | null | undefined> => getAccountName(addr),
(addr: string) => addr,
hours(3, 300),
);
export const getAccountName = async (addr: string): Promise<string | null | undefined> => {
const tronAcc = await fetchTronAccount(addr);
const acc = tronAcc[0];
const accountName: string | null | undefined =
acc && acc.account_name ? hexToAscii(acc.account_name) : undefined;
accountNamesCache.hydrate(addr, accountName); // put it in cache
return accountName;
};
const superRepresentativesCache = makeLRUCache(
async (): Promise<SuperRepresentative[]> => {
const superRepresentatives = await fetchSuperRepresentatives();
log(
"tron/superRepresentatives",
"loaded " + superRepresentatives.length + " super representatives",
);
return superRepresentatives;
},
() => "",
hours(1, 300),
);
export const getTronSuperRepresentatives = async (): Promise<SuperRepresentative[]> => {
return await superRepresentativesCache();
};
export const hydrateSuperRepresentatives = (list: SuperRepresentative[]) => {
log("tron/superRepresentatives", "hydrate " + list.length + " super representatives");
superRepresentativesCache.hydrate("", list);
};
const fetchSuperRepresentatives = async (): Promise<SuperRepresentative[]> => {
const result = await fetch<{ witnesses: SuperRepresentative[] }>(`/wallet/listwitnesses`);
const sorted = result.witnesses.sort((a, b) => b.voteCount - a.voteCount);
const superRepresentatives = sorted.map(w => ({
...w,
address: encode58Check(w.address),
voteCount: w.voteCount || 0,
isJobs: w.isJobs || false,
}));
hydrateSuperRepresentatives(superRepresentatives); // put it in cache
return superRepresentatives;
};
export const getNextVotingDate = async (): Promise<Date> => {
const { num } = await fetch(`/wallet/getnextmaintenancetime`);
return new Date(num);
};
export const getTronSuperRepresentativeData = async (
max: number | null | undefined,
): Promise<SuperRepresentativeData> => {
const list = await getTronSuperRepresentatives();
const nextVotingDate = await getNextVotingDate();
return {
list: max ? take(list, max) : list,
totalVotes: sumBy(list, "voteCount"),
nextVotingDate,
};
};
export const voteTronSuperRepresentatives = async (
account: Account,
transaction: Transaction,
): Promise<SendTransactionDataSuccess> => {
const payload = {
owner_address: decode58Check(account.freshAddress),
votes: transaction.votes.map(v => ({
vote_address: decode58Check(v.address),
vote_count: v.voteCount,
})),
};
return await post(`/wallet/votewitnessaccount`, payload);
};
export const getUnwithdrawnReward = async (addr: string): Promise<BigNumber> => {
try {
const { reward = 0 } = await fetch(
`/wallet/getReward?address=${encodeURIComponent(decode58Check(addr))}`,
);
return new BigNumber(reward);
} catch {
return Promise.resolve(new BigNumber(0));
}
};
export const claimRewardTronTransaction = async (
account: Account,
): Promise<SendTransactionDataSuccess> => {
const url = `/wallet/withdrawbalance`;
const data = {
owner_address: decode58Check(account.freshAddress),
};
const result = await post(url, data);
return result;
};