@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
268 lines • 9.43 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.statusObservable = exports.requiresSatStackReady = exports.checkDescriptorExists = exports.fetchSatStackStatus = exports.isSatStackEnabled = exports.editSatStackConfig = exports.stringifySatStackConfig = exports.parseSatStackConfig = exports.checkRPCNodeConfig = exports.validateRPCNodeConfig = exports.isValidHost = exports.setMockStatus = void 0;
const network_1 = __importDefault(require("@ledgerhq/live-network/network"));
const logs_1 = require("@ledgerhq/logs");
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const semver_1 = __importDefault(require("semver"));
const currencies_1 = require("../../currencies");
const live_env_1 = require("@ledgerhq/live-env");
const errors_1 = require("../../errors");
const explorer_1 = require("@ledgerhq/coin-bitcoin/explorer");
const minVersionMatch = ">=0.11.1";
function isAcceptedVersion(version) {
return !!version && semver_1.default.satisfies(semver_1.default.coerce(version) || "", minVersionMatch);
}
let mockStatus = {
type: "ready",
};
function setMockStatus(s) {
mockStatus = s;
}
exports.setMockStatus = setMockStatus;
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);
}
exports.isValidHost = isValidHost;
// we would call this only during the user validation of the rpc node configuration
function validateRPCNodeConfig(config) {
const errors = [];
if (!config.host) {
errors.push({
field: "host",
error: new errors_1.RPCHostRequired(),
});
}
else if (!isValidHost(config.host)) {
errors.push({
field: "host",
error: new errors_1.RPCHostInvalid(),
});
}
if (!config.username) {
errors.push({
field: "username",
error: new errors_1.RPCUserRequired(),
});
}
if (!config.password) {
errors.push({
field: "password",
error: new errors_1.RPCPassRequired(),
});
}
return errors;
}
exports.validateRPCNodeConfig = validateRPCNodeConfig;
// we would call this only during the "Testing node connection" step
// Check if the node is accessible by RPC. promise fails if not.
async function checkRPCNodeConfig(config) {
const errors = validateRPCNodeConfig(config);
if (errors.length) {
throw errors[0].error;
}
if ((0, live_env_1.getEnv)("MOCK")) {
if (mockStatus.type === "node-disconnected") {
throw new Error("mock disconnected");
}
return;
}
const { host, username, password, tls } = config;
await (0, network_1.default)({
url: `http${tls ? "s" : ""}://${host}`,
method: "POST",
data: {
jsonrpc: "1.0",
id: "ledger-live-full-node-check",
method: "getblockchaininfo",
params: [],
},
auth: {
username,
password,
},
});
}
exports.checkRPCNodeConfig = checkRPCNodeConfig;
// we would need to call this any time we would "Edit" the flow
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;
}
}
exports.parseSatStackConfig = parseSatStackConfig;
// we would need this at the end of the setup flow, when we save to a jss.json configuration file
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);
}
exports.stringifySatStackConfig = stringifySatStackConfig;
// We would need it to apply an edition over an existing sats stack configuration (before saving it over)
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,
};
}
exports.editSatStackConfig = editSatStackConfig;
function isSatStackEnabled() {
return Boolean((0, live_env_1.getEnv)("SATSTACK"));
}
exports.isSatStackEnabled = isSatStackEnabled;
// 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
async function fetchSatStackStatus() {
if (!isSatStackEnabled()) {
return {
type: "satstack-disconnected",
};
}
if ((0, live_env_1.getEnv)("MOCK")) {
return mockStatus;
}
const ce = (0, explorer_1.getCurrencyExplorer)((0, currencies_1.getCryptoCurrencyById)("bitcoin"));
const r = await (0, network_1.default)({
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",
};
}
exports.fetchSatStackStatus = fetchSatStackStatus;
async function checkDescriptorExists(descriptor) {
if ((0, live_env_1.getEnv)("MOCK")) {
return true;
}
const r = await (0, network_1.default)({
method: "POST",
url: `${(0, live_env_1.getEnv)("EXPLORER_SATSTACK")}/control/descriptors/has`,
data: {
descriptor,
},
});
(0, logs_1.log)("satstack", "checkDescriptorExists " + descriptor + " is " + r.data.exists);
return Boolean(r.data.exists);
}
exports.checkDescriptorExists = checkDescriptorExists;
async function requiresSatStackReady() {
if (isSatStackEnabled()) {
const status = await fetchSatStackStatus();
switch (status.type) {
case "ready":
return;
case "syncing":
case "scanning":
throw new errors_1.SatStackStillSyncing();
case "satstack-outdated":
throw new errors_1.SatStackVersionTooOld();
default:
throw new errors_1.SatStackAccessDown();
}
}
}
exports.requiresSatStackReady = requiresSatStackReady;
exports.statusObservable = (0, rxjs_1.interval)(1000).pipe((0, operators_1.switchMap)(() => (0, rxjs_1.from)(fetchSatStackStatus())), (0, operators_1.filter)((e, i) => i > 4 || e.type !== "satstack-disconnected"), (0, operators_1.share)());
//# sourceMappingURL=satstack.js.map