UNPKG

@ledgerhq/live-common

Version:
251 lines • 8.16 kB
import network from "@ledgerhq/live-network/network"; import { log } from "@ledgerhq/logs"; import { from, interval } from "rxjs"; import { filter, share, switchMap } from "rxjs/operators"; import semver from "semver"; import { getCryptoCurrencyById } from "../../currencies"; import { getEnv } from "@ledgerhq/live-env"; import { RPCHostInvalid, RPCHostRequired, RPCPassRequired, RPCUserRequired, SatStackAccessDown, SatStackStillSyncing, SatStackVersionTooOld, } from "../../errors"; import { getCurrencyExplorer } from "@ledgerhq/coin-bitcoin/explorer"; const minVersionMatch = ">=0.11.1"; function isAcceptedVersion(version) { return !!version && semver.satisfies(semver.coerce(version) || "", minVersionMatch); } let mockStatus = { type: "ready", }; export function setMockStatus(s) { mockStatus = s; } export function isValidHost(host) { const pattern = new RegExp("^" + // beginning of url "(((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,})|^[a-z\\d-]{2,}|" + // domain name "((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*$"); // port and path return !!pattern.test(host); } // we would call this only during the user validation of the rpc node configuration export function validateRPCNodeConfig(config) { const errors = []; if (!config.host) { errors.push({ field: "host", error: new RPCHostRequired(), }); } else if (!isValidHost(config.host)) { errors.push({ field: "host", error: new RPCHostInvalid(), }); } if (!config.username) { errors.push({ field: "username", error: new RPCUserRequired(), }); } if (!config.password) { errors.push({ field: "password", error: new RPCPassRequired(), }); } return errors; } // we would call this only during the "Testing node connection" step // Check if the node is accessible by RPC. promise fails if not. export async function checkRPCNodeConfig(config) { const errors = validateRPCNodeConfig(config); if (errors.length) { throw errors[0].error; } if (getEnv("MOCK")) { if (mockStatus.type === "node-disconnected") { throw new Error("mock disconnected"); } return; } const { host, username, password, tls } = config; await network({ url: `http${tls ? "s" : ""}://${host}`, method: "POST", data: { jsonrpc: "1.0", id: "ledger-live-full-node-check", method: "getblockchaininfo", params: [], }, auth: { username, password, }, }); } // we would need to call this any time we would "Edit" the flow export function parseSatStackConfig(json) { const obj = JSON.parse(json); if (obj && typeof obj === "object") { const { accounts, rpcurl, rpcuser, rpcpass, notls, ...extra } = obj; if (!rpcurl || typeof rpcurl !== "string") return; if (!rpcuser || typeof rpcuser !== "string") return; if (!rpcpass || typeof rpcpass !== "string") return; const result = { node: { host: rpcurl, username: rpcuser, password: rpcpass, tls: !notls, }, accounts: [], extra, }; if (accounts && typeof accounts === "object" && Array.isArray(accounts)) { result.accounts = accounts .map(a => { const { external, internal, ...extra } = a; if (!external || typeof external !== "string") return; if (!internal || typeof internal !== "string") return; return { descriptor: { external, internal, }, extra, }; }) .filter(Boolean); } return result; } } // we would need this at the end of the setup flow, when we save to a jss.json configuration file export function stringifySatStackConfig(config) { return JSON.stringify({ accounts: config.accounts.map(a => ({ external: a.descriptor.external, internal: a.descriptor.internal, ...a.extra, })), rpcurl: config.node.host, rpcuser: config.node.username, rpcpass: config.node.password, notls: !config.node.tls, ...config.extra, }, null, 2); } // We would need it to apply an edition over an existing sats stack configuration (before saving it over) export function editSatStackConfig(existing, edit) { const accounts = existing.accounts.concat( // append accounts that would not already exist (edit.accounts || []).filter(a => !existing.accounts.some(existing => a.descriptor.internal === existing.descriptor.internal))); return { ...existing, ...edit, // edit is patching existing fields accounts, }; } export function isSatStackEnabled() { return Boolean(getEnv("SATSTACK")); } // We would need it any time we want to check if the Sats Stack is up and what status is it at currently // - during the configuration flow (on last step) (actively polling) // - on the settings screen (actively polling) // - before doing a sync // - before doing an add account // NB the promise is never rejected export async function fetchSatStackStatus() { if (!isSatStackEnabled()) { return { type: "satstack-disconnected", }; } if (getEnv("MOCK")) { return mockStatus; } const ce = getCurrencyExplorer(getCryptoCurrencyById("bitcoin")); const r = await network({ method: "GET", url: `${ce.endpoint}/blockchain/${ce.version}/explorer/status`, }).catch(() => null); if (!r || !r.data) { return { type: "satstack-disconnected", }; } const { chain, status, sync_progress, scan_progress, version } = r.data; if (!isAcceptedVersion(version)) { return { type: "satstack-outdated", }; } if (chain !== "main") { return { type: "invalid-chain", found: chain, }; } if (status === "initializing" || status === "pending-scan") { return { type: "initializing", }; } if (status === "node-disconnected") { return { type: "node-disconnected", }; } if (status === "syncing") { return { type: "syncing", progress: (sync_progress || 0) / 100, }; } if (status === "scanning") { return { type: "scanning", progress: (scan_progress || 0) / 100, }; } return { type: "ready", }; } export async function checkDescriptorExists(descriptor) { if (getEnv("MOCK")) { return true; } const r = await network({ method: "POST", url: `${getEnv("EXPLORER_SATSTACK")}/control/descriptors/has`, data: { descriptor, }, }); log("satstack", "checkDescriptorExists " + descriptor + " is " + r.data.exists); return Boolean(r.data.exists); } export async function requiresSatStackReady() { if (isSatStackEnabled()) { const status = await fetchSatStackStatus(); switch (status.type) { case "ready": return; case "syncing": case "scanning": throw new SatStackStillSyncing(); case "satstack-outdated": throw new SatStackVersionTooOld(); default: throw new SatStackAccessDown(); } } } export const statusObservable = interval(1000).pipe(switchMap(() => from(fetchSatStackStatus())), filter((e, i) => i > 4 || e.type !== "satstack-disconnected"), share()); //# sourceMappingURL=satstack.js.map