Skip to content

Commit

Permalink
feat(lnd): SendPaymentV2
Browse files Browse the repository at this point in the history
This migrates the call we use to send payments with lnd from the
deprecated `SendPaymentSync` to `SendPaymentV2` which allows for multi
path payments, among other improvements. As part of this change, the
lnd proto files have been updated to their v0.11.x versions and the
version of lnd used in simulation tests has been updated to v0.11.1 as
well.

Closes #1590.
  • Loading branch information
sangaman committed Nov 10, 2020
1 parent c1af693 commit 51c60dc
Show file tree
Hide file tree
Showing 22 changed files with 36,267 additions and 15,914 deletions.
143 changes: 86 additions & 57 deletions lib/lndclient/LndClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ import { SwapClientType, SwapRole, SwapState } from '../constants/enums';
import Logger from '../Logger';
import { InvoicesClient } from '../proto/lndinvoices_grpc_pb';
import * as lndinvoices from '../proto/lndinvoices_pb';
import { LightningClient, WalletUnlockerClient } from '../proto/lndrpc_grpc_pb';
import { RouterClient } from '../proto/lndrouter_grpc_pb';
import * as lndrouter from '../proto/lndrouter_pb';
import { LightningClient } from '../proto/lndrpc_grpc_pb';
import * as lndrpc from '../proto/lndrpc_pb';
import { WalletUnlockerClient } from '../proto/lndwalletunlocker_grpc_pb';
import * as lndwalletunlocker from '../proto/lndwalletunlocker_pb';
import { BASE_MAX_CLIENT_WAIT_TIME, MAX_FEE_RATIO, MAX_PAYMENT_TIME } from '../swaps/consts';
import swapErrors from '../swaps/errors';
import SwapClient, { ChannelBalance, ClientStatus, PaymentState, SwapClientInfo, WithdrawArguments } from '../swaps/SwapClient';
import { CloseChannelParams, OpenChannelParams, SwapCapacities, SwapDeal } from '../swaps/types';
Expand All @@ -34,8 +39,6 @@ interface LndClient {
emit(event: 'initialized'): boolean;
}

const MAXFEE = 0.03;
const BASE_MAX_CLIENT_WAIT_TIME = 6000;
const GRPC_CLIENT_OPTIONS = {
'grpc.ssl_target_name_override': 'localhost',
'grpc.default_authority': 'localhost',
Expand All @@ -52,6 +55,7 @@ class LndClient extends SwapClient {
/** The maximum time to wait for a client to be ready for making grpc calls, can be used for exponential backoff. */
private maxClientWaitTime = BASE_MAX_CLIENT_WAIT_TIME;
private invoices?: InvoicesClient;
private router?: RouterClient;
/** The path to the lnd admin macaroon, will be undefined if `nomacaroons` is enabled */
private macaroonpath?: string;
private meta = new grpc.Metadata();
Expand Down Expand Up @@ -504,6 +508,7 @@ class LndClient extends SwapClient {
}

this.invoices = new InvoicesClient(this.uri, this.credentials);
this.router = new RouterClient(this.uri, this.credentials);
try {
const randomHash = crypto.randomBytes(32).toString('hex');
this.logger.debug(`checking hold invoice support with hash: ${randomHash}`);
Expand Down Expand Up @@ -576,13 +581,13 @@ class LndClient extends SwapClient {
// client's default.
finalCltvDelta: this.finalLock,
});
const preimage = await this.executeSendRequest(request);
const preimage = await this.sendPaymentV2(request);
return preimage;
}

public sendPayment = async (deal: SwapDeal): Promise<string> => {
assert(deal.state === SwapState.Active);
let request: lndrpc.SendRequest;
let request: lndrouter.SendPaymentRequest;
assert(deal.makerCltvDelta, 'swap deal must have a makerCltvDelta');
if (deal.role === SwapRole.Taker) {
// we are the taker paying the maker
Expand All @@ -609,16 +614,71 @@ class LndClient extends SwapClient {
cltvLimit: deal.takerMaxTimeLock! + 3,
});
}
const preimage = await this.executeSendRequest(request);
this.logger.debug(`sending payment of ${request.getAmt()} with hash ${deal.rHash}`);
const preimage = await this.sendPaymentV2(request);
return preimage;
}

/**
* Sends a payment through the Lightning Network.
* @returns the preimage in hex format
*/
private sendPaymentSync = (request: lndrpc.SendRequest): Promise<lndrpc.SendResponse> => {
this.logger.trace(`sending payment with request: ${JSON.stringify(request.toObject())}`);
return this.unaryCall<lndrpc.SendRequest, lndrpc.SendResponse>('sendPaymentSync', request);
private sendPaymentV2 = (request: lndrouter.SendPaymentRequest): Promise<string> => {
return new Promise<string>((resolve, reject) => {
if (!this.router) {
reject(swapErrors.FINAL_PAYMENT_ERROR(errors.UNAVAILABLE(this.currency, this.status).message));
return;
}
if (!this.isConnected()) {
reject(swapErrors.FINAL_PAYMENT_ERROR(errors.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: lndrpc.Payment) => {
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(swapErrors.FINAL_PAYMENT_ERROR(lndrpc.PaymentFailureReason[response.getFailureReason()]));
break;
case lndrpc.PaymentFailureReason.FAILURE_REASON_INCORRECT_PAYMENT_DETAILS:
reject(swapErrors.PAYMENT_REJECTED);
break;
default:
reject(swapErrors.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(swapErrors.FINAL_PAYMENT_ERROR(err.message));
} else {
reject(swapErrors.UNKNOWN_PAYMENT_ERROR(JSON.stringify(err)));
}
});
});
}

/**
Expand All @@ -627,15 +687,15 @@ class LndClient extends SwapClient {
private buildSendRequest = (
{ rHash, destination, amount, finalCltvDelta, cltvLimit }:
{ rHash: string, destination: string, amount: number, finalCltvDelta: number, cltvLimit?: number },
): lndrpc.SendRequest => {
const request = new lndrpc.SendRequest();
request.setPaymentHashString(rHash);
request.setDestString(destination);
): lndrouter.SendPaymentRequest => {
const request = new lndrouter.SendPaymentRequest();
request.setPaymentHash(Buffer.from(rHash, 'hex'));
request.setDest(Buffer.from(destination, 'hex'));
request.setAmt(amount);
request.setFinalCltvDelta(finalCltvDelta);
const fee = new lndrpc.FeeLimit();
fee.setFixed(Math.floor(MAXFEE * request.getAmt()));
request.setFeeLimit(fee);
request.setTimeoutSeconds(MAX_PAYMENT_TIME / 1000);
const fee = Math.floor(MAX_FEE_RATIO * request.getAmt());
request.setFeeLimitSat(fee);
if (cltvLimit) {
// cltvLimit is used to enforce the maximum
// duration/length of the payment.
Expand All @@ -644,41 +704,6 @@ class LndClient extends SwapClient {
return request;
}

/**
* Executes the provided lndrpc.SendRequest
*/
private executeSendRequest = async (
request: lndrpc.SendRequest,
): Promise<string> => {
if (!this.isConnected()) {
throw swapErrors.FINAL_PAYMENT_ERROR(errors.UNAVAILABLE(this.currency, this.status).message);
}
this.logger.debug(`sending payment of ${request.getAmt()} with hash ${request.getPaymentHashString()} to ${request.getDestString()}`);
let sendPaymentResponse: lndrpc.SendResponse;
try {
sendPaymentResponse = await this.sendPaymentSync(request);
} catch (err) {
this.logger.error('got exception from sendPaymentSync', err);
if (typeof err.message === 'string' && err.message.includes('chain backend is still syncing')) {
throw swapErrors.FINAL_PAYMENT_ERROR(err.message);
} else {
throw swapErrors.UNKNOWN_PAYMENT_ERROR(err.message);
}
}
const paymentError = sendPaymentResponse.getPaymentError();
if (paymentError) {
if (paymentError.includes('UnknownPaymentHash') || paymentError.includes('IncorrectOrUnknownPaymentDetails')) {
throw swapErrors.PAYMENT_REJECTED;
} else {
throw swapErrors.FINAL_PAYMENT_ERROR(paymentError);
}
}
const preimage = base64ToHex(sendPaymentResponse.getPaymentPreimage_asB64());

this.logger.debug(`sent payment with hash ${request.getPaymentHashString()}, preimage is ${preimage}`);
return preimage;
}

/**
* Gets a new address for the internal lnd wallet.
*/
Expand Down Expand Up @@ -878,7 +903,7 @@ class LndClient extends SwapClient {
request.setFinalCltvDelta(finalLock);
request.setPubKey(destination);
const fee = new lndrpc.FeeLimit();
fee.setFixed(Math.floor(MAXFEE * request.getAmt()));
fee.setFixed(Math.floor(MAX_FEE_RATIO * request.getAmt()));
request.setFeeLimit(fee);

let route: lndrpc.Route | undefined;
Expand Down Expand Up @@ -924,8 +949,8 @@ class LndClient extends SwapClient {
}

public initWallet = async (walletPassword: string, seedMnemonic: string[], restore = false, backup?: Uint8Array):
Promise<lndrpc.InitWalletResponse.AsObject> => {
const request = new lndrpc.InitWalletRequest();
Promise<lndwalletunlocker.InitWalletResponse.AsObject> => {
const request = new lndwalletunlocker.InitWalletRequest();

// from the master seed/mnemonic we derive a child mnemonic for this specific client
const childMnemonic = await deriveChild(seedMnemonic, this.label);
Expand All @@ -942,7 +967,7 @@ class LndClient extends SwapClient {
snapshot.setMultiChanBackup(multiChanBackup);
request.setChannelBackups(snapshot);
}
const initWalletResponse = await this.unaryWalletUnlockerCall<lndrpc.InitWalletRequest, lndrpc.InitWalletResponse>(
const initWalletResponse = await this.unaryWalletUnlockerCall<lndwalletunlocker.InitWalletRequest, lndwalletunlocker.InitWalletResponse>(
'initWallet', request,
);
if (this.initWalletResolve) {
Expand All @@ -955,9 +980,9 @@ class LndClient extends SwapClient {
}

public unlockWallet = async (walletPassword: string): Promise<void> => {
const request = new lndrpc.UnlockWalletRequest();
const request = new lndwalletunlocker.UnlockWalletRequest();
request.setWalletPassword(Uint8Array.from(Buffer.from(walletPassword, 'utf8')));
await this.unaryWalletUnlockerCall<lndrpc.UnlockWalletRequest, lndrpc.UnlockWalletResponse>(
await this.unaryWalletUnlockerCall<lndwalletunlocker.UnlockWalletRequest, lndwalletunlocker.UnlockWalletResponse>(
'unlockWallet', request,
);
this.setUnlocked();
Expand Down Expand Up @@ -1200,6 +1225,10 @@ class LndClient extends SwapClient {
this.invoices.close();
this.invoices = undefined;
}
if (this.router) {
this.router.close();
this.router = undefined;
}
if (this.initWalletResolve) {
this.initWalletResolve(false);
this.initWalletResolve = undefined;
Expand Down
Loading

0 comments on commit 51c60dc

Please sign in to comment.