diff --git a/renderer/components/Pay/Pay.js b/renderer/components/Pay/Pay.js index 008437fb8c9..f1ac26ed243 100644 --- a/renderer/components/Pay/Pay.js +++ b/renderer/components/Pay/Pay.js @@ -131,16 +131,15 @@ class Pay extends React.Component { const isNowSummary = currentStep === PAY_FORM_STEPS.summary && prevState.currentStep !== PAY_FORM_STEPS.summary if (isNowSummary) { - let payeeNodeKey if (invoice) { - ;({ payeeNodeKey } = invoice) + const { paymentRequest } = invoice + queryRoutes(paymentRequest, this.amountInSats()) } else if (isPubkey) { const { values: { payReq }, } = this.formApi.getState() - payeeNodeKey = payReq + queryRoutes(payReq, this.amountInSats()) } - payeeNodeKey && queryRoutes(payeeNodeKey, this.amountInSats()) } } diff --git a/renderer/reducers/pay.js b/renderer/reducers/pay.js index f8e813037a1..9a32b1ba74d 100644 --- a/renderer/reducers/pay.js +++ b/renderer/reducers/pay.js @@ -1,3 +1,4 @@ +import { randomBytes, createHash } from 'crypto' import get from 'lodash/get' import { createSelector } from 'reselect' import { send } from 'redux-electron-ipc' @@ -8,13 +9,18 @@ import { CoinBig } from '@zap/utils/coin' import createReducer from '@zap/utils/createReducer' import { estimateFeeRange } from '@zap/utils/fee' import { isAutopayEnabled } from '@zap/utils/featureFlag' -import { decodePayReq, getTag } from '@zap/utils/crypto' +import { decodePayReq, isPubkey, getTag } from '@zap/utils/crypto' import { showError } from './notification' import { settingsSelectors } from './settings' import { walletSelectors } from './wallet' import { infoSelectors } from './info' import { showAutopayNotification, autopaySelectors } from './autopay' -import { payInvoice } from './payment' +import { + payInvoice, + DEFAULT_CLTV_DELTA, + PREIMAGE_BYTE_LENGTH, + KEYSEND_PREIMAGE_TYPE, +} from './payment' import { createInvoice } from './invoice' import messages from './messages' @@ -214,36 +220,77 @@ export const queryFees = (address, amountInSats) => async (dispatch, getState) = /** * queryRoutes - Find valid routes to make a payment to a node. * - * @param {object} invoice Decoded bolt11 invoice + * @param {object} payReq Payment request or node pubkey + * @param {number} amt Payment amount (in sats) + * @param {number} finalCltvDelta The number of blocks the last hop has to reveal the preimage * @returns {Function} Thunk */ -export const queryRoutes = invoice => async (dispatch, getState) => { - const { payeeNodeKey, millisatoshis } = invoice - const amountInSats = millisatoshis / 1000 +export const queryRoutes = (payReq, amt, finalCltvDelta = DEFAULT_CLTV_DELTA) => async ( + dispatch, + getState +) => { + const isKeysend = isPubkey(payReq) + let pubkey + let paymentHash + + let payload = { + useMissionControl: true, + finalCltvDelta, + } + + // Keysend payment. + if (isKeysend) { + pubkey = payReq + const preimage = randomBytes(PREIMAGE_BYTE_LENGTH) + paymentHash = createHash('sha256') + .update(preimage) + .digest() + + payload = { + ...payload, + amt, + dest: Buffer.from(payReq, 'hex'), + dest_custom_records: { + [KEYSEND_PREIMAGE_TYPE]: preimage, + }, + } + } + + // Bolt11 invoice payment. + else { + const invoice = decodePayReq(payReq) + const { millisatoshis } = invoice + const amountInSats = millisatoshis / 1000 + paymentHash = getTag(invoice, 'payment_hash') + pubkey = invoice.payeeNodeKey + + payload = { + ...payload, + amt: amountInSats, + dest: Buffer.from(pubkey, 'hex'), + } + } const callQueryRoutes = async () => { const { routes } = await grpc.services.Lightning.queryRoutes({ - pubKey: payeeNodeKey, - amt: amountInSats, - useMissionControl: true, + ...payload, + pubKey: pubkey, }) return routes } const callProbePayment = async () => { const routes = [] - const route = await grpc.services.Router.probePayment({ - dest: Buffer.from(payeeNodeKey, 'hex'), - amt: amountInSats, - finalCltvDelta: getTag(invoice, 'min_final_cltv_expiry'), - }) + const route = await grpc.services.Router.probePayment(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 keysnd. + route.paymentHash = paymentHash routes.push(route) return routes } - dispatch({ type: QUERY_ROUTES, pubKey: payeeNodeKey }) + dispatch({ type: QUERY_ROUTES, pubKey: pubkey }) try { let routes = [] diff --git a/renderer/reducers/payment.js b/renderer/reducers/payment.js index d18b4bf3d4b..2dbe2fd93ec 100644 --- a/renderer/reducers/payment.js +++ b/renderer/reducers/payment.js @@ -19,6 +19,10 @@ import { networkSelectors } from './network' import { showError } from './notification' import messages from './messages' +export const DEFAULT_CLTV_DELTA = 43 +export const KEYSEND_PREIMAGE_TYPE = '5482373484' +export const PREIMAGE_BYTE_LENGTH = 32 + // ------------------------------------ // Initial State // ------------------------------------ @@ -186,7 +190,7 @@ const decPaymentRetry = paymentId => ({ * Controller code that wraps the send action and schedules automatic retries in the case of a failure. * * @param {object} options Options - * @param {string} options.payReq Payment request + * @param {string} options.payReq Payment request or node pubkey * @param {number} options.amt Payment amount (in sats) * @param {number} options.feeLimit The max fee to apply * @param {number} options.retries Number of remaining retries @@ -213,26 +217,23 @@ export const payInvoice = ({ feeLimit: feeLimit ? { fixed: feeLimit } : null, allowSelfPayment: true, } + // Keysend payment. if (isKeysend) { - const defaultCltvDelta = 43 - const keySendPreimageType = '5482373484' - const preimageByteLength = 32 - - const preimage = randomBytes(preimageByteLength) + pubkey = payReq + const preimage = randomBytes(PREIMAGE_BYTE_LENGTH) paymentHash = createHash('sha256') .update(preimage) .digest() - pubkey = payReq payload = { ...payload, paymentHash, amt, - finalCltvDelta: defaultCltvDelta, - dest: Buffer.from(payReq, 'hex'), + finalCltvDelta: DEFAULT_CLTV_DELTA, + dest: Buffer.from(pubkey, 'hex'), destCustomRecords: { - [keySendPreimageType]: preimage, + [KEYSEND_PREIMAGE_TYPE]: preimage, }, } } @@ -246,7 +247,7 @@ export const payInvoice = ({ paymentRequest = invoice.paymentRequest // eslint-disable-line prefer-destructuring payload = { ...payload, - amt: !millisatoshis && amt, + amt: millisatoshis ? null : amt, paymentRequest, } } @@ -289,8 +290,9 @@ export const payInvoice = ({ try { const routeToUse = { ...route } delete routeToUse.isExact + delete routeToUse.paymentHash result = await grpc.services.Router.sendToRoute({ - paymentHash: Buffer.from(paymentHash, 'hex'), + paymentHash: route.paymentHash ? Buffer.from(route.paymentHash, 'hex') : null, route: routeToUse, }) } catch (error) { diff --git a/services/grpc/router.methods.js b/services/grpc/router.methods.js index ed7df92bcb2..f3ca2d8ed1d 100644 --- a/services/grpc/router.methods.js +++ b/services/grpc/router.methods.js @@ -12,6 +12,8 @@ const PAYMENT_FEE_LIMIT = config.payments.feeLimit const PAYMENT_PROBE_TIMEOUT = config.payments.probeTimeout const PAYMENT_PROBE_FEE_LIMIT = config.payments.probeFeeLimit +export const KEYSEND_PREIMAGE_TYPE = '5482373484' + // ------------------------------------ // Wrappers / Overrides // ------------------------------------ @@ -23,14 +25,16 @@ const PAYMENT_PROBE_FEE_LIMIT = config.payments.probeFeeLimit * @returns {Promise} The route route when state is SUCCEEDED */ async function probePayment(options) { - // Use a payload that has the payment hash set to some random bytes. - // This will cause the payment to fail at the final destination. const payload = defaults(omitBy(options, isNil), { - payment_hash: new Uint8Array(randomBytes(32)), timeout_seconds: PAYMENT_PROBE_TIMEOUT, fee_limit_sat: PAYMENT_PROBE_FEE_LIMIT, + allow_self_payment: true, }) + // 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.payment_hash = new Uint8Array(randomBytes(32)) + logGrpcCmd('Router.probePayment', payload) let result @@ -55,6 +59,15 @@ async function probePayment(options) { case 'FAILED_INCORRECT_PAYMENT_DETAILS': grpcLog.info('PROBE SUCCESS: %o', data) + // FIXME: For some reason the custom_records 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. + data.route.hops = data.route.hops.map(hop => { + Object.keys(hop.custom_records).forEach(key => { + hop.custom_records[KEYSEND_PREIMAGE_TYPE] = hop.custom_records[key] + delete hop.custom_records[key] + }) + return hop + }) result = data.route break