diff --git a/App.js b/App.js index f80b99c4..4d441fa7 100644 --- a/App.js +++ b/App.js @@ -10,7 +10,7 @@ import { Chain } from './models/bitcoinUnits'; import QuickActions from 'react-native-quick-actions'; import * as Sentry from '@sentry/react-native'; import OnAppLaunch from './class/onAppLaunch'; -import DeeplinkSchemaMatch from './class/deeplinkSchemaMatch'; +import DeeplinkSchemaMatch from './class/deeplink-schema-match'; import BitcoinBIP70TransactionDecode from './bip70/bip70'; const A = require('./analytics'); diff --git a/class/deeplinkSchemaMatch.js b/class/deeplink-schema-match.js similarity index 94% rename from class/deeplinkSchemaMatch.js rename to class/deeplink-schema-match.js index 5cbbd405..597bfb3d 100644 --- a/class/deeplinkSchemaMatch.js +++ b/class/deeplink-schema-match.js @@ -5,6 +5,7 @@ import RNFS from 'react-native-fs'; import url from 'url'; import { Chain } from '../models/bitcoinUnits'; const bitcoin = require('bitcoinjs-lib'); +const bip21 = require('bip21'); const BlueApp: AppStorage = require('../BlueApp'); class DeeplinkSchemaMatch { @@ -194,6 +195,7 @@ class DeeplinkSchemaMatch { static isBitcoinAddress(address) { address = address .replace('bitcoin:', '') + .replace('BITCOIN:', '') .replace('bitcoin=', '') .split('?')[0]; let isValidBitcoinAddress = false; @@ -228,14 +230,14 @@ class DeeplinkSchemaMatch { } static isBothBitcoinAndLightning(url) { - if (url.includes('lightning') && url.includes('bitcoin')) { - const txInfo = url.split(/(bitcoin:|lightning:|lightning=|bitcoin=)+/); + if (url.includes('lightning') && (url.includes('bitcoin') || url.includes('BITCOIN'))) { + const txInfo = url.split(/(bitcoin:|BITCOIN:|lightning:|lightning=|bitcoin=)+/); let bitcoin; let lndInvoice; for (const [index, value] of txInfo.entries()) { try { // Inside try-catch. We dont wan't to crash in case of an out-of-bounds error. - if (value.startsWith('bitcoin')) { + if (value.startsWith('bitcoin') || value.startsWith('BITCOIN')) { bitcoin = `bitcoin:${txInfo[index + 1]}`; if (!DeeplinkSchemaMatch.isBitcoinAddress(bitcoin)) { bitcoin = false; @@ -261,6 +263,14 @@ class DeeplinkSchemaMatch { } return undefined; } + + static bip21decode(uri) { + return bip21.decode(uri.replace('BITCOIN:', 'bitcoin:')); + } + + static bip21encode() { + return bip21.encode.apply(bip21, arguments); + } } export default DeeplinkSchemaMatch; diff --git a/screen/receive/details.js b/screen/receive/details.js index 9be52613..409d8ead 100644 --- a/screen/receive/details.js +++ b/screen/receive/details.js @@ -2,7 +2,6 @@ import React, { useEffect, useState, useCallback } from 'react'; import { View, InteractionManager, Platform, TextInput, KeyboardAvoidingView, Keyboard, StyleSheet, ScrollView } from 'react-native'; import QRCode from 'react-native-qrcode-svg'; import { useNavigation, useNavigationParam } from 'react-navigation-hooks'; -import bip21 from 'bip21'; import { BlueLoading, SafeBlueArea, @@ -21,6 +20,7 @@ import Share from 'react-native-share'; import { Chain, BitcoinUnit } from '../../models/bitcoinUnits'; import Modal from 'react-native-modal'; import HandoffSettings from '../../class/handoff'; +import DeeplinkSchemaMatch from '../../class/deeplink-schema-match'; import Handoff from 'react-native-handoff'; /** @type {AppStorage} */ const BlueApp = require('../../BlueApp'); @@ -75,7 +75,7 @@ const ReceiveDetails = () => { setAddress(wallet.getAddress()); } InteractionManager.runAfterInteractions(async () => { - const bip21encoded = bip21.encode(address); + const bip21encoded = DeeplinkSchemaMatch.bip21encode(address); setBip21encoded(bip21encoded); }); }, [wallet]); @@ -116,7 +116,7 @@ const ReceiveDetails = () => { const createCustomAmountAddress = () => { setIsCustom(true); setIsCustomModalVisible(false); - setBip21encoded(bip21.encode(address, { amount: customAmount, label: customLabel })); + setBip21encoded(DeeplinkSchemaMatch.bip21encode(address, { amount: customAmount, label: customLabel })); }; const clearCustomAmount = () => { @@ -124,7 +124,7 @@ const ReceiveDetails = () => { setIsCustomModalVisible(false); setCustomAmount(''); setCustomLabel(''); - setBip21encoded(bip21.encode(address)); + setBip21encoded(DeeplinkSchemaMatch.bip21encode(address)); }; const renderCustomAmountModal = () => { diff --git a/screen/send/details.js b/screen/send/details.js index 19f58622..44d08fcb 100644 --- a/screen/send/details.js +++ b/screen/send/details.js @@ -35,18 +35,16 @@ import Modal from 'react-native-modal'; import NetworkTransactionFees, { NetworkTransactionFee } from '../../models/networkTransactionFees'; import BitcoinBIP70TransactionDecode from '../../bip70/bip70'; import { BitcoinUnit, Chain } from '../../models/bitcoinUnits'; -import { HDSegwitBech32Wallet, LightningCustodianWallet, WatchOnlyWallet } from '../../class'; +import { AppStorage, HDSegwitBech32Wallet, LightningCustodianWallet, WatchOnlyWallet } from '../../class'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import { BitcoinTransaction } from '../../models/bitcoinTransactionInfo'; import DocumentPicker from 'react-native-document-picker'; import RNFS from 'react-native-fs'; -import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch'; +import DeeplinkSchemaMatch from '../../class/deeplink-schema-match'; const bitcoin = require('bitcoinjs-lib'); -const bip21 = require('bip21'); let BigNumber = require('bignumber.js'); const { width } = Dimensions.get('window'); -/** @type {AppStorage} */ -let BlueApp = require('../../BlueApp'); +let BlueApp: AppStorage = require('../../BlueApp'); let loc = require('../../loc'); const btcAddressRx = /^[a-zA-Z0-9]{26,35}$/; @@ -73,6 +71,7 @@ export default class SendDetails extends Component { if (props.navigation.state.params) fromAddress = props.navigation.state.params.fromAddress; let fromSecret; if (props.navigation.state.params) fromSecret = props.navigation.state.params.fromSecret; + /** @type {LegacyWallet} */ let fromWallet = null; if (props.navigation.state.params) fromWallet = props.navigation.state.params.fromWallet; @@ -136,12 +135,10 @@ export default class SendDetails extends Component { bip70TransactionExpiration: bip70.bip70TransactionExpiration, }); } else { + console.warn('2'); let recipients = this.state.addresses; - const dataWithoutSchema = data.replace('bitcoin:', ''); - if ( - btcAddressRx.test(dataWithoutSchema) || - ((dataWithoutSchema.indexOf('bc1') === 0 || dataWithoutSchema.indexOf('BC1') === 0) && dataWithoutSchema.indexOf('?') === -1) - ) { + const dataWithoutSchema = data.replace('bitcoin:', '').replace('BITCOIN:', ''); + if (this.state.fromWallet.isAddressValid(dataWithoutSchema)) { recipients[[this.state.recipientsScrollIndex]].address = dataWithoutSchema; this.setState({ address: recipients, @@ -155,12 +152,12 @@ export default class SendDetails extends Component { if (!data.toLowerCase().startsWith('bitcoin:')) { data = `bitcoin:${data}`; } - const decoded = bip21.decode(data); + const decoded = DeeplinkSchemaMatch.bip21decode(data); address = decoded.address; options = decoded.options; } catch (error) { data = data.replace(/(amount)=([^&]+)/g, '').replace(/(amount)=([^&]+)&/g, ''); - const decoded = bip21.decode(data); + const decoded = DeeplinkSchemaMatch.bip21decode(data); decoded.options.amount = 0; address = decoded.address; options = decoded.options; @@ -277,7 +274,7 @@ export default class SendDetails extends Component { let address = uri || ''; let memo = ''; try { - parsedBitcoinUri = bip21.decode(uri); + parsedBitcoinUri = DeeplinkSchemaMatch.bip21decode(uri); address = parsedBitcoinUri.hasOwnProperty('address') ? parsedBitcoinUri.address : address; if (parsedBitcoinUri.hasOwnProperty('options')) { if (parsedBitcoinUri.options.hasOwnProperty('amount')) { diff --git a/screen/wallets/list.js b/screen/wallets/list.js index 787daa09..b60222bd 100644 --- a/screen/wallets/list.js +++ b/screen/wallets/list.js @@ -20,7 +20,7 @@ import { PlaceholderWallet } from '../../class'; import WalletImport from '../../class/walletImport'; import ViewPager from '@react-native-community/viewpager'; import ScanQRCode from '../send/ScanQRCode'; -import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch'; +import DeeplinkSchemaMatch from '../../class/deeplink-schema-match'; let EV = require('../../events'); let A = require('../../analytics'); /** @type {AppStorage} */ diff --git a/tests/unit/deepLinkSchemaMatch.test.js b/tests/unit/deeplink-schema-match.test.js similarity index 51% rename from tests/unit/deepLinkSchemaMatch.test.js rename to tests/unit/deeplink-schema-match.test.js index 45de7c76..2b844276 100644 --- a/tests/unit/deepLinkSchemaMatch.test.js +++ b/tests/unit/deeplink-schema-match.test.js @@ -1,5 +1,5 @@ /* global describe, it */ -import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch'; +import DeeplinkSchemaMatch from '../../class/deeplink-schema-match'; const assert = require('assert'); describe('unit - DeepLinkSchemaMatch', function() { @@ -7,12 +7,18 @@ describe('unit - DeepLinkSchemaMatch', function() { assert.ok(DeeplinkSchemaMatch.hasSchema('bitcoin:12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG')); assert.ok(DeeplinkSchemaMatch.hasSchema('bitcoin:bc1qh6tf004ty7z7un2v5ntu4mkf630545gvhs45u7?amount=666&label=Yo')); assert.ok(DeeplinkSchemaMatch.hasSchema('bitcoin:BC1QH6TF004TY7Z7UN2V5NTU4MKF630545GVHS45U7?amount=666&label=Yo')); + assert.ok(DeeplinkSchemaMatch.hasSchema('BITCOIN:BC1Q3RL0MKYK0ZRTXFMQN9WPCD3GNAZ00YV9YP0HXE')); + assert.ok(DeeplinkSchemaMatch.hasSchema('BITCOIN:BC1Q3RL0MKYK0ZRTXFMQN9WPCD3GNAZ00YV9YP0HXE?amount=666&label=Yo')); }); it('isBitcoin Address', () => { assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG')); + assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK')); assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('bc1qykcp2x3djgdtdwelxn9z4j2y956npte0a4sref')); assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('BC1QYKCP2X3DJGDTDWELXN9Z4J2Y956NPTE0A4SREF')); + assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('bitcoin:BC1QH6TF004TY7Z7UN2V5NTU4MKF630545GVHS45U7')); + assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('BITCOIN:BC1Q3RL0MKYK0ZRTXFMQN9WPCD3GNAZ00YV9YP0HXE')); + assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('BITCOIN:BC1Q3RL0MKYK0ZRTXFMQN9WPCD3GNAZ00YV9YP0HXE?amount=666&label=Yo')); }); it('isLighting Invoice', () => { @@ -26,7 +32,12 @@ describe('unit - DeepLinkSchemaMatch', function() { it('isBoth Bitcoin & Invoice', () => { assert.ok( DeeplinkSchemaMatch.isBothBitcoinAndLightning( - 'bitcoin:1DamianM2k8WfNEeJmyqSe2YW1upB7UATx?amount=0.000001&lightning=lnbc1u1pwry044pp53xlmkghmzjzm3cljl6729cwwqz5hhnhevwfajpkln850n7clft4sdqlgfy4qv33ypmj7sj0f32rzvfqw3jhxaqcqzysxq97zvuq5zy8ge6q70prnvgwtade0g2k5h2r76ws7j2926xdjj2pjaq6q3r4awsxtm6k5prqcul73p3atveljkn6wxdkrcy69t6k5edhtc6q7lgpe4m5k4', + 'bitcoin:BC1Q3RL0MKYK0ZRTXFMQN9WPCD3GNAZ00YV9YP0HXE?amount=0.000001&lightning=lnbc1u1pwry044pp53xlmkghmzjzm3cljl6729cwwqz5hhnhevwfajpkln850n7clft4sdqlgfy4qv33ypmj7sj0f32rzvfqw3jhxaqcqzysxq97zvuq5zy8ge6q70prnvgwtade0g2k5h2r76ws7j2926xdjj2pjaq6q3r4awsxtm6k5prqcul73p3atveljkn6wxdkrcy69t6k5edhtc6q7lgpe4m5k4', + ), + ); + assert.ok( + DeeplinkSchemaMatch.isBothBitcoinAndLightning( + 'BITCOIN:12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG?amount=0.000001&lightning=lnbc1u1pwry044pp53xlmkghmzjzm3cljl6729cwwqz5hhnhevwfajpkln850n7clft4sdqlgfy4qv33ypmj7sj0f32rzvfqw3jhxaqcqzysxq97zvuq5zy8ge6q70prnvgwtade0g2k5h2r76ws7j2926xdjj2pjaq6q3r4awsxtm6k5prqcul73p3atveljkn6wxdkrcy69t6k5edhtc6q7lgpe4m5k4', ), ); }); @@ -55,4 +66,34 @@ describe('unit - DeepLinkSchemaMatch', function() { }); }); }); + + it('decodes bip21', () => { + let decoded = DeeplinkSchemaMatch.bip21decode('bitcoin:1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH?amount=20.3&label=Foobar'); + assert.deepStrictEqual(decoded, { + address: '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH', + options: { + amount: 20.3, + label: 'Foobar', + }, + }); + + decoded = DeeplinkSchemaMatch.bip21decode('BITCOIN:1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH?amount=20.3&label=Foobar'); + assert.deepStrictEqual(decoded, { + address: '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH', + options: { + amount: 20.3, + label: 'Foobar', + }, + }); + }); + + it('encodes bip21', () => { + let encoded = DeeplinkSchemaMatch.bip21encode('1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH'); + assert.strictEqual(encoded, 'bitcoin:1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH'); + encoded = DeeplinkSchemaMatch.bip21encode('1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH', { + amount: 20.3, + label: 'Foobar', + }); + assert.strictEqual(encoded, 'bitcoin:1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH?amount=20.3&label=Foobar'); + }); });