DEL: BIP70

This commit is contained in:
marcosrdz 2020-06-14 21:15:58 -04:00 committed by Overtorment
parent a43d21241b
commit 35b4a30bdf
5 changed files with 58 additions and 211 deletions

4
App.js
View File

@ -14,7 +14,6 @@ import QuickActions from 'react-native-quick-actions';
import * as Sentry from '@sentry/react-native';
import OnAppLaunch from './class/on-app-launch';
import DeeplinkSchemaMatch from './class/deeplink-schema-match';
import BitcoinBIP70TransactionDecode from './bip70/bip70';
const A = require('./analytics');
if (process.env.NODE_ENV !== 'development') {
@ -116,8 +115,7 @@ export default class App extends React.Component {
return wallet.isInvoiceGeneratedByWallet(clipboard) || wallet.weOwnAddress(clipboard);
}
});
const isBitcoinAddress =
DeeplinkSchemaMatch.isBitcoinAddress(clipboard) || BitcoinBIP70TransactionDecode.matchesPaymentURL(clipboard);
const isBitcoinAddress = DeeplinkSchemaMatch.isBitcoinAddress(clipboard);
const isLightningInvoice = DeeplinkSchemaMatch.isLightningInvoice(clipboard);
const isLNURL = DeeplinkSchemaMatch.isLnUrl(clipboard);
const isBothBitcoinAndLightning = DeeplinkSchemaMatch.isBothBitcoinAndLightning(clipboard);

View File

@ -1,85 +0,0 @@
import Frisbee from 'frisbee';
export class BitcoinBIP70Transaction {
constructor(amount, address, memo, fee, expires) {
this.amount = amount;
this.address = address;
this.memo = memo;
this.fee = fee;
this.expires = expires;
}
}
export class BitcoinBIP70TransactionError {
constructor(errorMessage) {
this.errorMessage = errorMessage;
}
}
export default class BitcoinBIP70TransactionDecode {
static decode(data) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
try {
let url;
if (data.match(/bitcoin:\?r=https?:\/\/\S+/gi)) {
url = data.toString().split('bitcoin:?r=')[1];
} else if (data.startsWith('https://bitpay.com/i/') || data.startsWith('https://www.bitpay.com/i/')) {
url = data.toString();
}
const api = new Frisbee({
baseURI: url,
headers: {
Accept: 'application/payment-request',
},
});
const response = await api.get();
if (response && response.body) {
const parsedJSON = JSON.parse(response.body);
// Check that the invoice has not expired
const expires = new Date(parsedJSON.expires).getTime();
const now = new Date().getTime();
if (now > expires) {
throw new BitcoinBIP70TransactionError('This invoice has expired.');
}
//
const decodedTransaction = new BitcoinBIP70Transaction(
parsedJSON.outputs[0].amount,
parsedJSON.outputs[0].address,
parsedJSON.memo,
parsedJSON.requiredFeeRate.toFixed(0),
parsedJSON.expires,
);
console.log(decodedTransaction);
resolve(decodedTransaction);
} else {
console.log('Could not fetch transaction details: ' + response.err);
throw new BitcoinBIP70TransactionError('Unable to fetch transaction details. Please, make sure the provided link is valid.');
}
} catch (err) {
console.warn(err);
reject(err);
}
});
}
static isExpired(transactionExpires) {
if (transactionExpires === null) {
return false;
}
const expires = new Date(transactionExpires).getTime();
const now = new Date().getTime();
return now > expires;
}
static matchesPaymentURL(data) {
return (
data !== null &&
(data.match(/bitcoin:\?r=https?:\/\/\S+/gi) !== null ||
data.startsWith('https://bitpay.com/i/') ||
data.startsWith('https://www.bitpay.com/i/'))
);
}
}

View File

@ -1,6 +1,5 @@
import { AppStorage, LightningCustodianWallet } from './';
import AsyncStorage from '@react-native-community/async-storage';
import BitcoinBIP70TransactionDecode from '../bip70/bip70';
import RNFS from 'react-native-fs';
import url from 'url';
import { Chain } from '../models/bitcoinUnits';
@ -76,7 +75,7 @@ class DeeplinkSchemaMatch {
},
},
]);
} else if (DeeplinkSchemaMatch.isBitcoinAddress(event.url) || BitcoinBIP70TransactionDecode.matchesPaymentURL(event.url)) {
} else if (DeeplinkSchemaMatch.isBitcoinAddress(event.url)) {
completionHandler([
'SendDetailsRoot',
{

View File

@ -40,7 +40,7 @@
"e2e:release": "detox build -c android.emu.release; npm run e2e:release-no-build",
"e2e:release-no-build": "detox test -c android.emu.release --record-videos all --take-screenshots all --headless",
"e2e:debug": "(test -f android/app/build/outputs/apk/debug/app-debug.apk && test -f android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk) || detox build -c android.emu.debug; detox test -c android.emu.debug",
"lint": "eslint *.js screen/**/*.js bip70/ blue_modules/crypto.js class/**/*.js models/ loc/ tests/**/*.js",
"lint": "eslint *.js screen/**/*.js blue_modules/crypto.js class/**/*.js models/ loc/ tests/**/*.js",
"lint:fix": "npm run lint -- --fix",
"lint:quickfix": "git status --porcelain | grep -v '\\.json' | grep '\\.js' --color=never | awk '{print $2}' | xargs eslint --fix; exit 0",
"unit": "jest tests/unit/*"

View File

@ -33,7 +33,6 @@ import Slider from '@react-native-community/slider';
import PropTypes from 'prop-types';
import Modal from 'react-native-modal';
import NetworkTransactionFees, { NetworkTransactionFee } from '../../models/networkTransactionFees';
import BitcoinBIP70TransactionDecode from '../../bip70/bip70';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import { AppStorage, HDSegwitBech32Wallet, LightningCustodianWallet, WatchOnlyWallet } from '../../class';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
@ -263,7 +262,6 @@ export default class SendDetails extends Component {
fee: 1,
feeSliderValue: 1,
amountUnit: fromWallet.preferredBalanceUnit, // default for whole screen
bip70TransactionExpiration: null,
renderWalletSelectionButtonHidden: false,
};
}
@ -286,66 +284,51 @@ export default class SendDetails extends Component {
*/
processAddressData = data => {
this.setState({ isLoading: true }, async () => {
if (BitcoinBIP70TransactionDecode.matchesPaymentURL(data)) {
const bip70 = await this.processBIP70Invoice(data);
const recipients = this.state.addresses;
const dataWithoutSchema = data.replace('bitcoin:', '').replace('BITCOIN:', '');
if (this.state.fromWallet.isAddressValid(dataWithoutSchema)) {
recipients[[this.state.recipientsScrollIndex]].address = dataWithoutSchema;
const units = this.state.units;
units[this.state.recipientsScrollIndex] = BitcoinUnit.BTC; // also resetting current unit to BTC
this.setState({
addresses: [bip70.recipient],
memo: bip70.memo,
feeSliderValue: bip70.feeSliderValue,
fee: bip70.fee,
address: recipients,
isLoading: false,
amountUnit: BitcoinUnit.BTC,
bip70TransactionExpiration: bip70.bip70TransactionExpiration,
units,
});
} else {
const recipients = this.state.addresses;
const dataWithoutSchema = data.replace('bitcoin:', '').replace('BITCOIN:', '');
if (this.state.fromWallet.isAddressValid(dataWithoutSchema)) {
recipients[[this.state.recipientsScrollIndex]].address = dataWithoutSchema;
let address = '';
let options;
try {
if (!data.toLowerCase().startsWith('bitcoin:')) {
data = `bitcoin:${data}`;
}
const decoded = DeeplinkSchemaMatch.bip21decode(data);
address = decoded.address;
options = decoded.options;
} catch (error) {
data = data.replace(/(amount)=([^&]+)/g, '').replace(/(amount)=([^&]+)&/g, '');
const decoded = DeeplinkSchemaMatch.bip21decode(data);
decoded.options.amount = 0;
address = decoded.address;
options = decoded.options;
this.setState({ isLoading: false });
}
console.log(options);
if (btcAddressRx.test(address) || address.indexOf('bc1') === 0 || address.indexOf('BC1') === 0) {
const units = this.state.units;
units[this.state.recipientsScrollIndex] = BitcoinUnit.BTC; // also resetting current unit to BTC
recipients[[this.state.recipientsScrollIndex]].address = address;
recipients[[this.state.recipientsScrollIndex]].amount = options.amount;
this.setState({
address: recipients,
bip70TransactionExpiration: null,
addresses: recipients,
memo: options.label || options.message,
isLoading: false,
amountUnit: BitcoinUnit.BTC,
units,
});
} else {
let address = '';
let options;
try {
if (!data.toLowerCase().startsWith('bitcoin:')) {
data = `bitcoin:${data}`;
}
const decoded = DeeplinkSchemaMatch.bip21decode(data);
address = decoded.address;
options = decoded.options;
} catch (error) {
data = data.replace(/(amount)=([^&]+)/g, '').replace(/(amount)=([^&]+)&/g, '');
const decoded = DeeplinkSchemaMatch.bip21decode(data);
decoded.options.amount = 0;
address = decoded.address;
options = decoded.options;
this.setState({ isLoading: false });
}
console.log(options);
if (btcAddressRx.test(address) || address.indexOf('bc1') === 0 || address.indexOf('BC1') === 0) {
const units = this.state.units;
units[this.state.recipientsScrollIndex] = BitcoinUnit.BTC; // also resetting current unit to BTC
recipients[[this.state.recipientsScrollIndex]].address = address;
recipients[[this.state.recipientsScrollIndex]].amount = options.amount;
this.setState({
addresses: recipients,
memo: options.label || options.message,
bip70TransactionExpiration: null,
isLoading: false,
amountUnit: BitcoinUnit.BTC,
units,
});
} else {
this.setState({ isLoading: false });
}
this.setState({ isLoading: false });
}
}
});
@ -359,21 +342,14 @@ export default class SendDetails extends Component {
let initialMemo = '';
if (this.props.route.params.uri) {
const uri = this.props.route.params.uri;
if (BitcoinBIP70TransactionDecode.matchesPaymentURL(uri)) {
const { recipient, memo, fee, feeSliderValue } = await this.processBIP70Invoice(uri);
addresses.push(recipient);
try {
const { address, amount, memo } = this.decodeBitcoinUri(uri);
addresses.push(new BitcoinTransaction(address, amount, currency.btcToSatoshi(amount)));
initialMemo = memo;
this.setState({ addresses, memo: initialMemo, fee, feeSliderValue, isLoading: false, amountUnit: BitcoinUnit.BTC });
} else {
try {
const { address, amount, memo } = this.decodeBitcoinUri(uri);
addresses.push(new BitcoinTransaction(address, amount, currency.btcToSatoshi(amount)));
initialMemo = memo;
this.setState({ addresses, memo: initialMemo, isLoading: false, amountUnit: BitcoinUnit.BTC });
} catch (error) {
console.log(error);
alert('Error: Unable to decode Bitcoin address');
}
this.setState({ addresses, memo: initialMemo, isLoading: false, amountUnit: BitcoinUnit.BTC });
} catch (error) {
console.log(error);
alert('Error: Unable to decode Bitcoin address');
}
} else if (this.props.route.params.address) {
addresses.push(new BitcoinTransaction(this.props.route.params.address));
@ -406,17 +382,13 @@ export default class SendDetails extends Component {
});
if (this.props.route.params.uri) {
if (BitcoinBIP70TransactionDecode.matchesPaymentURL(this.props.route.params.uri)) {
this.processBIP70Invoice(this.props.route.params.uri);
} else {
try {
const { address, amount, memo } = this.decodeBitcoinUri(this.props.route.params.uri);
this.setState({ address, amount, memo, isLoading: false });
} catch (error) {
console.log(error);
this.setState({ isLoading: false });
alert('Error: Unable to decode Bitcoin address');
}
try {
const { address, amount, memo } = this.decodeBitcoinUri(this.props.route.params.uri);
this.setState({ address, amount, memo, isLoading: false });
} catch (error) {
console.log(error);
this.setState({ isLoading: false });
alert('Error: Unable to decode Bitcoin address');
}
}
} else {
@ -459,32 +431,6 @@ export default class SendDetails extends Component {
return { address, amount, memo };
}
async processBIP70Invoice(text) {
try {
if (BitcoinBIP70TransactionDecode.matchesPaymentURL(text)) {
Keyboard.dismiss();
return BitcoinBIP70TransactionDecode.decode(text)
.then(response => {
const recipient = new BitcoinTransaction(response.address, currency.satoshiToBTC(response.amount), response.amount);
return {
recipient,
memo: response.memo,
fee: response.fee,
feeSliderValue: response.fee,
bip70TransactionExpiration: response.expires,
};
})
.catch(error => {
alert(error.errorMessage);
throw error;
});
}
} catch (error) {
return false;
}
throw new Error('BIP70: Unable to process.');
}
async createTransaction() {
Keyboard.dismiss();
this.setState({ isLoading: true });
@ -504,9 +450,6 @@ export default class SendDetails extends Component {
// first sanity check is that sending amount is not bigger than available balance
error = loc.send.details.total_exceeds_balance;
console.log('validation error');
} else if (BitcoinBIP70TransactionDecode.isExpired(this.state.bip70TransactionExpiration)) {
error = 'Transaction has expired.';
console.log('validation error');
} else if (transaction.address) {
const address = transaction.address.trim().toLowerCase();
if (address.startsWith('lnb') || address.startsWith('lightning:lnb')) {
@ -986,23 +929,15 @@ export default class SendDetails extends Component {
onChangeText={async text => {
text = text.trim();
const transactions = this.state.addresses;
try {
const { recipient, memo, fee, feeSliderValue } = await this.processBIP70Invoice(text);
transactions[index].address = recipient.address;
transactions[index].amount = recipient.amount;
this.setState({ addresses: transactions, memo: memo, fee, feeSliderValue, isLoading: false });
} catch (_e) {
const { address, amount, memo } = this.decodeBitcoinUri(text);
item.address = address || text;
item.amount = amount || item.amount;
transactions[index] = item;
this.setState({
addresses: transactions,
memo: memo || this.state.memo,
isLoading: false,
bip70TransactionExpiration: null,
});
}
const { address, amount, memo } = this.decodeBitcoinUri(text);
item.address = address || text;
item.amount = amount || item.amount;
transactions[index] = item;
this.setState({
addresses: transactions,
memo: memo || this.state.memo,
isLoading: false,
});
}}
onBarScanned={this.processAddressData}
address={item.address}