xud
Version:
Exchange Union Daemon
917 lines • 62.6 kB
JavaScript
"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