UNPKG

@ledgerhq/coin-stellar

Version:
373 lines 15.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getRecipientAccount = exports.getLastBlock = exports.loadAccount = exports.broadcastTransaction = exports.fetchSigners = exports.fetchSequence = exports.fetchAccountNetworkInfo = exports.fetchOperations = exports.fetchAllOperations = exports.fetchAccount = exports.fetchBaseFee = exports.MIN_BALANCE = exports.BASE_RESERVE_MIN_COUNT = exports.BASE_RESERVE = void 0; const currencies_1 = require("@ledgerhq/coin-framework/currencies"); const currencies_2 = require("@ledgerhq/cryptoassets/currencies"); const errors_1 = require("@ledgerhq/errors"); const cache_1 = require("@ledgerhq/live-network/cache"); const logs_1 = require("@ledgerhq/logs"); const stellar_sdk_1 = require("@stellar/stellar-sdk"); const bignumber_js_1 = require("bignumber.js"); const config_1 = __importDefault(require("../config")); const types_1 = require("../types"); const serialization_1 = require("./serialization"); const polyfill_1 = require("../polyfill"); const FALLBACK_BASE_FEE = 100; const TRESHOLD_LOW = 0.5; const TRESHOLD_MEDIUM = 0.75; const FETCH_LIMIT = 100; const currency = (0, currencies_2.getCryptoCurrencyById)("stellar"); // Horizon client instance is cached to avoid costly rebuild at every request // Watch out: cache key is the URL, coin module can be instantiated several times with different URLs const servers = new Map(); function getServer() { const url = config_1.default.getCoinConfig().explorer.url; let server = servers.get(url); if (server === undefined) { server = new stellar_sdk_1.Horizon.Server(url); servers.set(url, server); } return server; } // Constants exports.BASE_RESERVE = 0.5; exports.BASE_RESERVE_MIN_COUNT = 2; exports.MIN_BALANCE = 1; // Due to the inconsistency between the axios version (1.6.5) used by `stellar-sdk` // and the version (0.26.1) used by `@ledgerhq/live-network/network`, it is not possible to use the interceptors // provided by `@ledgerhq/live-network/network`. stellar_sdk_1.Horizon.AxiosClient.interceptors.request.use(config => { if (!config_1.default.getCoinConfig().enableNetworkLogs) { return config; } const { url, method, data } = config; (0, logs_1.log)("network", `${method} ${url}`, { data }); return config; }); // This function allows to fix the URL, because the url returned by the Stellar SDK is not the correct one. // It replaces the host of the URL returned with the host of the explorer. function useConfigHost(url) { const u = new URL(url); u.host = new URL(config_1.default.getCoinConfig().explorer.url).host; return u.toString(); } stellar_sdk_1.Horizon.AxiosClient.interceptors.response.use(response => { if (config_1.default.getCoinConfig().enableNetworkLogs) { const { url, method } = response.config; (0, logs_1.log)("network-success", `${response.status} ${method} ${url}`, { data: response.data }); } // FIXME: workaround for the Stellar SDK not using the correct URL: the "next" URL // included in server responses points to the node itself instead of our reverse proxy... // (https://github.com/stellar/js-stellar-sdk/issues/637) const next_href = response?.data?._links?.next?.href; if (next_href) { response.data._links.next.href = useConfigHost(next_href); } response?.data?._embedded?.records?.forEach((r) => { const href = r.transaction?._links?.ledger?.href; if (href) r.transaction._links.ledger.href = useConfigHost(href); }); return response; }); async function fetchBaseFee() { // For tests if (config_1.default.getCoinConfig().useStaticFees) { return { baseFee: 100, recommendedFee: 100, networkCongestionLevel: types_1.NetworkCongestionLevel.LOW, }; } const baseFee = new bignumber_js_1.BigNumber(stellar_sdk_1.BASE_FEE).toNumber() || FALLBACK_BASE_FEE; let recommendedFee = baseFee; let networkCongestionLevel = types_1.NetworkCongestionLevel.MEDIUM; try { const feeStats = await getServer().feeStats(); const ledgerCapacityUsage = feeStats.ledger_capacity_usage; recommendedFee = new bignumber_js_1.BigNumber(feeStats.fee_charged.mode).toNumber(); if (new bignumber_js_1.BigNumber(ledgerCapacityUsage).toNumber() > TRESHOLD_LOW && new bignumber_js_1.BigNumber(ledgerCapacityUsage).toNumber() <= TRESHOLD_MEDIUM) { networkCongestionLevel = types_1.NetworkCongestionLevel.MEDIUM; } else if (new bignumber_js_1.BigNumber(ledgerCapacityUsage).toNumber() > TRESHOLD_MEDIUM) { networkCongestionLevel = types_1.NetworkCongestionLevel.HIGH; } else { networkCongestionLevel = types_1.NetworkCongestionLevel.LOW; } } catch (e) { // do nothing, will use defaults } return { baseFee, recommendedFee, networkCongestionLevel, }; } exports.fetchBaseFee = fetchBaseFee; /** * Get all account-related data * * @async * @param {string} addr */ async function fetchAccount(addr) { let account = {}; let assets = []; let balance = "0"; try { account = await getServer().accounts().accountId(addr).call(); balance = account.balances?.find(balance => { return balance.asset_type === "native"; })?.balance || "0"; // Getting all non-native (XLM) assets on the account assets = account.balances?.filter(balance => { return balance.asset_type !== "native"; }); } catch (e) { balance = "0"; } const formattedBalance = (0, currencies_1.parseCurrencyUnit)(currency.units[0], balance); const spendableBalance = await (0, serialization_1.getAccountSpendableBalance)(formattedBalance, account); return { blockHeight: account.sequence ? new bignumber_js_1.BigNumber(account.sequence).toNumber() : 0, balance: formattedBalance, spendableBalance, assets, }; } exports.fetchAccount = fetchAccount; /** * Fetch operations for a single account from indexer * * @param {string} accountId * @param {string} addr * @param {string} order - "desc" or "asc" order of returned records * @param {string} cursor - point to start fetching records * @param {number} maxOperations - maximum number of operations to return, stops fetching after reaching this threshold * * @return {Operation[]} */ async function fetchAllOperations(accountId, addr, order, cursor = "", maxOperations) { if (!addr) { return []; } const limit = config_1.default.getCoinConfig().explorer.fetchLimit ?? FETCH_LIMIT; let operations = []; let fetchedOpsCount = limit; try { let rawOperations = await getServer() .operations() .forAccount(addr) .limit(limit) .order(order) .cursor(cursor) .includeFailed(true) .join("transactions") .call(); if (!rawOperations || !rawOperations.records.length) { return []; } operations = operations.concat(await (0, serialization_1.rawOperationsToOperations)(rawOperations.records, addr, accountId, 0)); while (rawOperations.records.length > 0) { if (maxOperations && fetchedOpsCount >= maxOperations) { break; } fetchedOpsCount += limit; rawOperations = await rawOperations.next(); operations = operations.concat(await (0, serialization_1.rawOperationsToOperations)(rawOperations.records, addr, accountId, 0)); } return operations; } catch (e) { // FIXME: terrible hacks, because Stellar SDK fails to cast network failures to typed errors in react-native... // (https://github.com/stellar/js-stellar-sdk/issues/638) const errorMsg = e ? String(e) : ""; if (e instanceof stellar_sdk_1.NotFoundError || errorMsg.match(/status code 404/)) { return []; } if (errorMsg.match(/status code 4[0-9]{2}/)) { throw new errors_1.LedgerAPI4xx(); } if (errorMsg.match(/status code 5[0-9]{2}/)) { throw new errors_1.LedgerAPI5xx(); } if (e instanceof stellar_sdk_1.NetworkError || errorMsg.match(/ECONNRESET|ECONNREFUSED|ENOTFOUND|EPIPE|ETIMEDOUT/) || errorMsg.match(/undefined is not an object/)) { throw new errors_1.NetworkDown(); } throw e; } } exports.fetchAllOperations = fetchAllOperations; // https://developers.stellar.org/docs/data/horizon/api-reference/get-operations-by-account-id async function fetchOperations({ accountId, addr, minHeight, order, cursor, limit, }) { const noResult = [[], ""]; if (!addr) { return noResult; } const defaultFetchLimit = config_1.default.getCoinConfig().explorer.fetchLimit ?? FETCH_LIMIT; try { const rawOperations = await getServer() .operations() .forAccount(addr) .limit(limit ?? defaultFetchLimit) .order(order) .cursor(cursor ?? "") .includeFailed(true) .join("transactions") .call(); if (!rawOperations || !rawOperations.records.length) { return noResult; } const rawOps = rawOperations.records; const filteredOps = await (0, serialization_1.rawOperationsToOperations)(rawOps, addr, accountId, minHeight); // in this context, if we have filtered out operations it means those operations were < minHeight, so we are done const nextCursor = filteredOps.length == rawOps.length ? rawOps[rawOps.length - 1].paging_token : ""; return [filteredOps, nextCursor]; } catch (e) { // FIXME: terrible hacks, because Stellar SDK fails to cast network failures to typed errors in react-native... // (https://github.com/stellar/js-stellar-sdk/issues/638) // update 2025-04-01: in case of NetworkError, the error.response fields are undefined. Hence we cannot rely on status code // the only way to check is the errror message, which may break at some point const errorMsg = e ? String(e) : ""; if (e instanceof stellar_sdk_1.NotFoundError || errorMsg.match(/status code 404/)) { return noResult; } if (errorMsg.match(/too many requests/i)) { throw new errors_1.LedgerAPI4xx("status code 4xx", { status: 429, url: undefined, method: "GET" }); } if (errorMsg.match(/status code 4[0-9]{2}/)) { throw new errors_1.LedgerAPI4xx(); } if (errorMsg.match(/status code 5[0-9]{2}/)) { throw new errors_1.LedgerAPI5xx(); } if (e instanceof stellar_sdk_1.NetworkError || errorMsg.match(/ECONNRESET|ECONNREFUSED|ENOTFOUND|EPIPE|ETIMEDOUT/) || errorMsg.match(/undefined is not an object/)) { throw new errors_1.NetworkDown(); } throw e; } } exports.fetchOperations = fetchOperations; async function fetchAccountNetworkInfo(account) { try { const extendedAccount = await getServer().accounts().accountId(account.freshAddress).call(); const baseReserve = (0, serialization_1.getReservedBalance)(extendedAccount); const { recommendedFee, networkCongestionLevel, baseFee } = await fetchBaseFee(); return { family: "stellar", fees: new bignumber_js_1.BigNumber(recommendedFee.toString()), baseFee: new bignumber_js_1.BigNumber(baseFee.toString()), baseReserve, networkCongestionLevel, }; } catch (error) { return { family: "stellar", fees: new bignumber_js_1.BigNumber(0), baseFee: new bignumber_js_1.BigNumber(0), baseReserve: new bignumber_js_1.BigNumber(0), }; } } exports.fetchAccountNetworkInfo = fetchAccountNetworkInfo; async function fetchSequence(account) { const extendedAccount = await loadAccount(account.freshAddress); return extendedAccount ? new bignumber_js_1.BigNumber(extendedAccount.sequence) : new bignumber_js_1.BigNumber(0); } exports.fetchSequence = fetchSequence; async function fetchSigners(account) { try { const extendedAccount = await getServer().accounts().accountId(account.freshAddress).call(); return extendedAccount.signers; } catch (error) { return []; } } exports.fetchSigners = fetchSigners; async function broadcastTransaction(signedTransaction) { (0, polyfill_1.patchHermesTypedArraysIfNeeded)(); const transaction = new stellar_sdk_1.Transaction(signedTransaction, stellar_sdk_1.Networks.PUBLIC); // Immediately restore (0, polyfill_1.unpatchHermesTypedArrays)(); const res = await getServer().submitTransaction(transaction, { skipMemoRequiredCheck: true, }); return res.hash; } exports.broadcastTransaction = broadcastTransaction; async function loadAccount(addr) { if (!addr || !addr.length) { return null; } try { return await getServer().loadAccount(addr); } catch (e) { return null; } } exports.loadAccount = loadAccount; async function getLastBlock() { const ledger = await getServer().ledgers().order("desc").limit(1).call(); return { height: ledger.records[0].sequence, hash: ledger.records[0].hash, time: new Date(ledger.records[0].closed_at), }; } exports.getLastBlock = getLastBlock; exports.getRecipientAccount = (0, cache_1.makeLRUCache)(async ({ recipient }) => await recipientAccount(recipient), extract => extract.recipient, { max: 300, ttl: 5 * 60, }); async function recipientAccount(address) { if (!address) { return null; } let accountAddress = address; const isMuxedAccount = stellar_sdk_1.StrKey.isValidMed25519PublicKey(address); if (isMuxedAccount) { const muxedAccount = stellar_sdk_1.MuxedAccount.fromAddress(address, "0"); accountAddress = muxedAccount.baseAccount().accountId(); } const account = await loadAccount(accountAddress); if (!account) { return null; } return { id: account.id, isMuxedAccount, assetIds: account.balances.reduce((allAssets, balance) => { return [...allAssets, getBalanceId(balance)]; }, []), }; } function getBalanceId(balance) { switch (balance.asset_type) { case "native": return "native"; case "liquidity_pool_shares": return balance.liquidity_pool_id || null; case "credit_alphanum4": case "credit_alphanum12": return `${balance.asset_code}:${balance.asset_issuer}`; default: return null; } } //# sourceMappingURL=horizon.js.map