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}
/>
`;