diff --git a/app/components/Form/Form.js b/app/components/Form/Form.js index c90daf62..6378b072 100644 --- a/app/components/Form/Form.js +++ b/app/components/Form/Form.js @@ -1,34 +1,38 @@ import React from 'react' import PropTypes from 'prop-types' - import X from 'components/Icon/X' - -import Pay from './Pay' +import { Modal } from 'components/UI' +import Pay from 'containers/Pay' import Request from './Request' - import styles from './Form.scss' -const FORM_TYPES = { - PAY_FORM: Pay, - REQUEST_FORM: Request -} - const Form = ({ formType, formProps, closeForm }) => { if (!formType) { return null } - const FormComponent = FORM_TYPES[formType] - return ( -
-
- - - -
- -
- ) + switch (formType) { + case 'PAY_FORM': + return ( +
+ + + +
+ ) + + case 'REQUEST_FORM': + return ( +
+
+ + + +
+ +
+ ) + } } Form.propTypes = { diff --git a/app/components/Pay/Pay.js b/app/components/Pay/Pay.js index 90f6e87c..d83e24d3 100644 --- a/app/components/Pay/Pay.js +++ b/app/components/Pay/Pay.js @@ -30,7 +30,7 @@ const ShowHidePayReq = Keyframes.Spring({ small: { height: 48 }, big: async (next, cancel, ownProps) => { ownProps.context.focusPayReqInput() - await next({ height: 130 }) + await next({ height: 130, immediate: true }) } }) @@ -124,17 +124,20 @@ class Pay extends React.Component { } static defaultProps = { + initialPayReq: null, initialAmountCrypto: null, initialAmountFiat: null, isProcessing: false, isQueryingFees: false, isQueryingRoutes: false, nodes: [], + onchainFees: {}, routes: [] } state = { currentStep: 'address', + previousStep: null, isLn: null, isOnchain: null } @@ -142,21 +145,18 @@ class Pay extends React.Component { amountInput = React.createRef() payReqInput = React.createRef() - /** - * If we have an address when the component mounts, run the payReq change handler to compure isLn / isOnchain. - */ - componentDidMount() { - const { formApi } = this - if (formApi.getValue('payReq')) { - this.handlePayReqOnChange() + componentDidUpdate(prevProps, prevState) { + const { initialPayReq, queryRoutes } = this.props + const { currentStep, invoice, isLn, isOnchain } = this.state + + // If initialPayReq has been set, reset the form and submit as new + if (initialPayReq && initialPayReq !== prevProps.initialPayReq) { + this.formApi.reset() + this.formApi.setValue('payReq', initialPayReq) + this.handlePayReqChange() } - } - /** - * If we have gone back to the address step, focus the address input and unmark all fields from being touched. - */ - componentDidUpdate(prevProps, prevState) { - const { currentStep } = this.state + // If we have gone back to the address step, unmark all fields from being touched. if (currentStep !== prevState.currentStep) { if (currentStep === 'address') { Object.keys(this.formApi.getState().touched).forEach(field => { @@ -164,6 +164,21 @@ class Pay extends React.Component { }) } } + + // If we now have a valid onchain address, trigger the form submit. + if (isOnchain && isOnchain !== prevState.isOnchain) { + this.formApi.submitForm() + } + + // If we now have a valid offchain address, trigger the form submit. + if (isLn && isLn !== prevState.isLn) { + this.formApi.submitForm() + // And if now have a valid lightning invoice, call queryRoutes. + if (invoice) { + const { satoshis, payeeNodeKey } = invoice + queryRoutes(payeeNodeKey, satoshis) + } + } } /** @@ -172,15 +187,17 @@ class Pay extends React.Component { */ onSubmit = values => { const { currentStep, isOnchain } = this.state - const { cryptoCurrency, payInvoice, sendCoins } = this.props + const { cryptoCurrency, onchainFees, payInvoice, routes, sendCoins } = this.props + const feeLimit = getMaxFee(routes) if (currentStep === 'summary') { return isOnchain ? sendCoins({ value: values.amountCrypto, addr: values.payReq, - currency: cryptoCurrency + currency: cryptoCurrency, + satPerByte: onchainFees.fastestFee }) - : payInvoice(values.payReq) + : payInvoice(values.payReq, feeLimit) } else { this.nextStep() } @@ -193,15 +210,6 @@ class Pay extends React.Component { this.formApi = formApi } - /** - * set the amountFiat field. - */ - setAmountFiat = () => { - if (this.amountInput.current) { - this.amountInput.current.focus() - } - } - /** * Focus the payReq input. */ @@ -241,7 +249,7 @@ class Pay extends React.Component { const { currentStep } = this.state const nextStep = Math.max(this.steps().indexOf(currentStep) - 1, 0) if (currentStep !== nextStep) { - this.setState({ currentStep: this.steps()[nextStep] }) + this.setState({ currentStep: this.steps()[nextStep], previousStep: currentStep }) } } @@ -252,15 +260,15 @@ class Pay extends React.Component { const { currentStep } = this.state const nextStep = Math.min(this.steps().indexOf(currentStep) + 1, this.steps().length - 1) if (currentStep !== nextStep) { - this.setState({ currentStep: this.steps()[nextStep] }) + this.setState({ currentStep: this.steps()[nextStep], previousStep: currentStep }) } } /** * Set isLn/isOnchain state based on payReq value. */ - handlePayReqOnChange = () => { - const { chain, network, queryRoutes } = this.props + handlePayReqChange = () => { + const { chain, network } = this.props const payReq = this.formApi.getValue('payReq') const state = { isLn: null, @@ -278,8 +286,6 @@ class Pay extends React.Component { return } state.isLn = true - const { satoshis, payeeNodeKey } = invoice - queryRoutes(payeeNodeKey, satoshis) } // Otherwise, see if we have a valid onchain address. @@ -289,10 +295,19 @@ class Pay extends React.Component { // Update the state with our findings. this.setState(state) + } - // As soon as we have detected a valid address, submit the form. - if (state.isLn || state.isOnchain) { - this.formApi.submitForm() + /** + * Handle the case when the form is mountedwith an initialPayReq. + * This is the earliest possibleplace we can do this because the form is not initialised in ComponentDidMount. + */ + handleChange = formState => { + const { initialPayReq } = this.props + const { currentStep, previousStep } = this.state + // If this is the first time the address page is showing and we have an initialPayReq, process the request + // as if the user had entered it themselves. + if (currentStep === 'address' && !previousStep && initialPayReq && formState.values.payReq) { + this.handlePayReqChange() } } @@ -333,7 +348,14 @@ class Pay extends React.Component { } renderHelpText = () => { - const { currentStep } = this.state + const { initialPayReq } = this.props + const { currentStep, previousStep } = this.state + + // Do not render the help text if the form has just loadad with an initial payment request. + if (initialPayReq && !previousStep) { + return null + } + return ( ( - + @@ -380,15 +402,16 @@ class Pay extends React.Component { {styles => ( { - const { currentStep } = this.state + const { currentStep, isOnchain } = this.state const { cryptoCurrency, cryptoCurrencies, @@ -415,6 +438,12 @@ class Pay extends React.Component { initialAmountCrypto, initialAmountFiat } = this.props + + // Do not render unless we are working with an onchain address. + if (!isOnchain) { + return null + } + return ( { - const { isOnchain } = this.state + const { currentStep, isOnchain } = this.state const { cryptoCurrency, cryptoCurrencyTicker, @@ -498,6 +529,7 @@ class Pay extends React.Component { routes, setCryptoCurrency } = this.props + const formState = this.formApi.getState() let minFee, maxFee if (routes.length) { @@ -505,41 +537,56 @@ class Pay extends React.Component { maxFee = getMaxFee(routes) } - // convert entered amount to satoshis - if (isOnchain) { - const amountInSatoshis = convert(cryptoCurrency, 'sats', formState.values.amountCrypto) - return ( - - ) - } else if (isLn) { - return ( - - ) + const render = () => { + // convert entered amount to satoshis + if (isOnchain) { + const amountInSatoshis = convert(cryptoCurrency, 'sats', formState.values.amountCrypto) + return ( + + ) + } else if (isLn) { + return ( + + ) + } } + + return ( + + {show => show && (styles => {render()})} + + ) } /** @@ -582,6 +629,7 @@ class Pay extends React.Component { css={{ height: '100%' }} {...rest} getApi={this.setFormApi} + onChange={this.handleChange} onSubmit={this.onSubmit} > {({ formState }) => { @@ -632,10 +680,16 @@ class Pay extends React.Component { - {this.renderHelpText()} - {this.renderAddressField()} - {isOnchain && this.renderAmountFields()} - {currentStep === 'summary' && this.renderSummary()} + + {this.renderHelpText()} + + {this.renderAddressField()} + {this.renderAmountFields()} + + + {this.renderSummary()} + + diff --git a/app/components/Pay/PaySummaryLightning.js b/app/components/Pay/PaySummaryLightning.js index 77556042..d39dbd9a 100644 --- a/app/components/Pay/PaySummaryLightning.js +++ b/app/components/Pay/PaySummaryLightning.js @@ -78,6 +78,23 @@ class PaySummaryLightning extends React.PureComponent { const fiatAmount = satoshisToFiat(satoshis, currentTicker[fiatCurrency].last) const nodeAlias = getNodeAlias(payeeNodeKey, nodes) + // Select an appropriate fee message... + // Default to unknown. + let feeMessage = messages.fee_unknown + + // If thex max fee is 0 or 1 then show a message like "less than 1". + if (maxFee === 0 || maxFee === 1) { + feeMessage = messages.fee_less_than_1 + } + // Otherwise, if we have both a min and max fee that are different, present the fee range. + else if (minFee !== null && maxFee !== null && minFee !== maxFee) { + feeMessage = messages.fee_range + } + // Finally, if we at least have a max fee then present it as upto that amount. + else if (maxFee) { + feeMessage = messages.fee_upto + } + return ( @@ -108,7 +125,7 @@ class PaySummaryLightning extends React.PureComponent { - {} + {} @@ -127,10 +144,8 @@ class PaySummaryLightning extends React.PureComponent { - ) : minFee === null || maxFee === null ? ( - ) : ( - + feeMessage && ) } /> @@ -141,13 +156,7 @@ class PaySummaryLightning extends React.PureComponent { left={} right={ - {cryptoCurrencyTicker} - {!isQueryingRoutes && - maxFee && ( - - (+ {maxFee} msats) - - )} + {cryptoCurrencyTicker} } /> diff --git a/app/components/Pay/PaySummaryOnChain.js b/app/components/Pay/PaySummaryOnChain.js index 3b87fba8..0de81c71 100644 --- a/app/components/Pay/PaySummaryOnChain.js +++ b/app/components/Pay/PaySummaryOnChain.js @@ -51,7 +51,7 @@ class PaySummaryOnChain extends React.Component { onchainFees: {} } - componenDidMount() { + componentDidMount() { const { queryFees } = this.props queryFees() } @@ -122,11 +122,11 @@ class PaySummaryOnChain extends React.Component { ) : !fee ? ( - + ) : ( - {fee} satoshis + {fee} satoshis () @@ -143,7 +143,7 @@ class PaySummaryOnChain extends React.Component { right={ {cryptoCurrencyTicker} - {!isQueryingFees && fee && (+ {fee} satoshis per byte} + {!isQueryingFees && fee && (+ {fee} satoshis per byte)} } /> diff --git a/app/components/Pay/messages.js b/app/components/Pay/messages.js index 173a8134..25458593 100644 --- a/app/components/Pay/messages.js +++ b/app/components/Pay/messages.js @@ -15,11 +15,12 @@ export default defineMessages({ back: 'Back', send: 'Send', fee: 'Fee', - fee_range: 'between {minFee} and {maxFee} msat', - unknown: 'unknown', + fee_less_than_1: 'less than 1 satoshi', + fee_range: 'between {minFee} and {maxFee} satoshis', + fee_upto: 'up to {maxFee} satoshi', + fee_unknown: 'unknown', + fee_per_byte: 'per byte', amount: 'Amount', - per_byte: 'per byte', - upto: 'up to', total: 'Total', memo: 'Memo', description: diff --git a/app/components/UI/Input.js b/app/components/UI/Input.js index 8321e282..b4243a9e 100644 --- a/app/components/UI/Input.js +++ b/app/components/UI/Input.js @@ -49,7 +49,6 @@ class Input extends React.Component { onChange, onBlur, onFocus, - initialValue, forwardedRef, theme, fieldApi, @@ -90,7 +89,7 @@ class Input extends React.Component { )} {...rest} ref={this.inputRef} - value={!value && value !== 0 ? '' : initialValue || value} + value={!value && value !== 0 ? '' : value} onChange={e => { setValue(e.target.value) if (onChange) { diff --git a/app/components/UI/LightningInvoiceInput.js b/app/components/UI/LightningInvoiceInput.js index 3ef8fe30..6cd22ed1 100644 --- a/app/components/UI/LightningInvoiceInput.js +++ b/app/components/UI/LightningInvoiceInput.js @@ -56,6 +56,7 @@ class LightningInvoiceInput extends React.Component { placeholder={intl.formatMessage({ ...messages.payreq_placeholder })} rows={5} {...this.props} + spellCheck="false" validate={this.validate} /> ) diff --git a/app/components/UI/Modal.js b/app/components/UI/Modal.js index 58f65e67..d0dd6d29 100644 --- a/app/components/UI/Modal.js +++ b/app/components/UI/Modal.js @@ -36,12 +36,12 @@ class Modal extends React.Component { diff --git a/app/components/UI/Range.js b/app/components/UI/Range.js index 185bc4ae..7f1055dc 100644 --- a/app/components/UI/Range.js +++ b/app/components/UI/Range.js @@ -28,7 +28,7 @@ const Input = styled.input` const Range = asField(({ fieldState, fieldApi, ...props }) => { const { value } = fieldState const { setValue, setTouched } = fieldApi - const { onChange, onBlur, initialValue, forwardedRef, ...rest } = props + const { onChange, onBlur, forwardedRef, ...rest } = props return ( { {...rest} type="range" ref={forwardedRef} - value={value || initialValue || '0'} + value={value || 0} onChange={e => { setValue(e.target.value) if (onChange) { diff --git a/app/components/UI/TextArea.js b/app/components/UI/TextArea.js index 0675026d..8bafeb06 100644 --- a/app/components/UI/TextArea.js +++ b/app/components/UI/TextArea.js @@ -50,7 +50,6 @@ class TextArea extends React.PureComponent { onChange, onBlur, onFocus, - initialValue, forwardedRef, theme, fieldApi, @@ -92,7 +91,7 @@ class TextArea extends React.PureComponent { )} {...rest} ref={this.inputRef} - value={!value && value !== 0 ? '' : initialValue || value} + value={!value && value !== 0 ? '' : value} onChange={e => { setValue(e.target.value) if (onChange) { diff --git a/app/components/Value/Value.js b/app/components/Value/Value.js index 15121b53..8398dd98 100644 --- a/app/components/Value/Value.js +++ b/app/components/Value/Value.js @@ -10,7 +10,13 @@ const Value = ({ value, currency, currentTicker, fiatTicker }) => { if (currency === 'fiat') { price = currentTicker[fiatTicker].last } - return {Number(convert('sats', currency, value, price))} + return ( + + {Number(convert('sats', currency, value, price)) + .toFixed(8) + .replace(/\.?0+$/, '')} + + ) } Value.propTypes = { diff --git a/app/containers/Pay.js b/app/containers/Pay.js new file mode 100644 index 00000000..b75f4de8 --- /dev/null +++ b/app/containers/Pay.js @@ -0,0 +1,41 @@ +import { connect } from 'react-redux' +import { Pay } from 'components/Pay' +import { tickerSelectors, setCurrency, setFiatTicker } from 'reducers/ticker' +import { queryFees, queryRoutes } from 'reducers/pay' +import { infoSelectors } from 'reducers/info' +import { sendCoins } from 'reducers/transaction' +import { payInvoice } from 'reducers/payment' + +const mapStateToProps = state => ({ + chain: state.info.chain, + network: infoSelectors.testnetSelector(state) ? 'testnet' : 'mainnet', + cryptoName: tickerSelectors.cryptoName(state), + channelBalance: state.balance.channelBalance, + currentTicker: tickerSelectors.currentTicker(state), + cryptoCurrency: state.ticker.currency, + cryptoCurrencyTicker: tickerSelectors.currencyName(state), + cryptoCurrencies: state.ticker.currencyFilters, + fiatCurrencies: state.ticker.fiatTickers, + fiatCurrency: state.ticker.fiatTicker, + initialPayReq: state.pay.payReq, + isQueryingFees: state.pay.isQueryingFees, + isQueryingRoutes: state.pay.isQueryingRoutes, + nodes: state.network.nodes, + onchainFees: state.pay.onchainFees, + routes: state.pay.routes, + walletBalance: state.balance.walletBalance +}) + +const mapDispatchToProps = { + payInvoice, + setCryptoCurrency: setCurrency, + setFiatCurrency: setFiatTicker, + sendCoins, + queryFees, + queryRoutes +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Pay) diff --git a/app/lib/lnd/methods/index.js b/app/lib/lnd/methods/index.js index 74be7be9..5bff9d51 100644 --- a/app/lib/lnd/methods/index.js +++ b/app/lib/lnd/methods/index.js @@ -40,8 +40,11 @@ export default function(lnd, log, event, msg, data) { // Data looks like { pubkey: String, amount: Number } networkController .queryRoutes(lnd, data) - .then(routes => event.sender.send('receiveQueryRoutes', routes)) - .catch(error => log.error('queryRoutes:', error)) + .then(routes => event.sender.send('queryRoutesSuccess', routes)) + .catch(error => { + log.error('queryRoutes:', error) + event.sender.send('queryRoutesFailure', { error: error.toString() }) + }) break case 'getInvoiceAndQueryRoutes': // Data looks like { pubkey: String, amount: Number } diff --git a/app/lib/lnd/methods/networkController.js b/app/lib/lnd/methods/networkController.js index f45f6112..51696c41 100644 --- a/app/lib/lnd/methods/networkController.js +++ b/app/lib/lnd/methods/networkController.js @@ -58,13 +58,12 @@ export function describeGraph(lnd) { * @param {[type]} amount [description] * @return {[type]} [description] */ -export function queryRoutes(lnd, { pubkey, amount }) { +export function queryRoutes(lnd, { pubkey, amount, numRoutes = 15 }) { return new Promise((resolve, reject) => { - lnd.queryRoutes({ pub_key: pubkey, amt: amount }, (err, data) => { + lnd.queryRoutes({ pub_key: pubkey, amt: amount, num_routes: numRoutes }, (err, data) => { if (err) { return reject(err) } - resolve(data) }) }) diff --git a/app/lib/lnd/methods/paymentsController.js b/app/lib/lnd/methods/paymentsController.js index f497eb04..52a78f63 100644 --- a/app/lib/lnd/methods/paymentsController.js +++ b/app/lib/lnd/methods/paymentsController.js @@ -4,16 +4,19 @@ * @param {[type]} paymentRequest [description] * @return {[type]} [description] */ -export function sendPaymentSync(lnd, { paymentRequest }) { +export function sendPaymentSync(lnd, { paymentRequest, feeLimit }) { return new Promise((resolve, reject) => { - lnd.sendPaymentSync({ payment_request: paymentRequest }, (error, data) => { - if (error) { - return reject(error) - } else if (!data || !data.payment_route) { - return reject(data.payment_error) + lnd.sendPaymentSync( + { payment_request: paymentRequest, fee_limit: { fixed: feeLimit } }, + (error, data) => { + if (error) { + return reject(error) + } else if (!data || !data.payment_route) { + return reject(data.payment_error) + } + resolve(data) } - resolve(data) - }) + ) }) } @@ -23,15 +26,18 @@ export function sendPaymentSync(lnd, { paymentRequest }) { * @param {[type]} paymentRequest [description] * @return {[type]} [description] */ -export function sendPayment(lnd, { paymentRequest }) { +export function sendPayment(lnd, { paymentRequest, feeLimit }) { return new Promise((resolve, reject) => { - lnd.sendPayment({ payment_request: paymentRequest }, (err, data) => { - if (err) { - return reject(err) - } + lnd.sendPayment( + { payment_request: paymentRequest, fee_limit: { fixed: feeLimit } }, + (err, data) => { + if (err) { + return reject(err) + } - resolve(data) - }) + resolve(data) + } + ) }) } diff --git a/app/lib/lnd/methods/walletController.js b/app/lib/lnd/methods/walletController.js index d9bc2fbe..5c69a426 100644 --- a/app/lib/lnd/methods/walletController.js +++ b/app/lib/lnd/methods/walletController.js @@ -74,9 +74,9 @@ export function getTransactions(lnd) { * @param {[type]} amount [description] * @return {[type]} [description] */ -export function sendCoins(lnd, { addr, amount }) { +export function sendCoins(lnd, { addr, amount, target_conf, sat_per_byte }) { return new Promise((resolve, reject) => { - lnd.sendCoins({ addr, amount }, (err, data) => { + lnd.sendCoins({ addr, amount, target_conf, sat_per_byte }, (err, data) => { if (err) { return reject(err) } diff --git a/app/lib/utils/api.js b/app/lib/utils/api.js index 00e33f74..ed121989 100644 --- a/app/lib/utils/api.js +++ b/app/lib/utils/api.js @@ -35,3 +35,11 @@ export function requestSuggestedNodes() { url: BASE_URL }).then(response => response.data) } + +export function requestFees() { + const BASE_URL = 'https://bitcoinfees.earn.com/api/v1/fees/recommended' + return axios({ + method: 'get', + url: BASE_URL + }).then(response => response.data) +} diff --git a/app/lib/utils/crypto.js b/app/lib/utils/crypto.js index 39b3a74d..bd327c1a 100644 --- a/app/lib/utils/crypto.js +++ b/app/lib/utils/crypto.js @@ -150,13 +150,13 @@ export const getNodeAlias = (pubkey, nodes = []) => { /** * Given a list of routest, find the minimum fee. * @param {QueryRoutesResponse} routes - * @return {Number} minimum fee. + * @return {Number} minimum fee rounded up to the nearest satoshi. */ export const getMinFee = (routes = []) => { if (!routes || !routes.length) { return null } - return routes.reduce((min, b) => Math.min(min, b.total_fees_msat), routes[0].total_fees_msat) + return routes.reduce((min, b) => Math.min(min, b.total_fees), routes[0].total_fees) } /** @@ -168,7 +168,7 @@ export const getMaxFee = routes => { if (!routes || !routes.length) { return null } - return routes.reduce((max, b) => Math.max(max, b.total_fees_msat), routes[0].total_fees_msat) + return routes.reduce((max, b) => Math.max(max, b.total_fees), routes[0].total_fees) } /** diff --git a/app/main.dev.js b/app/main.dev.js index 897c8d89..0993e44a 100644 --- a/app/main.dev.js +++ b/app/main.dev.js @@ -166,8 +166,8 @@ app.on('ready', async () => { app.on('open-url', (event, url) => { mainLog.debug('open-url') event.preventDefault() - const payreq = url.split(':')[1] - zap.sendMessage('lightningPaymentUri', { payreq }) + const payReq = url.split(':')[1] + zap.sendMessage('lightningPaymentUri', { payReq }) zap.mainWindow.show() }) diff --git a/app/reducers/index.js b/app/reducers/index.js index 70ec5d92..7a9ddcbc 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -13,6 +13,7 @@ import peers from './peers' import channels from './channels' import contactsform from './contactsform' import form from './form' +import pay from './pay' import payform from './payform' import requestform from './requestform' import invoice from './invoice' @@ -42,6 +43,7 @@ const rootReducer = combineReducers({ channels, contactsform, form, + pay, payform, requestform, invoice, diff --git a/app/reducers/ipc.js b/app/reducers/ipc.js index 2611c478..0d37def7 100644 --- a/app/reducers/ipc.js +++ b/app/reducers/ipc.js @@ -27,7 +27,7 @@ import { channelGraphData, channelGraphStatus } from './channels' -import { lightningPaymentUri } from './payform' +import { lightningPaymentUri, queryRoutesSuccess, queryRoutesFailure } from './pay' import { receivePayments, paymentSuccessful, paymentFailed } from './payment' import { receiveInvoices, @@ -84,6 +84,9 @@ const ipc = createIpc({ lightningPaymentUri, + queryRoutesSuccess, + queryRoutesFailure, + paymentSuccessful, paymentFailed, diff --git a/app/reducers/network.js b/app/reducers/network.js index f61e0681..68dfe6dd 100644 --- a/app/reducers/network.js +++ b/app/reducers/network.js @@ -1,6 +1,7 @@ import { createSelector } from 'reselect' import { ipcRenderer } from 'electron' import { bech32 } from 'lib/utils' +import { setError } from './error' // ------------------------------------ // Constants @@ -10,6 +11,7 @@ export const RECEIVE_DESCRIBE_NETWORK = 'RECEIVE_DESCRIBE_NETWORK' export const GET_QUERY_ROUTES = 'GET_QUERY_ROUTES' export const RECEIVE_QUERY_ROUTES = 'RECEIVE_QUERY_ROUTES' +export const RECEIVE_QUERY_ROUTES_FAILED = 'RECEIVE_QUERY_ROUTES_FAILED' export const SET_CURRENT_ROUTE = 'SET_CURRENT_ROUTE' @@ -142,6 +144,11 @@ export const queryRoutes = (pubkey, amount) => dispatch => { ipcRenderer.send('lnd', { msg: 'queryRoutes', data: { pubkey, amount } }) } +export const queryRoutesFailed = (event, { error }) => dispatch => { + dispatch({ type: RECEIVE_QUERY_ROUTES_FAILED }) + dispatch(setError(error)) +} + export const receiveQueryRoutes = (event, { routes }) => dispatch => dispatch({ type: RECEIVE_QUERY_ROUTES, routes }) @@ -176,6 +183,11 @@ const ACTION_HANDLERS = { networkLoading: false, selectedNode: { pubkey: state.selectedNode.pubkey, routes, currentRoute: routes[0] } }), + [RECEIVE_QUERY_ROUTES_FAILED]: state => ({ + ...state, + networkLoading: false, + selectedNode: {} + }), [SET_CURRENT_ROUTE]: (state, { route }) => ({ ...state, currentRoute: route }), @@ -294,6 +306,7 @@ const initialState = { nodes: [], edges: [], selectedChannel: {}, + selectedNode: {}, currentTab: 1, diff --git a/app/reducers/pay.js b/app/reducers/pay.js new file mode 100644 index 00000000..c7794d59 --- /dev/null +++ b/app/reducers/pay.js @@ -0,0 +1,131 @@ +import { ipcRenderer } from 'electron' +import get from 'lodash.get' +import { requestFees } from 'lib/utils/api' +import { setFormType } from './form' + +// ------------------------------------ +// Constants +// ------------------------------------ +export const QUERY_FEES = 'QUERY_FEES' +export const QUERY_FEES_SUCCESS = 'QUERY_FEES_SUCCESS' +export const QUERY_FEES_FAILURE = 'QUERY_FEES_FAILURE' + +export const QUERY_ROUTES = 'QUERY_ROUTES' +export const QUERY_ROUTES_SUCCESS = 'QUERY_ROUTES_SUCCESS' +export const QUERY_ROUTES_FAILURE = 'QUERY_ROUTES_FAILURE' + +export const SET_PAY_REQ = 'SET_PAY_REQ' + +// ------------------------------------ +// Actions +// ------------------------------------ +export const queryFees = () => async dispatch => { + dispatch({ type: QUERY_FEES }) + try { + const onchainFees = await requestFees() + dispatch({ type: QUERY_FEES_SUCCESS, onchainFees }) + } catch (e) { + const error = get(e, 'response.statusText', e) + dispatch({ type: QUERY_FEES_FAILURE, error }) + } +} + +export const queryRoutes = (pubKey, amount) => dispatch => { + dispatch({ type: QUERY_ROUTES, pubKey }) + ipcRenderer.send('lnd', { msg: 'queryRoutes', data: { pubkey: pubKey, amount } }) +} + +export const queryRoutesSuccess = (event, { routes }) => dispatch => + dispatch({ type: QUERY_ROUTES_SUCCESS, routes }) + +export const queryRoutesFailure = () => dispatch => { + dispatch({ type: QUERY_ROUTES_FAILURE }) +} + +export function setPayReq(payReq) { + return { + type: SET_PAY_REQ, + payReq + } +} + +export const lightningPaymentUri = (event, { payReq }) => dispatch => { + dispatch(setPayReq(payReq)) + dispatch(setFormType('PAY_FORM')) + dispatch(setPayReq(null)) +} + +// ------------------------------------ +// Action Handlers +// ------------------------------------ +const ACTION_HANDLERS = { + [QUERY_FEES]: state => ({ + ...state, + isQueryingFees: true, + onchainFees: {}, + queryFeesError: null + }), + [QUERY_FEES_SUCCESS]: (state, { onchainFees }) => ({ + ...state, + isQueryingFees: false, + onchainFees, + queryFeesError: null + }), + [QUERY_FEES_FAILURE]: (state, { error }) => ({ + ...state, + isQueryingFees: false, + onchainFees: {}, + queryFeesError: error + }), + [QUERY_ROUTES]: (state, { pubKey }) => ({ + ...state, + isQueryingRoutes: true, + pubKey, + queryRoutesError: null, + routes: [] + }), + [QUERY_ROUTES_SUCCESS]: (state, { routes }) => ({ + ...state, + isQueryingRoutes: false, + queryRoutesError: null, + routes + }), + [QUERY_ROUTES_FAILURE]: (state, { error }) => ({ + ...state, + isQueryingRoutes: false, + pubKey: null, + queryRoutesError: error, + routes: [] + }), + [SET_PAY_REQ]: (state, { payReq }) => ({ + ...state, + payReq + }) +} + +// ------------------------------------ +// Initial State +// ------------------------------------ +const initialState = { + isQueryingRoutes: false, + isQueryingFees: false, + onchainFees: { + fastestFee: null, + halfHourFee: null, + hourFee: null + }, + payReq: null, + pubKey: null, + queryFeesError: null, + queryRoutesError: null, + routes: [] +} + +// ------------------------------------ +// Reducer +// ------------------------------------ +export default function activityReducer(state = initialState, action) { + const handler = ACTION_HANDLERS[action.type] + + return handler ? handler(state, action) : state +} diff --git a/app/reducers/payment.js b/app/reducers/payment.js index 25dc379a..c6744776 100644 --- a/app/reducers/payment.js +++ b/app/reducers/payment.js @@ -119,9 +119,9 @@ export const paymentFailed = (event, { error }) => dispatch => { dispatch(setError(error)) } -export const payInvoice = paymentRequest => (dispatch, getState) => { +export const payInvoice = (paymentRequest, feeLimit) => (dispatch, getState) => { dispatch(sendPayment()) - ipcRenderer.send('lnd', { msg: 'sendPayment', data: { paymentRequest } }) + ipcRenderer.send('lnd', { msg: 'sendPayment', data: { paymentRequest, feeLimit } }) // Set an interval to call tick which will continuously tick down the ticker until the payment goes through or it hits // 0 and throws an error. We also call setPaymentInterval so we are storing the interval. This allows us to clear the diff --git a/app/reducers/transaction.js b/app/reducers/transaction.js index 4f587e92..fcd19397 100644 --- a/app/reducers/transaction.js +++ b/app/reducers/transaction.js @@ -86,13 +86,16 @@ export const receiveTransactions = (event, { transactions }) => (dispatch, getSt dispatch(fetchBalance()) } -export const sendCoins = ({ value, addr, currency }) => dispatch => { +export const sendCoins = ({ value, addr, currency, targetConf, satPerByte }) => dispatch => { // backend needs amount in satoshis no matter what currency we are using const amount = btc.convert(currency, 'sats', value) // submit the transaction to LND dispatch(sendTransaction()) - ipcRenderer.send('lnd', { msg: 'sendCoins', data: { amount, addr } }) + ipcRenderer.send('lnd', { + msg: 'sendCoins', + data: { amount, addr, currency, target_conf: targetConf, sat_per_byte: satPerByte } + }) // Close the form modal once the payment was sent to LND // we will do the loading/success UX on the main page diff --git a/internals/webpack/webpack.config.renderer.dev.js b/internals/webpack/webpack.config.renderer.dev.js index 041adec6..d5d43d75 100644 --- a/internals/webpack/webpack.config.renderer.dev.js +++ b/internals/webpack/webpack.config.renderer.dev.js @@ -155,6 +155,7 @@ export default merge.smart(baseConfig, { 'http://localhost:*', 'ws://localhost:*', 'https://blockchain.info', + 'https://bitcoinfees.earn.com', 'https://zap.jackmallers.com' ], 'script-src': ["'self'", 'http://localhost:*', "'unsafe-eval'"], diff --git a/internals/webpack/webpack.config.renderer.prod.js b/internals/webpack/webpack.config.renderer.prod.js index b8ed5dec..56886542 100644 --- a/internals/webpack/webpack.config.renderer.prod.js +++ b/internals/webpack/webpack.config.renderer.prod.js @@ -98,7 +98,12 @@ export default merge.smart(baseConfig, { new CspHtmlWebpackPlugin({ 'default-src': "'self'", 'object-src': "'none'", - 'connect-src': ["'self'", 'https://blockchain.info', 'https://zap.jackmallers.com'], + 'connect-src': [ + "'self'", + 'https://blockchain.info', + 'https://bitcoinfees.earn.com', + 'https://zap.jackmallers.com' + ], 'script-src': ["'self'"], 'font-src': ["'self'", 'data:', 'https://s3.amazonaws.com', 'https://fonts.gstatic.com'], 'style-src': ["'self'", 'blob:', 'https://s3.amazonaws.com', "'unsafe-inline'"] diff --git a/stories/components/form.stories.js b/stories/components/form.stories.js index caa9286d..a92c7a3c 100644 --- a/stories/components/form.stories.js +++ b/stories/components/form.stories.js @@ -221,7 +221,7 @@ storiesOf('Components.Form', module) - + diff --git a/stories/pages/pay.stories.js b/stories/pages/pay.stories.js index 22c91c63..0955a322 100644 --- a/stories/pages/pay.stories.js +++ b/stories/pages/pay.stories.js @@ -60,18 +60,21 @@ const store = new Store({ }) const mockPayInvoice = async () => { + action('mockPayInvoice') store.set({ isProcessing: true }) await delay(2000) store.set({ isProcessing: false }) } const mockSendCoins = async () => { + action('mockSendCoins') store.set({ isProcessing: true }) await delay(2000) store.set({ isProcessing: false }) } const mockQueryFees = async () => { + action('mockQueryFees') store.set({ isQueryingFees: true }) await delay(2000) store.set({ @@ -85,6 +88,7 @@ const mockQueryFees = async () => { } const mockQueryRoutes = async pubKey => { + action('mockQueryRoutes', pubKey) store.set({ isQueryingRoutes: true }) await delay(2000) const nodes = store.get('nodes') @@ -162,9 +166,10 @@ storiesOf('Containers.Pay', module) {}, - setPayInput: () => {}, - fetchInvoice: () => {}, - setCurrency: () => {}, - - onPayAmountBlur: () => {}, - - onPayInputBlur: () => {}, - - onPaySubmit: () => {} -} - -describe('Form', () => { - describe('should show the form without an input', () => { - const el = mountWithIntl( - - - - ) - - it('should contain Pay', () => { - expect(el.find('input#paymentRequest').props.value).toBe(undefined) - }) - }) - - describe('should show lightning with a lightning input', () => { - const props = { ...defaultProps, isLn: true } - const el = mountWithIntl( - - - - ) - - it('should contain Pay', () => { - expect(el.find('input#paymentRequest').props.value).toBe(undefined) - }) - }) - - describe('should show on-chain with an on-chain input', () => { - const props = { ...defaultProps, isOnchain: true } - const el = mountWithIntl( - - - - ) - - it('should contain Pay', () => { - expect(el.find('input#paymentRequest').props.value).toBe(undefined) - }) - }) -}) diff --git a/test/unit/components/Pay/PaySummaryOnchain.spec.js b/test/unit/components/Pay/PaySummaryOnchain.spec.js index 29a40140..8ca4bcd6 100644 --- a/test/unit/components/Pay/PaySummaryOnchain.spec.js +++ b/test/unit/components/Pay/PaySummaryOnchain.spec.js @@ -34,6 +34,7 @@ const props = { } ], fiatCurrency: 'USD', + queryFees: jest.fn(), setCryptoCurrency: jest.fn() } diff --git a/test/unit/components/Pay/__snapshots__/PaySummaryLightning.spec.js.snap b/test/unit/components/Pay/__snapshots__/PaySummaryLightning.spec.js.snap index 2b7e6a7c..56a39e9c 100644 --- a/test/unit/components/Pay/__snapshots__/PaySummaryLightning.spec.js.snap +++ b/test/unit/components/Pay/__snapshots__/PaySummaryLightning.spec.js.snap @@ -81,6 +81,7 @@ exports[`component.Form.PaySummaryLightning should render correctly 1`] = ` textAlign="right" > @@ -99,8 +100,13 @@ exports[`component.Form.PaySummaryLightning should render correctly 1`] = ` right={ } /> diff --git a/test/unit/components/Pay/__snapshots__/PaySummaryOnchain.spec.js.snap b/test/unit/components/Pay/__snapshots__/PaySummaryOnchain.spec.js.snap index 0849acec..9dfbcd42 100644 --- a/test/unit/components/Pay/__snapshots__/PaySummaryOnchain.spec.js.snap +++ b/test/unit/components/Pay/__snapshots__/PaySummaryOnchain.spec.js.snap @@ -99,7 +99,7 @@ exports[`component.Form.PaySummaryOnchain should render correctly 1`] = ` right={ } diff --git a/test/unit/components/UI/__snapshots__/LightningInvoiceInput.spec.js.snap b/test/unit/components/UI/__snapshots__/LightningInvoiceInput.spec.js.snap index 47195d67..157c149f 100644 --- a/test/unit/components/UI/__snapshots__/LightningInvoiceInput.spec.js.snap +++ b/test/unit/components/UI/__snapshots__/LightningInvoiceInput.spec.js.snap @@ -52,6 +52,7 @@ exports[`component.UI.LightningInvoiceInput should render correctly 1`] = ` placeholder="Paste a Lightning Payment Request or Bitcoin Address here" required={false} rows={5} + spellCheck="false" value="" width={1} /> diff --git a/test/unit/components/UI/__snapshots__/Modal.spec.js.snap b/test/unit/components/UI/__snapshots__/Modal.spec.js.snap index 2bfe5c18..d2a8a2c5 100644 --- a/test/unit/components/UI/__snapshots__/Modal.spec.js.snap +++ b/test/unit/components/UI/__snapshots__/Modal.spec.js.snap @@ -3,7 +3,9 @@ exports[`component.UI.Modal should render correctly 1`] = ` .c2 { margin-left: auto; + padding: 8px; cursor: pointer; + opacity: 1; } .c3 { @@ -49,7 +51,6 @@ exports[`component.UI.Modal should render correctly 1`] = ` >
diff --git a/test/unit/components/UI/__snapshots__/Range.spec.js.snap b/test/unit/components/UI/__snapshots__/Range.spec.js.snap index da3ab301..0080515e 100644 --- a/test/unit/components/UI/__snapshots__/Range.spec.js.snap +++ b/test/unit/components/UI/__snapshots__/Range.spec.js.snap @@ -42,7 +42,7 @@ exports[`component.UI.Range should render correctly 1`] = ` onChange={[Function]} step={1} type="range" - value="0" + value={0} /> `;