@ledgerhq/coin-stellar
Version:
Ledger Stellar Coin integration
373 lines • 15.2 kB
JavaScript
;
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