UNPKG

xud

Version:
917 lines 62.6 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 }); const assert_1 = __importDefault(require("assert")); const crypto_1 = __importDefault(require("crypto")); const fs_1 = require("fs"); const grpc_1 = __importDefault(require("grpc")); const path_1 = __importDefault(require("path")); const enums_1 = require("../constants/enums"); const lndinvoices_grpc_pb_1 = require("../proto/lndinvoices_grpc_pb"); const lndinvoices = __importStar(require("../proto/lndinvoices_pb")); const lndrouter_grpc_pb_1 = require("../proto/lndrouter_grpc_pb"); const lndrouter = __importStar(require("../proto/lndrouter_pb")); const lndrpc_grpc_pb_1 = require("../proto/lndrpc_grpc_pb"); const lndrpc = __importStar(require("../proto/lndrpc_pb")); const lndwalletunlocker_grpc_pb_1 = require("../proto/lndwalletunlocker_grpc_pb"); const lndwalletunlocker = __importStar(require("../proto/lndwalletunlocker_pb")); const consts_1 = require("../swaps/consts"); const errors_1 = __importDefault(require("../swaps/errors")); const SwapClient_1 = __importStar(require("../swaps/SwapClient")); const seedutil_1 = require("../utils/seedutil"); const utils_1 = require("../utils/utils"); const errors_2 = __importDefault(require("./errors")); const GRPC_CLIENT_OPTIONS = { 'grpc.ssl_target_name_override': 'localhost', 'grpc.default_authority': 'localhost', }; /** A class representing a client to interact with lnd. */ let LndClient = /** @class */ (() => { class LndClient extends SwapClient_1.default { /** * Creates an lnd client. */ constructor({ config, logger, currency }) { super(logger, config.disable); this.type = enums_1.SwapClientType.Lnd; /** The maximum time to wait for a client to be ready for making grpc calls, can be used for exponential backoff. */ this.maxClientWaitTime = consts_1.BASE_MAX_CLIENT_WAIT_TIME; this.meta = new grpc_1.default.Metadata(); this.invoiceSubscriptions = new Map(); this.totalOutboundAmount = 0; this.totalInboundAmount = 0; this.maxChannelOutboundAmount = 0; this.maxChannelInboundAmount = 0; this.waitForClientReady = (client) => { return new Promise((resolve, reject) => { client.waitForReady(Date.now() + this.maxClientWaitTime, (err) => { if (err) { if (err.message === 'Failed to connect before the deadline') { this.maxClientWaitTime *= 10; // exponentially backoff the max wait time if we reach the deadline resolve(); } reject(err); } else { this.maxClientWaitTime = consts_1.BASE_MAX_CLIENT_WAIT_TIME; // reset our max wait time resolve(); } }); }); }; /** * Initializes the client for calls to lnd and verifies that we can connect to it. * @param awaitingCreate whether xud is waiting for its node key to be created */ this.initSpecific = () => __awaiter(this, void 0, void 0, function* () { const { certpath, macaroonpath, nomacaroons, host, port } = this.config; let lndCert; try { lndCert = yield fs_1.promises.readFile(certpath); } catch (err) { if (err.code === 'ENOENT') { // if we have not created the lnd wallet yet and the tls.cert file can // not be found, we will briefly wait for the cert to be created in // case lnd has not been run before and is being started in parallel // with xud const certDir = path_1.default.join(certpath, '..'); const CERT_TIMEOUT = 3000; lndCert = yield new Promise((resolve) => { this.logger.debug(`watching ${certDir} for tls.cert to be created`); const timeout = setTimeout(() => { fsWatcher.close(); resolve(undefined); }, CERT_TIMEOUT); const fsWatcher = fs_1.watch(certDir, (event, filename) => { if (event === 'change' && filename === 'tls.cert') { this.logger.debug('tls.cert was created'); fsWatcher.close(); clearTimeout(timeout); fs_1.promises.readFile(certpath).then(resolve).catch((err) => { this.logger.error(err); resolve(undefined); }); } }); }); } } if (lndCert) { this.logger.debug(`loaded tls cert from ${certpath}`); this.credentials = grpc_1.default.credentials.createSsl(lndCert); } else { this.logger.error(`could not load tls cert from ${certpath}, is lnd installed?`); this.setStatus(SwapClient_1.ClientStatus.Misconfigured); this.initRetryTimeout = setTimeout(this.init, LndClient.RECONNECT_INTERVAL); return; } if (!nomacaroons) { this.macaroonpath = macaroonpath; try { yield this.loadMacaroon(); } catch (err) { this.logger.info(`could not load macaroon at ${macaroonpath}, this is normal before creating a new wallet`); } } else { this.logger.info('macaroons are disabled'); } this.uri = `${host}:${port}`; if (this.initRetryTimeout) { clearTimeout(this.initRetryTimeout); this.initRetryTimeout = undefined; } }); this.setReservedInboundAmount = (_reservedInboundAmount) => { return; // not currently used for lnd }; /** Lnd specific procedure to mark the client as locked. */ this.lock = () => { if (!this.walletUnlocker) { this.walletUnlocker = new lndwalletunlocker_grpc_pb_1.WalletUnlockerClient(this.uri, this.credentials, GRPC_CLIENT_OPTIONS); } if (this.lightning) { this.lightning.close(); this.lightning = undefined; } if (!this.isWaitingUnlock()) { this.setStatus(SwapClient_1.ClientStatus.WaitingUnlock); } this.emit('locked'); }; /** Lnd specific procedure to mark the client as unlocked. */ this.setUnlocked = () => { // we should close and unreference the wallet unlocker service when we set the status to Unlocked if (this.walletUnlocker) { this.walletUnlocker.close(); this.walletUnlocker = undefined; } if (this.isWaitingUnlock()) { this.setStatus(SwapClient_1.ClientStatus.Unlocked); } else { // we should not be calling this method we were in the WaitingUnlock status this.logger.warn(`tried to set client status to WaitingUnlock from status ${this.status}`); } }; this.updateCapacity = () => __awaiter(this, void 0, void 0, function* () { yield this.channelBalance().catch((err) => __awaiter(this, void 0, void 0, function* () { this.logger.error('failed to update total outbound capacity', err); })); }); this.unaryCall = (methodName, params) => { return new Promise((resolve, reject) => { if (this.hasNoInvoiceSupport()) { reject(errors_2.default.NO_HOLD_INVOICE_SUPPORT); return; } if (!this.isOperational()) { reject(errors_2.default.DISABLED); return; } if (!this.lightning) { reject(errors_2.default.UNAVAILABLE(this.currency, this.status)); return; } this.lightning[methodName](params, this.meta, (err, response) => { if (err) { if (err.code === grpc_1.default.status.UNAVAILABLE) { this.disconnect(); } else if (err.code === grpc_1.default.status.UNIMPLEMENTED) { this.lock(); } this.logger.trace(`error on ${methodName}: ${err.message}`); reject(err); } else { resolve(response); } }); }); }; this.loadMacaroon = () => __awaiter(this, void 0, void 0, function* () { if (this.macaroonpath) { const adminMacaroon = yield fs_1.promises.readFile(this.macaroonpath); this.meta.add('macaroon', adminMacaroon.toString('hex')); this.logger.debug(`loaded macaroon from ${this.macaroonpath}`); } }); this.unaryInvoiceCall = (methodName, params) => { return new Promise((resolve, reject) => { if (!this.isOperational()) { reject(errors_2.default.DISABLED); return; } if (!this.invoices) { reject(errors_2.default.UNAVAILABLE(this.currency, this.status)); return; } this.invoices[methodName](params, this.meta, (err, response) => { if (err) { if (err.code === grpc_1.default.status.UNAVAILABLE) { this.disconnect(); } else if (err.code === grpc_1.default.status.UNIMPLEMENTED) { this.lock(); } this.logger.trace(`error on ${methodName}: ${err.message}`); reject(err); } else { resolve(response); } }); }); }; this.unaryWalletUnlockerCall = (methodName, params) => { return new Promise((resolve, reject) => { if (!this.isOperational()) { reject(errors_2.default.DISABLED); return; } if (!this.walletUnlocker) { reject(errors_2.default.UNAVAILABLE(this.currency, this.status)); return; } this.walletUnlocker[methodName](params, this.meta, (err, response) => { if (err) { if (err.code === grpc_1.default.status.UNAVAILABLE) { this.disconnect(); } if (err.code === grpc_1.default.status.UNIMPLEMENTED) { this.logger.debug(`lnd already unlocked before ${methodName} call`); resolve(); } else { this.logger.debug(`error on ${methodName}: ${err.message}`); reject(err); } } else { resolve(response); } }); }); }; this.getLndInfo = () => __awaiter(this, void 0, void 0, function* () { let channels; let chains; let blockheight; let uris; let version; let alias; let status = 'Ready'; if (this.hasNoInvoiceSupport()) { status = errors_2.default.NO_HOLD_INVOICE_SUPPORT(this.currency).message; } else if (!this.isOperational()) { status = errors_2.default.DISABLED(this.currency).message; } else if (this.isDisconnected()) { status = errors_2.default.UNAVAILABLE(this.currency, this.status).message; } else { try { const getInfoResponse = yield this.getInfo(); const closedChannelsResponse = yield this.getClosedChannels(); channels = { active: getInfoResponse.getNumActiveChannels(), inactive: getInfoResponse.getNumInactiveChannels(), pending: getInfoResponse.getNumPendingChannels(), closed: closedChannelsResponse.getChannelsList().length, }; chains = getInfoResponse.getChainsList().map(value => value.toObject()); blockheight = getInfoResponse.getBlockHeight(); uris = getInfoResponse.getUrisList(); version = getInfoResponse.getVersion(); alias = getInfoResponse.getAlias(); if (this.isOutOfSync()) { status = errors_2.default.UNAVAILABLE(this.currency, this.status).message; } else if (channels.active <= 0) { status = errors_2.default.NO_ACTIVE_CHANNELS(this.currency).message; } } catch (err) { this.logger.error('getinfo error', err); status = err.message; } } return { status, channels, chains, blockheight, uris, version, alias, }; }); /** * Waits for the lnd wallet to be initialized and for its macaroons to be created then attempts * to verify the connection to lnd. */ this.awaitWalletInit = () => __awaiter(this, void 0, void 0, function* () { /** * Whether the lnd wallet has been created via an InitWallet call, * `false` if we close the client before the lnd wallet is created. */ let isWalletInitialized; if (this.status === SwapClient_1.ClientStatus.Initialized) { // we are waiting for the lnd wallet to be initialized by xud and for the lnd macaroons to be created this.logger.info('waiting for wallet to be initialized...'); this.walletUnlocker = new lndwalletunlocker_grpc_pb_1.WalletUnlockerClient(this.uri, this.credentials); yield this.waitForClientReady(this.walletUnlocker); this.lock(); isWalletInitialized = yield new Promise((resolve) => { this.initWalletResolve = resolve; }); } else if (this.status === SwapClient_1.ClientStatus.Unlocked) { // the lnd wallet has been created but its macaroons have not been written to the file system yet isWalletInitialized = true; } else { assert_1.default.fail('awaitWalletInit should not be called from a status besides Initialized or Unlocked'); } if (isWalletInitialized) { // admin.macaroon will not necessarily be created by the time lnd responds to a successful // InitWallet call, so we watch the folder that we expect it to be in for it to be created yield this.watchThenLoadMacaroon(); // once we've loaded the macaroon we can attempt to verify the conneciton this.verifyConnection().catch(this.logger.error); } }); this.verifyConnection = () => __awaiter(this, void 0, void 0, function* () { if (!this.isOperational()) { throw (errors_2.default.DISABLED); } if (this.isWaitingUnlock()) { return; // temporary workaround to prevent unexplained lnd crashes after unlock } if (this.macaroonpath && this.meta.get('macaroon').length === 0) { // we have not loaded the macaroon yet - it is not created until the lnd wallet is initialized if (!this.isWaitingUnlock() && !this.initWalletResolve) { // check that we are not already waiting for wallet init & unlock this.awaitWalletInit().catch(this.logger.error); } return; } this.logger.info(`trying to verify connection to lnd at ${this.uri}`); this.lightning = new lndrpc_grpc_pb_1.LightningClient(this.uri, this.credentials, GRPC_CLIENT_OPTIONS); try { yield this.waitForClientReady(this.lightning); const getInfoResponse = yield this.getInfo(); if (getInfoResponse.getSyncedToChain()) { // check if the lnd pub key value is different from the one we had previously. let newPubKey; let newUris = []; if (this.identityPubKey !== getInfoResponse.getIdentityPubkey()) { newPubKey = getInfoResponse.getIdentityPubkey(); this.logger.debug(`pubkey is ${newPubKey}`); this.identityPubKey = newPubKey; newUris = getInfoResponse.getUrisList(); if (newUris.length) { this.logger.debug(`uris are ${newUris}`); } else { this.logger.debug('no uris advertised'); } this.urisList = newUris; } // check if the chain this lnd instance uses has changed const chain = getInfoResponse.getChainsList()[0]; const chainIdentifier = `${chain.getChain()}-${chain.getNetwork()}`; if (!this.chainIdentifier) { this.chainIdentifier = chainIdentifier; this.logger.debug(`chain is ${chainIdentifier}`); } else if (this.chainIdentifier !== chainIdentifier) { // we switched chains for this lnd client while xud was running which is not supported this.logger.error(`chain switched from ${this.chainIdentifier} to ${chainIdentifier}`); this.setStatus(SwapClient_1.ClientStatus.Misconfigured); } if (this.walletUnlocker) { // WalletUnlocker service is disabled when the main Lightning service is available this.walletUnlocker.close(); this.walletUnlocker = undefined; } this.invoices = new lndinvoices_grpc_pb_1.InvoicesClient(this.uri, this.credentials); this.router = new lndrouter_grpc_pb_1.RouterClient(this.uri, this.credentials); try { const randomHash = crypto_1.default.randomBytes(32).toString('hex'); this.logger.debug(`checking hold invoice support with hash: ${randomHash}`); yield this.addInvoice({ rHash: randomHash, units: 1 }); yield this.removeInvoice(randomHash); } catch (err) { if (err.code !== grpc_1.default.status.UNAVAILABLE) { // mark the client as not having hold invoice support if the invoice calls failed due to // reasons other than generic grpc connectivity errors this.logger.error('could not add hold invoice', err); this.setStatus(SwapClient_1.ClientStatus.NoHoldInvoiceSupport); } throw err; // we don't want to proceed with marking the client as connected, regardless of the error } yield this.setConnected(newPubKey, newUris); } else { this.setStatus(SwapClient_1.ClientStatus.OutOfSync); this.logger.warn(`lnd is out of sync with chain, retrying in ${LndClient.RECONNECT_INTERVAL} ms`); } } catch (err) { const errStr = typeof (err) === 'string' ? err : JSON.stringify(err); this.logger.error(`could not verify connection at ${this.uri}, error: ${errStr}, retrying in ${LndClient.RECONNECT_INTERVAL} ms`); } }); /** * Returns general information concerning the lightning node including it’s identity pubkey, alias, the chains it * is connected to, and information concerning the number of open+pending channels. */ this.getInfo = () => { return this.unaryCall('getInfo', new lndrpc.GetInfoRequest()); }; /** * Returns closed channels that this node was a participant in. */ this.getClosedChannels = () => { return this.unaryCall('closedChannels', new lndrpc.ClosedChannelsRequest()); }; this.deposit = () => __awaiter(this, void 0, void 0, function* () { const depositAddress = yield this.newAddress(); return depositAddress; }); this.withdraw = ({ amount, destination, all = false, fee }) => __awaiter(this, void 0, void 0, function* () { const request = new lndrpc.SendCoinsRequest(); request.setAddr(destination); if (fee) { request.setSatPerByte(fee); } if (all) { request.setSendAll(all); } else if (amount) { request.setAmount(amount); } const withdrawResponse = yield this.unaryCall('sendCoins', request); return withdrawResponse.getTxid(); }); this.sendSmallestAmount = (rHash, destination) => __awaiter(this, void 0, void 0, function* () { const request = this.buildSendRequest({ rHash, destination, amount: 1, // In case of sanity swaps we don't know the // takerCltvDelta or the makerCltvDelta. Using our // client's default. finalCltvDelta: this.finalLock, }); const preimage = yield this.sendPaymentV2(request); return preimage; }); this.sendPayment = (deal) => __awaiter(this, void 0, void 0, function* () { assert_1.default(deal.state === enums_1.SwapState.Active); let request; assert_1.default(deal.makerCltvDelta, 'swap deal must have a makerCltvDelta'); if (deal.role === enums_1.SwapRole.Taker) { // we are the taker paying the maker assert_1.default(deal.destination, 'swap deal as taker must have a destination'); request = this.buildSendRequest({ rHash: deal.rHash, destination: deal.destination, amount: deal.makerAmount, // Using the agreed upon makerCltvDelta. Maker won't accept // our payment if we provide a smaller value. finalCltvDelta: deal.makerCltvDelta, }); } else { // we are the maker paying the taker assert_1.default(deal.takerPubKey, 'swap deal as maker must have a takerPubKey'); assert_1.default(deal.takerCltvDelta, 'swap deal as maker must have a takerCltvDelta'); request = this.buildSendRequest({ rHash: deal.rHash, destination: deal.takerPubKey, amount: deal.takerAmount, finalCltvDelta: deal.takerCltvDelta, // Enforcing the maximum duration/length of the payment by specifying // the cltvLimit. We add 3 blocks to offset the block padding set by lnd. cltvLimit: deal.takerMaxTimeLock + 3, }); } this.logger.debug(`sending payment of ${request.getAmt()} with hash ${deal.rHash}`); const preimage = yield this.sendPaymentV2(request); return preimage; }); /** * Sends a payment through the Lightning Network. * @returns the preimage in hex format */ this.sendPaymentV2 = (request) => { return new Promise((resolve, reject) => { if (!this.router) { reject(errors_1.default.FINAL_PAYMENT_ERROR(errors_2.default.UNAVAILABLE(this.currency, this.status).message)); return; } if (!this.isConnected()) { reject(errors_1.default.FINAL_PAYMENT_ERROR(errors_2.default.UNAVAILABLE(this.currency, this.status).message)); return; } this.logger.trace(`sending payment with request: ${JSON.stringify(request.toObject())}`); const call = this.router.sendPaymentV2(request, this.meta); call.on('data', (response) => { switch (response.getStatus()) { case lndrpc.Payment.PaymentStatus.FAILED: switch (response.getFailureReason()) { case lndrpc.PaymentFailureReason.FAILURE_REASON_TIMEOUT: case lndrpc.PaymentFailureReason.FAILURE_REASON_NO_ROUTE: case lndrpc.PaymentFailureReason.FAILURE_REASON_ERROR: case lndrpc.PaymentFailureReason.FAILURE_REASON_INSUFFICIENT_BALANCE: reject(errors_1.default.FINAL_PAYMENT_ERROR(lndrpc.PaymentFailureReason[response.getFailureReason()])); break; case lndrpc.PaymentFailureReason.FAILURE_REASON_INCORRECT_PAYMENT_DETAILS: reject(errors_1.default.PAYMENT_REJECTED); break; default: reject(errors_1.default.UNKNOWN_PAYMENT_ERROR(response.getFailureReason().toString())); break; } break; case lndrpc.Payment.PaymentStatus.SUCCEEDED: resolve(response.getPaymentPreimage()); break; default: // in-flight status, we'll wait for a final status update event break; } }); call.on('end', () => { call.removeAllListeners(); }); call.on('error', (err) => { call.removeAllListeners(); this.logger.error('error event from sendPaymentV2', err); if (typeof err.message === 'string' && err.message.includes('chain backend is still syncing')) { reject(errors_1.default.FINAL_PAYMENT_ERROR(err.message)); } else { reject(errors_1.default.UNKNOWN_PAYMENT_ERROR(JSON.stringify(err))); } }); }); }; /** * Builds a lndrpc.SendRequest */ this.buildSendRequest = ({ rHash, destination, amount, finalCltvDelta, cltvLimit }) => { const request = new lndrouter.SendPaymentRequest(); request.setPaymentHash(Buffer.from(rHash, 'hex')); request.setDest(Buffer.from(destination, 'hex')); request.setAmt(amount); request.setFinalCltvDelta(finalCltvDelta); request.setTimeoutSeconds(consts_1.MAX_PAYMENT_TIME / 1000); const fee = Math.floor(consts_1.MAX_FEE_RATIO * request.getAmt()); request.setFeeLimitSat(fee); if (cltvLimit) { // cltvLimit is used to enforce the maximum // duration/length of the payment. request.setCltvLimit(cltvLimit); } return request; }; /** * Gets a new address for the internal lnd wallet. */ this.newAddress = (addressType = lndrpc.AddressType.WITNESS_PUBKEY_HASH) => __awaiter(this, void 0, void 0, function* () { const request = new lndrpc.NewAddressRequest(); request.setType(addressType); const newAddressResponse = yield this.unaryCall('newAddress', request); return newAddressResponse.getAddress(); }); /** * Returns the total of unspent outputs for the internal lnd wallet. */ this.walletBalance = () => __awaiter(this, void 0, void 0, function* () { const walletBalanceResponse = yield this.unaryCall('walletBalance', new lndrpc.WalletBalanceRequest()); return walletBalanceResponse.toObject(); }); /** * Updates all balances related to channels including active, inactive, and pending balances. * Sets trading limits for this client accordingly. */ this.updateChannelBalances = () => __awaiter(this, void 0, void 0, function* () { const [channels, pendingChannels] = yield Promise.all([this.listChannels(), this.pendingChannels()]); let maxOutbound = 0; let maxInbound = 0; let balance = 0; let inactiveBalance = 0; let totalOutboundAmount = 0; let totalInboundAmount = 0; channels.toObject().channelsList.forEach((channel) => { if (channel.active) { balance += channel.localBalance; const outbound = Math.max(0, channel.localBalance - channel.localChanReserveSat); totalOutboundAmount += outbound; if (maxOutbound < outbound) { maxOutbound = outbound; } const inbound = Math.max(0, channel.remoteBalance - channel.remoteChanReserveSat); totalInboundAmount += inbound; if (maxInbound < inbound) { maxInbound = inbound; } } else { inactiveBalance += channel.localBalance; } }); if (this.maxChannelOutboundAmount !== maxOutbound) { this.maxChannelOutboundAmount = maxOutbound; this.logger.debug(`new channel maximum outbound capacity: ${maxOutbound}`); } if (this.maxChannelInboundAmount !== maxInbound) { this.maxChannelInboundAmount = maxInbound; this.logger.debug(`new channel inbound capacity: ${maxInbound}`); } if (this.totalOutboundAmount !== totalOutboundAmount) { this.totalOutboundAmount = totalOutboundAmount; this.logger.debug(`new channel total outbound capacity: ${totalOutboundAmount}`); } if (this.totalInboundAmount !== totalInboundAmount) { this.totalInboundAmount = totalInboundAmount; this.logger.debug(`new channel total inbound capacity: ${totalInboundAmount}`); } const pendingOpenBalance = pendingChannels.toObject().pendingOpenChannelsList. reduce((sum, pendingChannel) => { var _a, _b; return sum + ((_b = (_a = pendingChannel.channel) === null || _a === void 0 ? void 0 : _a.localBalance) !== null && _b !== void 0 ? _b : 0); }, 0); return { maxOutbound, maxInbound, totalOutboundAmount, totalInboundAmount, balance, inactiveBalance, pendingOpenBalance, }; }); this.channelBalance = () => __awaiter(this, void 0, void 0, function* () { const { balance, inactiveBalance, pendingOpenBalance } = yield this.updateChannelBalances(); return { balance, inactiveBalance, pendingOpenBalance }; }); this.swapCapacities = () => __awaiter(this, void 0, void 0, function* () { const { maxOutbound, maxInbound, totalInboundAmount, totalOutboundAmount } = yield this.updateChannelBalances(); // get fresh balances return { maxOutboundChannelCapacity: maxOutbound, maxInboundChannelCapacity: maxInbound, totalOutboundCapacity: totalOutboundAmount, totalInboundCapacity: totalInboundAmount, }; }); this.getHeight = () => __awaiter(this, void 0, void 0, function* () { const info = yield this.getInfo(); return info.getBlockHeight(); }); /** * Connects to another lnd node. */ this.connectPeer = (pubkey, address) => { const request = new lndrpc.ConnectPeerRequest(); const lightningAddress = new lndrpc.LightningAddress(); lightningAddress.setHost(address); lightningAddress.setPubkey(pubkey); request.setAddr(lightningAddress); return this.unaryCall('connectPeer', request); }; /** * Opens a channel given peerPubKey and amount. */ this.openChannel = ({ remoteIdentifier, units, uris, pushUnits = 0, fee = 0 }) => __awaiter(this, void 0, void 0, function* () { if (!remoteIdentifier) { // TODO: better handling for for unrecognized peers & force closing channels throw new Error('peer not connected to swap client'); } if (uris) { yield this.connectPeerAddresses(uris); } const openResponse = yield this.openChannelSync(remoteIdentifier, units, pushUnits, fee); return openResponse.hasFundingTxidStr() ? openResponse.getFundingTxidStr() : utils_1.base64ToHex(openResponse.getFundingTxidBytes_asB64()); }); /** * Tries to connect to a given list of a peer's uris in sequential order. * @returns `true` when successful, otherwise `false`. */ this.connectPeerAddresses = (peerListeningUris) => __awaiter(this, void 0, void 0, function* () { const splitListeningUris = peerListeningUris .map((uri) => { const splitUri = uri.split('@'); return { peerPubKey: splitUri[0], address: splitUri[1], }; }); for (const uri of splitListeningUris) { const { peerPubKey, address } = uri; try { yield this.connectPeer(peerPubKey, address); return true; } catch (e) { if (e.message && e.message.includes('already connected')) { return true; } this.logger.trace(`connectPeer to ${uri} failed: ${e}`); } } return false; }); /** * Opens a channel with a connected lnd node. */ this.openChannelSync = (nodePubkeyString, localFundingAmount, pushSat = 0, fee = 0) => { const request = new lndrpc.OpenChannelRequest; request.setNodePubkeyString(nodePubkeyString); request.setLocalFundingAmount(localFundingAmount); request.setPushSat(pushSat); request.setSatPerByte(fee); return this.unaryCall('openChannelSync', request); }; /** * Lists all open channels for this node. */ this.listChannels = () => { return this.unaryCall('listChannels', new lndrpc.ListChannelsRequest()); }; /** * Lists all pending channels for this node. */ this.pendingChannels = () => { return this.unaryCall('pendingChannels', new lndrpc.PendingChannelsRequest()); }; this.getRoute = (units, destination, _currency, finalLock = this.finalLock) => __awaiter(this, void 0, void 0, function* () { const request = new lndrpc.QueryRoutesRequest(); request.setAmt(units); request.setFinalCltvDelta(finalLock); request.setPubKey(destination); const fee = new lndrpc.FeeLimit(); fee.setFixed(Math.floor(consts_1.MAX_FEE_RATIO * request.getAmt())); request.setFeeLimit(fee); let route; try { // QueryRoutes no longer returns more than one route route = (yield this.queryRoutes(request)).getRoutesList()[0]; } catch (err) { if (typeof err.message === 'string' && err.message.includes('insufficient local balance')) { throw errors_1.default.INSUFFICIENT_BALANCE; } if (typeof err.message !== 'string' || (!err.message.includes('unable to find a path to destination') && !err.message.includes('target not found'))) { this.logger.error(`error calling queryRoutes to ${destination}, amount ${units}, finalCltvDelta ${finalLock}`, err); throw err; } } if (route) { this.logger.debug(`found a route to ${destination} for ${units} units with finalCltvDelta ${finalLock}: ${route}`); } else { this.logger.debug(`could not find a route to ${destination} for ${units} units with finalCltvDelta ${finalLock}`); } return route; }); this.canRouteToNode = (_destination) => __awaiter(this, void 0, void 0, function* () { // lnd doesn't currently have a way to see if any route exists, regardless of balance // for example, if we have a direct channel to peer but no balance in the channel and // no other routes, QueryRoutes will return nothing as of lnd v0.8.1. // For now we err on the side of leniency and assume a route may exist. return true; }); /** * Lists all routes to destination. */ this.queryRoutes = (request) => { return this.unaryCall('queryRoutes', request); }; this.initWallet = (walletPassword, seedMnemonic, restore = false, backup) => __awaiter(this, void 0, void 0, function* () { this.walletPassword = walletPassword; const request = new lndwalletunlocker.InitWalletRequest(); // from the master seed/mnemonic we derive a child mnemonic for this specific client const childMnemonic = yield seedutil_1.deriveChild(seedMnemonic, this.label); request.setCipherSeedMnemonicList(childMnemonic); request.setWalletPassword(Uint8Array.from(Buffer.from(walletPassword, 'utf8'))); if (restore) { request.setRecoveryWindow(2500); } if (backup && backup.byteLength) { const snapshot = new lndrpc.ChanBackupSnapshot(); const multiChanBackup = new lndrpc.MultiChanBackup(); multiChanBackup.setMultiChanBackup(backup); snapshot.setMultiChanBackup(multiChanBackup); request.setChannelBackups(snapshot); } const initWalletResponse = yield this.unaryWalletUnlockerCall('initWallet', request); if (this.initWalletResolve) { this.initWalletResolve(true); } this.setUnlocked(); this.logger.info('wallet initialized'); return initWalletResponse.toObject(); }); this.unlockWallet = (walletPassword) => __awaiter(this, void 0, void 0, function* () { this.walletPassword = walletPassword; const request = new lndwalletunlocker.UnlockWalletRequest(); request.setWalletPassword(Uint8Array.from(Buffer.from(walletPassword, 'utf8'))); yield this.unaryWalletUnlockerCall('unlockWallet', request); this.setUnlocked(); this.logger.info('wallet unlocked'); }); /** * Watches for a change in the admin.macaroon file at the configured path, * then loads the macaroon. */ this.watchThenLoadMacaroon = () => __awaiter(this, void 0, void 0, function* () { const watchMacaroonPromise = new Promise((resolve) => { this.watchMacaroonResolve = resolve; }); const macaroonDir = path_1.default.join(this.macaroonpath, '..'); const fsWatcher = fs_1.watch(macaroonDir, (event, filename) => { if (event === 'change' && filename === 'admin.macaroon') { this.logger.debug('admin.macaroon was created'); if (this.watchMacaroonResolve) { this.watchMacaroonResolve(true); } } }); this.logger.debug(`watching ${macaroonDir} for admin.macaroon to be created`); const macaroonCreated = yield watchMacaroonPromise; fsWatcher.close(); this.watchMacaroonResolve = undefined; if (macaroonCreated) { try { yield this.loadMacaroon(); } catch (err) { this.logger.error(`could not load macaroon from ${this.macaroonpath}`); this.setStatus(SwapClient_1.ClientStatus.Disabled); } } }); this.changePassword = (oldPassword, newPassword) => __awaiter(this, void 0, void 0, function* () { this.walletPassword = newPassword; const request = new lndwalletunlocker.ChangePasswordRequest(); request.setCurrentPassword(Uint8Array.from(Buffer.from(oldPassword, 'utf8'))); request.setNewPassword(Uint8Array.from(Buffer.from(newPassword, 'utf8'))); yield this.unaryWalletUnlockerCall('changePassword', request); // the macaroons change every time lnd changes its password, so we must remove the old one and reload the new one this.meta.remove('macaroon'); // admin.macaroon will not necessarily be created by the time lnd responds to a successful // ChangePassword call, so we watch the folder that we expect it to be in for it to be created yield this.watchThenLoadMacaroon(); this.setUnlocked(); this.logger.info('password changed & wallet unlocked'); }); this.addInvoice = ({ rHash, units, expiry = this.finalLock }) => __awaiter(this, void 0, void 0, function* () { const addHoldInvoiceRequest = new lndinvoices.AddHoldInvoiceRequest(); addHoldInvoiceRequest.setHash(utils_1.hexToUint8Array(rHash)); addHoldInvoiceRequest.setValue(units); addHoldInvoiceRequest.setCltvExpiry(expiry); yield this.addHoldInvoice(addHoldInvoiceRequest); this.logger.debug(`added invoice of ${units} for ${rHash} with cltvExpiry ${expiry}`); this.subscribeSingleInvoice(rHash); }); this.settleInvoice = (rHash, rPreimage) => __awaiter(this, void 0, void 0, function* () { this.logger.debug(`settling invoice for ${rHash} with preimage ${rPreimage}`); const settleInvoiceRequest = new lndinvoices.SettleInvoiceMsg(); se