diff --git a/renderer/components/Form/LightningInvoiceInput.js b/renderer/components/Form/LightningInvoiceInput.js index 1ad4b7aa49e..e989520c7c3 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 { useFieldState } from 'informed' -import { isOnchain, isLn, decodePayReq } from '@zap/utils/crypto' +import { isOnchain, isBolt11, isPubkey, decodePayReq } from '@zap/utils/crypto' import { Message } from 'components/UI' import TextArea from './TextArea' import messages from './messages' @@ -20,7 +20,7 @@ const validate = (intl, network, chain, value) => { { chain: chainName } ) // If we have a LN invoice, ensure the invoice has an amount. - if (isLn(value, chain, network)) { + if (isBolt11(value, chain, network)) { try { const invoice = decodePayReq(value) if (!invoice) { @@ -33,7 +33,7 @@ const validate = (intl, network, chain, value) => { } catch (e) { return invalidRequestMessage } - } else if (!isOnchain(value, chain, network)) { + } else if (!isOnchain(value, chain, network) && !isPubkey(value)) { return invalidRequestMessage } } @@ -45,7 +45,7 @@ const LightningInvoiceInput = props => { const intl = useIntl() const fieldState = useFieldState(field) const { value } = fieldState - let chainName = isLn(value, chain, network) ? 'lightning' : chain + let chainName = isBolt11(value, chain, network) || isPubkey(value) ? 'lightning' : chain if (network !== 'mainnet') { chainName += ` (${network})` } @@ -62,7 +62,7 @@ const LightningInvoiceInput = props => { {value && !fieldState.error && ( { - const { isLn, isOnchain, invoice } = this.state + const { isBolt11, isPubkey, isOnchain, invoice } = this.state let steps = [PAY_FORM_STEPS.address] - if (isLn) { + if (isBolt11) { // If we have an invoice and the invoice has an amount, this is a 2 step form. if (invoice && (invoice.satoshis || invoice.millisatoshis)) { steps.push(PAY_FORM_STEPS.summary) @@ -270,7 +284,7 @@ class Pay extends React.Component { else { steps = [PAY_FORM_STEPS.address, PAY_FORM_STEPS.amount, PAY_FORM_STEPS.summary] } - } else if (isOnchain) { + } else if (isOnchain || isPubkey) { steps = [PAY_FORM_STEPS.address, PAY_FORM_STEPS.amount, PAY_FORM_STEPS.summary] } return steps @@ -301,7 +315,7 @@ class Pay extends React.Component { } /** - * handlePayReqChange - Set isLn/isOnchain state based on payReq value. + * handlePayReqChange - Set isBolt11/isOnchain/isPubkey state based on payReq value. * * @param {string} payReq Payment request */ @@ -309,20 +323,21 @@ class Pay extends React.Component { const { chain, network } = this.props const state = { currentStep: PAY_FORM_STEPS.address, - isLn: null, + isBolt11: null, + isPubkey: null, isOnchain: null, invoice: null, } // See if the user has entered a valid lightning payment request. - if (isLn(payReq, chain, network)) { + if (isBolt11(payReq, chain, network)) { try { const invoice = decodePayReq(payReq) state.invoice = invoice } catch (e) { return } - state.isLn = true + state.isBolt11 = true } // Otherwise, see if we have a valid onchain address. @@ -330,12 +345,17 @@ class Pay extends React.Component { state.isOnchain = true } + // Or a valid pubkey. + else if (isPubkey(payReq)) { + state.isPubkey = true + } + // Update the state with our findings. this.setState(state) } render() { - const { currentStep, invoice, isLn, isOnchain, previousStep } = this.state + const { currentStep, invoice, isBolt11, isOnchain, isPubkey, previousStep } = this.state const { chain, chainName, @@ -374,8 +394,9 @@ class Pay extends React.Component { @@ -393,8 +414,9 @@ class Pay extends React.Component { initialAmountFiat={initialAmountFiat} intl={intl} invoice={invoice} - isLn={isLn} + isBolt11={isBolt11} isOnchain={isOnchain} + isPubkey={isPubkey} isQueryingFees={isQueryingFees} lndTargetConfirmations={lndTargetConfirmations} network={network} @@ -415,9 +437,10 @@ class Pay extends React.Component { currentStep={currentStep} formState={formState} invoice={invoice} - isLn={isLn} + isBolt11={isBolt11} isOnchain={isOnchain} isProcessing={isProcessing} + isPubkey={isPubkey} maxOneTimeSend={maxOneTimeSend} previousStep={this.goToPreviousStep} walletBalanceConfirmed={walletBalanceConfirmed} diff --git a/renderer/components/Pay/PayAddressField.js b/renderer/components/Pay/PayAddressField.js index 884473681b5..d35281006b2 100644 --- a/renderer/components/Pay/PayAddressField.js +++ b/renderer/components/Pay/PayAddressField.js @@ -27,7 +27,7 @@ class PayAddressField extends React.Component { formState: PropTypes.object, handlePayReqChange: PropTypes.func.isRequired, intl: intlShape.isRequired, - isLn: PropTypes.bool, + isBolt11: PropTypes.bool, network: PropTypes.string.isRequired, redirectPayReq: PropTypes.object, } @@ -48,11 +48,11 @@ class PayAddressField extends React.Component { } getPaymentRequestLabel = () => { - const { currentStep, isLn } = this.props + const { currentStep, isBolt11 } = this.props let payReqLabel = 'request_label_onchain' if (currentStep === PAY_FORM_STEPS.address) { payReqLabel = 'request_label_combined' - } else if (isLn) { + } else if (isBolt11) { payReqLabel = 'request_label_offchain' } @@ -66,11 +66,11 @@ class PayAddressField extends React.Component { currentStep, formState, handlePayReqChange, - isLn, + isBolt11, network, redirectPayReq, } = this.props - const addressFieldState = currentStep === PAY_FORM_STEPS.address || isLn ? 'big' : 'small' + const addressFieldState = currentStep === PAY_FORM_STEPS.address || isBolt11 ? 'big' : 'small' const { payReq } = formState.values const { submits } = formState diff --git a/renderer/components/Pay/PayPanelBody.js b/renderer/components/Pay/PayPanelBody.js index de806c82a88..0297c3fda9a 100644 --- a/renderer/components/Pay/PayPanelBody.js +++ b/renderer/components/Pay/PayPanelBody.js @@ -21,8 +21,9 @@ const PayPanelBody = props => { initialAmountFiat, intl, invoice, - isLn, + isBolt11, isOnchain, + isPubkey, isQueryingFees, lndTargetConfirmations, network, @@ -49,7 +50,7 @@ const PayPanelBody = props => { formApi={formState} handlePayReqChange={handlePayReqChange} intl={intl} - isLn={isLn} + isBolt11={isBolt11} network={network} redirectPayReq={redirectPayReq} /> @@ -62,6 +63,7 @@ const PayPanelBody = props => { intl={intl} invoice={invoice} isOnchain={isOnchain} + isPubkey={isPubkey} isQueryingFees={isQueryingFees} lndTargetConfirmations={lndTargetConfirmations} onchainFees={onchainFees} @@ -73,6 +75,7 @@ const PayPanelBody = props => { currentStep={currentStep} formApi={formApi} isOnchain={isOnchain} + isPubkey={isPubkey} lndTargetConfirmations={lndTargetConfirmations} onchainFees={onchainFees} routes={routes} @@ -95,8 +98,9 @@ PayPanelBody.propTypes = { initialAmountFiat: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), intl: intlShape.isRequired, invoice: PropTypes.object, - isLn: PropTypes.bool, + isBolt11: PropTypes.bool, isOnchain: PropTypes.bool, + isPubkey: PropTypes.bool, isQueryingFees: PropTypes.bool, lndTargetConfirmations: PropTypes.shape({ fast: PropTypes.number.isRequired, diff --git a/renderer/components/Pay/PayPanelFooter.js b/renderer/components/Pay/PayPanelFooter.js index fde951009d6..9f15fdb375e 100644 --- a/renderer/components/Pay/PayPanelFooter.js +++ b/renderer/components/Pay/PayPanelFooter.js @@ -12,11 +12,19 @@ import { PAY_FORM_STEPS } from './constants' const isEnoughFunds = props => { // convert entered amount to satoshis - const { amountInSats, channelBalance, invoice, isLn, isOnchain, walletBalanceConfirmed } = props + const { + amountInSats, + channelBalance, + invoice, + isBolt11, + isOnchain, + isPubkey, + walletBalanceConfirmed, + } = props // Determine whether we have enough funds available. let hasEnoughFunds = true - if (isLn && invoice) { + if ((isBolt11 && invoice) || isPubkey) { hasEnoughFunds = amountInSats <= channelBalance } else if (isOnchain) { hasEnoughFunds = amountInSats <= walletBalanceConfirmed @@ -58,8 +66,9 @@ const PayPanelFooter = props => { cryptoUnitName, currentStep, formState, - isLn, + isBolt11, isOnchain, + isPubkey, isProcessing, previousStep, walletBalanceConfirmed, @@ -67,14 +76,14 @@ const PayPanelFooter = props => { } = props const renderLiquidityWarning = props => { - const { currentStep, maxOneTimeSend, cryptoUnit, isLn, amountInSats } = props + const { currentStep, maxOneTimeSend, cryptoUnit, isBolt11, isPubkey, amountInSats } = props if (currentStep !== PAY_FORM_STEPS.summary) { return null } const isNotEnoughFunds = !isEnoughFunds(props) - const isAboveMax = isLn && amountInSats > maxOneTimeSend + const isAboveMax = (isBolt11 || isPubkey) && amountInSats > maxOneTimeSend const formattedMax = intl.formatNumber(convert('sats', cryptoUnit, maxOneTimeSend), { maximumFractionDigits: 8, }) @@ -100,7 +109,8 @@ const PayPanelFooter = props => { // Determine which buttons should be visible. const hasBackButton = currentStep !== PAY_FORM_STEPS.address - const hasSubmitButton = currentStep !== PAY_FORM_STEPS.address || isOnchain || isLn + const hasSubmitButton = + currentStep !== PAY_FORM_STEPS.address || isOnchain || isBolt11 || isPubkey return ( @@ -157,9 +167,10 @@ PayPanelFooter.propTypes = { formState: PropTypes.object.isRequired, intl: intlShape.isRequired, invoice: PropTypes.object, - isLn: PropTypes.bool, + isBolt11: PropTypes.bool, isOnchain: PropTypes.bool, isProcessing: PropTypes.bool, + isPubkey: PropTypes.bool, maxOneTimeSend: PropTypes.string.isRequired, previousStep: PropTypes.func.isRequired, walletBalanceConfirmed: PropTypes.string.isRequired, diff --git a/renderer/components/Pay/PayPanelHeader.js b/renderer/components/Pay/PayPanelHeader.js index 140feb265b5..207fdcde064 100644 --- a/renderer/components/Pay/PayPanelHeader.js +++ b/renderer/components/Pay/PayPanelHeader.js @@ -7,9 +7,9 @@ import messages from './messages' import { PAY_HEADER_TYPES } from './constants' const PayPanelHeader = props => { - const { chainName, cryptoUnitName, isLn, isOnchain } = props + const { chainName, cryptoUnitName, isBolt11, isOnchain, isPubkey } = props let headerType = null - if (isLn) { + if (isBolt11 || isPubkey) { headerType = PAY_HEADER_TYPES.offchain } else if (isOnchain) { headerType = PAY_HEADER_TYPES.onchain @@ -33,8 +33,9 @@ const PayPanelHeader = props => { PayPanelHeader.propTypes = { chainName: PropTypes.string.isRequired, cryptoUnitName: PropTypes.string.isRequired, - isLn: PropTypes.bool, + isBolt11: PropTypes.bool, isOnchain: PropTypes.bool, + isPubkey: PropTypes.bool, } export default PayPanelHeader diff --git a/renderer/components/Pay/PaySummary.js b/renderer/components/Pay/PaySummary.js index 7ca72240dfa..0727d3f8584 100644 --- a/renderer/components/Pay/PaySummary.js +++ b/renderer/components/Pay/PaySummary.js @@ -8,7 +8,15 @@ import { PAY_FORM_STEPS } from './constants' import { getFeeRate } from './utils' const PaySummary = props => { - const { amountInSats, formApi, isOnchain, lndTargetConfirmations, onchainFees, routes } = props + const { + amountInSats, + formApi, + isOnchain, + isPubkey, + lndTargetConfirmations, + onchainFees, + routes, + } = props const formState = formApi.getState() const { speed, payReq, isCoinSweep } = formState.values @@ -29,6 +37,7 @@ const PaySummary = props => { return ( = 0 && maxFee < 1) { + if (CoinBig(maxFee).gte(0) && CoinBig(maxFee).lt(1)) { feeMessage = messages.fee_less_than_1 } // Otherwise, if we have both a min and max fee that are different, present the fee range. @@ -87,6 +98,10 @@ class PaySummaryLightning extends React.Component { feeMessage = messages.fee_upto } + const totalAmountInSatoshis = hasMaxFee + ? CoinBig.sum(amountInSatoshis, maxFee).toString() + : amountInSatoshis + return ( @@ -140,7 +155,7 @@ class PaySummaryLightning extends React.Component { right={ - + } /> diff --git a/renderer/components/Request/RequestSummary.js b/renderer/components/Request/RequestSummary.js index b53a6d12a9c..dc184bc0d72 100644 --- a/renderer/components/Request/RequestSummary.js +++ b/renderer/components/Request/RequestSummary.js @@ -12,7 +12,7 @@ import { intlShape } from '@zap/i18n' import messages from './messages' const RequestSummary = ({ invoice = {}, payReq, intl, showNotification, ...rest }) => { - const decodedInvoice = useMemo(() => decodePayReq(payReq), [payReq]) + const decodedInvoice = useMemo(() => (payReq ? decodePayReq(payReq) : {}), [payReq]) const [isExpired, setIsExpired] = useState(false) const [expiryDelta, setExpiryDelta] = useState( decodedInvoice.timeExpireDate - getUnixTime() / 1000 diff --git a/renderer/reducers/payment.js b/renderer/reducers/payment.js index b9a4eba1777..25de79e58cd 100644 --- a/renderer/reducers/payment.js +++ b/renderer/reducers/payment.js @@ -1,11 +1,12 @@ import config from 'config' +import { randomBytes, createHash } from 'crypto' import { createSelector } from 'reselect' import uniqBy from 'lodash/uniqBy' import find from 'lodash/find' import createReducer from '@zap/utils/createReducer' import errorToUserFriendly from '@zap/utils/userFriendlyErrors' import { getIntl } from '@zap/i18n' -import { decodePayReq, getNodeAlias, getTag } from '@zap/utils/crypto' +import { decodePayReq, getNodeAlias, getTag, isPubkey } from '@zap/utils/crypto' import { convert } from '@zap/utils/btc' import delay from '@zap/utils/delay' import genId from '@zap/utils/genId' @@ -115,10 +116,7 @@ export function getPayments() { * @returns {Function} Thunk */ export const sendPayment = data => dispatch => { - const invoice = decodePayReq(data.paymentRequest) - const paymentHash = getTag(invoice, 'payment_hash') - - if (!paymentHash) { + if (!data.paymentHash) { dispatch(showError(getIntl().formatMessage(messages.payment_send_error))) return } @@ -128,7 +126,7 @@ export const sendPayment = data => dispatch => { status: PAYMENT_STATUS_SENDING, isSending: true, creation_date: Math.round(new Date() / 1000), - payment_hash: paymentHash, + payment_hash: data.paymentHash, } dispatch({ @@ -188,38 +186,71 @@ export const payInvoice = ({ retries = 0, originalPaymentId, }) => async dispatch => { - let paymentId = originalPaymentId + let paymentHash + const paymentId = originalPaymentId || genId() + const isKeysend = isPubkey(payReq) + + let payload = { + paymentId, + amt, + fee_limit: { fixed: feeLimit }, + allow_self_payment: true, + } - // If we already have an id then this is a retry. Decrease the retry count. - if (originalPaymentId) { - dispatch(decPaymentRetry(originalPaymentId)) + // Keysend payment. + if (isKeysend) { + const defaultCltvDelta = 43 + const keySendPreimageType = '5482373484' + const preimageByteLength = 32 + const preimage = randomBytes(preimageByteLength) + const secret = preimage.toString('hex') + paymentHash = createHash('sha256') + .update(preimage) + .digest() + + payload = { + ...payload, + payment_hash: paymentHash, + final_cltv_delta: defaultCltvDelta, + dest: Buffer.from(payReq, 'hex'), + dest_custom_records: { + [keySendPreimageType]: Buffer.from(secret, 'hex'), + }, + } } - // Otherwise, add to paymentsSending in the state. + // Bolt11 invoice payent. else { - // Generate a unique id for the payment that we will use to track the payment across retry attempts. - paymentId = genId() + const invoice = decodePayReq(payReq) + paymentHash = getTag(invoice, 'payment_hash') + payload = { + ...payload, + payment_request: payReq, + } + } + + // If we already have an id then this is a retry. Decrease the retry count. + // Otherwise, add to paymentsSending in the state. + if (originalPaymentId) { + dispatch(decPaymentRetry(originalPaymentId)) + } else { dispatch( sendPayment({ paymentId, - paymentRequest: payReq, + paymentHash, feeLimit, value: amt, remainingRetries: retries, maxRetries: retries, + isKeysend, }) ) } // Submit the payment to LND. try { - const data = await grpc.services.Lightning.sendPayment({ - paymentId, - payment_request: payReq, - amt, - fee_limit: { fixed: feeLimit }, - }) + const data = await grpc.services.Lightning.sendPayment(payload) dispatch(paymentSuccessful(data)) } catch (e) { const { details: data, message: error } = e diff --git a/services/grpc/lightning.methods.js b/services/grpc/lightning.methods.js index 035229ba6eb..33b89fb1fe3 100644 --- a/services/grpc/lightning.methods.js +++ b/services/grpc/lightning.methods.js @@ -268,6 +268,7 @@ async function sendPayment(payload = {}) { return new Promise((resolve, reject) => { try { + logGrpcCmd('Lightning.sendPayment', payload) const call = this.service.sendPayment(payload) call.on('data', data => { diff --git a/test/unit/components/Pay/PayAddressField.spec.js b/test/unit/components/Pay/PayAddressField.spec.js index 6af3d1fa49f..5098f8b1bef 100644 --- a/test/unit/components/Pay/PayAddressField.spec.js +++ b/test/unit/components/Pay/PayAddressField.spec.js @@ -8,7 +8,7 @@ const props = { chain: 'Bitcoin', handlePayReqChange: () => {}, intl: {}, - isLn: null, + isBolt11: null, network: 'testnet', redirectPayReq: {}, formState: { values: {}, submits: 0 }, @@ -26,7 +26,7 @@ describe('component.Pay.PayAddressField', () => { describe('and it is an LN transaction', () => { it('should render correctly', () => { const wrapper = shallow( - + ) expect(toJSON(wrapper)).toMatchSnapshot() }) @@ -35,7 +35,7 @@ describe('component.Pay.PayAddressField', () => { describe('and it is an on-chain transaction', () => { it('should render correctly', () => { const wrapper = shallow( - + ) expect(toJSON(wrapper)).toMatchSnapshot() }) diff --git a/test/unit/components/Pay/PayPanelHeader.spec.js b/test/unit/components/Pay/PayPanelHeader.spec.js index b41cfd9081b..b17247c12ab 100644 --- a/test/unit/components/Pay/PayPanelHeader.spec.js +++ b/test/unit/components/Pay/PayPanelHeader.spec.js @@ -11,7 +11,7 @@ const props = { describe('component.Pay.PayPanelHeader', () => { describe('is an LN transaction', () => { it('should render correctly', () => { - const wrapper = shallow() + const wrapper = shallow() expect(toJSON(wrapper)).toMatchSnapshot() }) }) diff --git a/test/unit/components/Pay/__snapshots__/PayAddressField.spec.js.snap b/test/unit/components/Pay/__snapshots__/PayAddressField.spec.js.snap index e656d9acfb0..8ba7f4eba53 100644 --- a/test/unit/components/Pay/__snapshots__/PayAddressField.spec.js.snap +++ b/test/unit/components/Pay/__snapshots__/PayAddressField.spec.js.snap @@ -22,7 +22,7 @@ exports[`component.Pay.PayAddressField current step is "address" should render c }, "handlePayReqChange": [Function], "intl": Object {}, - "isLn": null, + "isBolt11": null, "network": "testnet", "redirectPayReq": Object {}, }, @@ -61,7 +61,7 @@ exports[`component.Pay.PayAddressField current step is "address" should render c } handlePayReqChange={[Function]} intl={Object {}} - isLn={null} + isBolt11={null} network="testnet" redirectPayReq={Object {}} />, @@ -118,7 +118,7 @@ exports[`component.Pay.PayAddressField current step is "summary" and it is an LN }, "handlePayReqChange": [Function], "intl": Object {}, - "isLn": true, + "isBolt11": true, "network": "testnet", "redirectPayReq": Object {}, }, @@ -157,7 +157,7 @@ exports[`component.Pay.PayAddressField current step is "summary" and it is an LN } handlePayReqChange={[Function]} intl={Object {}} - isLn={true} + isBolt11={true} network="testnet" redirectPayReq={Object {}} />, @@ -214,7 +214,7 @@ exports[`component.Pay.PayAddressField current step is "summary" and it is an on }, "handlePayReqChange": [Function], "intl": Object {}, - "isLn": false, + "isBolt11": false, "network": "testnet", "redirectPayReq": Object {}, }, @@ -253,7 +253,7 @@ exports[`component.Pay.PayAddressField current step is "summary" and it is an on } handlePayReqChange={[Function]} intl={Object {}} - isLn={false} + isBolt11={false} network="testnet" redirectPayReq={Object {}} />, diff --git a/test/unit/components/Pay/__snapshots__/PaySummaryLightning.spec.js.snap b/test/unit/components/Pay/__snapshots__/PaySummaryLightning.spec.js.snap index 5622abeeda8..f0757142e6b 100644 --- a/test/unit/components/Pay/__snapshots__/PaySummaryLightning.spec.js.snap +++ b/test/unit/components/Pay/__snapshots__/PaySummaryLightning.spec.js.snap @@ -138,7 +138,7 @@ exports[`component.Form.PaySummaryLightning should render correctly 1`] = ` mr={2} /> } diff --git a/test/unit/utils/crypto.spec.js b/test/unit/utils/crypto.spec.js index 080da04eb81..3af55e46137 100644 --- a/test/unit/utils/crypto.spec.js +++ b/test/unit/utils/crypto.spec.js @@ -1,7 +1,7 @@ /* eslint-disable max-len */ import { formatValue, - isLn, + isBolt11, isOnchain, getMinFee, getMaxFee, @@ -13,22 +13,22 @@ const VALID_BITCOIN_MAINNET_LN = const VALID_BITCOIN_TESTNET_LN = 'lntb10u1pdue0gxpp5uljstna5aenp3yku3ft4wn8y63qfqdgqfh4cqqxz8z58undqp8hqdqqcqzysxqyz5vqqwaeuuh0fy52tqx6rrq6kya4lwm6v523wyqe9nesd5a3mszcq7j4e9mv8rd2vhmp7ycxswtktvs8gqq8lu5awjwfevnvfc4rzp8fmacpp4h27e' -describe('Crypto.isLn', () => { +describe('Crypto.isBolt11', () => { describe('Bitcoin', () => { describe('Mainnet', () => { it('should pass with a valid invoice ', () => { - expect(isLn(VALID_BITCOIN_MAINNET_LN, 'bitcoin', 'mainnet')).toBeTruthy() + expect(isBolt11(VALID_BITCOIN_MAINNET_LN, 'bitcoin', 'mainnet')).toBeTruthy() }) it('should fail with an invalid invoice ', () => { - expect(isLn(VALID_BITCOIN_TESTNET_LN, 'bitcoin', 'mainnet')).toBeFalsy() + expect(isBolt11(VALID_BITCOIN_TESTNET_LN, 'bitcoin', 'mainnet')).toBeFalsy() }) }) describe('Testnet', () => { it('should pass with a valid invoice', () => { - expect(isLn(VALID_BITCOIN_TESTNET_LN, 'bitcoin', 'testnet')).toBeTruthy() + expect(isBolt11(VALID_BITCOIN_TESTNET_LN, 'bitcoin', 'testnet')).toBeTruthy() }) it('should fail with an invalid invoice ', () => { - expect(isLn(VALID_BITCOIN_MAINNET_LN, 'bitcoin', 'testnet')).toBeFalsy() + expect(isBolt11(VALID_BITCOIN_MAINNET_LN, 'bitcoin', 'testnet')).toBeFalsy() }) }) }) diff --git a/utils/crypto.js b/utils/crypto.js index 3d713eb62a7..aa09455d0ea 100644 --- a/utils/crypto.js +++ b/utils/crypto.js @@ -142,14 +142,14 @@ export const isOnchain = (input, chain, network) => { } /** - * isLn - Test to see if a string is a valid lightning address. + * isBolt11 - Test to see if a string is a valid lightning address. * * @param {string} input Value to check * @param {string} chain Chain name * @param {string} network Network name * @returns {boolean} Boolean indicating whether the address is a lightning address */ -export const isLn = (input, chain = 'bitcoin', network = 'mainnet') => { +export const isBolt11 = (input, chain = 'bitcoin', network = 'mainnet') => { if (!input || typeof input !== 'string') { return false } @@ -164,6 +164,20 @@ export const isLn = (input, chain = 'bitcoin', network = 'mainnet') => { } } +/** + * isPubkey - Test to see if a string is a valid pubkey. + * + * @param {string} input Value to check + * @returns {boolean} Boolean indicating whether the address is a pubkey + */ +export const isPubkey = input => { + if (!input || typeof input !== 'string') { + return false + } + const isHex = /^[0-9a-fA-F]+$/.test(input) + return isHex && input.length === 66 +} + /** * getNodeAlias - Get a nodes alias. *