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

Commit

Permalink
fix(ui): map send payment error codes to messages
Browse files Browse the repository at this point in the history
  • Loading branch information
mrfelton committed May 17, 2020
1 parent 762c137 commit 1974463
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 27 deletions.
17 changes: 12 additions & 5 deletions renderer/components/Activity/ErrorDetailsDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ const ErrorDetailsDialog = ({ error, isOpen, onCopy, onClose, position, ...rest
return null
}

const { details, header } = error
const { details: { message, code } = {}, header } = error
const headerEl = (
<Heading.h2 mb={4}>
{header || <FormattedMessage {...messages.error_dialog_header} />}
</Heading.h2>
)

const handleCopy = () => {
copy(details)
copy([code, message].join(': '))
onCopy && onCopy()
}

Expand All @@ -34,9 +34,16 @@ const ErrorDetailsDialog = ({ error, isOpen, onCopy, onClose, position, ...rest
}
onClose={onClose}
>
<Text color="gray" px={4}>
{details}
</Text>
{code && (
<Text color="lightGray" pb={3} px={4}>
{code}
</Text>
)}
{message && (
<Text color="gray" px={4}>
{message}
</Text>
)}
</Dialog>
</DialogOverlay>
)
Expand Down
19 changes: 19 additions & 0 deletions renderer/reducers/payment/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
})
48 changes: 29 additions & 19 deletions renderer/reducers/payment/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -132,30 +131,39 @@ 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
const RETRIABLE_ERRORS = [
'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.
]

// If we found a related entery in paymentsSending, gracefully remove it and handle as error case.
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,
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -256,17 +263,16 @@ 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
}
}
}

// Otherwise, just use sendPayment.
else {
data = await routerSendPayment(payload)
await routerSendPayment(payload)
}
}

Expand All @@ -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 }))
}
}

Expand Down
12 changes: 12 additions & 0 deletions renderer/reducers/payment/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
5 changes: 3 additions & 2 deletions renderer/reducers/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }))
}
}

Expand Down Expand Up @@ -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 })
}

/**
Expand Down
7 changes: 6 additions & 1 deletion services/grpc/router.methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ const defaultPaymentOptions = {
allowSelfPayment: true,
}

const defaultPaymentOptionsV2 = {
...defaultPaymentOptions,
maxParts: PAYMENT_MAX_PARTS,
}

// ------------------------------------
// Overrides
// ------------------------------------
Expand Down Expand Up @@ -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.
Expand Down
17 changes: 17 additions & 0 deletions utils/userFriendlyErrors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
}

/**
Expand Down

0 comments on commit 1974463

Please sign in to comment.