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

Commit

Permalink
feat: parse bip21 uris in pay address form
Browse files Browse the repository at this point in the history
  • Loading branch information
mrfelton committed Jul 10, 2020
1 parent 6f4ffad commit 8e56936
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 19 deletions.
4 changes: 2 additions & 2 deletions renderer/components/Form/LightningInvoiceInput.js
Expand Up @@ -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'
Expand Down Expand Up @@ -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
}
}
Expand Down
70 changes: 54 additions & 16 deletions renderer/components/Pay/Pay.js
Expand Up @@ -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'
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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 || {}
Expand All @@ -90,40 +102,52 @@ 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) {
this.formApi.submitForm()
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()
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -362,6 +398,7 @@ class Pay extends React.Component {
const { currentStep, invoice, paymentType, previousStep } = this.state

const {
bip21decoded,
chain,
chainName,
addFilter,
Expand Down Expand Up @@ -405,6 +442,7 @@ class Pay extends React.Component {
<Panel.Body py={3}>
<PayPanelBody
amountInSats={this.amountInSats()}
bip21decoded={bip21decoded}
chain={chain}
chainName={chainName}
cryptoUnit={cryptoUnit}
Expand Down
6 changes: 5 additions & 1 deletion renderer/components/Pay/PayAmountFields.js
Expand Up @@ -5,6 +5,7 @@ import { FormattedMessage } from 'react-intl'
import debounce from 'lodash/debounce'
import { Keyframes } from 'react-spring/renderprops.cjs'
import { intlShape } from '@zap/i18n'
import { CoinBig } from '@zap/utils/coin'
import { TransactionFeeInput, Toggle, Label } from 'components/Form'
import { Bar } from 'components/UI'
import { CurrencyFieldGroup } from 'containers/Form'
Expand Down Expand Up @@ -40,10 +41,13 @@ class PayAmountFields extends React.Component {
const { payReq: address } = formState.values
const amount = formApi.getValue('amountCrypto') || 0
const amountInSats = getAmountInSats(amount, cryptoUnit, invoice)
queryFees(address, amountInSats)
if (CoinBig(amountInSats).gt(0)) {
queryFees(address, amountInSats)
}
}, 500)

static propTypes = {
bip21decoded: PropTypes.object,
cryptoUnit: PropTypes.string.isRequired,
currentStep: PropTypes.string.isRequired,
formApi: PropTypes.object.isRequired,
Expand Down
3 changes: 3 additions & 0 deletions renderer/components/Pay/PayPanelBody.js
Expand Up @@ -10,6 +10,7 @@ import { PAYMENT_TYPES } from './constants'
const PayPanelBody = props => {
const {
amountInSats,
bip21decoded,
chain,
chainName,
cryptoUnit,
Expand Down Expand Up @@ -54,6 +55,7 @@ const PayPanelBody = props => {
redirectPayReq={redirectPayReq}
/>
<PayAmountFields
bip21decoded={bip21decoded}
cryptoUnit={cryptoUnit}
currentStep={currentStep}
formApi={formApi}
Expand Down Expand Up @@ -83,6 +85,7 @@ const PayPanelBody = props => {

PayPanelBody.propTypes = {
amountInSats: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
bip21decoded: PropTypes.object,
chain: PropTypes.string.isRequired,
chainName: PropTypes.string.isRequired,
cryptoUnit: PropTypes.string.isRequired,
Expand Down
20 changes: 20 additions & 0 deletions utils/crypto.js
Expand Up @@ -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'
Expand Down Expand Up @@ -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.
*
Expand Down

0 comments on commit 8e56936

Please sign in to comment.