Merge branch 'master' into snyk-upgrade-b1100653bd03b2f06bd39f36add6adf7

This commit is contained in:
marcosrdz 2020-09-22 08:39:20 -04:00
commit f478bab16e
16 changed files with 416 additions and 192 deletions

View file

@ -323,6 +323,31 @@ class DeeplinkSchemaMatch {
static bip21encode() {
return bip21.encode.apply(bip21, arguments);
}
static decodeBitcoinUri(uri) {
let amount = '';
let parsedBitcoinUri = null;
let address = uri || '';
let memo = '';
let payjoinUrl = '';
try {
parsedBitcoinUri = DeeplinkSchemaMatch.bip21decode(uri);
address = 'address' in parsedBitcoinUri ? parsedBitcoinUri.address : address;
if ('options' in parsedBitcoinUri) {
if ('amount' in parsedBitcoinUri.options) {
amount = parsedBitcoinUri.options.amount.toString();
amount = parsedBitcoinUri.options.amount;
}
if ('label' in parsedBitcoinUri.options) {
memo = parsedBitcoinUri.options.label || memo;
}
if ('pj' in parsedBitcoinUri.options) {
payjoinUrl = parsedBitcoinUri.options.pj;
}
}
} catch (_) {}
return { address, amount, memo, payjoinUrl };
}
}
export default DeeplinkSchemaMatch;

View file

@ -214,6 +214,32 @@ export class HDSegwitBech32Transaction {
return { fee, feeRate, targets, changeAmount, utxos, unconfirmedUtxos };
}
/**
* We get _all_ our UTXOs (even spent kek),
* and see if each input in this transaction's UTXO is in there. If its not there - its an unknown
* input, we dont own it (possibly a payjoin transaction), and we cant do RBF
*
* @returns {Promise<boolean>}
*/
async thereAreUnknownInputsInTx() {
if (!this._wallet) throw new Error('Wallet required for this method');
if (!this._txDecoded) await this._fetchTxhexAndDecode();
const spentUtxos = this._wallet.getDerivedUtxoFromOurTransaction(true);
for (const inp of this._txDecoded.ins) {
const txidInUtxo = reverse(inp.hash).toString('hex');
let found = false;
for (const spentU of spentUtxos) {
if (spentU.txid === txidInUtxo && spentU.vout === inp.index) found = true;
}
if (!found) {
return true;
}
}
}
/**
* Checks if all outputs belong to us, that
* means we already canceled this tx and we can only bump fees
@ -224,6 +250,8 @@ export class HDSegwitBech32Transaction {
if (!this._wallet) throw new Error('Wallet required for this method');
if (!this._txDecoded) await this._fetchTxhexAndDecode();
if (await this.thereAreUnknownInputsInTx()) return false;
// if theres at least one output we dont own - we can cancel this transaction!
for (const outp of this._txDecoded.outs) {
if (!this._wallet.weOwnAddress(SegwitBech32Wallet.scriptPubKeyToAddress(outp.script))) return true;
@ -232,6 +260,15 @@ export class HDSegwitBech32Transaction {
return false;
}
async canBumpTx() {
if (!this._wallet) throw new Error('Wallet required for this method');
if (!this._txDecoded) await this._fetchTxhexAndDecode();
if (await this.thereAreUnknownInputsInTx()) return false;
return true;
}
/**
* Creates an RBF transaction that can replace previous one and basically cancel it (rewrite
* output to the one our wallet controls). Note, this cannot add more utxo in RBF transaction if

View file

@ -0,0 +1,85 @@
/* global alert */
import * as bitcoin from 'bitcoinjs-lib';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
const delay = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
// Implements IPayjoinClientWallet
// https://github.com/bitcoinjs/payjoin-client/blob/master/ts_src/wallet.ts
export default class PayjoinTransaction {
constructor(psbt, broadcast, wallet) {
this._psbt = psbt;
this._broadcast = broadcast;
this._wallet = wallet;
this._payjoinPsbt = false;
}
async getPsbt() {
// Nasty hack to get this working for now
const unfinalized = this._psbt.clone();
unfinalized.data.inputs.forEach((input, index) => {
delete input.finalScriptWitness;
const address = bitcoin.address.fromOutputScript(input.witnessUtxo.script);
const wif = this._wallet._getWifForAddress(address);
const keyPair = bitcoin.ECPair.fromWIF(wif);
unfinalized.signInput(index, keyPair);
});
return unfinalized;
}
/**
* Doesnt conform to spec but needed for user-facing wallet software to find out txid of payjoined transaction
*
* @returns {boolean|Psbt}
*/
getPayjoinPsbt() {
return this._payjoinPsbt;
}
async signPsbt(payjoinPsbt) {
// Do this without relying on private methods
payjoinPsbt.data.inputs.forEach((input, index) => {
const address = bitcoin.address.fromOutputScript(input.witnessUtxo.script);
try {
const wif = this._wallet._getWifForAddress(address);
const keyPair = bitcoin.ECPair.fromWIF(wif);
payjoinPsbt.signInput(index, keyPair).finalizeInput(index);
} catch (e) {}
});
this._payjoinPsbt = payjoinPsbt;
return this._payjoinPsbt;
}
async broadcastTx(txHex) {
try {
const result = await this._broadcast(txHex);
if (!result) {
throw new Error(`Broadcast failed`);
}
return '';
} catch (e) {
return 'Error: ' + e.message;
}
}
async scheduleBroadcastTx(txHex, milliseconds) {
delay(milliseconds).then(async () => {
const result = await this.broadcastTx(txHex);
if (result === '') {
// TODO: Improve the wording of this error message
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
alert('Something was wrong with the payjoin transaction, the original transaction sucessfully broadcast.');
}
});
}
async isOwnOutputScript(outputScript) {
const address = bitcoin.address.fromOutputScript(outputScript);
return this._wallet.weOwnAddress(address);
}
}

View file

@ -718,16 +718,29 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
return this._utxo;
}
getDerivedUtxoFromOurTransaction() {
getDerivedUtxoFromOurTransaction(returnSpentUtxoAsWell = false) {
const utxos = [];
// its faster to pre-build hashmap of owned addresses than to query `this.weOwnAddress()`, which in turn
// iterates over all addresses in hierarchy
const ownedAddressesHashmap = {};
for (let c = 0; c < this.next_free_address_index + 1; c++) {
ownedAddressesHashmap[this._getExternalAddressByIndex(c)] = true;
}
for (let c = 0; c < this.next_free_change_address_index + 1; c++) {
ownedAddressesHashmap[this._getInternalAddressByIndex(c)] = true;
}
for (const tx of this.getTransactions()) {
for (const output of tx.outputs) {
let address = false;
if (output.scriptPubKey && output.scriptPubKey.addresses && output.scriptPubKey.addresses[0]) {
address = output.scriptPubKey.addresses[0];
}
if (this.weOwnAddress(address)) {
if (ownedAddressesHashmap[address]) {
const value = new BigNumber(output.value).multipliedBy(100000000).toNumber();
const wif = returnSpentUtxoAsWell ? false : this._getWifForAddress(address);
// ^^^ faster, as we probably dont need WIFs for spent UTXO
utxos.push({
txid: tx.txid,
txId: tx.txid,
@ -736,13 +749,15 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
value,
amount: value,
confirmations: tx.confirmations,
wif: this._getWifForAddress(address),
wif,
height: BlueElectrum.estimateCurrentBlockheight() - tx.confirmations,
});
}
}
}
if (returnSpentUtxoAsWell) return utxos;
// got all utxos we ever had. lets filter out the ones that are spent:
const ret = [];
for (const utxo of utxos) {

View file

@ -115,6 +115,10 @@ export class AbstractWallet {
return false;
}
allowPayJoin() {
return false;
}
weOwnAddress(address) {
throw Error('not implemented');
}

View file

@ -28,4 +28,8 @@ export class HDSegwitBech32Wallet extends AbstractHDElectrumWallet {
allowRBF() {
return true;
}
allowPayJoin() {
return true;
}
}

View file

@ -1281,7 +1281,7 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 5.6.0;
MARKETING_VERSION = 5.6.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@ -1321,7 +1321,7 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 5.6.0;
MARKETING_VERSION = 5.6.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",

27
package-lock.json generated
View file

@ -6171,11 +6171,6 @@
"file-uri-to-path": "1.0.0"
}
},
"bip174": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/bip174/-/bip174-1.0.1.tgz",
"integrity": "sha512-Mq2aFs1TdMfxBpYPg7uzjhsiXbAtoVq44TNjEWtvuZBiBgc3m7+n55orYMtTAxdg7jWbL4DtH0MKocJER4xERQ=="
},
"bip21": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/bip21/-/bip21-2.0.3.tgz",
@ -6224,12 +6219,12 @@
"integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow=="
},
"bitcoinjs-lib": {
"version": "5.1.10",
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-5.1.10.tgz",
"integrity": "sha512-CesUqtBtnYc+SOMsYN9jWQWhdohW1MpklUkF7Ukn4HiAyN6yxykG+cIJogfRt6x5xcgH87K1Q+Mnoe/B+du1Iw==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-5.2.0.tgz",
"integrity": "sha512-5DcLxGUDejgNBYcieMIUfjORtUeNWl828VWLHJGVKZCb4zIS1oOySTUr0LGmcqJBQgTBz3bGbRQla4FgrdQEIQ==",
"requires": {
"bech32": "^1.1.2",
"bip174": "^1.0.1",
"bip174": "^2.0.1",
"bip32": "^2.0.4",
"bip66": "^1.1.0",
"bitcoin-ops": "^1.4.0",
@ -6243,6 +6238,13 @@
"typeforce": "^1.11.3",
"varuint-bitcoin": "^1.0.4",
"wif": "^2.0.1"
},
"dependencies": {
"bip174": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/bip174/-/bip174-2.0.1.tgz",
"integrity": "sha512-i3X26uKJOkDTAalYAp0Er+qGMDhrbbh2o93/xiPyAN2s25KrClSpe3VXo/7mNJoqA5qfko8rLS2l3RWZgYmjKQ=="
}
}
},
"bl": {
@ -14087,6 +14089,13 @@
}
}
},
"payjoin-client": {
"version": "git+https://github.com/bitcoinjs/payjoin-client.git#31d2118a4c0d00192d975f3a6da2a96238f8f7a5",
"from": "git+https://github.com/bitcoinjs/payjoin-client.git#31d2118a4c0d00192d975f3a6da2a96238f8f7a5",
"requires": {
"bitcoinjs-lib": "^5.2.0"
}
},
"pbkdf2": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz",

View file

@ -82,7 +82,7 @@
"bip21": "2.0.3",
"bip32": "2.0.5",
"bip39": "2.6.0",
"bitcoinjs-lib": "5.1.10",
"bitcoinjs-lib": "5.2.0",
"bolt11": "1.2.7",
"buffer": "5.6.0",
"buffer-reverse": "1.0.1",
@ -104,6 +104,7 @@
"lottie-react-native": "3.5.0",
"metro-react-native-babel-preset": "0.63.0",
"path-browserify": "1.0.1",
"payjoin-client": "git+https://github.com/bitcoinjs/payjoin-client.git#31d2118a4c0d00192d975f3a6da2a96238f8f7a5",
"pbkdf2": "3.1.1",
"prettier": "2.1.1",
"process": "0.11.10",

View file

@ -1,7 +1,9 @@
/* global alert */
import React, { Component } from 'react';
import { ActivityIndicator, FlatList, TouchableOpacity, StyleSheet, View } from 'react-native';
import { ActivityIndicator, FlatList, TouchableOpacity, StyleSheet, Switch, View } from 'react-native';
import { Text } from 'react-native-elements';
import { PayjoinClient } from 'payjoin-client';
import PayjoinTransaction from '../../class/payjoin-transaction';
import { BlueButton, BlueText, SafeBlueArea, BlueCard, BlueSpacing40, BlueNavigationStyle } from '../../BlueComponents';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import PropTypes from 'prop-types';
@ -32,7 +34,10 @@ export default class Confirm extends Component {
this.state = {
isLoading: false,
fee: props.route.params.fee,
isPayjoinEnabled: false,
payjoinUrl: props.route.params.fromWallet.allowPayJoin() ? props.route.params?.payjoinUrl : false,
psbt: props.route.params?.psbt,
fee: props.route.params?.fee,
feeSatoshi: new Bignumber(props.route.params.fee).multipliedBy(100000000).toNumber(),
memo: props.route.params.memo,
recipients: props.route.params.recipients,
@ -50,59 +55,63 @@ export default class Confirm extends Component {
this.isBiometricUseCapableAndEnabled = await Biometric.isBiometricUseCapableAndEnabled();
}
broadcast() {
send() {
this.setState({ isLoading: true }, async () => {
try {
// await BlueElectrum.ping();
await BlueElectrum.waitTillConnected();
if (this.isBiometricUseCapableAndEnabled) {
if (!(await Biometric.unlockWithBiometrics())) {
this.setState({ isLoading: false });
return;
}
}
const result = await this.state.fromWallet.broadcastTx(this.state.tx);
if (!result) {
throw new Error(loc.errors.broadcast);
const txids2watch = [];
if (!this.state.isPayjoinEnabled) {
await this.broadcast(this.state.tx);
} else {
const txid = bitcoin.Transaction.fromHex(this.state.tx).getId();
notifications.majorTomToGroundControl([], [], [txid]);
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs
let amount = 0;
const recipients = this.state.recipients;
if (recipients[0].amount === BitcoinUnit.MAX || (!recipients[0].amount && !recipients[0].value)) {
amount = this.state.fromWallet.getBalance() - this.state.feeSatoshi;
} else {
for (const recipient of recipients) {
amount += recipient.amount ? +recipient.amount : recipient.value;
}
}
// wallets that support new createTransaction() instead of deprecated createTx()
if (
[
HDSegwitBech32Wallet.type,
HDSegwitP2SHWallet.type,
HDLegacyP2PKHWallet.type,
HDLegacyBreadwalletWallet.type,
HDLegacyElectrumSeedP2PKHWallet.type,
LegacyWallet.type,
SegwitP2SHWallet.type,
SegwitBech32Wallet.type,
].includes(this.state.fromWallet.type)
) {
amount = formatBalanceWithoutSuffix(amount, BitcoinUnit.BTC, false);
}
this.props.navigation.navigate('Success', {
fee: Number(this.state.fee),
amount,
dismissModal: () => this.props.navigation.dangerouslyGetParent().pop(),
const wallet = new PayjoinTransaction(this.state.psbt, txHex => this.broadcast(txHex), this.state.fromWallet);
const payjoinClient = new PayjoinClient({
wallet,
payjoinUrl: this.state.payjoinUrl,
});
this.setState({ isLoading: false });
await payjoinClient.run();
const payjoinPsbt = wallet.getPayjoinPsbt();
if (payjoinPsbt) {
const tx = payjoinPsbt.extractTransaction();
txids2watch.push(tx.getId());
}
}
const txid = bitcoin.Transaction.fromHex(this.state.tx).getId();
txids2watch.push(txid);
notifications.majorTomToGroundControl([], [], txids2watch);
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs
let amount = 0;
const recipients = this.state.recipients;
if (recipients[0].amount === BitcoinUnit.MAX || (!recipients[0].amount && !recipients[0].value)) {
amount = this.state.fromWallet.getBalance() - this.state.feeSatoshi;
} else {
for (const recipient of recipients) {
amount += recipient.amount ? +recipient.amount : recipient.value;
}
}
// wallets that support new createTransaction() instead of deprecated createTx()
if (
[
HDSegwitBech32Wallet.type,
HDSegwitP2SHWallet.type,
HDLegacyP2PKHWallet.type,
HDLegacyBreadwalletWallet.type,
HDLegacyElectrumSeedP2PKHWallet.type,
LegacyWallet.type,
SegwitP2SHWallet.type,
SegwitBech32Wallet.type,
].includes(this.state.fromWallet.type)
) {
amount = formatBalanceWithoutSuffix(amount, BitcoinUnit.BTC, false);
}
this.props.navigation.navigate('Success', {
fee: Number(this.state.fee),
amount,
dismissModal: () => this.props.navigation.dangerouslyGetParent().pop(),
});
this.setState({ isLoading: false });
} catch (error) {
ReactNativeHapticFeedback.trigger('notificationError', {
ignoreAndroidSystemSettings: false,
@ -113,6 +122,24 @@ export default class Confirm extends Component {
});
}
async broadcast(tx) {
await BlueElectrum.ping();
await BlueElectrum.waitTillConnected();
if (this.isBiometricUseCapableAndEnabled) {
if (!(await Biometric.unlockWithBiometrics())) {
return;
}
}
const result = await this.state.fromWallet.broadcastTx(tx);
if (!result) {
throw new Error(loc.errors.broadcast);
}
return result;
}
_renderItem = ({ index, item }) => {
return (
<>
@ -168,11 +195,13 @@ export default class Confirm extends Component {
{currency.satoshiToLocalCurrency(this.state.feeSatoshi)})
</Text>
<BlueSpacing40 />
{this.state.isLoading ? (
<ActivityIndicator />
) : (
<BlueButton onPress={() => this.broadcast()} title={loc.send.confirm_sendNow} />
{!!this.state.payjoinUrl && (
<View style={styles.payjoinWrapper}>
<Text style={styles.payjoinText}>Payjoin</Text>
<Switch value={this.state.isPayjoinEnabled} onValueChange={isPayjoinEnabled => this.setState({ isPayjoinEnabled })} />
</View>
)}
{this.state.isLoading ? <ActivityIndicator /> : <BlueButton onPress={() => this.send()} title={loc.send.confirm_sendNow} />}
<TouchableOpacity
testID="TransactionDetailsButton"
@ -287,11 +316,20 @@ const styles = StyleSheet.create({
fontWeight: '500',
alignSelf: 'center',
},
payjoinWrapper: {
flexDirection: 'row',
marginHorizontal: 20,
marginBottom: 10,
justifyContent: 'space-between',
alignItems: 'center',
},
payjoinText: { color: '#81868e', fontSize: 14 },
});
Confirm.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.func,
dismiss: PropTypes.func,
navigate: PropTypes.func,
dangerouslyGetParent: PropTypes.func,
}),

View file

@ -332,10 +332,10 @@ export default class SendDetails extends Component {
if (this.props.route.params.uri) {
const uri = this.props.route.params.uri;
try {
const { address, amount, memo } = this.decodeBitcoinUri(uri);
const { address, amount, memo, payjoinUrl } = DeeplinkSchemaMatch.decodeBitcoinUri(uri);
addresses.push(new BitcoinTransaction(address, amount, currency.btcToSatoshi(amount)));
initialMemo = memo;
this.setState({ addresses, memo: initialMemo, isLoading: false, amountUnit: BitcoinUnit.BTC });
this.setState({ addresses, memo: initialMemo, isLoading: false, amountUnit: BitcoinUnit.BTC, payjoinUrl });
} catch (error) {
console.log(error);
alert(loc.send.details_error_decode);
@ -374,10 +374,11 @@ export default class SendDetails extends Component {
if (this.props.route.params.uri) {
try {
const { address, amount, memo } = this.decodeBitcoinUri(this.props.route.params.uri);
this.setState({ address, amount, memo });
const { address, amount, memo, payjoinUrl } = DeeplinkSchemaMatch.decodeBitcoinUri(this.props.route.params.uri);
this.setState({ address, amount, memo, isLoading: false, payjoinUrl });
} catch (error) {
console.log(error);
this.setState({ isLoading: false });
alert(loc.send.details_error_decode);
}
}
@ -400,27 +401,6 @@ export default class SendDetails extends Component {
this.setState({ renderWalletSelectionButtonHidden: false, isAmountToolbarVisibleForAndroid: false });
};
decodeBitcoinUri(uri) {
let amount = '';
let parsedBitcoinUri = null;
let address = uri || '';
let memo = '';
try {
parsedBitcoinUri = DeeplinkSchemaMatch.bip21decode(uri);
address = 'address' in parsedBitcoinUri ? parsedBitcoinUri.address : address;
if ('options' in parsedBitcoinUri) {
if ('amount' in parsedBitcoinUri.options) {
amount = parsedBitcoinUri.options.amount.toString();
amount = parsedBitcoinUri.options.amount;
}
if ('label' in parsedBitcoinUri.options) {
memo = parsedBitcoinUri.options.label || memo;
}
}
} catch (_) {}
return { address, amount, memo };
}
async createTransaction() {
Keyboard.dismiss();
this.setState({ isLoading: true });
@ -619,6 +599,8 @@ export default class SendDetails extends Component {
tx: tx.toHex(),
recipients: targets,
satoshiPerByte: requestedSatPerByte,
payjoinUrl: this.state.payjoinUrl,
psbt,
});
this.setState({ isLoading: false });
}
@ -1053,7 +1035,7 @@ export default class SendDetails extends Component {
onChangeText={async text => {
text = text.trim();
const transactions = this.state.addresses;
const { address, amount, memo } = this.decodeBitcoinUri(text);
const { address, amount, memo, payjoinUrl } = DeeplinkSchemaMatch.decodeBitcoinUri(text);
item.address = address || text;
item.amount = amount || item.amount;
transactions[index] = item;
@ -1061,6 +1043,7 @@ export default class SendDetails extends Component {
addresses: transactions,
memo: memo || this.state.memo,
isLoading: false,
payjoinUrl,
});
this.reCalcTx();
}}

View file

@ -1,19 +1,104 @@
import React, { Component } from 'react';
import React, { useEffect, useRef } from 'react';
import LottieView from 'lottie-react-native';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { View, StyleSheet } from 'react-native';
import { Text } from 'react-native-elements';
import { BlueButton, SafeBlueArea, BlueCard } from '../../BlueComponents';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import PropTypes from 'prop-types';
import loc from '../../loc';
import { BlueCurrentTheme } from '../../components/themes';
import { useNavigation, useRoute, useTheme } from '@react-navigation/native';
const Success = () => {
const { colors } = useTheme();
const { dangerouslyGetParent } = useNavigation();
const { amount, fee = 0, amountUnit = BitcoinUnit.BTC, invoiceDescription = '' } = useRoute().params;
const animationRef = useRef();
const stylesHook = StyleSheet.create({
root: {
backgroundColor: colors.elevated,
},
amountValue: {
color: colors.alternativeTextColor2,
},
amountUnit: {
color: colors.alternativeTextColor2,
},
});
useEffect(() => {
console.log('send/success - useEffect');
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const pop = () => {
dangerouslyGetParent().pop();
};
useEffect(() => {
animationRef.current.reset();
animationRef.current.resume();
}, [colors]);
return (
<SafeBlueArea style={[styles.root, stylesHook.root]}>
<BlueCard style={styles.amout}>
{amount > 0 && (
<View style={styles.view}>
<Text style={[styles.amountValue, stylesHook.amountValue]}>{amount}</Text>
<Text style={[styles.amountUnit, stylesHook.amountUnit]}>{' ' + amountUnit}</Text>
</View>
)}
{fee > 0 && (
<Text style={styles.feeText}>
{loc.send.create_fee}: {fee} {BitcoinUnit.BTC}
</Text>
)}
{fee <= 0 && (
<Text numberOfLines={0} style={styles.feeText}>
{invoiceDescription}
</Text>
)}
</BlueCard>
<View style={styles.ready}>
<LottieView
style={styles.lottie}
source={require('../../img/bluenice.json')}
autoPlay
ref={animationRef}
loop={false}
colorFilters={[
{
keypath: 'spark',
color: colors.success,
},
{
keypath: 'circle',
color: colors.success,
},
{
keypath: 'Oval',
color: colors.successCheck,
},
]}
/>
</View>
<BlueCard>
<BlueButton onPress={pop} title={loc.send.success_done} />
</BlueCard>
</SafeBlueArea>
);
};
Success.navigationOptions = {
headerShown: false,
gesturesEnabled: false,
};
export default Success;
const styles = StyleSheet.create({
root: {
flex: 1,
paddingTop: 19,
backgroundColor: BlueCurrentTheme.colors.elevated,
},
amout: {
alignItems: 'center',
@ -26,12 +111,10 @@ const styles = StyleSheet.create({
paddingBottom: 16,
},
amountValue: {
color: BlueCurrentTheme.colors.alternativeTextColor2,
fontSize: 36,
fontWeight: '600',
},
amountUnit: {
color: BlueCurrentTheme.colors.alternativeTextColor2,
fontSize: 16,
marginHorizontal: 4,
paddingBottom: 6,
@ -61,92 +144,3 @@ const styles = StyleSheet.create({
height: 400,
},
});
export default class Success extends Component {
constructor(props) {
super(props);
console.log('send/success constructor');
this.state = {
amount: props.route.params.amount,
fee: props.route.params.fee || 0,
amountUnit: props.route.params.amountUnit || BitcoinUnit.BTC,
invoiceDescription: props.route.params.invoiceDescription || '',
};
}
componentDidMount() {
console.log('send/success - componentDidMount');
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
}
render() {
return (
<SafeBlueArea style={styles.root}>
<BlueCard style={styles.amout}>
<View style={styles.view}>
<Text style={styles.amountValue}>{this.state.amount}</Text>
<Text style={styles.amountUnit}>{' ' + this.state.amountUnit}</Text>
</View>
{this.state.fee > 0 && (
<Text style={styles.feeText}>
{loc.send.create_fee}: {this.state.fee} {BitcoinUnit.BTC}
</Text>
)}
{this.state.fee <= 0 && (
<Text numberOfLines={0} style={styles.feeText}>
{this.state.invoiceDescription}
</Text>
)}
</BlueCard>
<View style={styles.ready}>
<LottieView
style={styles.lottie}
source={require('../../img/bluenice.json')}
autoPlay
loop={false}
colorFilters={[
{
keypath: 'spark',
color: BlueCurrentTheme.colors.success,
},
{
keypath: 'circle',
color: BlueCurrentTheme.colors.success,
},
{
keypath: 'Oval',
color: BlueCurrentTheme.colors.successCheck,
},
]}
/>
</View>
<BlueCard>
<BlueButton onPress={() => this.props.navigation.dangerouslyGetParent().pop()} title={loc.send.success_done} />
</BlueCard>
</SafeBlueArea>
);
}
}
Success.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.func,
navigate: PropTypes.func,
dangerouslyGetParent: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({
amount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
fee: PropTypes.number,
}),
}),
}),
route: PropTypes.shape({
params: PropTypes.object,
}),
};
Success.navigationOptions = {
headerShown: false,
gesturesEnabled: false,
};

View file

@ -215,7 +215,12 @@ export default class TransactionsStatus extends Component {
}
const tx = new HDSegwitBech32Transaction(null, this.state.tx.hash, this.state.wallet);
if ((await tx.isOurTransaction()) && (await tx.getRemoteConfirmationsNum()) === 0 && (await tx.isSequenceReplaceable())) {
if (
(await tx.isOurTransaction()) &&
(await tx.getRemoteConfirmationsNum()) === 0 &&
(await tx.isSequenceReplaceable()) &&
(await tx.canBumpTx())
) {
return this.setState({ isRBFBumpFeePossible: buttonStatus.possible });
} else {
return this.setState({ isRBFBumpFeePossible: buttonStatus.notPossible });

View file

@ -2,6 +2,7 @@ vim ios/BlueWallet/Info.plist
vim ios/BlueWalletWatch/Info.plist
vim "ios/BlueWalletWatch Extension/Info.plist"
vim "ios/TodayExtension/Info.plist"
vim ios/BlueWallet.xcodeproj/project.pbxproj
vim android/app/build.gradle
vim package.json
vim package-lock.json

View file

@ -135,6 +135,7 @@ describe('HDSegwitBech32Transaction', () => {
const tt = new HDSegwitBech32Transaction(null, '881c54edd95cbdd1583d6b9148eb35128a47b64a2e67a5368a649d6be960f08e', hd);
assert.strictEqual(await tt.canCancelTx(), true);
assert.strictEqual(await tt.canBumpTx(), true);
const { tx } = await tt.createRBFbumpFee(17);

View file

@ -1,6 +1,7 @@
/* global describe, it */
/* global describe, it, jest */
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
const assert = require('assert');
jest.useFakeTimers();
describe('unit - DeepLinkSchemaMatch', function () {
it('hasSchema', () => {
@ -221,6 +222,27 @@ describe('unit - DeepLinkSchemaMatch', function () {
assert.strictEqual(encoded, 'bitcoin:1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH?amount=20.3&label=Foobar');
});
it('can decodeBitcoinUri', () => {
assert.deepStrictEqual(
DeeplinkSchemaMatch.decodeBitcoinUri(
'bitcoin:bc1qnapskphjnwzw2w3dk4anpxntunc77v6qrua0f7?amount=0.0001&pj=https://btc.donate.kukks.org/BTC/pj',
),
{
address: 'bc1qnapskphjnwzw2w3dk4anpxntunc77v6qrua0f7',
amount: 0.0001,
memo: '',
payjoinUrl: 'https://btc.donate.kukks.org/BTC/pj',
},
);
assert.deepStrictEqual(DeeplinkSchemaMatch.decodeBitcoinUri('BITCOIN:1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH?amount=20.3&label=Foobar'), {
address: '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH',
amount: 20.3,
memo: 'Foobar',
payjoinUrl: '',
});
});
it('recognizes files', () => {
// txn files:
assert.ok(DeeplinkSchemaMatch.isTXNFile('file://com.android.externalstorage.documents/document/081D-1403%3Atxhex.txn'));