UNPKG

@ledgerhq/coin-tron

Version:
414 lines 17.1 kB
import { hours, makeLRUCache } from "@ledgerhq/live-network/cache"; import network from "@ledgerhq/live-network"; import { promiseAllBatched } from "@ledgerhq/live-promise"; import { log } from "@ledgerhq/logs"; 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 from "tronweb"; import { TronTransactionExpired } from "../types/errors"; import coinConfig from "../config"; import { abiEncodeTrc20Transfer, hexToAscii } from "./utils"; import { decode58Check, encode58Check, formatTrongridTrc20TxResponse, formatTrongridTxResponse, } from "./format"; import { isMalformedTransactionTronAPI, isTransactionTronAPI, } from "./types"; import { stringify } from "querystring"; const getBaseApiUrl = () => coinConfig.getCoinConfig().explorer.url; export async function post(endPoint, body) { const { data } = await network({ method: "POST", url: `${getBaseApiUrl()}${endPoint}`, data: body, }); // Ugly but trongrid send a 200 status event if there are errors if ("Error" in data) { log("tron-error", stringify(data.Error), { endPoint, body, }); throw new Error(stringify(data.Error)); } return data; } async function fetch(endPoint) { return fetchWithBaseUrl(`${getBaseApiUrl()}${endPoint}`); } async function fetchWithBaseUrl(url) { const { data } = await network({ url }); // Ugly but trongrid send a 200 status event if there are errors if ("Error" in data) { log("tron-error", stringify(data.Error), { url, }); throw new Error(stringify(data.Error)); } return data; } export const freezeTronTransaction = async (account, transaction) => { const txData = { 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, transaction) => { const txData = { 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, _transaction) => { const txData = { owner_address: decode58Check(account.freshAddress), }; const url = `/wallet/withdrawexpireunfreeze`; const result = await post(url, txData); return result; }; export const unDelegateResourceTransaction = async (account, transaction) => { const txData = { 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, transaction) => { const txData = { 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, transaction, resource) { const url = `/wallet/getdelegatedresourcev2`; const { delegatedResource = [], } = 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 async function craftTrc20Transaction(tokenAddress, recipientAddress, senderAddress, amount) { const txData = { function_selector: "transfer(address,uint256)", fee_limit: 50000000, 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 extendTronTxExpirationTimeBy10mn(preparedTransaction); } export async function craftStandardTransaction(tokenAddress, recipientAddress, senderAddress, amount, isTransferAsset) { const url = isTransferAsset ? `/wallet/transferasset` : `/wallet/createtransaction`; const txData = { to_address: recipientAddress, owner_address: senderAddress, amount: Number(amount), asset_name: tokenAddress && Buffer.from(tokenAddress).toString("hex"), }; const preparedTransaction = await post(url, txData); return await extendTronTxExpirationTimeBy10mn(preparedTransaction); } const getTokenInfo = (subAccount) => { 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, transaction, subAccount) => { const [tokenType, tokenId] = getTokenInfo(subAccount); const decodeRecipient = decode58Check(transaction.recipient); const decodeSender = decode58Check(account.freshAddress); // trc20 if (tokenType === "trc20" && tokenId) { const tokenContractAddress = subAccount.token.contractAddress; return craftTrc20Transaction(tokenContractAddress, decodeRecipient, decodeSender, transaction.amount); } else { const isTransferAsset = subAccount ? true : false; return craftStandardTransaction(tokenId, decodeRecipient, decodeSender, transaction.amount, isTransferAsset); } }; async function extendTronTxExpirationTimeBy10mn(preparedTransaction) { const VAULT_EXPIRATION_TIME = 600; const HttpProvider = TronWeb.providers.HttpProvider; const fullNode = new HttpProvider(getBaseApiUrl()); const solidityNode = new HttpProvider(getBaseApiUrl()); const eventServer = new HttpProvider(getBaseApiUrl()); const tronWeb = new TronWeb(fullNode, solidityNode, eventServer); //FIXME: test it and rewrite it return tronWeb.transactionBuilder.extendExpiration(preparedTransaction, VAULT_EXPIRATION_TIME); } /** * @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) => { const result = await post("/wallet/broadcasttransaction", trxTransaction); if (result.result !== true) { if (result.code === "TRANSACTION_EXPIRATION_ERROR") { throw new TronTransactionExpired(); } else { throw new Error(result.message); } } return result.txid; }; export const broadcastHexTron = async (rawTransaction) => { const result = await post(`/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) { try { const data = await fetch(`/v1/accounts/${addr}`); return data.data; } catch (e) { return []; } } export async function getLastBlock() { const data = await fetch(`/wallet/getnowblock`); return { height: data.block_header.raw_data.number, hash: data.blockID, time: new Date(data.block_header.raw_data.timestamp), }; } // For the moment, fetching transaction info is the only way to get fees from a transaction // Export for test purpose only export async function fetchTronTxDetail(txId) { const { fee, blockNumber, withdraw_amount, unfreeze_amount } = await fetch(`/wallet/gettransactioninfobyid?value=${encodeURIComponent(txId)}`); return { fee, blockNumber, withdraw_amount, unfreeze_amount, }; } async function getAllTransactions(initialUrl, shouldFetchMoreTxs, getTxs) { let all = []; let url = initialUrl; while (url && shouldFetchMoreTxs(all)) { const { nextUrl, results } = await getTxs(url); url = nextUrl; all = all.concat(results); } return all; } const getTransactions = (cacheTransactionInfoById) => async (url) => { const transactions = await fetchWithBaseUrl(url); const nextUrl = transactions.meta.links?.next?.replace(/https:\/\/api(\.[a-z]*)?.trongrid.io/, getBaseApiUrl()); const results = await promiseAllBatched(3, transactions.data || [], async (tx) => { if (isMalformedTransactionTronAPI(tx)) { return tx; } const txID = tx.txID; const detail = cacheTransactionInfoById[txID] || (await fetchTronTxDetail(txID)); cacheTransactionInfoById[txID] = detail; return { ...tx, detail }; }); return { results, nextUrl, }; }; const getTrc20 = async (url) => { const transactions = await fetchWithBaseUrl(url); return { results: transactions.data, nextUrl: transactions.meta.links?.next?.replace(/https:\/\/api(\.[a-z]*)?.trongrid.io/, getBaseApiUrl()), }; }; export async function fetchTronAccountTxs(addr, shouldFetchMoreTxs, cacheTransactionInfoById) { const entireTxs = (await getAllTransactions(`${getBaseApiUrl()}/v1/accounts/${addr}/transactions?limit=100`, shouldFetchMoreTxs, getTransactions(cacheTransactionInfoById))) .filter((tx) => isTransactionTronAPI(tx)) .filter(tx => { // custom smart contract tx has internal txs const hasInternalTxs = tx.txID && tx.internal_transactions && tx.internal_transactions.length > 0; // and also a duplicated malformed tx that we have to ignore const isDuplicated = tx.tx_id; const type = tx.raw_data.contract[0].type; if (hasInternalTxs) { // log once log("tron-error", `unsupported transaction ${tx.txID}`); } return !isDuplicated && !hasInternalTxs && type !== "TriggerSmartContract"; }) .map(tx => formatTrongridTxResponse(tx)); // we need to fetch and filter trc20 transactions from another endpoint const entireTrc20Txs = (await getAllTransactions(`${getBaseApiUrl()}/v1/accounts/${addr}/transactions/trc20?get_detail=true`, shouldFetchMoreTxs, getTrc20)).map(tx => formatTrongridTrc20TxResponse(tx)); const txInfos = compact(entireTxs.concat(entireTrc20Txs)).sort((a, b) => b.date.getTime() - a.date.getTime()); return txInfos; } export const getContractUserEnergyRatioConsumption = async (address) => { 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) => { try { const data = await post(`/wallet/getcontract`, { value: decode58Check(addr), }); return Object.keys(data).length !== 0 ? data : undefined; } catch (e) { return undefined; } }; export const getTronAccountNetwork = async (address) => { 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) => { try { const result = await post(`/wallet/validateaddress`, { address: decode58Check(address), }); return result.result || false; } catch (e) { // 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) const accountNamesCache = makeLRUCache(async (addr) => getAccountName(addr), (addr) => addr, hours(3, 300)); // cache for super representative brokerages (brokerage is unchanged over time) const srBrokeragesCache = makeLRUCache(async (addr) => getBrokerage(addr), (addr) => addr, hours(3, 300)); export const getAccountName = async (addr) => { const tronAcc = await fetchTronAccount(addr); const acc = tronAcc[0]; const accountName = acc && acc.account_name ? hexToAscii(acc.account_name) : undefined; accountNamesCache.hydrate(addr, accountName); // put it in cache return accountName; }; export const getBrokerage = async (addr) => { const { brokerage } = await fetch(`/wallet/getBrokerage?address=${encodeURIComponent(addr)}`); srBrokeragesCache.hydrate(addr, brokerage); // put it in cache return brokerage; }; const superRepresentativesCache = makeLRUCache(async () => { const superRepresentatives = await fetchSuperRepresentatives(); log("tron/superRepresentatives", "loaded " + superRepresentatives.length + " super representatives"); return superRepresentatives; }, () => "", hours(1, 300)); export const getTronSuperRepresentatives = async () => { return await superRepresentativesCache(); }; export const hydrateSuperRepresentatives = (list) => { log("tron/superRepresentatives", "hydrate " + list.length + " super representatives"); superRepresentativesCache.hydrate("", list); }; const fetchSuperRepresentatives = async () => { const result = await fetch(`/wallet/listwitnesses`); const sorted = result.witnesses.sort((a, b) => b.voteCount - a.voteCount); const superRepresentatives = await promiseAllBatched(3, sorted, async (w) => { const encodedAddress = encode58Check(w.address); const accountName = await accountNamesCache(encodedAddress); const brokerage = await srBrokeragesCache(encodedAddress); return { ...w, address: encodedAddress, name: accountName, brokerage, voteCount: w.voteCount || 0, isJobs: w.isJobs || false, }; }); hydrateSuperRepresentatives(superRepresentatives); // put it in cache return superRepresentatives; }; export const getNextVotingDate = async () => { const { num } = await fetch(`/wallet/getnextmaintenancetime`); return new Date(num); }; export const getTronSuperRepresentativeData = async (max) => { 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, transaction) => { 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) => { try { const { reward = 0 } = await fetch(`/wallet/getReward?address=${encodeURIComponent(decode58Check(addr))}`); return new BigNumber(reward); } catch (e) { return Promise.resolve(new BigNumber(0)); } }; export const claimRewardTronTransaction = async (account) => { const url = `/wallet/withdrawbalance`; const data = { owner_address: decode58Check(account.freshAddress), }; const result = await post(url, data); return result; }; //# sourceMappingURL=index.js.map