From 1974463c8d457e275d0debef34ed224aff1c2c79 Mon Sep 17 00:00:00 2001 From: Tom Kirkpatrick Date: Sat, 16 May 2020 19:38:40 +0200 Subject: [PATCH] fix(ui): map send payment error codes to messages --- .../components/Activity/ErrorDetailsDialog.js | 17 +++++-- renderer/reducers/payment/messages.js | 19 ++++++++ renderer/reducers/payment/reducer.js | 48 +++++++++++-------- renderer/reducers/payment/utils.js | 12 +++++ renderer/reducers/transaction.js | 5 +- services/grpc/router.methods.js | 7 ++- utils/userFriendlyErrors.js | 17 +++++++ 7 files changed, 98 insertions(+), 27 deletions(-) diff --git a/renderer/components/Activity/ErrorDetailsDialog.js b/renderer/components/Activity/ErrorDetailsDialog.js index 5db6ac90685..4816212962b 100644 --- a/renderer/components/Activity/ErrorDetailsDialog.js +++ b/renderer/components/Activity/ErrorDetailsDialog.js @@ -10,7 +10,7 @@ const ErrorDetailsDialog = ({ error, isOpen, onCopy, onClose, position, ...rest return null } - const { details, header } = error + const { details: { message, code } = {}, header } = error const headerEl = ( {header || } @@ -18,7 +18,7 @@ const ErrorDetailsDialog = ({ error, isOpen, onCopy, onClose, position, ...rest ) const handleCopy = () => { - copy(details) + copy([code, message].join(': ')) onCopy && onCopy() } @@ -34,9 +34,16 @@ const ErrorDetailsDialog = ({ error, isOpen, onCopy, onClose, position, ...rest } onClose={onClose} > - - {details} - + {code && ( + + {code} + + )} + {message && ( + + {message} + + )} ) diff --git a/renderer/reducers/payment/messages.js b/renderer/reducers/payment/messages.js index e4dd724a703..db6c2fefed5 100644 --- a/renderer/reducers/payment/messages.js +++ b/renderer/reducers/payment/messages.js @@ -5,4 +5,23 @@ import { defineMessages } from 'react-intl' /* eslint-disable max-len */ export default defineMessages({ unknown: 'Unknown', + terminated_early: 'Payment attempt terminated early.', + in_flight: 'Payment is still in flight.', + succeeded: 'Payment completed successfully.', + failed_timeout: 'There are more routes to try, but the payment timeout was exceeded.', + no_route: 'Unable to find route.', + failed_no_route: + 'All possible routes were tried and failed permanently. Or there were no routes to the destination at all.', + failed_error: 'A non-recoverable error has occured.', + failed_incorrect_payment_details: + 'Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta).', + failed_insufficient_balance: 'Insufficient local balance.', + failure_reason_none: "Payment isn't failed (yet).", + failure_reason_timeout: 'There are more routes to try, but the payment timeout was exceeded.', + failure_reason_no_route: + 'All possible routes were tried and failed permanently. Or were there no routes to the destination at all.', + failure_reason_error: 'A non-recoverable error has occured.', + failure_reason_incorrect_payment_details: + 'Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta).', + failure_reason_insufficient_balance: 'Insufficient local balance.', }) diff --git a/renderer/reducers/payment/reducer.js b/renderer/reducers/payment/reducer.js index b68874738ce..e243598e308 100644 --- a/renderer/reducers/payment/reducer.js +++ b/renderer/reducers/payment/reducer.js @@ -2,7 +2,6 @@ import config from 'config' import uniqBy from 'lodash/uniqBy' import find from 'lodash/find' import createReducer from '@zap/utils/createReducer' -import errorToUserFriendly from '@zap/utils/userFriendlyErrors' import { isPubkey } from '@zap/utils/crypto' import delay from '@zap/utils/delay' import genId from '@zap/utils/genId' @@ -13,7 +12,7 @@ import { fetchBalance } from 'reducers/balance' import { fetchChannels } from 'reducers/channels' import { infoSelectors } from 'reducers/info' import { paymentsSending } from './selectors' -import { prepareKeysendPayload, prepareBolt11Payload } from './utils' +import { prepareKeysendPayload, prepareBolt11Payload, errorCodeToMessage } from './utils' import * as constants from './constants' const { @@ -100,10 +99,10 @@ export const paymentComplete = paymentId => async dispatch => { /** * paymentSuccessful - Success handler for payInvoice. * - * @param {{paymentId}} paymentId Payment id (internal) + * @param {string} paymentId Payment id (internal) * @returns {Function} Thunk */ -export const paymentSuccessful = ({ paymentId }) => async (dispatch, getState) => { +export const paymentSuccessful = paymentId => async (dispatch, getState) => { const paymentSending = find(paymentsSending(getState()), { paymentId }) // If we found a related entry in paymentsSending, gracefully remove it and handle as success case. @@ -132,12 +131,12 @@ export const paymentSuccessful = ({ paymentId }) => async (dispatch, getState) = /** * paymentFailed - Error handler for payInvoice. * - * @param {Error} error Error - * @param {object} details Failed payment details - * + * @param {object} options Options + * @param {string} options.paymentId Internal payment id + * @param {number} options.error Error * @returns {Function} Thunk */ -export const paymentFailed = (error, { paymentId }) => async (dispatch, getState) => { +export const paymentFailed = ({ paymentId, error }) => async (dispatch, getState) => { const paymentSending = find(paymentsSending(getState()), { paymentId }) // errors that trigger retry mechanism @@ -145,9 +144,18 @@ export const paymentFailed = (error, { paymentId }) => async (dispatch, getState 'payment attempt not completed before timeout', // ErrPaymentAttemptTimeout 'unable to find a path to destination', // ErrNoPathFound 'target not found', // ErrTargetNotInNetwork + + // SendPayment error codes. 'FAILED_NO_ROUTE', 'FAILED_ERROR', 'FAILED_TIMEOUT', + + // SendPaymentV2 error codes. + 'FAILURE_REASON_NO_ROUTE', + 'FAILURE_REASON_ERROR', + 'FAILURE_REASON_TIMEOUT', + + // Internal codes. 'TERMINATED_EARLY', // Triggered if sendPayment aborts without giveing a proper response. ] @@ -155,7 +163,7 @@ export const paymentFailed = (error, { paymentId }) => async (dispatch, getState if (paymentSending) { const { creationDate, paymentRequest, remainingRetries, maxRetries } = paymentSending // if we have retries left and error is eligible for retry - rebroadcast payment - if (paymentRequest && remainingRetries && RETRIABLE_ERRORS.includes(error)) { + if (paymentRequest && remainingRetries && RETRIABLE_ERRORS.includes(error.code)) { const data = { ...paymentSending, payReq: paymentRequest, @@ -170,13 +178,13 @@ export const paymentFailed = (error, { paymentId }) => async (dispatch, getState await delay(2000 - (Date.now() - creationDate * 1000)) // Mark the payment as failed. - dispatch({ type: PAYMENT_FAILED, paymentId, error: errorToUserFriendly(error) }) + dispatch({ type: PAYMENT_FAILED, paymentId, error }) } } } /** - * payInvoice - Pay a lightniung invoice. + * payInvoice - Pay a lightning invoice. * Controller code that wraps the send action and schedules automatic retries in the case of a failure. * * @param {object} options Options @@ -234,7 +242,6 @@ export const payInvoice = ({ // Submit the payment to LND. try { - let data = { paymentId } // Use Router service if lnd version supports it. if (infoSelectors.hasRouterSupport(getState())) { // If we have been supplied with exact route, attempt to use that route. @@ -256,9 +263,8 @@ export const payInvoice = ({ // We don't know for sure that the node has been compiled with the Router service. // Fall bak to using sendPayment in the event of an error. mainLog.warn('Unable to pay invoice using sendToRoute: %s', error.message) - data = await routerSendPayment(payload) + await routerSendPayment(payload) } else { - error.details = data throw error } } @@ -266,7 +272,7 @@ export const payInvoice = ({ // Otherwise, just use sendPayment. else { - data = await routerSendPayment(payload) + await routerSendPayment(payload) } } @@ -279,10 +285,14 @@ export const payInvoice = ({ await grpc.services.Lightning.sendPayment(payload) } - dispatch(paymentSuccessful(data)) - } catch (e) { - const { details, message } = e - dispatch(paymentFailed(message, details)) + dispatch(paymentSuccessful(paymentId)) + } catch (error) { + const userMessage = errorCodeToMessage(error.message) + if (userMessage) { + error.code = error.message + error.message = userMessage + } + dispatch(paymentFailed({ paymentId, error })) } } diff --git a/renderer/reducers/payment/utils.js b/renderer/reducers/payment/utils.js index e68fe15e6ad..3d646a1b7de 100644 --- a/renderer/reducers/payment/utils.js +++ b/renderer/reducers/payment/utils.js @@ -198,3 +198,15 @@ export const getDisplayNodeName = payment => { const intl = getIntl() return intl.formatMessage({ ...messages.unknown }) } + +/** + * errorCodeToMessage - Convert an error code to an error message. + * + * @param {string} code Error code + * @returns {string|null} error message + */ +export const errorCodeToMessage = code => { + const intl = getIntl() + const msg = messages[code.toLowerCase()] + return msg ? intl.formatMessage({ ...msg }) : null +} diff --git a/renderer/reducers/transaction.js b/renderer/reducers/transaction.js index de9648c79fa..f3f322ea805 100644 --- a/renderer/reducers/transaction.js +++ b/renderer/reducers/transaction.js @@ -188,7 +188,8 @@ export const sendCoins = ({ await grpc.services.Lightning.sendCoins(payload) dispatch(transactionSuccessful({ ...payload, internalId })) } catch (e) { - dispatch(transactionFailed({ error: e.message, internalId })) + e.message = errorToUserFriendly(e.message) + dispatch(transactionFailed({ internalId, error: e })) } } @@ -229,7 +230,7 @@ export const transactionFailed = ({ internalId, error }) => async (dispatch, get await delay(2000 - (Date.now() - timestamp * 1000)) // Mark the payment as failed. - dispatch({ type: TRANSACTION_FAILED, internalId, error: errorToUserFriendly(error) }) + dispatch({ type: TRANSACTION_FAILED, internalId, error }) } /** diff --git a/services/grpc/router.methods.js b/services/grpc/router.methods.js index 31b81ace1eb..452c4404773 100644 --- a/services/grpc/router.methods.js +++ b/services/grpc/router.methods.js @@ -26,6 +26,11 @@ const defaultPaymentOptions = { allowSelfPayment: true, } +const defaultPaymentOptionsV2 = { + ...defaultPaymentOptions, + maxParts: PAYMENT_MAX_PARTS, +} + // ------------------------------------ // Overrides // ------------------------------------ @@ -190,7 +195,7 @@ async function sendPayment(options = {}) { * @returns {Promise} Original payload augmented with lnd sendPaymentV2 response data */ async function sendPaymentV2(options = {}) { - const payload = defaults(omitBy(options, isNil), defaultPaymentOptions) + const payload = defaults(omitBy(options, isNil), defaultPaymentOptionsV2) logGrpcCmd('Router.sendPaymentV2', payload) // Our response will always include the original payload. diff --git a/utils/userFriendlyErrors.js b/utils/userFriendlyErrors.js index f75da87d5ea..ea86808f7b0 100644 --- a/utils/userFriendlyErrors.js +++ b/utils/userFriendlyErrors.js @@ -2,6 +2,23 @@ const userFriendlyErrors = { /* eslint-disable max-len */ 'Error: 11 OUT_OF_RANGE: EOF': "The person you're trying to connect to isn't available or rejected the connection. Their public key may have changed or the server may no longer be responding.", + IN_FLIGHT: 'Payment is still in flight.', + SUCCEEDED: 'Payment completed successfully.', + FAILED_TIMEOUT: 'There are more routes to try, but the payment timeout was exceeded.', + FAILED_NO_ROUTE: + 'All possible routes were tried and failed permanently. Or were no routes to the destination at all.', + FAILED_ERROR: 'A non-recoverable error has occured.', + FAILED_INCORRECT_PAYMENT_DETAILS: + 'Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta).', + FAILED_INSUFFICIENT_BALANCE: 'Insufficient local balance.', + FAILURE_REASON_NONE: "Payment isn't failed (yet).", + FAILURE_REASON_TIMEOUT: 'There are more routes to try, but the payment timeout was exceeded.', + FAILURE_REASON_NO_ROUTE: + 'All possible routes were tried and failed permanently. Or were no routes to the destination at all.', + FAILURE_REASON_ERROR: ' A non-recoverable error has occured.', + FAILURE_REASON_INCORRECT_PAYMENT_DETAILS: + 'Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta).', + FAILURE_REASON_INSUFFICIENT_BALANCE: 'Insufficient local balance.', } /**