UNPKG

@ledgerhq/coin-tron

Version:
541 lines 24.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.claimRewardTronTransaction = exports.getUnwithdrawnReward = exports.voteTronSuperRepresentatives = exports.getTronSuperRepresentativeData = exports.getNextVotingDate = exports.hydrateSuperRepresentatives = exports.getTronSuperRepresentatives = exports.getAccountName = exports.accountNamesCache = exports.validateAddress = exports.getTronAccountNetwork = exports.fetchTronContract = exports.getContractUserEnergyRatioConsumption = exports.defaultFetchParams = exports.broadcastHexTron = exports.broadcastTron = exports.DEFAULT_EXPIRATION = exports.createTronTransaction = exports.DEFAULT_TRC20_FEES_LIMIT = exports.legacyUnfreezeTronTransaction = exports.unDelegateResourceTransaction = exports.withdrawExpireUnfreezeTronTransaction = exports.unfreezeTronTransaction = exports.freezeTronTransaction = void 0; exports.post = post; exports.getDelegatedResource = getDelegatedResource; exports.craftTrc20Transaction = craftTrc20Transaction; exports.craftStandardTransaction = craftStandardTransaction; exports.fetchTronAccount = fetchTronAccount; exports.getLastBlock = getLastBlock; exports.getBlock = getBlock; exports.getBlockWithTransactions = getBlockWithTransactions; exports.getTransactionInfoByBlockNum = getTransactionInfoByBlockNum; exports.fetchTronAccountTxsPage = fetchTronAccountTxsPage; exports.fetchTronAccountTxs = fetchTronAccountTxs; const querystring_1 = require("querystring"); const errors_1 = require("@ledgerhq/errors"); const live_network_1 = __importDefault(require("@ledgerhq/live-network")); const cache_1 = require("@ledgerhq/live-network/cache"); const logs_1 = require("@ledgerhq/logs"); const bignumber_js_1 = require("bignumber.js"); const compact_1 = __importDefault(require("lodash/compact")); const drop_1 = __importDefault(require("lodash/drop")); const sumBy_1 = __importDefault(require("lodash/sumBy")); const take_1 = __importDefault(require("lodash/take")); const tronweb_1 = require("tronweb"); const config_1 = __importDefault(require("../config")); const errors_2 = require("../types/errors"); const format_1 = require("./format"); const types_1 = require("./types"); const utils_1 = require("./utils"); const getBaseApiUrl = () => config_1.default.getCoinConfig().explorer.url; function isValidNativeTx(tx) { // 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) { return tx.type === "TriggerSmartContract" && !tx.hasFailed; } async function post(endPoint, body) { const { data } = await (0, live_network_1.default)({ 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; const message = (0, querystring_1.stringify)(error); const nonEmptyMessage = message === "" ? error.toString() : message; (0, logs_1.log)("tron-error", nonEmptyMessage, { endPoint, body }); throw new Error(nonEmptyMessage); } return data; } async function fetch(endPoint) { return fetchWithBaseUrl(`${getBaseApiUrl()}${endPoint}`); } async function fetchWithBaseUrl(url) { const { data } = await (0, live_network_1.default)({ url }); // Ugly but trongrid send a 200 status event if there are errors if ("Error" in data) { (0, logs_1.log)("tron-error", (0, querystring_1.stringify)(data.Error), { url, }); throw new Error((0, querystring_1.stringify)(data.Error)); } return data; } const freezeTronTransaction = async (account, transaction) => { const txData = { frozen_balance: transaction.amount.toNumber(), resource: transaction.resource, owner_address: (0, format_1.decode58Check)(account.freshAddress), }; const url = `/wallet/freezebalancev2`; const result = await post(url, txData); return result; }; exports.freezeTronTransaction = freezeTronTransaction; const unfreezeTronTransaction = async (account, transaction) => { const txData = { owner_address: (0, format_1.decode58Check)(account.freshAddress), resource: transaction.resource, unfreeze_balance: transaction.amount.toNumber(), }; const url = `/wallet/unfreezebalancev2`; const result = await post(url, txData); return result; }; exports.unfreezeTronTransaction = unfreezeTronTransaction; const withdrawExpireUnfreezeTronTransaction = async (account, _transaction) => { const txData = { owner_address: (0, format_1.decode58Check)(account.freshAddress), }; const url = `/wallet/withdrawexpireunfreeze`; const result = await post(url, txData); return result; }; exports.withdrawExpireUnfreezeTronTransaction = withdrawExpireUnfreezeTronTransaction; const unDelegateResourceTransaction = async (account, transaction) => { const txData = { balance: transaction.amount.toNumber(), resource: transaction.resource, owner_address: (0, format_1.decode58Check)(account.freshAddress), receiver_address: (0, format_1.decode58Check)(transaction.recipient), }; const url = `/wallet/undelegateresource`; const result = await post(url, txData); return result; }; exports.unDelegateResourceTransaction = unDelegateResourceTransaction; const legacyUnfreezeTronTransaction = async (account, transaction) => { const txData = { resource: transaction.resource, owner_address: (0, format_1.decode58Check)(account.freshAddress), receiver_address: transaction.recipient ? (0, format_1.decode58Check)(transaction.recipient) : undefined, }; const url = `/wallet/unfreezebalance`; const result = await post(url, txData); return result; }; exports.legacyUnfreezeTronTransaction = legacyUnfreezeTronTransaction; async function getDelegatedResource(account, transaction, resource) { const url = `/wallet/getdelegatedresourcev2`; const { delegatedResource = [], } = await post(url, { fromAddress: (0, format_1.decode58Check)(account.freshAddress), toAddress: (0, format_1.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_js_1.BigNumber(amount); } exports.DEFAULT_TRC20_FEES_LIMIT = 50000000; async function craftTrc20Transaction(tokenAddress, recipientAddress, senderAddress, amount, customFees, expiration) { const txData = { function_selector: "transfer(address,uint256)", fee_limit: customFees ? customFees : exports.DEFAULT_TRC20_FEES_LIMIT, call_value: 0, contract_address: (0, format_1.decode58Check)(tokenAddress), parameter: (0, utils_1.abiEncodeTrc20Transfer)(recipientAddress, new bignumber_js_1.BigNumber(amount.toString())), owner_address: senderAddress, }; const url = `/wallet/triggersmartcontract`; const { transaction: preparedTransaction } = await post(url, txData); return await extendExpiration(preparedTransaction, expiration); } async function craftStandardTransaction(tokenAddress, recipientAddress, senderAddress, amount, isTransferAsset, memo, expiration) { 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"), extra_data: memo && Buffer.from(memo).toString("hex"), }; const preparedTransaction = await post(url, txData); return await extendExpiration(preparedTransaction, expiration); } const getTokenInfo = (subAccount) => { const tokenInfo = subAccount && subAccount.type === "TokenAccount" ? (0, drop_1.default)(subAccount.token.id.split("/"), 1) : [undefined, undefined]; return tokenInfo; }; // Send trx or trc10/trc20 tokens const createTronTransaction = async (account, transaction, subAccount) => { const [tokenType, tokenId] = getTokenInfo(subAccount); const decodeRecipient = (0, format_1.decode58Check)(transaction.recipient); const decodeSender = (0, format_1.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); } }; exports.createTronTransaction = createTronTransaction; /** Default expiration of 10 minutes (in seconds) after crafting time. */ exports.DEFAULT_EXPIRATION = 600; async function extendExpiration(preparedTransaction, expiration) { const extension = expiration ?? exports.DEFAULT_EXPIRATION; const nodeExpiration = 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) { (0, logs_1.log)("tron/extendExpiration", "Invalid extension provided", { preparedTransaction, extensionInS: extension, extensionInMs: extension * 1000, minFinalExpiration, }); throw new errors_1.InvalidTransactionError(); } const HttpProvider = tronweb_1.providers.HttpProvider; const fullNode = new HttpProvider(getBaseApiUrl()); const solidityNode = new HttpProvider(getBaseApiUrl()); const eventServer = new HttpProvider(getBaseApiUrl()); const tronWeb = new tronweb_1.TronWeb(fullNode, solidityNode, eventServer); return tronWeb.transactionBuilder.extendExpiration(preparedTransaction, extension); } /** * @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 */ const broadcastTron = async (trxTransaction) => { const result = await post("/wallet/broadcasttransaction", trxTransaction); if (result.result !== true) { if (result.code === "TRANSACTION_EXPIRATION_ERROR") { throw new errors_2.TronTransactionExpired(); } else { throw new Error(`${result.code}: ${result.message}`); } } return result.txid; }; exports.broadcastTron = broadcastTron; 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; }; exports.broadcastHexTron = broadcastHexTron; /** * {@link https://github.com/tronprotocol/java-tron/blob/develop/framework/src/main/java/org/tron/core/services/http/GetAccountServlet.java | Tron Framework} */ async function fetchTronAccount(addr) { try { const data = await fetch(`/v1/accounts/${addr}`); return data.data; } catch { return []; } } async function getLastBlock() { const data = await fetch(`/wallet/getnowblock`); return toBlock(data); } async function getBlock(blockNumber) { const data = await post(`/wallet/getblock`, { id_or_num: String(blockNumber), detail: false, }); return toBlock(data); } async function getBlockWithTransactions(blockNumber) { return post(`/wallet/getblock`, { id_or_num: String(blockNumber), detail: true }); } function toBlock(data) { const timestamp = data.block_header.raw_data.timestamp; const ret = { height: data.block_header.raw_data.number, hash: data.blockID, }; if (timestamp) { ret.time = new Date(timestamp); } return ret; } async function getTransactionInfoByBlockNum(blockNum) { return post(`/wallet/gettransactioninfobyblocknum`, { num: blockNum }); } 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 = async (url) => { const transactions = await fetchWithBaseUrl(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) => { const transactions = await fetchWithBaseUrl(url); return { results: transactions.data, nextUrl: transactions.meta.links?.next?.replace(/https:\/\/api(\.[a-z]*)?.trongrid.io/, getBaseApiUrl()), }; }; exports.defaultFetchParams = { limitPerCall: 100, minTimestamp: 0, order: "desc", }; async function fetchSinglePage(url, getTxs) { const { results, nextUrl } = await getTxs(url); return { results, hasNextPage: !!nextUrl }; } async function fetchTronAccountTxsPage(addr, params) { 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(`${getBaseApiUrl()}/v1/accounts/${addr}/transactions?${queryParams}`, getTransactions), fetchSinglePage(`${getBaseApiUrl()}/v1/accounts/${addr}/transactions/trc20?${queryParams}&get_detail=true`, getTrc20), ]); const nativeTxsFormatted = await Promise.all(nativeResult.results .filter(types_1.isTransactionTronAPI) .filter(isValidNativeTx) .map(tx => (0, format_1.formatTrongridTxResponse)(tx, exports.accountNamesCache))); const trc20TxsFormatted = (0, compact_1.default)(trc20Result.results.map(format_1.formatTrongridTrc20TxResponse)); const trc20TxIds = new Set(trc20TxsFormatted.map(t => t.txID)); const nativeDeduped = (0, compact_1.default)(nativeTxsFormatted) .filter(tx => !trc20TxIds.has(tx.txID)) .filter(tx => !isSuccessfulTriggerSmartContract(tx)); return { nativeTxs: { txs: nativeDeduped, hasNextPage: nativeResult.hasNextPage }, trc20Txs: { txs: trc20TxsFormatted, hasNextPage: trc20Result.hasNextPage }, }; } async function fetchTronAccountTxs(addr, shouldFetchMoreTxs, params) { 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(`${getBaseApiUrl()}/v1/accounts/${addr}/transactions?${queryParams}`, shouldFetchMoreTxs, getTransactions)) .filter(types_1.isTransactionTronAPI) .filter(isValidNativeTx) .map(tx => (0, format_1.formatTrongridTxResponse)(tx, exports.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(`${getBaseApiUrl()}/v1/accounts/${addr}/transactions/trc20?${queryParams}&get_detail=true`, shouldFetchMoreTxs, getTrc20); function isValid(tx) { const ret = tx?.detail?.ret; return Array.isArray(ret) && ret.length > 0; } function getInvalidTxIndexes(txs) { const invalids = []; 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, message) { if (!predicate) { throw new Error(message); } } // Merge the two results function mergeAccs(acc1, acc2) { assert(acc1.txs.length === acc2.txs.length, "accs should have the same length"); const accRet = { 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, times) { assert(times > 0, "getTrc20TxsWithRetry: couldn't fetch trc20 transactions after several attempts"); const ret = await callTrc20Endpoint(); const thisAcc = { txs: ret, invalids: getInvalidTxIndexes(ret), }; const newAcc = acc ? mergeAccs(acc, thisAcc) : thisAcc; if (newAcc.invalids.length === 0) { return newAcc.txs; } else { (0, logs_1.log)("coin-tron", `getTrc20TxsWithRetry: got ${newAcc.invalids.length} invalid trc20 transactions, retrying...`); return await getTrc20TxsWithRetry(newAcc, times - 1); } } const trc20Txs = (0, compact_1.default)((await getTrc20TxsWithRetry(null, 3)).map(format_1.formatTrongridTrc20TxResponse)); const trc20TxIds = new Set(trc20Txs.map(t => t.txID)); const nativeDeduped = (0, compact_1.default)(nativeTxs) .filter(tx => !trc20TxIds.has(tx.txID)) .filter(tx => !isSuccessfulTriggerSmartContract(tx)); const txInfos = nativeDeduped .concat(trc20Txs) .sort((a, b) => b.date.getTime() - a.date.getTime()); return txInfos; } const getContractUserEnergyRatioConsumption = async (address) => { const result = await (0, exports.fetchTronContract)(address); if (result) { const { consume_user_resource_percent } = result; return consume_user_resource_percent; } return 0; }; exports.getContractUserEnergyRatioConsumption = getContractUserEnergyRatioConsumption; const fetchTronContract = async (addr) => { try { const data = await post(`/wallet/getcontract`, { value: (0, format_1.decode58Check)(addr), }); return Object.keys(data).length !== 0 ? data : undefined; } catch { return undefined; } }; exports.fetchTronContract = fetchTronContract; const getTronAccountNetwork = async (address) => { const result = await fetch(`/wallet/getaccountresource?address=${encodeURIComponent((0, format_1.decode58Check)(address))}`); const { freeNetUsed = 0, freeNetLimit = 0, NetUsed = 0, NetLimit = 0, EnergyUsed = 0, EnergyLimit = 0, } = result; return { family: "tron", freeNetUsed: new bignumber_js_1.BigNumber(freeNetUsed), freeNetLimit: new bignumber_js_1.BigNumber(freeNetLimit), netUsed: new bignumber_js_1.BigNumber(NetUsed), netLimit: new bignumber_js_1.BigNumber(NetLimit), energyUsed: new bignumber_js_1.BigNumber(EnergyUsed), energyLimit: new bignumber_js_1.BigNumber(EnergyLimit), }; }; exports.getTronAccountNetwork = getTronAccountNetwork; const validateAddress = async (address) => { try { const result = await post(`/wallet/validateaddress`, { address: (0, format_1.decode58Check)(address), }); return result.result || false; } catch (e) { // FIXME we should not silent errors! (0, logs_1.log)("tron-error", "validateAddress fails with " + e.message, { address, }); return false; } }; exports.validateAddress = validateAddress; // cache for account names (name is unchanged over time) exports.accountNamesCache = (0, cache_1.makeLRUCache)(async (addr) => (0, exports.getAccountName)(addr), (addr) => addr, (0, cache_1.hours)(3, 300)); const getAccountName = async (addr) => { const tronAcc = await fetchTronAccount(addr); const acc = tronAcc[0]; const accountName = acc && acc.account_name ? (0, utils_1.hexToAscii)(acc.account_name) : undefined; exports.accountNamesCache.hydrate(addr, accountName); // put it in cache return accountName; }; exports.getAccountName = getAccountName; const superRepresentativesCache = (0, cache_1.makeLRUCache)(async () => { const superRepresentatives = await fetchSuperRepresentatives(); (0, logs_1.log)("tron/superRepresentatives", "loaded " + superRepresentatives.length + " super representatives"); return superRepresentatives; }, () => "", (0, cache_1.hours)(1, 300)); const getTronSuperRepresentatives = async () => { return await superRepresentativesCache(); }; exports.getTronSuperRepresentatives = getTronSuperRepresentatives; const hydrateSuperRepresentatives = (list) => { (0, logs_1.log)("tron/superRepresentatives", "hydrate " + list.length + " super representatives"); superRepresentativesCache.hydrate("", list); }; exports.hydrateSuperRepresentatives = hydrateSuperRepresentatives; const fetchSuperRepresentatives = async () => { const result = await fetch(`/wallet/listwitnesses`); const sorted = result.witnesses.sort((a, b) => b.voteCount - a.voteCount); const superRepresentatives = sorted.map(w => ({ ...w, address: (0, format_1.encode58Check)(w.address), voteCount: w.voteCount || 0, isJobs: w.isJobs || false, })); (0, exports.hydrateSuperRepresentatives)(superRepresentatives); // put it in cache return superRepresentatives; }; const getNextVotingDate = async () => { const { num } = await fetch(`/wallet/getnextmaintenancetime`); return new Date(num); }; exports.getNextVotingDate = getNextVotingDate; const getTronSuperRepresentativeData = async (max) => { const list = await (0, exports.getTronSuperRepresentatives)(); const nextVotingDate = await (0, exports.getNextVotingDate)(); return { list: max ? (0, take_1.default)(list, max) : list, totalVotes: (0, sumBy_1.default)(list, "voteCount"), nextVotingDate, }; }; exports.getTronSuperRepresentativeData = getTronSuperRepresentativeData; const voteTronSuperRepresentatives = async (account, transaction) => { const payload = { owner_address: (0, format_1.decode58Check)(account.freshAddress), votes: transaction.votes.map(v => ({ vote_address: (0, format_1.decode58Check)(v.address), vote_count: v.voteCount, })), }; return await post(`/wallet/votewitnessaccount`, payload); }; exports.voteTronSuperRepresentatives = voteTronSuperRepresentatives; const getUnwithdrawnReward = async (addr) => { try { const { reward = 0 } = await fetch(`/wallet/getReward?address=${encodeURIComponent((0, format_1.decode58Check)(addr))}`); return new bignumber_js_1.BigNumber(reward); } catch { return Promise.resolve(new bignumber_js_1.BigNumber(0)); } }; exports.getUnwithdrawnReward = getUnwithdrawnReward; const claimRewardTronTransaction = async (account) => { const url = `/wallet/withdrawbalance`; const data = { owner_address: (0, format_1.decode58Check)(account.freshAddress), }; const result = await post(url, data); return result; }; exports.claimRewardTronTransaction = claimRewardTronTransaction; //# sourceMappingURL=index.js.map