From b720736eea479b1b82ef74136739fdbe2915d6d9 Mon Sep 17 00:00:00 2001 From: Tom Kirkpatrick Date: Thu, 18 Jun 2020 18:35:16 +0200 Subject: [PATCH] fix(wallet): use sendpaymentv2 for probing if available --- renderer/reducers/pay/reducer.js | 14 ++++- renderer/reducers/payment/reducer.js | 11 +++- services/grpc/router.methods.js | 86 ++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 4 deletions(-) diff --git a/renderer/reducers/pay/reducer.js b/renderer/reducers/pay/reducer.js index 0a8533bfa6b..8f3652d7410 100644 --- a/renderer/reducers/pay/reducer.js +++ b/renderer/reducers/pay/reducer.js @@ -94,12 +94,18 @@ export const queryRoutes = (payReqOrPubkey, amt, feeLimit) => async (dispatch, g let paymentHash let payload - const defaultPaymentOptions = pick(currentConfig.payments, [ + const paymentOptions = pick(currentConfig.payments, [ 'allowSelfPayment', 'timeoutSeconds', 'feeLimit', ]) + const defaultPaymentOptions = { + allowSelfPayment: paymentOptions.allowSelfPayment, + timeoutSeconds: paymentOptions.timeoutSeconds, + feeLimitSat: paymentOptions.feeLimit, + } + // Prepare payload for lnd. if (isKeysend) { const options = prepareKeysendProbe(payReqOrPubkey, amt, feeLimit) @@ -120,8 +126,12 @@ export const queryRoutes = (payReqOrPubkey, amt, feeLimit) => async (dispatch, g } const callProbePayment = async () => { + const routerProbePayment = infoSelectors.hasSendPaymentV2Support(getState()) + ? grpc.services.Router.probePaymentV2 + : grpc.services.Router.probePayment + const routes = [] - const route = await grpc.services.Router.probePayment(payload) + const route = await routerProbePayment(payload) // Flag this as an exact route. This can be used as a hint for whether to use sendToRoute to fulfil the payment. route.isExact = true // Store the payment hash for use with keysend. diff --git a/renderer/reducers/payment/reducer.js b/renderer/reducers/payment/reducer.js index bd744610ab5..7bd0654affd 100644 --- a/renderer/reducers/payment/reducer.js +++ b/renderer/reducers/payment/reducer.js @@ -228,13 +228,20 @@ export const payInvoice = ({ const isKeysend = isPubkey(payReq) let payload - const defaultPaymentOptions = pick(currentConfig.payments, [ + const paymentOptions = pick(currentConfig.payments, [ 'allowSelfPayment', 'timeoutSeconds', 'feeLimit', 'maxParts', ]) - defaultPaymentOptions.paymentId = paymentId + + const defaultPaymentOptions = { + allowSelfPayment: paymentOptions.allowSelfPayment, + timeoutSeconds: paymentOptions.timeoutSeconds, + feeLimitSat: paymentOptions.feeLimit, + maxParts: paymentOptions.feeLimit, + paymentId, + } // Prepare payload for lnd. if (isKeysend) { diff --git a/services/grpc/router.methods.js b/services/grpc/router.methods.js index 0557d716a3f..04392f50c2e 100644 --- a/services/grpc/router.methods.js +++ b/services/grpc/router.methods.js @@ -87,6 +87,91 @@ async function probePayment(payload) { }) } +/** + * probePaymentV2 - Call lnd grpc sendPayment method with a fake payment hash in order to probe for a route. + * + * @param {object} payload Payload + * @returns {Promise} The route route when state is SUCCEEDED + */ +async function probePaymentV2(payload) { + // Use a payload that has the payment hash set to some random bytes. + // This will cause the payment to fail at the final destination. + payload.paymentHash = new Uint8Array(generatePreimage()) + + logGrpcCmd('Router.probePaymentV2', payload) + + let result + let error + + const decorateError = e => { + e.details = result + return e + } + + return new Promise((resolve, reject) => { + grpcLog.time('probePayment') + const call = this.service.sendPaymentV2(payload) + let route + + call.on('data', data => { + grpcLog.debug('PROBE DATA :%o', data) + + switch (data.status) { + case 'IN_FLIGHT': + grpcLog.info('PROBE IN_FLIGHT...') + break + + case 'FAILED': + if (data.failureReason !== 'FAILURE_REASON_INCORRECT_PAYMENT_DETAILS') { + grpcLog.warn('PROBE FAILED: %o', data) + error = new Error(data.status) + break + } + + grpcLog.info('PROBE SUCCESS: %o', data) + // Prior to lnd v0.10.0 sendPayment would return a single route under the `route` key. + route = data.route || data.htlcs[0].route + // FIXME: For some reason the customRecords key is corrupt in the grpc response object. + // For now, assume that if a custom_record key is set that it is a keysend record and fix it accordingly. + route.hops = route.hops.map(hop => { + Object.keys(hop.customRecords).forEach(key => { + hop.customRecords[KEYSEND_PREIMAGE_TYPE] = hop.customRecords[key] + delete hop.customRecords[key] + }) + return hop + }) + result = route + break + + default: + grpcLog.warn('PROBE FAILED: %o', data) + error = new Error(data.status) + } + }) + + call.on('status', status => { + grpcLog.info('PROBE STATUS :%o', status) + }) + + call.on('error', e => { + grpcLog.warn('PROBE ERROR :%o', e) + error = new Error(e.details || e.message) + }) + + call.on('end', () => { + grpcLog.timeEnd('probePaymentV2') + grpcLog.info('PROBE END') + if (error) { + return reject(decorateError(error)) + } + if (!result) { + return reject(decorateError(new Error('TERMINATED_EARLY'))) + } + return resolve(result) + }) + }) +} + /** * sendPayment - Call lnd grpc sendPayment method. * @@ -224,6 +309,7 @@ async function sendPaymentV2(payload = {}) { export default { probePayment, + probePaymentV2, sendPayment, sendPaymentV2, }