UNPKG

xud

Version:
654 lines 34.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.isLndClient = exports.isConnextClient = void 0; const assert_1 = __importDefault(require("assert")); const events_1 = require("events"); const ConnextClient_1 = __importDefault(require("../connextclient/ConnextClient")); const enums_1 = require("../constants/enums"); const errors_1 = __importDefault(require("../lndclient/errors")); const LndClient_1 = __importDefault(require("../lndclient/LndClient")); const cryptoUtils_1 = require("../utils/cryptoUtils"); const UnitConverter_1 = require("../utils/UnitConverter"); const errors_2 = __importDefault(require("./errors")); const SwapClient_1 = __importStar(require("./SwapClient")); function isConnextClient(swapClient) { return (swapClient.type === enums_1.SwapClientType.Connext); } exports.isConnextClient = isConnextClient; function isLndClient(swapClient) { return (swapClient.type === enums_1.SwapClientType.Lnd); } exports.isLndClient = isLndClient; class SwapClientManager extends events_1.EventEmitter { constructor(config, loggers, unitConverter, models) { super(); this.config = config; this.loggers = loggers; this.unitConverter = unitConverter; this.models = models; /** A map between currencies and all enabled swap clients */ this.swapClients = new Map(); this.misconfiguredClients = new Set(); /** A map of supported currency tickers to the inbound amount that is reserved by existing orders. */ this.inboundReservedAmounts = new Map(); /** A map of supported currency tickers to the outbound amount that is reserved by existing orders. */ this.outboundReservedAmounts = new Map(); /** * Starts all swap clients, binds event listeners * and waits for the swap clients to initialize. * @returns A promise that resolves upon successful initialization, rejects otherwise. */ this.init = () => __awaiter(this, void 0, void 0, function* () { const initPromises = []; // setup configured LND clients and initialize them for (const currency in this.config.lnd) { const lndConfig = this.config.lnd[currency]; if (!lndConfig.disable) { const lndClient = new LndClient_1.default({ currency, config: lndConfig, logger: this.loggers.lnd.createSubLogger(currency), }); this.swapClients.set(currency, lndClient); initPromises.push(lndClient.init()); } } if (!this.config.connext.disable) { // setup Connext const currencyInstances = yield this.models.Currency.findAll(); this.connextClient = new ConnextClient_1.default({ currencyInstances, unitConverter: this.unitConverter, config: this.config.connext, logger: this.loggers.connext, }); } // bind event listeners before all swap clients have initialized this.bind(); yield Promise.all(initPromises); this.swapClients.forEach((swapClient, currency) => { if (swapClient.isDisabled()) { // delete any swap clients that are disabled this.swapClients.delete(currency); } else if (swapClient.isMisconfigured()) { // track misconfigured swap clients separately this.swapClients.delete(currency); this.misconfiguredClients.add(swapClient); } }); if (this.connextClient) { if (this.connextClient.isMisconfigured()) { this.misconfiguredClients.add(this.connextClient); } else if (!this.connextClient.isDisabled()) { // associate swap clients with currencies managed by connext client for (const currency of this.connextClient.tokenAddresses.keys()) { this.swapClients.set(currency, this.connextClient); } } } }); /** * Initializes Connext client with a seed * @returns true if Connext */ this.initConnext = (seed) => __awaiter(this, void 0, void 0, function* () { try { if (!this.config.connext.disable && this.connextClient) { this.connextClient.setSeed(seed); yield this.connextClient.init(); return true; } } catch (err) { this.loggers.connext.error('could not initialize connext', err); } return false; }); /** * Checks to make sure all enabled lnd instances are available and waits * up to a short time for them to become available in case they are not. * Throws an error if any lnd clients remain unreachable. */ this.waitForLnd = () => __awaiter(this, void 0, void 0, function* () { const lndClients = this.getLndClientsMap().values(); const lndAvailablePromises = []; for (const lndClient of lndClients) { lndAvailablePromises.push(new Promise((resolve, reject) => { if (lndClient.isDisconnected() || lndClient.isNotInitialized()) { const onAvailable = () => { clearTimeout(timer); lndClient.removeListener('locked', onAvailable); lndClient.removeListener('connectionVerified', onAvailable); resolve(); }; lndClient.on('connectionVerified', onAvailable); lndClient.on('locked', onAvailable); const timer = setTimeout(() => { lndClient.removeListener('connectionVerified', onAvailable); lndClient.removeListener('locked', onAvailable); reject(lndClient.currency); }, SwapClient_1.default.RECONNECT_INTERVAL); } else { resolve(); } })); } try { yield Promise.all(lndAvailablePromises); } catch (currency) { throw errors_1.default.UNAVAILABLE(currency, SwapClient_1.ClientStatus.Disconnected); } }); this.tradingLimits = (currency) => __awaiter(this, void 0, void 0, function* () { var _a, _b; const swapClient = this.get(currency); if (swapClient) { const swapCapacities = yield swapClient.swapCapacities(currency); const reservedOutbound = (_a = this.outboundReservedAmounts.get(currency)) !== null && _a !== void 0 ? _a : 0; const reservedInbound = (_b = this.inboundReservedAmounts.get(currency)) !== null && _b !== void 0 ? _b : 0; const availableOutboundCapacity = Math.max(0, swapCapacities.totalOutboundCapacity - reservedOutbound); const availableInboundCapacity = Math.max(0, swapCapacities.totalInboundCapacity - reservedInbound); return { reservedSell: reservedOutbound, reservedBuy: reservedInbound, maxSell: Math.min(swapCapacities.maxOutboundChannelCapacity, availableOutboundCapacity), maxBuy: Math.min(swapCapacities.maxInboundChannelCapacity, availableInboundCapacity), }; } else { throw errors_2.default.SWAP_CLIENT_NOT_FOUND(currency); } }); /** * Checks whether a given order with would exceed our inbound or outbound swap * capacities when taking into consideration reserved amounts for standing * orders, throws an exception if so and returns otherwise. */ this.checkSwapCapacities = ({ quantity, price, isBuy, pairId }) => __awaiter(this, void 0, void 0, function* () { var _c, _d; const { outboundCurrency, inboundCurrency, outboundAmount, inboundAmount } = UnitConverter_1.UnitConverter.calculateInboundOutboundAmounts(quantity, price, isBuy, pairId); // check if clients exists const outboundSwapClient = this.get(outboundCurrency); const inboundSwapClient = this.get(inboundCurrency); if (!outboundSwapClient) { throw errors_2.default.SWAP_CLIENT_NOT_FOUND(outboundCurrency); } if (!inboundSwapClient) { throw errors_2.default.SWAP_CLIENT_NOT_FOUND(inboundCurrency); } const outboundCapacities = yield outboundSwapClient.swapCapacities(outboundCurrency); // check if sufficient outbound channel capacity exists const reservedOutbound = (_c = this.outboundReservedAmounts.get(outboundCurrency)) !== null && _c !== void 0 ? _c : 0; const availableOutboundCapacity = outboundCapacities.totalOutboundCapacity - reservedOutbound; if (outboundAmount > availableOutboundCapacity) { throw errors_2.default.INSUFFICIENT_OUTBOUND_CAPACITY(outboundCurrency, outboundAmount, availableOutboundCapacity); } // check if sufficient inbound channel capacity exists if (isConnextClient(inboundSwapClient)) { // connext has the unique ability to dynamically request additional inbound capacity aka collateral // we handle it differently and allow "lazy collateralization" if the total inbound capacity would // be exceeded when including the reserved inbound amounts, we only reject if this order alone would // exceed our inbound capacity inboundSwapClient.checkInboundCapacity(inboundAmount, inboundCurrency); } else { const inboundCapacities = yield inboundSwapClient.swapCapacities(inboundCurrency); const reservedInbound = (_d = this.inboundReservedAmounts.get(inboundCurrency)) !== null && _d !== void 0 ? _d : 0; const availableInboundCapacity = inboundCapacities.totalInboundCapacity - reservedInbound; if (inboundAmount > availableInboundCapacity) { throw errors_2.default.INSUFFICIENT_INBOUND_CAPACITY(inboundCurrency, inboundAmount, availableInboundCapacity); } } }); this.getOutboundReservedAmount = (currency) => { return this.outboundReservedAmounts.get(currency); }; this.getInboundReservedAmount = (currency) => { return this.inboundReservedAmounts.get(currency); }; this.addOutboundReservedAmount = (currency, amount) => { const outboundReservedAmount = this.outboundReservedAmounts.get(currency); const newOutboundReservedAmount = (outboundReservedAmount !== null && outboundReservedAmount !== void 0 ? outboundReservedAmount : 0) + amount; this.outboundReservedAmounts.set(currency, newOutboundReservedAmount); }; this.addInboundReservedAmount = (currency, amount) => { var _a; const inboundReservedAmount = this.inboundReservedAmounts.get(currency); const newInboundReservedAmount = (inboundReservedAmount !== null && inboundReservedAmount !== void 0 ? inboundReservedAmount : 0) + amount; this.inboundReservedAmounts.set(currency, newInboundReservedAmount); (_a = this.swapClients.get(currency)) === null || _a === void 0 ? void 0 : _a.setReservedInboundAmount(newInboundReservedAmount, currency); }; this.subtractOutboundReservedAmount = (currency, amount) => { const outboundReservedAmount = this.outboundReservedAmounts.get(currency); assert_1.default(outboundReservedAmount && outboundReservedAmount >= amount); this.outboundReservedAmounts.set(currency, outboundReservedAmount - amount); }; this.subtractInboundReservedAmount = (currency, amount) => { const inboundReservedAmount = this.inboundReservedAmounts.get(currency); assert_1.default(inboundReservedAmount && inboundReservedAmount >= amount); this.inboundReservedAmounts.set(currency, inboundReservedAmount - amount); }; this.setLogLevel = (level) => { for (const client of this.swapClients.values()) { client.logger.setLogLevel(level); } }; /** * Initializes wallets with seed and password. */ this.initWallets = ({ walletPassword, seedMnemonic, restore, lndBackups, nodeKey }) => __awaiter(this, void 0, void 0, function* () { // loop through swap clients to initialize locked lnd clients const initWalletPromises = []; const initializedLndWallets = []; let initializedConnext = false; for (const swapClient of this.swapClients.values()) { if (isLndClient(swapClient)) { if (swapClient.isWaitingUnlock()) { const initWalletPromise = swapClient.initWallet(walletPassword, seedMnemonic, restore, lndBackups ? lndBackups.get(swapClient.currency) : undefined).then(() => { initializedLndWallets.push(swapClient.currency); }).catch((err) => { swapClient.logger.error('could not initialize lnd wallet', err.message); }); initWalletPromises.push(initWalletPromise); } } else if (isConnextClient(swapClient)) { initializedConnext = yield this.initConnext(nodeKey.childSeed(enums_1.SwapClientType.Connext)); } } yield Promise.all(initWalletPromises); return { initializedLndWallets, initializedConnext, }; }); /** * Unlocks wallets with a password. * @returns an array of currencies for each lnd client that was unlocked */ this.unlockWallets = ({ walletPassword, nodeKey }) => __awaiter(this, void 0, void 0, function* () { // loop through swap clients to find locked lnd clients const unlockWalletPromises = []; const unlockedLndClients = []; const lockedLndClients = []; const oldEncryptedPasswords = yield this.models.Password.findAll(); for (const swapClient of this.swapClients.values()) { if (isLndClient(swapClient)) { if (swapClient.isWaitingUnlock()) { // first we check whether this lnd is using an old wallet password const oldEncryptedPassword = oldEncryptedPasswords.find((oldEncryptedPassword) => { return oldEncryptedPassword.swapClient === enums_1.SwapClientType.Lnd && oldEncryptedPassword.currency === swapClient.currency; }); const oldPassword = oldEncryptedPassword ? cryptoUtils_1.decrypt(oldEncryptedPassword.encryptedPassword, walletPassword).toString() : undefined; if (oldPassword) { // if we have an old password for this lnd client, then we use it to change its password // to the new password, which in turn will unlock the client const changePasswordPromise = swapClient.changePassword(oldPassword, walletPassword).then(() => { unlockedLndClients.push(swapClient.currency); return oldEncryptedPassword === null || oldEncryptedPassword === void 0 ? void 0 : oldEncryptedPassword.destroy(); // we can remove the old password from the database }).catch((err) => __awaiter(this, void 0, void 0, function* () { this.loggers.lnd.error(`could not change password for ${swapClient.currency}`, err); lockedLndClients.push(swapClient.currency); })); unlockWalletPromises.push(changePasswordPromise); } else { const unlockWalletPromise = swapClient.unlockWallet(walletPassword).then(() => { unlockedLndClients.push(swapClient.currency); }).catch((err) => __awaiter(this, void 0, void 0, function* () { let walletCreated = false; if (err.details === 'wallet not found') { // this wallet hasn't been initialized, so we will try to initialize it now const seedMnemonic = yield nodeKey.getMnemonic(); try { yield swapClient.initWallet(walletPassword !== null && walletPassword !== void 0 ? walletPassword : '', seedMnemonic); walletCreated = true; } catch (err) { swapClient.logger.error('could not initialize lnd wallet', err); } } if (!walletCreated) { lockedLndClients.push(swapClient.currency); swapClient.logger.debug(`could not unlock wallet: ${err.message}`); } })); unlockWalletPromises.push(unlockWalletPromise); } } else if (swapClient.isDisconnected() || swapClient.isMisconfigured() || swapClient.isNotInitialized()) { // if the swap client is not connected, we treat it as locked since lnd will likely be locked when it comes online lockedLndClients.push(swapClient.currency); } } else if (isConnextClient(swapClient)) { // TODO(connext): unlock Connext using connextSeed yield this.initConnext(nodeKey.childSeed(enums_1.SwapClientType.Connext)); } } yield Promise.all(unlockWalletPromises); return { unlockedLndClients, lockedLndClients }; }); /** * Changes the wallet passwords for all lnd clients by either calling ChangePassword * right away if lnd is in a WaitingUnlock state or, more often, by persisting the * current wallet password so that xud will try to automatically change it the next * time it is unlocked. */ this.changeLndPasswords = (oldPassword, newPassword) => __awaiter(this, void 0, void 0, function* () { const lndClients = this.getLndClientsMap().values(); const promises = []; for (const lndClient of lndClients) { if (lndClient.isWaitingUnlock()) { // we can change the password and unlock right now promises.push(lndClient.changePassword(oldPassword, newPassword)); } else if (lndClient.isOperational()) { const encryptedPassword = (yield cryptoUtils_1.encrypt(oldPassword, newPassword)).toString('base64'); promises.push(this.models.Password.create({ encryptedPassword, swapClient: enums_1.SwapClientType.Lnd, currency: lndClient.currency, })); } } yield Promise.all(promises); }); /** * Gets a swap client instance. * @param currency a currency that the swap client is linked to. * @returns swap client instance upon success, undefined otherwise. */ this.get = (currency) => { return this.swapClients.get(currency); }; /** * Returns whether the swap client manager has a client for a given currency. * @param currency the currency that the swap client is linked to. * @returns `true` if a swap client exists, false otherwise. */ this.has = (currency) => { return this.swapClients.has(currency); }; /** Gets the type of swap client for a given currency. */ this.getType = (currency) => { var _a; return (_a = this.swapClients.get(currency)) === null || _a === void 0 ? void 0 : _a.type; }; /** * Returns whether the swap client for a specified currency is connected. * @returns `true` if a swap client exists and is connected, otherwise `false` */ this.isConnected = (currency) => { const swapClient = this.swapClients.get(currency); return swapClient !== undefined && swapClient.isConnected(); }; /** * Adds a new swap client and currency association. * @param currency a currency that should be linked with a swap client. * @returns Nothing upon success, throws otherwise. */ this.add = (currency) => __awaiter(this, void 0, void 0, function* () { if (currency.tokenAddress) { if (currency.swapClient === enums_1.SwapClientType.Connext) { if (!this.connextClient) { throw errors_2.default.SWAP_CLIENT_NOT_CONFIGURED(currency.id); } this.swapClients.set(currency.id, this.connextClient); this.connextClient.tokenAddresses.set(currency.id, currency.tokenAddress); this.emit('connextUpdate', this.connextClient.tokenAddresses); } } else if (currency.swapClient === enums_1.SwapClientType.Lnd) { // in case of lnd we check if the configuration includes swap client // for the specified currency const config = this.config.lnd[currency.id]; if (!config) { throw errors_2.default.SWAP_CLIENT_NOT_CONFIGURED(currency.id); } } }); /** * Removes a new swap client and currency association. * @param currency a currency that should be unlinked from a swap client. * @returns Nothing upon success, throws otherwise. */ this.remove = (currency) => { var _a; const swapClient = this.get(currency); this.swapClients.delete(currency); if (swapClient && isConnextClient(swapClient)) { (_a = this.connextClient) === null || _a === void 0 ? void 0 : _a.tokenAddresses.delete(currency); } }; /** * Gets a map of all lnd clients. * @returns A map of currencies to lnd clients. */ this.getLndClientsMap = () => { const lndClients = new Map(); this.swapClients.forEach((swapClient, currency) => { if (isLndClient(swapClient)) { lndClients.set(currency, swapClient); } }); return lndClients; }; /** * Gets all lnd clients' info. * @returns A promise that resolves to an object containing lnd * clients' info, throws otherwise. */ this.getLndClientsInfo = () => __awaiter(this, void 0, void 0, function* () { const lndInfos = new Map(); // TODO: consider maintaining this list of pubkeys // (similar to how we're maintaining the list of connext currencies) // rather than determining it dynamically when needed. The benefits // would be slightly improved performance. const getInfoPromises = []; for (const [currency, swapClient] of this.swapClients.entries()) { if (isLndClient(swapClient) && !swapClient.isDisabled()) { getInfoPromises.push(swapClient.getLndInfo().then((lndInfo) => { lndInfos.set(currency, lndInfo); })); } } yield Promise.all(getInfoPromises); return lndInfos; }); /** * Closes all swap client instances gracefully. * @returns Nothing upon success, throws otherwise. */ this.close = () => { let connextClosed = false; for (const swapClient of this.swapClients.values()) { swapClient.close(); if (isConnextClient(swapClient)) { connextClosed = true; } } for (const swapClient of this.misconfiguredClients) { swapClient.close(); if (isConnextClient(swapClient)) { connextClosed = true; } } // we make sure to close connext client because it // might not be associated with any currency if (this.connextClient && !this.connextClient.isDisabled() && !connextClosed) { this.connextClient.close(); } }; this.deposit = (currency) => __awaiter(this, void 0, void 0, function* () { const swapClient = this.get(currency); if (!swapClient) { throw errors_2.default.SWAP_CLIENT_NOT_FOUND(currency); } const address = yield swapClient.deposit(); return address; }); this.withdraw = ({ currency, amount, destination, all, fee }) => __awaiter(this, void 0, void 0, function* () { const swapClient = this.get(currency); if (!swapClient) { throw errors_2.default.SWAP_CLIENT_NOT_FOUND(currency); } return yield swapClient.withdraw({ currency, amount, destination, all, fee }); }); /** * Closes a payment channel. * @param remoteIdentifier the identifier for the remote side of the channel. * @param currency a currency for the payment channel. * @param amount the amount to extract from the channel. If 0 or unspecified, * the entire off-chain balance for the specified currency will be extracted. * @returns Nothing upon success, throws otherwise. */ this.closeChannel = ({ remoteIdentifier, currency, force, destination, amount, fee }) => __awaiter(this, void 0, void 0, function* () { const swapClient = this.get(currency); if (!swapClient) { throw errors_2.default.SWAP_CLIENT_NOT_FOUND(currency); } const units = amount ? this.unitConverter.amountToUnits({ amount, currency, }) : undefined; return swapClient.closeChannel({ remoteIdentifier, currency, force, destination, units, fee, }); }); /** * Opens a payment channel. * @param remoteIdentifier the identifier for the remote side of the channel. * @param currency a currency for the payment channel. * @param amount the size of the payment channel local balance * @returns Nothing upon success, throws otherwise. */ this.openChannel = ({ remoteIdentifier, amount, currency, pushAmount = 0, fee = 0, uris }) => __awaiter(this, void 0, void 0, function* () { const swapClient = this.get(currency); if (!swapClient) { throw errors_2.default.SWAP_CLIENT_NOT_FOUND(currency); } const units = this.unitConverter.amountToUnits({ amount, currency, }); const pushUnits = this.unitConverter.amountToUnits({ currency, amount: pushAmount, }); return swapClient.openChannel({ remoteIdentifier, currency, units, uris, pushUnits, fee, }); }); /** * Checks whether it is possible to route a payment to a peer for a given currency. This does not * guarantee or test that a payment can be routed successfully, only determiens whether it is * possible to do so currently given the state of the specified currency's network and graph. */ this.canRouteToPeer = (peer, currency) => __awaiter(this, void 0, void 0, function* () { const swapClient = this.get(currency); if (!swapClient) { return false; } const destination = peer.getIdentifier(swapClient.type, currency); if (!destination) { return false; } let canRoute; try { canRoute = yield swapClient.canRouteToNode(destination, currency); } catch (_e) { canRoute = false; // if swap client calls are failing, we infer that we can't route payments } return canRoute; }); this.bind = () => { for (const [currency, swapClient] of this.swapClients.entries()) { if (isLndClient(swapClient)) { swapClient.on('connectionVerified', ({ newIdentifier, newUris }) => { if (newIdentifier) { this.emit('lndUpdate', { currency, uris: newUris, pubKey: newIdentifier, chain: swapClient.chain, }); } }); // lnd clients emit htlcAccepted evented we must handle swapClient.on('htlcAccepted', (rHash, amount) => { this.emit('htlcAccepted', swapClient, rHash, amount, currency); }); swapClient.on('locked', () => { if (isLndClient(swapClient) && swapClient.walletPassword) { swapClient.unlockWallet(swapClient.walletPassword).catch(swapClient.logger.error); } // TODO(connext): unlock ConnextClient when it's implemented }); } } // we handle connext separately because we don't want to attach // duplicate listeners in case connext client is associated with // multiple currencies if (this.connextClient && !this.connextClient.isDisabled() && !this.connextClient.isMisconfigured()) { this.connextClient.on('htlcAccepted', (rHash, amount, currency) => { this.emit('htlcAccepted', this.connextClient, rHash, amount, currency); }); this.connextClient.on('connectionVerified', (swapClientInfo) => { const { newIdentifier } = swapClientInfo; if (newIdentifier) { this.emit('connextUpdate', this.connextClient.tokenAddresses, newIdentifier); } }); } }; } } exports.default = SwapClientManager; //# sourceMappingURL=SwapClientManager.js.map