From 8e56936917c70db3a376aaef630067ea872788f3 Mon Sep 17 00:00:00 2001 From: Tom Kirkpatrick Date: Fri, 10 Jul 2020 14:16:57 +0200 Subject: [PATCH 1/2] feat: parse bip21 uris in pay address form --- .../components/Form/LightningInvoiceInput.js | 4 +- renderer/components/Pay/Pay.js | 70 ++++++++++++++----- renderer/components/Pay/PayAmountFields.js | 6 +- renderer/components/Pay/PayPanelBody.js | 3 + utils/crypto.js | 20 ++++++ 5 files changed, 84 insertions(+), 19 deletions(-) diff --git a/renderer/components/Form/LightningInvoiceInput.js b/renderer/components/Form/LightningInvoiceInput.js index 8b01af79a38..cc0d906c144 100644 --- a/renderer/components/Form/LightningInvoiceInput.js +++ b/renderer/components/Form/LightningInvoiceInput.js @@ -2,7 +2,7 @@ import React, { useCallback } from 'react' import PropTypes from 'prop-types' import { FormattedMessage, useIntl } from 'react-intl' import { useFormState } from 'informed' -import { isOnchain, isBolt11, isPubkey, decodePayReq } from '@zap/utils/crypto' +import { isOnchain, isBip21, isBolt11, isPubkey, decodePayReq } from '@zap/utils/crypto' import { Message } from 'components/UI' import TextArea from './TextArea' import messages from './messages' @@ -33,7 +33,7 @@ const validate = (intl, network, chain, value) => { } catch (e) { return invalidRequestMessage } - } else if (!isOnchain(value, chain, network) && !isPubkey(value)) { + } else if (!isOnchain(value, chain, network) && !isPubkey(value) && !isBip21(value)) { return invalidRequestMessage } } diff --git a/renderer/components/Pay/Pay.js b/renderer/components/Pay/Pay.js index 3b5563bce12..83976383c21 100644 --- a/renderer/components/Pay/Pay.js +++ b/renderer/components/Pay/Pay.js @@ -3,7 +3,16 @@ import PropTypes from 'prop-types' import config from 'config' import get from 'lodash/get' import { injectIntl } from 'react-intl' -import { decodePayReq, getMaxFeeInclusive, isOnchain, isBolt11, isPubkey } from '@zap/utils/crypto' +import bip21 from 'bip21' +import { + decodePayReq, + getMaxFeeInclusive, + isOnchain, + isBip21, + isBolt11, + isPubkey, +} from '@zap/utils/crypto' +import { convert } from '@zap/utils/btc' import { Panel } from 'components/UI' import { Form } from 'components/Form' import { getAmountInSats, getFeeRate } from './utils' @@ -16,6 +25,7 @@ import { intlShape } from '@zap/i18n' class Pay extends React.Component { static propTypes = { addFilter: PropTypes.func.isRequired, + bip21decoded: PropTypes.object, chain: PropTypes.string.isRequired, chainName: PropTypes.string.isRequired, channelBalance: PropTypes.string.isRequired, @@ -65,6 +75,8 @@ class Pay extends React.Component { previousStep: null, paymentType: PAYMENT_TYPES.none, loaded: false, + invoice: null, + bip21decoded: null, } // Set a flag so that we can trigger form submission in componentDidUpdate once the form is loaded. @@ -76,8 +88,8 @@ class Pay extends React.Component { } componentDidUpdate(prevProps, prevState) { - const { redirectPayReq, queryRoutes, setRedirectPayReq, queryFees } = this.props - const { currentStep, invoice, paymentType } = this.state + const { cryptoUnit, redirectPayReq, queryRoutes, setRedirectPayReq } = this.props + const { currentStep, bip21decoded, invoice, paymentType } = this.state const { address, amount } = redirectPayReq || {} const { payReq: prevPayReq } = prevProps || {} const { address: prevAddress, amount: prevAmount } = prevPayReq || {} @@ -90,32 +102,44 @@ class Pay extends React.Component { } // If payReq address or amount has has changed update the relevant form values. - const isChangedAddress = address !== prevAddress - const isChangedAmount = amount !== prevAmount - if (isChangedAddress || isChangedAmount) { + const isChangedRedirectPayReqAddress = address !== prevAddress + const isChangedRedirectPayReqAmount = amount !== prevAmount + if (isChangedRedirectPayReqAddress || isChangedRedirectPayReqAmount) { this.autoFillForm(address, amount) return } - // If we have gone back to the address step, unmark all fields from being touched. + // If we have changed page, unmark all fields from being touched. if (currentStep !== prevState.currentStep) { - if (currentStep === PAY_FORM_STEPS.address) { - Object.keys(this.formApi.getState().touched).forEach(field => { - this.formApi.setTouched(field, false) - }) - } + Object.keys(this.formApi.getState().touched).forEach(field => { + this.formApi.setTouched(field, false) + }) + } + + // If we now have a valid onchain address from pasted bip21 uri into address field + // fextract the values, fill out the form, and submit to next step. + const isNowBip21 = bip21decoded && bip21decoded !== prevState.bip21decoded + if (currentStep === PAY_FORM_STEPS.address && isNowBip21) { + this.formApi.reset() + this.formApi.setValue('payReq', bip21decoded.address) + this.formApi.setValue( + 'amountCrypto', + convert('btc', cryptoUnit, get(bip21decoded, 'options.amount')) + ) + this.formApi.submitForm() + return } - // If we now have a valid onchain address, trigger the form submit to move to the amount step. + // If we now have a valid onchain address from pasted bitcoin address into address field + // trigger the form submit to move to the amount step. const isNowOnchain = paymentType === PAYMENT_TYPES.onchain && paymentType !== prevState.paymentType if (currentStep === PAY_FORM_STEPS.address && isNowOnchain) { - queryFees() this.formApi.submitForm() return } - // If we now have a valid onchain address, trigger the form submit to move to the amount step. + // If we now have a valid pubkey, trigger the form submit to move to the amount step. const isNowPubkey = paymentType === PAYMENT_TYPES.pubkey && paymentType !== prevState.paymentType if (currentStep === PAY_FORM_STEPS.address && isNowPubkey) { @@ -123,7 +147,7 @@ class Pay extends React.Component { return } - // If we now have a valid lightning invoice submit the form. + // If we now have a valid lightning invoice, trigger the form submit to move to the amount step. const isNowLightning = invoice && invoice !== prevState.invoice if (currentStep === PAY_FORM_STEPS.address && isNowLightning) { this.formApi.submitForm() @@ -331,6 +355,7 @@ class Pay extends React.Component { currentStep: PAY_FORM_STEPS.address, paymentType: PAYMENT_TYPES.none, invoice: null, + bip21decoded: null, } // See if the user has entered a valid lightning payment request. @@ -344,6 +369,17 @@ class Pay extends React.Component { state.paymentType = PAYMENT_TYPES.bolt11 } + // Or a valid bip21 payment uri. + else if (isBip21(payReq)) { + try { + const bip21decoded = bip21.decode(payReq) + state.bip21decoded = bip21decoded + } catch (e) { + return + } + state.paymentType = PAYMENT_TYPES.onchain + } + // Otherwise, see if we have a valid onchain address. else if (isOnchain(payReq, chain, network)) { state.paymentType = PAYMENT_TYPES.onchain @@ -362,6 +398,7 @@ class Pay extends React.Component { const { currentStep, invoice, paymentType, previousStep } = this.state const { + bip21decoded, chain, chainName, addFilter, @@ -405,6 +442,7 @@ class Pay extends React.Component { { const { amountInSats, + bip21decoded, chain, chainName, cryptoUnit, @@ -54,6 +55,7 @@ const PayPanelBody = props => { redirectPayReq={redirectPayReq} /> { PayPanelBody.propTypes = { amountInSats: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + bip21decoded: PropTypes.object, chain: PropTypes.string.isRequired, chainName: PropTypes.string.isRequired, cryptoUnit: PropTypes.string.isRequired, diff --git a/utils/crypto.js b/utils/crypto.js index 648789da79e..68402e8c29e 100644 --- a/utils/crypto.js +++ b/utils/crypto.js @@ -4,6 +4,7 @@ import config from 'config' import range from 'lodash/range' import { address } from 'bitcoinjs-lib' import lightningRequestReq from 'bolt11' +import bip21 from 'bip21' import coininfo from 'coininfo' import { CoinBig } from '@zap/utils/coin' import { convert } from '@zap/utils/btc' @@ -145,6 +146,25 @@ export const isOnchain = (input, chain, network) => { } } +/** + * isBip21 - Test to see if a string is a valid bip21 payment uri + * + * @param {string} input Value to check + * @returns {boolean} Boolean indicating whether the address is a valid bip21 payment uri + */ +export const isBip21 = input => { + if (!input) { + return false + } + + try { + bip21.decode(input) + return true + } catch (e) { + return false + } +} + /** * isBolt11 - Test to see if a string is a valid lightning address. * From 26dc40cb2e15b03db10eae67cb77c03c730a0b9a Mon Sep 17 00:00:00 2001 From: Tom Kirkpatrick Date: Sat, 11 Jul 2020 12:23:05 +0200 Subject: [PATCH 2/2] fix: update pay form fiat value on initial set --- renderer/components/Form/CurrencyFieldGroup.js | 11 ++++------- renderer/components/Pay/Pay.js | 9 ++++----- renderer/components/Pay/PayPanelFooter.js | 4 ++++ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/renderer/components/Form/CurrencyFieldGroup.js b/renderer/components/Form/CurrencyFieldGroup.js index 055a0d6175f..f2202a0d533 100644 --- a/renderer/components/Form/CurrencyFieldGroup.js +++ b/renderer/components/Form/CurrencyFieldGroup.js @@ -46,11 +46,8 @@ const CurrencyFieldGroup = React.forwardRef( blockUpdates.current = true } - // informed calls onValueChange multiple time during value updates - // because of masks and patterns applied on top of UI elements - // give value a chance to settle before enabling updates again - const unblockLinkedUpdates = async () => { - await Promise.resolve() + // Enable updates of linked form values. + const unblockLinkedUpdates = () => { blockUpdates.current = false } @@ -70,7 +67,7 @@ const CurrencyFieldGroup = React.forwardRef( const lastPrice = currentTicker[fiatCurrency] const fiatValue = convert(cryptoUnit, 'fiat', value, lastPrice) formApi.setValue('amountFiat', fiatValue) - await unblockLinkedUpdates() + unblockLinkedUpdates() } onChange && onChange() } @@ -86,7 +83,7 @@ const CurrencyFieldGroup = React.forwardRef( const lastPrice = currentTicker[fiatCurrency] const cryptoValue = convert('fiat', cryptoUnit, value, lastPrice) formApi.setValue('amountCrypto', cryptoValue) - await unblockLinkedUpdates() + unblockLinkedUpdates() } onChange && onChange() } diff --git a/renderer/components/Pay/Pay.js b/renderer/components/Pay/Pay.js index 83976383c21..348b7035955 100644 --- a/renderer/components/Pay/Pay.js +++ b/renderer/components/Pay/Pay.js @@ -121,11 +121,10 @@ class Pay extends React.Component { const isNowBip21 = bip21decoded && bip21decoded !== prevState.bip21decoded if (currentStep === PAY_FORM_STEPS.address && isNowBip21) { this.formApi.reset() - this.formApi.setValue('payReq', bip21decoded.address) - this.formApi.setValue( - 'amountCrypto', - convert('btc', cryptoUnit, get(bip21decoded, 'options.amount')) - ) + this.formApi.setValues({ + payReq: bip21decoded.address, + amountCrypto: convert('btc', cryptoUnit, get(bip21decoded, 'options.amount')), + }) this.formApi.submitForm() return } diff --git a/renderer/components/Pay/PayPanelFooter.js b/renderer/components/Pay/PayPanelFooter.js index 994b3811f0b..bb97cc9010f 100644 --- a/renderer/components/Pay/PayPanelFooter.js +++ b/renderer/components/Pay/PayPanelFooter.js @@ -15,6 +15,10 @@ const isEnoughFunds = props => { // convert entered amount to satoshis const { amountInSats, channelBalance, invoice, paymentType, walletBalanceConfirmed } = props + if (amountInSats === 'NaN') { + return true + } + // Determine whether we have enough funds available. let hasEnoughFunds = true const isBolt11 = paymentType === PAYMENT_TYPES.bolt11