Browse Source

FIX: capitalized deeplink (closes #1005)

master
Overtorment 5 years ago
parent
commit
d552669dd2
  1. 2
      App.js
  2. 16
      class/deeplink-schema-match.js
  3. 8
      screen/receive/details.js
  4. 23
      screen/send/details.js
  5. 2
      screen/wallets/list.js
  6. 45
      tests/unit/deeplink-schema-match.test.js

2
App.js

@ -10,7 +10,7 @@ import { Chain } from './models/bitcoinUnits';
import QuickActions from 'react-native-quick-actions'; import QuickActions from 'react-native-quick-actions';
import * as Sentry from '@sentry/react-native'; import * as Sentry from '@sentry/react-native';
import OnAppLaunch from './class/onAppLaunch'; import OnAppLaunch from './class/onAppLaunch';
import DeeplinkSchemaMatch from './class/deeplinkSchemaMatch'; import DeeplinkSchemaMatch from './class/deeplink-schema-match';
import BitcoinBIP70TransactionDecode from './bip70/bip70'; import BitcoinBIP70TransactionDecode from './bip70/bip70';
const A = require('./analytics'); const A = require('./analytics');

16
class/deeplinkSchemaMatch.js → class/deeplink-schema-match.js

@ -5,6 +5,7 @@ import RNFS from 'react-native-fs';
import url from 'url'; import url from 'url';
import { Chain } from '../models/bitcoinUnits'; import { Chain } from '../models/bitcoinUnits';
const bitcoin = require('bitcoinjs-lib'); const bitcoin = require('bitcoinjs-lib');
const bip21 = require('bip21');
const BlueApp: AppStorage = require('../BlueApp'); const BlueApp: AppStorage = require('../BlueApp');
class DeeplinkSchemaMatch { class DeeplinkSchemaMatch {
@ -194,6 +195,7 @@ class DeeplinkSchemaMatch {
static isBitcoinAddress(address) { static isBitcoinAddress(address) {
address = address address = address
.replace('bitcoin:', '') .replace('bitcoin:', '')
.replace('BITCOIN:', '')
.replace('bitcoin=', '') .replace('bitcoin=', '')
.split('?')[0]; .split('?')[0];
let isValidBitcoinAddress = false; let isValidBitcoinAddress = false;
@ -228,14 +230,14 @@ class DeeplinkSchemaMatch {
} }
static isBothBitcoinAndLightning(url) { static isBothBitcoinAndLightning(url) {
if (url.includes('lightning') && url.includes('bitcoin')) { if (url.includes('lightning') && (url.includes('bitcoin') || url.includes('BITCOIN'))) {
const txInfo = url.split(/(bitcoin:|lightning:|lightning=|bitcoin=)+/); const txInfo = url.split(/(bitcoin:|BITCOIN:|lightning:|lightning=|bitcoin=)+/);
let bitcoin; let bitcoin;
let lndInvoice; let lndInvoice;
for (const [index, value] of txInfo.entries()) { for (const [index, value] of txInfo.entries()) {
try { try {
// Inside try-catch. We dont wan't to crash in case of an out-of-bounds error. // 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]}`; bitcoin = `bitcoin:${txInfo[index + 1]}`;
if (!DeeplinkSchemaMatch.isBitcoinAddress(bitcoin)) { if (!DeeplinkSchemaMatch.isBitcoinAddress(bitcoin)) {
bitcoin = false; bitcoin = false;
@ -261,6 +263,14 @@ class DeeplinkSchemaMatch {
} }
return undefined; return undefined;
} }
static bip21decode(uri) {
return bip21.decode(uri.replace('BITCOIN:', 'bitcoin:'));
}
static bip21encode() {
return bip21.encode.apply(bip21, arguments);
}
} }
export default DeeplinkSchemaMatch; export default DeeplinkSchemaMatch;

8
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 { View, InteractionManager, Platform, TextInput, KeyboardAvoidingView, Keyboard, StyleSheet, ScrollView } from 'react-native';
import QRCode from 'react-native-qrcode-svg'; import QRCode from 'react-native-qrcode-svg';
import { useNavigation, useNavigationParam } from 'react-navigation-hooks'; import { useNavigation, useNavigationParam } from 'react-navigation-hooks';
import bip21 from 'bip21';
import { import {
BlueLoading, BlueLoading,
SafeBlueArea, SafeBlueArea,
@ -21,6 +20,7 @@ import Share from 'react-native-share';
import { Chain, BitcoinUnit } from '../../models/bitcoinUnits'; import { Chain, BitcoinUnit } from '../../models/bitcoinUnits';
import Modal from 'react-native-modal'; import Modal from 'react-native-modal';
import HandoffSettings from '../../class/handoff'; import HandoffSettings from '../../class/handoff';
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
import Handoff from 'react-native-handoff'; import Handoff from 'react-native-handoff';
/** @type {AppStorage} */ /** @type {AppStorage} */
const BlueApp = require('../../BlueApp'); const BlueApp = require('../../BlueApp');
@ -75,7 +75,7 @@ const ReceiveDetails = () => {
setAddress(wallet.getAddress()); setAddress(wallet.getAddress());
} }
InteractionManager.runAfterInteractions(async () => { InteractionManager.runAfterInteractions(async () => {
const bip21encoded = bip21.encode(address); const bip21encoded = DeeplinkSchemaMatch.bip21encode(address);
setBip21encoded(bip21encoded); setBip21encoded(bip21encoded);
}); });
}, [wallet]); }, [wallet]);
@ -116,7 +116,7 @@ const ReceiveDetails = () => {
const createCustomAmountAddress = () => { const createCustomAmountAddress = () => {
setIsCustom(true); setIsCustom(true);
setIsCustomModalVisible(false); setIsCustomModalVisible(false);
setBip21encoded(bip21.encode(address, { amount: customAmount, label: customLabel })); setBip21encoded(DeeplinkSchemaMatch.bip21encode(address, { amount: customAmount, label: customLabel }));
}; };
const clearCustomAmount = () => { const clearCustomAmount = () => {
@ -124,7 +124,7 @@ const ReceiveDetails = () => {
setIsCustomModalVisible(false); setIsCustomModalVisible(false);
setCustomAmount(''); setCustomAmount('');
setCustomLabel(''); setCustomLabel('');
setBip21encoded(bip21.encode(address)); setBip21encoded(DeeplinkSchemaMatch.bip21encode(address));
}; };
const renderCustomAmountModal = () => { const renderCustomAmountModal = () => {

23
screen/send/details.js

@ -35,18 +35,16 @@ import Modal from 'react-native-modal';
import NetworkTransactionFees, { NetworkTransactionFee } from '../../models/networkTransactionFees'; import NetworkTransactionFees, { NetworkTransactionFee } from '../../models/networkTransactionFees';
import BitcoinBIP70TransactionDecode from '../../bip70/bip70'; import BitcoinBIP70TransactionDecode from '../../bip70/bip70';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits'; 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 ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { BitcoinTransaction } from '../../models/bitcoinTransactionInfo'; import { BitcoinTransaction } from '../../models/bitcoinTransactionInfo';
import DocumentPicker from 'react-native-document-picker'; import DocumentPicker from 'react-native-document-picker';
import RNFS from 'react-native-fs'; import RNFS from 'react-native-fs';
import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch'; import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
const bitcoin = require('bitcoinjs-lib'); const bitcoin = require('bitcoinjs-lib');
const bip21 = require('bip21');
let BigNumber = require('bignumber.js'); let BigNumber = require('bignumber.js');
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
/** @type {AppStorage} */ let BlueApp: AppStorage = require('../../BlueApp');
let BlueApp = require('../../BlueApp');
let loc = require('../../loc'); let loc = require('../../loc');
const btcAddressRx = /^[a-zA-Z0-9]{26,35}$/; 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; if (props.navigation.state.params) fromAddress = props.navigation.state.params.fromAddress;
let fromSecret; let fromSecret;
if (props.navigation.state.params) fromSecret = props.navigation.state.params.fromSecret; if (props.navigation.state.params) fromSecret = props.navigation.state.params.fromSecret;
/** @type {LegacyWallet} */
let fromWallet = null; let fromWallet = null;
if (props.navigation.state.params) fromWallet = props.navigation.state.params.fromWallet; if (props.navigation.state.params) fromWallet = props.navigation.state.params.fromWallet;
@ -136,12 +135,10 @@ export default class SendDetails extends Component {
bip70TransactionExpiration: bip70.bip70TransactionExpiration, bip70TransactionExpiration: bip70.bip70TransactionExpiration,
}); });
} else { } else {
console.warn('2');
let recipients = this.state.addresses; let recipients = this.state.addresses;
const dataWithoutSchema = data.replace('bitcoin:', ''); const dataWithoutSchema = data.replace('bitcoin:', '').replace('BITCOIN:', '');
if ( if (this.state.fromWallet.isAddressValid(dataWithoutSchema)) {
btcAddressRx.test(dataWithoutSchema) ||
((dataWithoutSchema.indexOf('bc1') === 0 || dataWithoutSchema.indexOf('BC1') === 0) && dataWithoutSchema.indexOf('?') === -1)
) {
recipients[[this.state.recipientsScrollIndex]].address = dataWithoutSchema; recipients[[this.state.recipientsScrollIndex]].address = dataWithoutSchema;
this.setState({ this.setState({
address: recipients, address: recipients,
@ -155,12 +152,12 @@ export default class SendDetails extends Component {
if (!data.toLowerCase().startsWith('bitcoin:')) { if (!data.toLowerCase().startsWith('bitcoin:')) {
data = `bitcoin:${data}`; data = `bitcoin:${data}`;
} }
const decoded = bip21.decode(data); const decoded = DeeplinkSchemaMatch.bip21decode(data);
address = decoded.address; address = decoded.address;
options = decoded.options; options = decoded.options;
} catch (error) { } catch (error) {
data = data.replace(/(amount)=([^&]+)/g, '').replace(/(amount)=([^&]+)&/g, ''); data = data.replace(/(amount)=([^&]+)/g, '').replace(/(amount)=([^&]+)&/g, '');
const decoded = bip21.decode(data); const decoded = DeeplinkSchemaMatch.bip21decode(data);
decoded.options.amount = 0; decoded.options.amount = 0;
address = decoded.address; address = decoded.address;
options = decoded.options; options = decoded.options;
@ -277,7 +274,7 @@ export default class SendDetails extends Component {
let address = uri || ''; let address = uri || '';
let memo = ''; let memo = '';
try { try {
parsedBitcoinUri = bip21.decode(uri); parsedBitcoinUri = DeeplinkSchemaMatch.bip21decode(uri);
address = parsedBitcoinUri.hasOwnProperty('address') ? parsedBitcoinUri.address : address; address = parsedBitcoinUri.hasOwnProperty('address') ? parsedBitcoinUri.address : address;
if (parsedBitcoinUri.hasOwnProperty('options')) { if (parsedBitcoinUri.hasOwnProperty('options')) {
if (parsedBitcoinUri.options.hasOwnProperty('amount')) { if (parsedBitcoinUri.options.hasOwnProperty('amount')) {

2
screen/wallets/list.js

@ -20,7 +20,7 @@ import { PlaceholderWallet } from '../../class';
import WalletImport from '../../class/walletImport'; import WalletImport from '../../class/walletImport';
import ViewPager from '@react-native-community/viewpager'; import ViewPager from '@react-native-community/viewpager';
import ScanQRCode from '../send/ScanQRCode'; import ScanQRCode from '../send/ScanQRCode';
import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch'; import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
let EV = require('../../events'); let EV = require('../../events');
let A = require('../../analytics'); let A = require('../../analytics');
/** @type {AppStorage} */ /** @type {AppStorage} */

45
tests/unit/deepLinkSchemaMatch.test.js → tests/unit/deeplink-schema-match.test.js

@ -1,5 +1,5 @@
/* global describe, it */ /* global describe, it */
import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch'; import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
const assert = require('assert'); const assert = require('assert');
describe('unit - DeepLinkSchemaMatch', function() { describe('unit - DeepLinkSchemaMatch', function() {
@ -7,12 +7,18 @@ describe('unit - DeepLinkSchemaMatch', function() {
assert.ok(DeeplinkSchemaMatch.hasSchema('bitcoin:12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG')); 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: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', () => { it('isBitcoin Address', () => {
assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG')); assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'));
assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK'));
assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('bc1qykcp2x3djgdtdwelxn9z4j2y956npte0a4sref')); assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('bc1qykcp2x3djgdtdwelxn9z4j2y956npte0a4sref'));
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', () => { it('isLighting Invoice', () => {
@ -26,7 +32,12 @@ describe('unit - DeepLinkSchemaMatch', function() {
it('isBoth Bitcoin & Invoice', () => { it('isBoth Bitcoin & Invoice', () => {
assert.ok( assert.ok(
DeeplinkSchemaMatch.isBothBitcoinAndLightning( 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');
});
}); });
Loading…
Cancel
Save