@ledgerhq/coin-tron
Version:
Ledger Tron Coin integration
541 lines • 24.8 kB
JavaScript
;
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