Skip to content
This repository has been archived by the owner on Nov 17, 2023. It is now read-only.

Commit

Permalink
fix(wallet): use sendpaymentv2 for probing if available
Browse files Browse the repository at this point in the history
  • Loading branch information
mrfelton committed Jun 18, 2020
1 parent c385954 commit b720736
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 4 deletions.
14 changes: 12 additions & 2 deletions renderer/reducers/pay/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.
Expand Down
11 changes: 9 additions & 2 deletions renderer/reducers/payment/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
86 changes: 86 additions & 0 deletions services/grpc/router.methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')))

This comment has been minimized.

Copy link
@korhaliv

korhaliv Jul 6, 2020

Member

Looks like in this case error will be decorated with empty result. Is this desirable?

This comment has been minimized.

Copy link
@mrfelton

mrfelton Jul 6, 2020

Author Member

TERMINATED_EARLY is used as a way to indicate that we didn't get a proper result back from lnd for some reason (an edge case that should never happen). It's used for logging purposes and to indicate that the probe failed and so we should fallback to calling queryRoutes to get a route fee. It's expected that there will be no result associated with the error in this case.

}
return resolve(result)
})
})
}

/**
* sendPayment - Call lnd grpc sendPayment method.
*
Expand Down Expand Up @@ -224,6 +309,7 @@ async function sendPaymentV2(payload = {}) {

export default {
probePayment,
probePaymentV2,
sendPayment,
sendPaymentV2,
}

0 comments on commit b720736

Please sign in to comment.