diff --git a/renderer/components/Activity/PaymentModal/Htlc.js b/renderer/components/Activity/PaymentModal/Htlc.js new file mode 100644 index 00000000000..f9dadb54a6f --- /dev/null +++ b/renderer/components/Activity/PaymentModal/Htlc.js @@ -0,0 +1,31 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Flex } from 'rebass/styled-components' +import { CoinBig } from '@zap/utils/coin' +import { Text } from 'components/UI' +import { CryptoSelector, CryptoValue } from 'containers/UI' +import HtlcHops from './HtlcHops' + +const Htlc = ({ route, isAmountVisible = true, ...rest }) => { + const amountExcludingFees = CoinBig(route.totalAmt) + .minus(route.totalFees) + .toString() + return ( + + {isAmountVisible && ( + + + + + )} + + + ) +} + +Htlc.propTypes = { + isAmountVisible: PropTypes.bool, + route: PropTypes.object.isRequired, +} + +export default Htlc diff --git a/renderer/components/Activity/PaymentModal/HtlcHops.js b/renderer/components/Activity/PaymentModal/HtlcHops.js new file mode 100644 index 00000000000..1b68895931c --- /dev/null +++ b/renderer/components/Activity/PaymentModal/HtlcHops.js @@ -0,0 +1,50 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Flex } from 'rebass/styled-components' +import { useIntl } from 'react-intl' +import { CoinBig } from '@zap/utils/coin' +import { getDisplayNodeName } from 'reducers/payment/utils' +import { Truncate } from 'components/Util' +import ArrowRight from 'components/Icon/ArrowRight' +import messages from './messages' + +const HtlcHops = ({ hops, ...rest }) => { + const { formatMessage, formatNumber } = useIntl() + return ( + + {hops.map((hop, index) => { + const displayName = getDisplayNodeName(hop) + const hasFee = CoinBig(hop.feeMsat).gt(0) + const isLast = index === hops.length - 1 + const multiHop = hops.length > 1 + return ( + + {multiHop && ( + + + + )} + + + ) + })} + + ) +} + +HtlcHops.propTypes = { + hops: PropTypes.array.isRequired, +} + +export default HtlcHops diff --git a/renderer/components/Activity/PaymentModal/Route.js b/renderer/components/Activity/PaymentModal/Route.js index 902c6387720..202af8d046e 100644 --- a/renderer/components/Activity/PaymentModal/Route.js +++ b/renderer/components/Activity/PaymentModal/Route.js @@ -1,70 +1,8 @@ import React from 'react' import PropTypes from 'prop-types' -import { Box, Flex } from 'rebass/styled-components' -import { useIntl } from 'react-intl' -import { CoinBig } from '@zap/utils/coin' -import { getDisplayNodeName } from 'reducers/payment/utils' -import { Truncate } from 'components/Util' -import { CryptoSelector, CryptoValue } from 'containers/UI' -import { Bar, Text } from 'components/UI' -import ArrowRight from 'components/Icon/ArrowRight' -import messages from './messages' - -const HtlcHops = ({ hops, ...rest }) => { - const { formatMessage, formatNumber } = useIntl() - return ( - - {hops.map(hop => { - const displayName = getDisplayNodeName(hop) - const hasFee = CoinBig(hop.feeMsat).gt(0) - return ( - - - - - - - ) - })} - - ) -} - -HtlcHops.propTypes = { - hops: PropTypes.array.isRequired, -} - -const Htlc = ({ htlc, isAmountVisible = true, ...rest }) => { - const amountExcludingFees = CoinBig(htlc.route.totalAmt) - .minus(htlc.route.totalFees) - .toString() - return ( - - {isAmountVisible && ( - - - - - )} - - - ) -} - -Htlc.propTypes = { - htlc: PropTypes.object.isRequired, - isAmountVisible: PropTypes.bool.isRequired, -} +import { Box } from 'rebass/styled-components' +import { Bar } from 'components/UI' +import Htlc from './Htlc' const Route = ({ htlcs, ...rest }) => { return ( @@ -75,7 +13,7 @@ const Route = ({ htlcs, ...rest }) => { return ( {!isFirst && } - + ) })} diff --git a/renderer/components/Activity/PaymentModal/index.js b/renderer/components/Activity/PaymentModal/index.js index 9360e9e78ee..455136dbce0 100644 --- a/renderer/components/Activity/PaymentModal/index.js +++ b/renderer/components/Activity/PaymentModal/index.js @@ -1 +1,4 @@ export PaymentModal from './PaymentModal' +export Route from './Route' +export Htlc from './Htlc' +export HtlcHops from './HtlcHops' diff --git a/renderer/components/Pay/PaySummary.js b/renderer/components/Pay/PaySummary.js index 4aa0c45f3b8..2d442574341 100644 --- a/renderer/components/Pay/PaySummary.js +++ b/renderer/components/Pay/PaySummary.js @@ -38,6 +38,7 @@ const PaySummary = props => { minFee={getMinFee(routes)} mt={-3} payReq={payReq} + route={routes[0]} /> ) } diff --git a/renderer/components/Pay/PaySummaryLightning.js b/renderer/components/Pay/PaySummaryLightning.js index 572af1d62bd..bb20bdd4b2e 100644 --- a/renderer/components/Pay/PaySummaryLightning.js +++ b/renderer/components/Pay/PaySummaryLightning.js @@ -9,6 +9,7 @@ import BigArrowRight from 'components/Icon/BigArrowRight' import { Bar, DataRow, Link, Spinner, Text, Tooltip } from 'components/UI' import { CryptoSelector, CryptoValue, FiatValue } from 'containers/UI' import { Truncate } from 'components/Util' +import { HtlcHops } from 'components/Activity/PaymentModal' import messages from './messages' const ConfigLink = ({ feeLimit, openModal, ...rest }) => ( @@ -34,6 +35,7 @@ class PaySummaryLightning extends React.Component { nodes: PropTypes.array, openModal: PropTypes.func.isRequired, payReq: PropTypes.string.isRequired, + route: PropTypes.object, } static defaultProps = { @@ -110,6 +112,7 @@ class PaySummaryLightning extends React.Component { minFee, nodes, payReq, + route, ...rest } = this.props @@ -157,9 +160,13 @@ class PaySummaryLightning extends React.Component { - - - + {route ? ( + + ) : ( + + + + )} diff --git a/renderer/containers/App/App.js b/renderer/containers/App/App.js index aaf9d7445e8..7e2401d48a0 100644 --- a/renderer/containers/App/App.js +++ b/renderer/containers/App/App.js @@ -15,6 +15,7 @@ import { } from 'reducers/lnurl' import { initBackupService } from 'reducers/backup' import { infoSelectors } from 'reducers/info' +import { paySelectors } from 'reducers/pay' import { setModals, modalSelectors } from 'reducers/modal' import { fetchSuggestedNodes } from 'reducers/channels' import { initTickers } from 'reducers/ticker' @@ -26,7 +27,7 @@ const mapStateToProps = state => ({ activeWalletSettings: walletSelectors.activeWalletSettings(state), isAppReady: appSelectors.isAppReady(state), isSyncedToGraph: infoSelectors.isSyncedToGraph(), - redirectPayReq: state.pay.redirectPayReq, + redirectPayReq: paySelectors.redirectPayReq(state), modals: modalSelectors.getModalState(state), lnurlWithdrawParams: lnurlSelectors.lnurlWithdrawParams(state), willShowLnurlAuthPrompt: lnurlSelectors.willShowLnurlAuthPrompt(state), diff --git a/renderer/containers/Channels/ChannelCreateForm.js b/renderer/containers/Channels/ChannelCreateForm.js index f534cc38c88..5c4a9a616f8 100644 --- a/renderer/containers/Channels/ChannelCreateForm.js +++ b/renderer/containers/Channels/ChannelCreateForm.js @@ -2,7 +2,7 @@ import { connect } from 'react-redux' import ChannelCreateForm from 'components/Channels/ChannelCreateForm' import { fetchTickers, tickerSelectors } from 'reducers/ticker' import { openChannel } from 'reducers/channels' -import { queryFees } from 'reducers/pay' +import { queryFees, paySelectors } from 'reducers/pay' import { balanceSelectors } from 'reducers/balance' import { updateContactFormSearchQuery, contactFormSelectors } from 'reducers/contactsform' import { walletSelectors } from 'reducers/wallet' @@ -15,8 +15,8 @@ const mapStateToProps = state => ({ cryptoUnit: tickerSelectors.cryptoUnit(state), walletBalance: balanceSelectors.walletBalanceConfirmed(state), cryptoUnitName: tickerSelectors.cryptoUnitName(state), - isQueryingFees: state.pay.isQueryingFees, - onchainFees: state.pay.onchainFees, + isQueryingFees: paySelectors.isQueryingFees(state), + onchainFees: paySelectors.onchainFees(state), lndTargetConfirmations: settingsSelectors.currentConfig(state).lndTargetConfirmations, }) diff --git a/renderer/containers/Pay.js b/renderer/containers/Pay.js index d808cb6045d..a525cc3e76a 100644 --- a/renderer/containers/Pay.js +++ b/renderer/containers/Pay.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux' import { Pay } from 'components/Pay' import { fetchTickers, tickerSelectors } from 'reducers/ticker' -import { setRedirectPayReq, queryFees, queryRoutes } from 'reducers/pay' +import { setRedirectPayReq, queryFees, queryRoutes, paySelectors } from 'reducers/pay' import { balanceSelectors } from 'reducers/balance' import { addFilter } from 'reducers/activity' import { channelsSelectors } from 'reducers/channels' @@ -18,11 +18,11 @@ const mapStateToProps = state => ({ channelBalance: balanceSelectors.channelBalance(state), cryptoUnit: tickerSelectors.cryptoUnit(state), cryptoUnitName: tickerSelectors.cryptoUnitName(state), - isQueryingFees: state.pay.isQueryingFees, + isQueryingFees: paySelectors.isQueryingFees(state), lndTargetConfirmations: settingsSelectors.currentConfig(state).lndTargetConfirmations, - redirectPayReq: state.pay.redirectPayReq, - onchainFees: state.pay.onchainFees, - routes: state.pay.routes, + redirectPayReq: paySelectors.redirectPayReq(state), + onchainFees: paySelectors.onchainFees(state), + routes: paySelectors.routes(state), maxOneTimeSend: channelsSelectors.maxOneTimeSend(state), walletBalanceConfirmed: balanceSelectors.walletBalanceConfirmed(state), }) diff --git a/renderer/containers/Pay/PaySummaryLightning.js b/renderer/containers/Pay/PaySummaryLightning.js index 2b72e5cca7d..332b1dd7ebe 100644 --- a/renderer/containers/Pay/PaySummaryLightning.js +++ b/renderer/containers/Pay/PaySummaryLightning.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux' import PaySummaryLightning from 'components/Pay/PaySummaryLightning' +import { paySelectors } from 'reducers/pay' import { settingsSelectors } from 'reducers/settings' import { tickerSelectors } from 'reducers/ticker' import { networkSelectors } from 'reducers/network' @@ -7,7 +8,7 @@ import { openModal } from 'reducers/modal' const mapStateToProps = state => ({ cryptoUnitName: tickerSelectors.cryptoUnitName(state), - isQueryingRoutes: state.pay.isQueryingRoutes, + isQueryingRoutes: paySelectors.isQueryingRoutes(state), nodes: networkSelectors.nodes(state), feeLimit: settingsSelectors.currentConfig(state).payments.feeLimit, }) diff --git a/renderer/containers/Pay/PaySummaryOnChain.js b/renderer/containers/Pay/PaySummaryOnChain.js index 2afb1e1575e..2e82aad86cc 100644 --- a/renderer/containers/Pay/PaySummaryOnChain.js +++ b/renderer/containers/Pay/PaySummaryOnChain.js @@ -1,14 +1,14 @@ import { connect } from 'react-redux' import PaySummaryOnChain from 'components/Pay/PaySummaryOnChain' import { tickerSelectors } from 'reducers/ticker' -import { queryFees } from 'reducers/pay' +import { queryFees, paySelectors } from 'reducers/pay' import { networkSelectors } from 'reducers/network' const mapStateToProps = state => ({ cryptoUnitName: tickerSelectors.cryptoUnitName(state), - isQueryingFees: state.pay.isQueryingFees, + isQueryingFees: paySelectors.isQueryingFees(state), nodes: networkSelectors.nodes(state), - onchainFees: state.pay.onchainFees, + onchainFees: paySelectors.onchainFees(state), }) const mapDispatchToProps = { diff --git a/renderer/reducers/index.js b/renderer/reducers/index.js index b233d800fa7..c2a9968bdad 100644 --- a/renderer/reducers/index.js +++ b/renderer/reducers/index.js @@ -44,6 +44,7 @@ import lnurl from './lnurl' * @property {import('./invoice').State} invoice Invoice reducer. * @property {import('./lnurl').State} lnurl Lnurl reducer. * @property {import('./network').State} network Network reducer. + * @property {import('./pay').State} pay Pay reducer. * @property {import('./payment').State} payment Payment reducer. * @property {import('./settings').State} settings Settings reducer. * @property {import('./transaction').State} transaction Transaction reducer. diff --git a/renderer/reducers/pay/index.js b/renderer/reducers/pay/index.js index 59f24a6d882..2770a77db3c 100644 --- a/renderer/reducers/pay/index.js +++ b/renderer/reducers/pay/index.js @@ -1,6 +1,7 @@ import payReducer from './reducer' export default payReducer +export paySelectors from './selectors' export * from './constants' export * from './reducer' export * from './ipc' diff --git a/renderer/reducers/pay/reducer.js b/renderer/reducers/pay/reducer.js index 8f3652d7410..053a153527d 100644 --- a/renderer/reducers/pay/reducer.js +++ b/renderer/reducers/pay/reducer.js @@ -22,10 +22,22 @@ const { SET_REDIRECT_PAY_REQ, } = constants +/** + * @typedef State + * @property {boolean} isQueryingRoutes Boolean indicating if routes are being probed + * @property {boolean} isQueryingFees Boolean indicating if fees are being queried + * @property {{fast:string|null, medium:string|null, slow:string|null}} onchainFees Onchain fee rates + * @property {string|null} queryFeesError Query fees error message + * @property {string|null} queryRoutesError Query routes error message + * @property {string|null} redirectPayReq Payrequest injected from external source + * @property {object[]} routes Routes from last probe attempt + */ + // ------------------------------------ // Initial State // ------------------------------------ +/** @type {State} */ const initialState = { isQueryingRoutes: false, isQueryingFees: false, diff --git a/renderer/reducers/pay/selectors.js b/renderer/reducers/pay/selectors.js new file mode 100644 index 00000000000..e17407008d0 --- /dev/null +++ b/renderer/reducers/pay/selectors.js @@ -0,0 +1,60 @@ +import { createSelector } from 'reselect' +import { networkSelectors } from 'reducers/network' +import { decorateRoute } from 'reducers/payment/utils' + +/** + * @typedef {import('../index').State} State + */ + +const routesSelector = state => state.pay.routes +const nodesSelector = state => networkSelectors.nodes(state) + +/** + * routes - Routes relating to current payment probe. + * + * @param {State} state Redux state + * @returns {object} Config overrides + */ +export const routes = createSelector(routesSelector, nodesSelector, (routes, nodes) => + routes.map(route => decorateRoute(route, nodes)) +) + +/** + * isQueryingFees - Is querying fees. + * + * @param {State} state Redux state + * @returns {boolean} Boolean indicating if fees are being queried + */ +const isQueryingFees = state => state.pay.isQueryingFees + +/** + * isQueryingRoutes - Is querying routes. + * + * @param {State} state Redux state + * @returns {boolean} Boolean indicating if routes are being probed + */ +const isQueryingRoutes = state => state.pay.isQueryingRoutes + +/** + * onchainFees - Onchain fee rates. + * + * @param {State} state Redux state + * @returns {{fast:string|null, medium:string|null, slow:string|null}} Onchain fee rates + */ +const onchainFees = state => state.pay.onchainFees + +/** + * redirectPayReq - Payrequest injected from external source. + * + * @param {State} state Redux state + * @returns {string|null} Payrequest injected from external source + */ +const redirectPayReq = state => state.pay.redirectPayReq + +export default { + isQueryingFees, + isQueryingRoutes, + onchainFees, + redirectPayReq, + routes, +} diff --git a/renderer/reducers/payment/utils.js b/renderer/reducers/payment/utils.js index 8f90c210c23..dc32da3f80e 100644 --- a/renderer/reducers/payment/utils.js +++ b/renderer/reducers/payment/utils.js @@ -80,15 +80,7 @@ export const decoratePayment = (payment, nodes = []) => { // Try to add some info about the nodes involved in payment htlcs. if (payment.htlcs) { - const decoratedHtlcs = cloneDeep(payment.htlcs) - decoratedHtlcs.map(htlc => { - htlc.route.hops.map(hop => { - hop.alias = getNodeAlias(hop.pubKey, nodes) - return hop - }) - return htlc - }) - decoration.htlcs = decoratedHtlcs + decoration.htlcs = decorateHtlcs(payment.htlcs, nodes) } return { @@ -97,6 +89,38 @@ export const decoratePayment = (payment, nodes = []) => { } } +/** + * decorateHtlcs - Decorate htlcs list with custom/computed properties. + * + * @param {object[]} htlcs Htlcs + * @param {object[]} nodes Nodes + * @returns {object} Decorated htlcs + */ +export const decorateHtlcs = (htlcs, nodes = []) => { + const decoratedHtlcs = cloneDeep(htlcs) + decoratedHtlcs.map(htlc => { + htlc.route = decorateRoute(htlc.route, nodes) + return htlc + }) + return decoratedHtlcs +} + +/** + * decorateRoute - Decorate route object with custom/computed properties. + * + * @param {object} route Route + * @param {object[]} nodes Nodes + * @returns {object} Decorated route + */ +export const decorateRoute = (route, nodes = []) => { + const decoratedRoute = cloneDeep(route) + decoratedRoute.hops = decoratedRoute.hops.map(hop => { + hop.alias = getNodeAlias(hop.pubKey, nodes) + return hop + }) + return decoratedRoute +} + /** * prepareKeysendPayload - Prepare a keysend payment. *