mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-02-22 06:52:41 +01:00
Merge branch 'master' into snyk-upgrade-b1100653bd03b2f06bd39f36add6adf7
This commit is contained in:
commit
f478bab16e
16 changed files with 416 additions and 192 deletions
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
85
class/payjoin-transaction.js
Normal file
85
class/payjoin-transaction.js
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -115,6 +115,10 @@ export class AbstractWallet {
|
|||
return false;
|
||||
}
|
||||
|
||||
allowPayJoin() {
|
||||
return false;
|
||||
}
|
||||
|
||||
weOwnAddress(address) {
|
||||
throw Error('not implemented');
|
||||
}
|
||||
|
|
|
@ -28,4 +28,8 @@ export class HDSegwitBech32Wallet extends AbstractHDElectrumWallet {
|
|||
allowRBF() {
|
||||
return true;
|
||||
}
|
||||
|
||||
allowPayJoin() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
27
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
|
|
|
@ -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();
|
||||
}}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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'));
|
||||
|
|
Loading…
Add table
Reference in a new issue