mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-02-22 15:04:50 +01:00
ADD: new choose fee workflow
This commit is contained in:
parent
9a01256cef
commit
46f1ced76c
10 changed files with 444 additions and 356 deletions
|
@ -36,15 +36,15 @@ import ToolTip from 'react-native-tooltip';
|
|||
import { BlurView } from '@react-native-community/blur';
|
||||
import ImagePicker from 'react-native-image-picker';
|
||||
import showPopupMenu from 'react-native-popup-menu-android';
|
||||
import NetworkTransactionFees, { NetworkTransactionFeeType } from './models/networkTransactionFees';
|
||||
import NetworkTransactionFees, { NetworkTransactionFee, NetworkTransactionFeeType } from './models/networkTransactionFees';
|
||||
import Biometric from './class/biometrics';
|
||||
import { getSystemName } from 'react-native-device-info';
|
||||
import { encodeUR } from 'bc-ur/dist';
|
||||
import QRCode from 'react-native-qrcode-svg';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import { useTheme } from '@react-navigation/native';
|
||||
import { BlueCurrentTheme } from './components/themes';
|
||||
import loc, { formatBalance, formatBalanceWithoutSuffix, formatBalancePlain, removeTrailingZeros, transactionTimeToReadable } from './loc';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import Lnurl from './class/lnurl';
|
||||
import ScanQRCode from './screen/send/ScanQRCode';
|
||||
/** @type {AppStorage} */
|
||||
|
@ -2288,15 +2288,24 @@ export class BlueReplaceFeeSuggestions extends Component {
|
|||
};
|
||||
|
||||
static defaultProps = {
|
||||
onFeeSelected: undefined,
|
||||
transactionMinimum: 1,
|
||||
};
|
||||
|
||||
state = { networkFees: undefined, selectedFeeType: NetworkTransactionFeeType.FAST, customFeeValue: 0 };
|
||||
state = {
|
||||
selectedFeeType: NetworkTransactionFeeType.FAST,
|
||||
customFeeValue: '1',
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
try {
|
||||
const cachedNetworkTransactionFees = JSON.parse(await AsyncStorage.getItem(NetworkTransactionFee.StorageKey));
|
||||
|
||||
if (cachedNetworkTransactionFees && 'fastestFee' in cachedNetworkTransactionFees) {
|
||||
this.setState({ networkFees: cachedNetworkTransactionFees });
|
||||
}
|
||||
} catch (_) {}
|
||||
const networkFees = await NetworkTransactionFees.recommendedFees();
|
||||
this.setState({ networkFees }, () => this.onFeeSelected(NetworkTransactionFeeType.FAST));
|
||||
this.setState({ networkFees });
|
||||
}
|
||||
|
||||
onFeeSelected = selectedFeeType => {
|
||||
|
@ -2311,103 +2320,117 @@ export class BlueReplaceFeeSuggestions extends Component {
|
|||
} else if (selectedFeeType === NetworkTransactionFeeType.SLOW) {
|
||||
this.setState({ selectedFeeType }, () => this.props.onFeeSelected(this.state.networkFees.slowFee));
|
||||
} else if (selectedFeeType === NetworkTransactionFeeType.CUSTOM) {
|
||||
this.props.onFeeSelected(this.state.customFeeValue);
|
||||
this.props.onFeeSelected(Number(this.state.customFeeValue));
|
||||
}
|
||||
};
|
||||
|
||||
onCustomFeeTextChange = customFee => {
|
||||
this.setState({ customFeeValue: Number(customFee), selectedFeeType: NetworkTransactionFeeType.CUSTOM }, () => {
|
||||
const customFeeValue = customFee.replace(/[^0-9]/g, '');
|
||||
this.setState({ customFeeValue, selectedFeeType: NetworkTransactionFeeType.CUSTOM }, () => {
|
||||
this.onFeeSelected(NetworkTransactionFeeType.CUSTOM);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { networkFees, selectedFeeType } = this.state;
|
||||
|
||||
return (
|
||||
<View>
|
||||
{this.state.networkFees && (
|
||||
<>
|
||||
<BlueText>Suggestions</BlueText>
|
||||
<BlueListItem
|
||||
onPress={() => this.onFeeSelected(NetworkTransactionFeeType.FAST)}
|
||||
containerStyle={{ paddingHorizontal: 0, marginHorizontal: 0, backgroundColor: BlueCurrentTheme.colors.transparent }}
|
||||
bottomDivider={false}
|
||||
title="Fast"
|
||||
rightTitle={`${this.state.networkFees.fastestFee} sat/b`}
|
||||
rightTitleStyle={{ fontSize: 13, color: BlueCurrentTheme.colors.alternativeTextColor }}
|
||||
{...(this.state.selectedFeeType === NetworkTransactionFeeType.FAST
|
||||
? { rightIcon: <Icon name="check" type="octaicon" color={BlueCurrentTheme.colors.successCheck} /> }
|
||||
: { hideChevron: true })}
|
||||
/>
|
||||
<BlueListItem
|
||||
onPress={() => this.onFeeSelected(NetworkTransactionFeeType.MEDIUM)}
|
||||
containerStyle={{ paddingHorizontal: 0, marginHorizontal: 0, backgroundColor: BlueCurrentTheme.colors.transparent }}
|
||||
bottomDivider={false}
|
||||
title="Medium"
|
||||
rightTitle={`${this.state.networkFees.mediumFee} sat/b`}
|
||||
rightTitleStyle={{ fontSize: 13, color: BlueCurrentTheme.colors.alternativeTextColor }}
|
||||
{...(this.state.selectedFeeType === NetworkTransactionFeeType.MEDIUM
|
||||
? { rightIcon: <Icon name="check" type="octaicon" color={BlueCurrentTheme.colors.successCheck} /> }
|
||||
: { hideChevron: true })}
|
||||
/>
|
||||
<BlueListItem
|
||||
onPress={() => this.onFeeSelected(NetworkTransactionFeeType.SLOW)}
|
||||
containerStyle={{ paddingHorizontal: 0, marginHorizontal: 0, backgroundColor: BlueCurrentTheme.colors.transparent }}
|
||||
bottomDivider={false}
|
||||
title="Slow"
|
||||
rightTitle={`${this.state.networkFees.slowFee} sat/b`}
|
||||
rightTitleStyle={{ fontSize: 13, color: BlueCurrentTheme.colors.alternativeTextColor }}
|
||||
{...(this.state.selectedFeeType === NetworkTransactionFeeType.SLOW
|
||||
? { rightIcon: <Icon name="check" type="octaicon" color={BlueCurrentTheme.colors.successCheck} /> }
|
||||
: { hideChevron: true })}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<TouchableOpacity onPress={() => this.customTextInput.focus()}>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginHorizontal: 0, alignItems: 'center' }}>
|
||||
<Text style={{ color: BlueCurrentTheme.colors.foregroundColor, fontSize: 16, fontWeight: '500' }}>Custom</Text>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
minHeight: 44,
|
||||
height: 44,
|
||||
minWidth: 48,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
marginVertical: 8,
|
||||
}}
|
||||
{networkFees &&
|
||||
[
|
||||
{
|
||||
label: loc.send.fee_fast,
|
||||
time: loc.send.fee_10m,
|
||||
type: NetworkTransactionFeeType.FAST,
|
||||
rate: networkFees.fastestFee,
|
||||
active: selectedFeeType === NetworkTransactionFeeType.FAST,
|
||||
},
|
||||
{
|
||||
label: loc.send.fee_medium,
|
||||
time: loc.send.fee_3h,
|
||||
type: NetworkTransactionFeeType.MEDIUM,
|
||||
rate: networkFees.mediumFee,
|
||||
active: selectedFeeType === NetworkTransactionFeeType.MEDIUM,
|
||||
},
|
||||
{
|
||||
label: loc.send.fee_slow,
|
||||
time: loc.send.fee_1d,
|
||||
type: NetworkTransactionFeeType.SLOW,
|
||||
rate: networkFees.slowFee,
|
||||
active: selectedFeeType === NetworkTransactionFeeType.SLOW,
|
||||
},
|
||||
].map(({ label, type, time, rate, active }, index) => (
|
||||
<TouchableOpacity
|
||||
key={label}
|
||||
onPress={() => this.onFeeSelected(type)}
|
||||
style={[
|
||||
{ paddingHorizontal: 16, paddingVertical: 8, marginBottom: 10 },
|
||||
active && { borderRadius: 8, backgroundColor: BlueCurrentTheme.colors.incomingBackgroundColor },
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
onChangeText={this.onCustomFeeTextChange}
|
||||
keyboardType="numeric"
|
||||
value={this.state.customFeeValue}
|
||||
ref={ref => (this.customTextInput = ref)}
|
||||
maxLength={9}
|
||||
style={{
|
||||
borderColor: BlueCurrentTheme.colors.formBorder,
|
||||
borderBottomColor: BlueCurrentTheme.colors.formBorder,
|
||||
borderWidth: 1.0,
|
||||
borderBottomWidth: 0.5,
|
||||
borderRadius: 4,
|
||||
minHeight: 33,
|
||||
maxWidth: 100,
|
||||
minWidth: 44,
|
||||
color: '#81868e',
|
||||
backgroundColor: BlueCurrentTheme.colors.inputBackgroundColor,
|
||||
textAlign: 'right',
|
||||
}}
|
||||
onFocus={() => this.onCustomFeeTextChange(this.state.customFeeValue)}
|
||||
defaultValue={`${this.props.transactionMinimum}`}
|
||||
placeholder="Custom sat/b"
|
||||
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
|
||||
/>
|
||||
<Text style={{ color: BlueCurrentTheme.colors.alternativeTextColor, marginHorizontal: 8 }}>sat/b</Text>
|
||||
{this.state.selectedFeeType === NetworkTransactionFeeType.CUSTOM && <Icon name="check" type="octaicon" color="#0070FF" />}
|
||||
</View>
|
||||
<BlueDismissKeyboardInputAccessory />
|
||||
<View style={{ justifyContent: 'space-between', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Text style={{ fontSize: 22, color: BlueCurrentTheme.colors.successColor, fontWeight: '600' }}>{label}</Text>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: BlueCurrentTheme.colors.successColor,
|
||||
borderRadius: 5,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 3,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: BlueCurrentTheme.colors.background }}>~{time}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ justifyContent: 'flex-end', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Text style={{ color: BlueCurrentTheme.colors.successColor }}>{rate} sat/byte</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
<TouchableOpacity
|
||||
onPress={() => this.customTextInput.focus()}
|
||||
style={[
|
||||
{ paddingHorizontal: 16, paddingVertical: 8, marginBottom: 10 },
|
||||
selectedFeeType === NetworkTransactionFeeType.CUSTOM && {
|
||||
borderRadius: 8,
|
||||
backgroundColor: BlueCurrentTheme.colors.incomingBackgroundColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={{ justifyContent: 'space-between', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Text style={{ fontSize: 22, color: BlueCurrentTheme.colors.successColor, fontWeight: '600' }}>{loc.send.fee_custom}</Text>
|
||||
</View>
|
||||
<View style={{ justifyContent: 'space-between', flexDirection: 'row', alignItems: 'center', marginTop: 5 }}>
|
||||
<TextInput
|
||||
onChangeText={this.onCustomFeeTextChange}
|
||||
keyboardType="numeric"
|
||||
value={this.state.customFeeValue}
|
||||
ref={ref => (this.customTextInput = ref)}
|
||||
maxLength={9}
|
||||
style={{
|
||||
backgroundColor: BlueCurrentTheme.colors.inputBackgroundColor,
|
||||
borderBottomColor: BlueCurrentTheme.colors.formBorder,
|
||||
borderBottomWidth: 0.5,
|
||||
borderColor: BlueCurrentTheme.colors.formBorder,
|
||||
borderRadius: 4,
|
||||
borderWidth: 1.0,
|
||||
color: '#81868e',
|
||||
flex: 1,
|
||||
marginRight: 10,
|
||||
minHeight: 33,
|
||||
paddingRight: 5,
|
||||
textAlign: 'right',
|
||||
}}
|
||||
onFocus={() => this.onCustomFeeTextChange(this.state.customFeeValue)}
|
||||
defaultValue={`${this.props.transactionMinimum}`}
|
||||
placeholder={loc.send.fee_satbyte}
|
||||
placeholderTextColor="#81868e"
|
||||
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
|
||||
/>
|
||||
<Text style={{ color: BlueCurrentTheme.colors.successColor }}>sat/byte</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<BlueText style={{ color: BlueCurrentTheme.colors.alternativeTextColor }}>
|
||||
The total fee rate (satoshi per byte) you want to pay should be higher than {this.props.transactionMinimum} sat/byte
|
||||
{loc.formatString(loc.send.fee_replace_min, { min: this.props.transactionMinimum })}
|
||||
</BlueText>
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -6,8 +6,6 @@ import { AbstractHDWallet } from './abstract-hd-wallet';
|
|||
const bitcoin = require('bitcoinjs-lib');
|
||||
const BlueElectrum = require('../../blue_modules/BlueElectrum');
|
||||
const HDNode = require('bip32');
|
||||
const coinSelectAccumulative = require('coinselect/accumulative');
|
||||
const coinSelectSplit = require('coinselect/split');
|
||||
const reverse = require('buffer-reverse');
|
||||
|
||||
/**
|
||||
|
@ -806,24 +804,10 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
|||
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}}
|
||||
*/
|
||||
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false, masterFingerprint) {
|
||||
if (!changeAddress) throw new Error('No change address provided');
|
||||
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate, changeAddress);
|
||||
|
||||
sequence = sequence || AbstractHDElectrumWallet.defaultRBFSequence;
|
||||
|
||||
let algo = coinSelectAccumulative;
|
||||
if (targets.length === 1 && targets[0] && !targets[0].value) {
|
||||
// we want to send MAX
|
||||
algo = coinSelectSplit;
|
||||
}
|
||||
|
||||
const { inputs, outputs, fee } = algo(utxos, targets, feeRate);
|
||||
|
||||
// .inputs and .outputs will be undefined if no solution was found
|
||||
if (!inputs || !outputs) {
|
||||
throw new Error('Not enough balance. Try sending smaller amount');
|
||||
}
|
||||
|
||||
let psbt = new bitcoin.Psbt();
|
||||
|
||||
let c = 0;
|
||||
const keypairs = {};
|
||||
const values = {};
|
||||
|
|
|
@ -253,20 +253,8 @@ export class LegacyWallet extends AbstractWallet {
|
|||
return broadcast.length === 64; // this means return string is txid (precise length), so it was broadcasted ok
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String, txhex: String, }>} List of spendable utxos
|
||||
* @param targets {Array.<{value: Number, address: String}>} Where coins are going. If theres only 1 target and that target has no value - this will send MAX to that address (respecting fee rate)
|
||||
* @param feeRate {Number} satoshi per byte
|
||||
* @param changeAddress {String} Excessive coins will go back to that address
|
||||
* @param sequence {Number} Used in RBF
|
||||
* @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case
|
||||
* @param masterFingerprint {number} Decimal number of wallet's master fingerprint
|
||||
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}}
|
||||
*/
|
||||
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false, masterFingerprint) {
|
||||
coinselect(utxos, targets, feeRate, changeAddress) {
|
||||
if (!changeAddress) throw new Error('No change address provided');
|
||||
sequence = sequence || 0xffffffff; // disable RBF by default
|
||||
|
||||
let algo = coinSelectAccumulative;
|
||||
if (targets.length === 1 && targets[0] && !targets[0].value) {
|
||||
|
@ -281,8 +269,24 @@ export class LegacyWallet extends AbstractWallet {
|
|||
throw new Error('Not enough balance. Try sending smaller amount');
|
||||
}
|
||||
|
||||
const psbt = new bitcoin.Psbt();
|
||||
return { inputs, outputs, fee };
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String, txhex: String, }>} List of spendable utxos
|
||||
* @param targets {Array.<{value: Number, address: String}>} Where coins are going. If theres only 1 target and that target has no value - this will send MAX to that address (respecting fee rate)
|
||||
* @param feeRate {Number} satoshi per byte
|
||||
* @param changeAddress {String} Excessive coins will go back to that address
|
||||
* @param sequence {Number} Used in RBF
|
||||
* @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case
|
||||
* @param masterFingerprint {number} Decimal number of wallet's master fingerprint
|
||||
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}}
|
||||
*/
|
||||
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false, masterFingerprint) {
|
||||
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate, changeAddress);
|
||||
sequence = sequence || 0xffffffff; // disable RBF by default
|
||||
const psbt = new bitcoin.Psbt();
|
||||
let c = 0;
|
||||
const values = {};
|
||||
let keyPair;
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { LegacyWallet } from './legacy-wallet';
|
||||
const bitcoin = require('bitcoinjs-lib');
|
||||
const coinSelectAccumulative = require('coinselect/accumulative');
|
||||
const coinSelectSplit = require('coinselect/split');
|
||||
|
||||
export class SegwitBech32Wallet extends LegacyWallet {
|
||||
static type = 'segwitBech32';
|
||||
|
@ -67,24 +65,9 @@ export class SegwitBech32Wallet extends LegacyWallet {
|
|||
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}}
|
||||
*/
|
||||
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false, masterFingerprint) {
|
||||
if (!changeAddress) throw new Error('No change address provided');
|
||||
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate, changeAddress);
|
||||
sequence = sequence || 0xffffffff; // disable RBF by default
|
||||
|
||||
let algo = coinSelectAccumulative;
|
||||
if (targets.length === 1 && targets[0] && !targets[0].value) {
|
||||
// we want to send MAX
|
||||
algo = coinSelectSplit;
|
||||
}
|
||||
|
||||
const { inputs, outputs, fee } = algo(utxos, targets, feeRate);
|
||||
|
||||
// .inputs and .outputs will be undefined if no solution was found
|
||||
if (!inputs || !outputs) {
|
||||
throw new Error('Not enough balance. Try sending smaller amount');
|
||||
}
|
||||
|
||||
const psbt = new bitcoin.Psbt();
|
||||
|
||||
let c = 0;
|
||||
const values = {};
|
||||
let keyPair;
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { LegacyWallet } from './legacy-wallet';
|
||||
const bitcoin = require('bitcoinjs-lib');
|
||||
const coinSelectAccumulative = require('coinselect/accumulative');
|
||||
const coinSelectSplit = require('coinselect/split');
|
||||
|
||||
/**
|
||||
* Creates Segwit P2SH Bitcoin address
|
||||
|
@ -78,24 +76,9 @@ export class SegwitP2SHWallet extends LegacyWallet {
|
|||
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}}
|
||||
*/
|
||||
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false, masterFingerprint) {
|
||||
if (!changeAddress) throw new Error('No change address provided');
|
||||
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate, changeAddress);
|
||||
sequence = sequence || 0xffffffff; // disable RBF by default
|
||||
|
||||
let algo = coinSelectAccumulative;
|
||||
if (targets.length === 1 && targets[0] && !targets[0].value) {
|
||||
// we want to send MAX
|
||||
algo = coinSelectSplit;
|
||||
}
|
||||
|
||||
const { inputs, outputs, fee } = algo(utxos, targets, feeRate);
|
||||
|
||||
// .inputs and .outputs will be undefined if no solution was found
|
||||
if (!inputs || !outputs) {
|
||||
throw new Error('Not enough balance. Try sending smaller amount');
|
||||
}
|
||||
|
||||
const psbt = new bitcoin.Psbt();
|
||||
|
||||
let c = 0;
|
||||
const values = {};
|
||||
let keyPair;
|
||||
|
|
25
loc/en.json
25
loc/en.json
|
@ -181,6 +181,15 @@
|
|||
"dynamic_prev": "Previous",
|
||||
"dynamic_start": "Start",
|
||||
"dynamic_stop": "Stop",
|
||||
"fee_10m": "10m",
|
||||
"fee_1d": "1d",
|
||||
"fee_3h": "3h",
|
||||
"fee_custom": "Custom",
|
||||
"fee_fast": "Fast",
|
||||
"fee_medium": "Medium",
|
||||
"fee_replace_min": "The total fee rate (satoshi per byte) you want to pay should be higher than {min} sat/byte",
|
||||
"fee_satbyte": "in sat/byte",
|
||||
"fee_slow": "Slow",
|
||||
"header": "Send",
|
||||
"input_clear": "Clear",
|
||||
"input_done": "Done",
|
||||
|
@ -243,6 +252,7 @@
|
|||
"general_adv_mode_e": "When enabled, you will see advanced options such as different wallet types, the ability to specify the LNDHub instance you wish to connect to and custom entropy during wallet creation.",
|
||||
"general_continuity": "Continuity",
|
||||
"general_continuity_e": "When enabled, you will be able to view selected wallets, and transactions, using your other Apple iCloud connected devices.",
|
||||
"groundcontrol_explanation": "GroundControl is a free opensource push notifications server for bitcoin wallets. You can install your own GroundControl server and put its URL here to not rely on BlueWallet's infrastructure. Leave blank to use default",
|
||||
"header": "settings",
|
||||
"language": "Language",
|
||||
"language_restart": "When selecting a new language, restarting BlueWallet may be required for the change to take effect.",
|
||||
|
@ -253,17 +263,16 @@
|
|||
"network": "Network",
|
||||
"network_broadcast": "Broadcast transaction",
|
||||
"network_electrum": "Electrum server",
|
||||
"not_a_valid_uri": "Not a valid URI",
|
||||
"notifications": "Notifications",
|
||||
"password": "Password",
|
||||
"password_explain": "Create the password you will use to decrypt the storage",
|
||||
"passwords_do_not_match": "Passwords do not match",
|
||||
"plausible_deniability": "Plausible deniability",
|
||||
"retype_password": "Re-type password",
|
||||
"notifications": "Notifications",
|
||||
"save": "Save",
|
||||
"saved": "Saved",
|
||||
"not_a_valid_uri": "Not a valid URI",
|
||||
"push_notifications": "Push notifications",
|
||||
"groundcontrol_explanation": "GroundControl is a free opensource push notifications server for bitcoin wallets. You can install your own GroundControl server and put its URL here to not rely on BlueWallet's infrastructure. Leave blank to use default"
|
||||
"retype_password": "Re-type password",
|
||||
"save": "Save",
|
||||
"saved": "Saved"
|
||||
},
|
||||
"transactions": {
|
||||
"cancel_explain": "We will replace this transaction with the one that pays you and has higher fees. This effectively cancels transaction. This is called RBF - Replace By Fee.",
|
||||
|
@ -286,11 +295,11 @@
|
|||
"enable_hw": "This wallet is not being used in conjunction with a hardwarde wallet. Would you like to enable hardware wallet use?",
|
||||
"list_conf": "conf",
|
||||
"list_title": "transactions",
|
||||
"transactions_count": "transactions count",
|
||||
"rbf_explain": "We will replace this transaction with the one with a higher fee, so it should be mined faster. This is called RBF - Replace By Fee.",
|
||||
"rbf_title": "Bump fee (RBF)",
|
||||
"status_bump": "Bump Fee",
|
||||
"status_cancel": "Cancel Transaction"
|
||||
"status_cancel": "Cancel Transaction",
|
||||
"transactions_count": "transactions count"
|
||||
},
|
||||
"wallets": {
|
||||
"add_bitcoin": "Bitcoin",
|
||||
|
|
|
@ -30,10 +30,11 @@ import {
|
|||
BlueListItem,
|
||||
BlueText,
|
||||
} from '../../BlueComponents';
|
||||
import Slider from '@react-native-community/slider';
|
||||
import Modal from 'react-native-modal';
|
||||
import RNFS from 'react-native-fs';
|
||||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import RNFS from 'react-native-fs';
|
||||
import * as bitcoin from 'bitcoinjs-lib';
|
||||
|
||||
import NetworkTransactionFees, { NetworkTransactionFee } from '../../models/networkTransactionFees';
|
||||
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
||||
|
@ -43,10 +44,9 @@ import DocumentPicker from 'react-native-document-picker';
|
|||
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
|
||||
import loc from '../../loc';
|
||||
import { BlueCurrentTheme } from '../../components/themes';
|
||||
const bitcoin = require('bitcoinjs-lib');
|
||||
const currency = require('../../blue_modules/currency');
|
||||
const BigNumber = require('bignumber.js');
|
||||
const BlueApp: AppStorage = require('../../BlueApp');
|
||||
const prompt = require('../../blue_modules/prompt');
|
||||
|
||||
const btcAddressRx = /^[a-zA-Z0-9]{26,35}$/;
|
||||
|
||||
|
@ -68,14 +68,11 @@ const styles = StyleSheet.create({
|
|||
modalContent: {
|
||||
backgroundColor: BlueCurrentTheme.colors.modal,
|
||||
padding: 22,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
borderTopColor: BlueCurrentTheme.colors.borderTopColor,
|
||||
borderWidth: BlueCurrentTheme.colors.borderWidth,
|
||||
minHeight: 200,
|
||||
height: 200,
|
||||
},
|
||||
advancedTransactionOptionsModalContent: {
|
||||
backgroundColor: BlueCurrentTheme.colors.modal,
|
||||
|
@ -90,51 +87,46 @@ const styles = StyleSheet.create({
|
|||
justifyContent: 'flex-end',
|
||||
margin: 0,
|
||||
},
|
||||
feeSliderInput: {
|
||||
backgroundColor: BlueCurrentTheme.colors.feeLabel,
|
||||
minWidth: 127,
|
||||
height: 60,
|
||||
feeModalItem: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
marginBottom: 10,
|
||||
},
|
||||
feeModalItemActive: {
|
||||
borderRadius: 8,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 8,
|
||||
backgroundColor: BlueCurrentTheme.colors.feeLabel,
|
||||
},
|
||||
feeSliderText: {
|
||||
fontWeight: '600',
|
||||
color: BlueCurrentTheme.colors.feeValue,
|
||||
marginBottom: 0,
|
||||
marginRight: 4,
|
||||
textAlign: 'right',
|
||||
fontSize: 36,
|
||||
},
|
||||
feeSliderUnit: {
|
||||
fontWeight: '600',
|
||||
color: BlueCurrentTheme.colors.feeValue,
|
||||
paddingRight: 4,
|
||||
textAlign: 'left',
|
||||
fontSize: 16,
|
||||
alignSelf: 'flex-end',
|
||||
marginBottom: 14,
|
||||
},
|
||||
sliderContainer: {
|
||||
flex: 1,
|
||||
marginTop: 32,
|
||||
minWidth: 240,
|
||||
width: 240,
|
||||
},
|
||||
slider: {
|
||||
flex: 1,
|
||||
},
|
||||
sliderLabels: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
feeModalRow: {
|
||||
justifyContent: 'space-between',
|
||||
marginTop: 14,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
sliderLabel: {
|
||||
fontWeight: '500',
|
||||
fontSize: 13,
|
||||
color: '#37c0a1',
|
||||
feeModalLabel: {
|
||||
fontSize: 22,
|
||||
color: BlueCurrentTheme.colors.successColor,
|
||||
fontWeight: '600',
|
||||
},
|
||||
feeModalTime: {
|
||||
backgroundColor: BlueCurrentTheme.colors.successColor,
|
||||
borderRadius: 5,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 3,
|
||||
},
|
||||
feeModalTimeText: {
|
||||
color: BlueCurrentTheme.colors.background,
|
||||
},
|
||||
feeModalValue: {
|
||||
color: BlueCurrentTheme.colors.successColor,
|
||||
},
|
||||
feeModalCustom: {
|
||||
height: 60,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
feeModalCustomText: {
|
||||
color: BlueCurrentTheme.colors.buttonAlternativeTextColor,
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
},
|
||||
createButton: {
|
||||
marginHorizontal: 56,
|
||||
|
@ -212,14 +204,6 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
feeValue: {
|
||||
color: BlueCurrentTheme.colors.feeValue,
|
||||
marginBottom: 0,
|
||||
marginRight: 4,
|
||||
textAlign: 'right',
|
||||
},
|
||||
feeUnit: {
|
||||
color: BlueCurrentTheme.colors.feeValue,
|
||||
paddingRight: 4,
|
||||
textAlign: 'left',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -257,8 +241,14 @@ export default class SendDetails extends Component {
|
|||
units: [],
|
||||
memo: '',
|
||||
networkTransactionFees: new NetworkTransactionFee(1, 1, 1),
|
||||
fee: 1,
|
||||
feeSliderValue: 1,
|
||||
fee: '1',
|
||||
feePrecalc: {
|
||||
current: null,
|
||||
slowFee: null,
|
||||
mediumFee: null,
|
||||
fastestFee: null,
|
||||
},
|
||||
feeUnit: fromWallet.getPreferredBalanceUnit(),
|
||||
amountUnit: fromWallet.preferredBalanceUnit, // default for whole screen
|
||||
renderWalletSelectionButtonHidden: false,
|
||||
width: Dimensions.get('window').width - 320,
|
||||
|
@ -361,39 +351,40 @@ export default class SendDetails extends Component {
|
|||
try {
|
||||
const cachedNetworkTransactionFees = JSON.parse(await AsyncStorage.getItem(NetworkTransactionFee.StorageKey));
|
||||
|
||||
if (cachedNetworkTransactionFees && 'mediumFee' in cachedNetworkTransactionFees) {
|
||||
if (cachedNetworkTransactionFees && 'fastestFee' in cachedNetworkTransactionFees) {
|
||||
this.setState({
|
||||
fee: cachedNetworkTransactionFees.fastestFee,
|
||||
fee: cachedNetworkTransactionFees.fastestFee.toString(),
|
||||
networkTransactionFees: cachedNetworkTransactionFees,
|
||||
feeSliderValue: cachedNetworkTransactionFees.fastestFee,
|
||||
});
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
await this.reCalcTx();
|
||||
|
||||
try {
|
||||
const recommendedFees = await NetworkTransactionFees.recommendedFees();
|
||||
if (recommendedFees && 'fastestFee' in recommendedFees) {
|
||||
await AsyncStorage.setItem(NetworkTransactionFee.StorageKey, JSON.stringify(recommendedFees));
|
||||
this.setState({
|
||||
fee: recommendedFees.fastestFee,
|
||||
fee: recommendedFees.fastestFee.toString(),
|
||||
networkTransactionFees: recommendedFees,
|
||||
feeSliderValue: recommendedFees.fastestFee,
|
||||
});
|
||||
|
||||
if (this.props.route.params.uri) {
|
||||
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(loc.send.details_error_decode);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.setState({ isLoading: false });
|
||||
}
|
||||
} catch (_e) {}
|
||||
} catch (_) {}
|
||||
|
||||
if (this.props.route.params.uri) {
|
||||
try {
|
||||
const { address, amount, memo } = this.decodeBitcoinUri(this.props.route.params.uri);
|
||||
this.setState({ address, amount, memo });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
alert(loc.send.details_error_decode);
|
||||
}
|
||||
}
|
||||
|
||||
await this.state.fromWallet.fetchUtxo();
|
||||
this.setState({ isLoading: false });
|
||||
await this.reCalcTx();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -433,13 +424,13 @@ export default class SendDetails extends Component {
|
|||
async createTransaction() {
|
||||
Keyboard.dismiss();
|
||||
this.setState({ isLoading: true });
|
||||
let error = false;
|
||||
const requestedSatPerByte = this.state.fee.toString().replace(/\D/g, '');
|
||||
const requestedSatPerByte = this.state.fee;
|
||||
for (const [index, transaction] of this.state.addresses.entries()) {
|
||||
let error;
|
||||
if (!transaction.amount || transaction.amount < 0 || parseFloat(transaction.amount) === 0) {
|
||||
error = loc.send.details_amount_field_is_not_valid;
|
||||
console.log('validation error');
|
||||
} else if (!this.state.fee || !requestedSatPerByte || parseFloat(requestedSatPerByte) < 1) {
|
||||
} else if (!requestedSatPerByte || parseFloat(requestedSatPerByte) < 1) {
|
||||
error = loc.send.details_fee_field_is_not_valid;
|
||||
console.log('validation error');
|
||||
} else if (!transaction.address) {
|
||||
|
@ -483,10 +474,6 @@ export default class SendDetails extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.createPsbtTransaction();
|
||||
} catch (Err) {
|
||||
|
@ -497,12 +484,88 @@ export default class SendDetails extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculating fee options by creating skeleton of future tx.
|
||||
*/
|
||||
reCalcTx = async (all = false) => {
|
||||
const wallet = this.state.fromWallet;
|
||||
const fees = this.state.networkTransactionFees;
|
||||
const changeAddress = await wallet.getChangeAddressAsync();
|
||||
const requestedSatPerByte = Number(this.state.fee);
|
||||
const feePrecalc = { ...this.state.feePrecalc };
|
||||
|
||||
const options = all
|
||||
? [
|
||||
{ key: 'current', fee: requestedSatPerByte },
|
||||
{ key: 'slowFee', fee: fees.slowFee },
|
||||
{ key: 'mediumFee', fee: fees.mediumFee },
|
||||
{ key: 'fastestFee', fee: fees.fastestFee },
|
||||
]
|
||||
: [{ key: 'current', fee: requestedSatPerByte }];
|
||||
|
||||
for (const opt of options) {
|
||||
let targets = [];
|
||||
for (const transaction of this.state.addresses) {
|
||||
if (transaction.amount === BitcoinUnit.MAX) {
|
||||
// single output with MAX
|
||||
targets = [{ address: transaction.address }];
|
||||
break;
|
||||
}
|
||||
const value = parseInt(transaction.amountSats);
|
||||
if (value > 0) {
|
||||
targets.push({ address: transaction.address, value });
|
||||
} else if (transaction.amount) {
|
||||
if (currency.btcToSatoshi(transaction.amount) > 0) {
|
||||
targets.push({ address: transaction.address, value: currency.btcToSatoshi(transaction.amount) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// replace wrong addresses with dump
|
||||
targets = targets.map(t => {
|
||||
try {
|
||||
bitcoin.address.toOutputScript(t.address);
|
||||
return t;
|
||||
} catch (e) {
|
||||
return { ...t, address: '36JxaUrpDzkEerkTf1FzwHNE1Hb7cCjgJV' };
|
||||
}
|
||||
});
|
||||
|
||||
let flag = false;
|
||||
while (true) {
|
||||
try {
|
||||
const { fee } = wallet.coinselect(
|
||||
wallet.getUtxo(),
|
||||
targets,
|
||||
opt.fee,
|
||||
changeAddress,
|
||||
this.state.isTransactionReplaceable ? HDSegwitBech32Wallet.defaultRBFSequence : HDSegwitBech32Wallet.finalRBFSequence,
|
||||
);
|
||||
|
||||
feePrecalc[opt.key] = fee;
|
||||
break;
|
||||
} catch (e) {
|
||||
if (e.message.includes('Not enough') && !flag) {
|
||||
flag = true;
|
||||
// if the outputs are too big, replace them with dust
|
||||
targets = targets.map(t => ({ ...t, value: 546 }));
|
||||
continue;
|
||||
}
|
||||
|
||||
feePrecalc[opt.key] = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ feePrecalc });
|
||||
};
|
||||
|
||||
async createPsbtTransaction() {
|
||||
/** @type {HDSegwitBech32Wallet} */
|
||||
const wallet = this.state.fromWallet;
|
||||
await wallet.fetchUtxo();
|
||||
const changeAddress = await wallet.getChangeAddressAsync();
|
||||
const requestedSatPerByte = +this.state.fee.toString().replace(/\D/g, '');
|
||||
const requestedSatPerByte = Number(this.state.fee);
|
||||
console.log({ requestedSatPerByte, utxo: wallet.getUtxo() });
|
||||
|
||||
let targets = [];
|
||||
|
@ -617,64 +680,91 @@ export default class SendDetails extends Component {
|
|||
};
|
||||
|
||||
renderFeeSelectionModal = () => {
|
||||
const { feePrecalc, fee, networkTransactionFees: nf } = this.state;
|
||||
const options = [
|
||||
{
|
||||
label: loc.send.fee_fast,
|
||||
time: loc.send.fee_10m,
|
||||
fee: feePrecalc.fastestFee,
|
||||
rate: nf.fastestFee,
|
||||
active: Number(fee) === nf.fastestFee,
|
||||
},
|
||||
{
|
||||
label: loc.send.fee_medium,
|
||||
time: loc.send.fee_3h,
|
||||
fee: feePrecalc.mediumFee,
|
||||
rate: nf.mediumFee,
|
||||
active: Number(fee) === nf.mediumFee,
|
||||
},
|
||||
{
|
||||
label: loc.send.fee_slow,
|
||||
time: loc.send.fee_1d,
|
||||
fee: feePrecalc.slowFee,
|
||||
rate: nf.slowFee,
|
||||
active: Number(fee) === nf.slowFee,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
deviceHeight={Dimensions.get('window').height}
|
||||
deviceWidth={this.state.width + this.state.width / 2}
|
||||
isVisible={this.state.isFeeSelectionModalVisible}
|
||||
style={styles.bottomModal}
|
||||
onBackdropPress={() => {
|
||||
if (this.state.fee < 1 || this.state.feeSliderValue < 1) {
|
||||
this.setState({ fee: Number(1), feeSliderValue: Number(1) });
|
||||
}
|
||||
Keyboard.dismiss();
|
||||
this.setState({ isFeeSelectionModalVisible: false });
|
||||
}}
|
||||
onBackdropPress={() => this.setState({ isFeeSelectionModalVisible: false })}
|
||||
>
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'position' : null}>
|
||||
<View style={styles.modalContent}>
|
||||
<TouchableOpacity style={styles.feeSliderInput} onPress={() => this.textInput.focus()}>
|
||||
<TextInput
|
||||
keyboardType="numeric"
|
||||
ref={ref => {
|
||||
this.textInput = ref;
|
||||
}}
|
||||
value={this.state.fee.toString()}
|
||||
onEndEditing={() => {
|
||||
if (this.state.fee < 1 || this.state.feeSliderValue < 1) {
|
||||
this.setState({ fee: Number(1), feeSliderValue: Number(1) });
|
||||
}
|
||||
}}
|
||||
onChangeText={value => {
|
||||
const newValue = value.replace(/\D/g, '');
|
||||
this.setState({ fee: newValue, feeSliderValue: Number(newValue) });
|
||||
}}
|
||||
maxLength={9}
|
||||
editable={!this.state.isLoading}
|
||||
placeholderTextColor="#37c0a1"
|
||||
placeholder={this.state.networkTransactionFees.mediumFee.toString()}
|
||||
style={styles.feeSliderText}
|
||||
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
|
||||
/>
|
||||
<Text style={styles.feeSliderUnit}>sat/b</Text>
|
||||
</TouchableOpacity>
|
||||
{this.state.networkTransactionFees.fastestFee > 1 && (
|
||||
<View style={styles.sliderContainer}>
|
||||
<Slider
|
||||
onValueChange={value => this.setState({ feeSliderValue: value.toFixed(0), fee: value.toFixed(0) })}
|
||||
minimumValue={1}
|
||||
maximumValue={Number(this.state.networkTransactionFees.fastestFee)}
|
||||
value={Number(this.state.feeSliderValue)}
|
||||
maximumTrackTintColor="#d8d8d8"
|
||||
minimumTrackTintColor="#37c0a1"
|
||||
style={styles.slider}
|
||||
/>
|
||||
<View style={styles.sliderLabels}>
|
||||
<Text style={styles.sliderLabel}>slow</Text>
|
||||
<Text style={styles.sliderLabel}>fast</Text>
|
||||
{options.map(({ label, time, fee, rate, active }, index) => (
|
||||
<TouchableOpacity
|
||||
key={label}
|
||||
onPress={() =>
|
||||
this.setState(({ feePrecalc }) => {
|
||||
feePrecalc.current = fee;
|
||||
return { isFeeSelectionModalVisible: false, fee: rate.toString(), feePrecalc };
|
||||
})
|
||||
}
|
||||
style={[styles.feeModalItem, active && styles.feeModalItemActive]}
|
||||
>
|
||||
<View style={styles.feeModalRow}>
|
||||
<Text style={styles.feeModalLabel}>{label}</Text>
|
||||
<View style={styles.feeModalTime}>
|
||||
<Text style={styles.feeModalTimeText}>~{time}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.feeModalRow}>
|
||||
<Text style={styles.feeModalValue}>{fee && this.formatFee(fee)}</Text>
|
||||
<Text style={styles.feeModalValue}>{rate} sat/byte</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
<TouchableOpacity
|
||||
style={styles.feeModalCustom}
|
||||
onPress={async () => {
|
||||
let error = loc.send.fee_satbyte;
|
||||
while (true) {
|
||||
let fee;
|
||||
|
||||
try {
|
||||
fee = await prompt(loc.send.create_fee, error, true, 'numeric');
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^\d+$/.test(fee)) {
|
||||
error = loc.send.details_fee_field_is_not_valid;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fee < 1) fee = '1';
|
||||
fee = Number(fee).toString(); // this will remove leading zeros if any
|
||||
this.setState({ fee, isFeeSelectionModalVisible: false });
|
||||
return;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text style={styles.feeModalCustomText}>{loc.send.fee_custom}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
|
@ -864,7 +954,7 @@ export default class SendDetails extends Component {
|
|||
|
||||
handlePageChange = e => {
|
||||
Keyboard.dismiss();
|
||||
var offset = e.nativeEvent.contentOffset;
|
||||
const offset = e.nativeEvent.contentOffset;
|
||||
if (offset) {
|
||||
const page = Math.round(offset.x / this.state.width);
|
||||
if (this.state.recipientsScrollIndex !== page) {
|
||||
|
@ -875,7 +965,7 @@ export default class SendDetails extends Component {
|
|||
|
||||
scrollViewCurrentIndex = () => {
|
||||
Keyboard.dismiss();
|
||||
var offset = this.scrollView.contentOffset;
|
||||
const offset = this.scrollView.contentOffset;
|
||||
if (offset) {
|
||||
const page = Math.round(offset.x / this.state.width);
|
||||
return page;
|
||||
|
@ -932,7 +1022,7 @@ export default class SendDetails extends Component {
|
|||
}
|
||||
const addresses = this.state.addresses;
|
||||
addresses[index] = item;
|
||||
this.setState({ addresses });
|
||||
this.setState({ addresses }, this.reCalcTx);
|
||||
}}
|
||||
unit={this.state.units[index] || this.state.amountUnit}
|
||||
inputAccessoryViewID={this.state.fromWallet.allowSendMax() ? BlueUseAllFundsButton.InputAccessoryViewID : null}
|
||||
|
@ -950,6 +1040,7 @@ export default class SendDetails extends Component {
|
|||
memo: memo || this.state.memo,
|
||||
isLoading: false,
|
||||
});
|
||||
this.reCalcTx();
|
||||
}}
|
||||
onBarScanned={this.processAddressData}
|
||||
address={item.address}
|
||||
|
@ -994,6 +1085,17 @@ export default class SendDetails extends Component {
|
|||
);
|
||||
};
|
||||
|
||||
formatFee = fee => {
|
||||
switch (this.state.feeUnit) {
|
||||
case BitcoinUnit.SATS:
|
||||
return fee + ' ' + BitcoinUnit.SATS;
|
||||
case BitcoinUnit.BTC:
|
||||
return currency.satoshiToBTC(fee) + ' ' + BitcoinUnit.BTC;
|
||||
case BitcoinUnit.LOCAL_CURRENCY:
|
||||
return currency.satoshiToLocalCurrency(fee);
|
||||
}
|
||||
};
|
||||
|
||||
onLayout = e => {
|
||||
this.setState({ width: e.nativeEvent.layout.width });
|
||||
};
|
||||
|
@ -1006,6 +1108,7 @@ export default class SendDetails extends Component {
|
|||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
|
||||
<View style={styles.root} onLayout={this.onLayout}>
|
||||
|
@ -1040,14 +1143,15 @@ export default class SendDetails extends Component {
|
|||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => this.setState({ isFeeSelectionModalVisible: true })}
|
||||
onPress={() => this.setState({ isFeeSelectionModalVisible: true }, () => this.reCalcTx(true))}
|
||||
disabled={this.state.isLoading}
|
||||
style={styles.fee}
|
||||
>
|
||||
<Text style={styles.feeLabel}>{loc.send.create_fee}</Text>
|
||||
<View style={styles.feeRow}>
|
||||
<Text style={styles.feeValue}>{this.state.fee}</Text>
|
||||
<Text style={styles.feeUnit}>sat/b</Text>
|
||||
<Text style={styles.feeValue}>
|
||||
{this.state.feePrecalc.current ? this.formatFee(this.state.feePrecalc.current) : this.state.fee + ' sat/byte'}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
{this.renderCreateButton()}
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
BlueNavigationStyle,
|
||||
BlueBigCheckmark,
|
||||
} from '../../BlueComponents';
|
||||
import { BlueCurrentTheme } from '../../components/themes';
|
||||
import { HDSegwitBech32Transaction, HDSegwitBech32Wallet } from '../../class';
|
||||
import loc from '../../loc';
|
||||
const EV = require('../../blue_modules/events');
|
||||
|
@ -38,7 +39,7 @@ const styles = StyleSheet.create({
|
|||
flex: 1,
|
||||
},
|
||||
hex: {
|
||||
color: '#0c2550',
|
||||
color: BlueCurrentTheme.colors.buttonAlternativeTextColor,
|
||||
fontWeight: '500',
|
||||
},
|
||||
hexInput: {
|
||||
|
@ -164,40 +165,21 @@ export default class CPFP extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.isLoading) {
|
||||
return (
|
||||
<View style={styles.root}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.stage === 3) {
|
||||
return this.renderStage3();
|
||||
}
|
||||
|
||||
if (this.state.stage === 2) {
|
||||
return this.renderStage2();
|
||||
}
|
||||
|
||||
if (this.state.nonReplaceable) {
|
||||
return (
|
||||
<SafeBlueArea style={styles.root}>
|
||||
<BlueSpacing20 />
|
||||
<BlueSpacing20 />
|
||||
<BlueSpacing20 />
|
||||
<BlueSpacing20 />
|
||||
<BlueSpacing20 />
|
||||
|
||||
<BlueText h4>{loc.transactions.cpfp_no_bump}</BlueText>
|
||||
</SafeBlueArea>
|
||||
);
|
||||
}
|
||||
|
||||
renderStage1(text) {
|
||||
return (
|
||||
<SafeBlueArea style={styles.explain}>
|
||||
<ScrollView>{this.renderStage1(loc.transactions.cpfp_exp)}</ScrollView>
|
||||
<SafeBlueArea style={styles.root}>
|
||||
<BlueSpacing />
|
||||
<BlueCard style={styles.center}>
|
||||
<BlueText>{text}</BlueText>
|
||||
<BlueSpacing20 />
|
||||
<BlueReplaceFeeSuggestions onFeeSelected={fee => this.setState({ newFeeRate: fee })} transactionMinimum={this.state.feeRate} />
|
||||
<BlueSpacing />
|
||||
<BlueButton
|
||||
disabled={this.state.newFeeRate <= this.state.feeRate}
|
||||
onPress={() => this.createTransaction()}
|
||||
title={loc.transactions.cpfp_create}
|
||||
/>
|
||||
</BlueCard>
|
||||
</SafeBlueArea>
|
||||
);
|
||||
}
|
||||
|
@ -235,24 +217,40 @@ export default class CPFP extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
renderStage1(text) {
|
||||
return (
|
||||
<SafeBlueArea style={styles.root}>
|
||||
<BlueSpacing />
|
||||
<BlueCard style={styles.center}>
|
||||
<BlueText>{text}</BlueText>
|
||||
render() {
|
||||
if (this.state.isLoading) {
|
||||
return (
|
||||
<View style={styles.root}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.stage === 3) {
|
||||
return this.renderStage3();
|
||||
}
|
||||
|
||||
if (this.state.stage === 2) {
|
||||
return this.renderStage2();
|
||||
}
|
||||
|
||||
if (this.state.nonReplaceable) {
|
||||
return (
|
||||
<SafeBlueArea style={styles.root}>
|
||||
<BlueSpacing20 />
|
||||
<BlueReplaceFeeSuggestions
|
||||
onFeeSelected={fee => this.setState({ newFeeRate: fee })}
|
||||
transactionMinimum={this.state.feeRate + 1}
|
||||
/>
|
||||
<BlueSpacing />
|
||||
<BlueButton
|
||||
disabled={this.state.newFeeRate <= this.state.feeRate}
|
||||
onPress={() => this.createTransaction()}
|
||||
title={loc.transactions.cpfp_create}
|
||||
/>
|
||||
</BlueCard>
|
||||
<BlueSpacing20 />
|
||||
<BlueSpacing20 />
|
||||
<BlueSpacing20 />
|
||||
<BlueSpacing20 />
|
||||
|
||||
<BlueText h4>{loc.transactions.cpfp_no_bump}</BlueText>
|
||||
</SafeBlueArea>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeBlueArea style={styles.explain}>
|
||||
<ScrollView>{this.renderStage1(loc.transactions.cpfp_exp)}</ScrollView>
|
||||
</SafeBlueArea>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/* global alert */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ActivityIndicator, View, ScrollView, StyleSheet } from 'react-native';
|
||||
import { BlueSpacing20, SafeBlueArea, BlueText, BlueNavigationStyle } from '../../BlueComponents';
|
||||
import PropTypes from 'prop-types';
|
||||
import { HDSegwitBech32Transaction, HDSegwitBech32Wallet } from '../../class';
|
||||
import CPFP from './CPFP';
|
||||
import loc from '../../loc';
|
||||
|
|
|
@ -213,7 +213,6 @@ const WalletTransactions = () => {
|
|||
backgroundColor: colors.background,
|
||||
},
|
||||
});
|
||||
const interval = setInterval(() => setTimeElapsed(prev => ({ timeElapsed: prev.timeElapsed + 1 })), 60000);
|
||||
|
||||
/**
|
||||
* Simple wrapper for `wallet.getTransactions()`, where `wallet` is current wallet.
|
||||
|
@ -237,6 +236,7 @@ const WalletTransactions = () => {
|
|||
useEffect(() => {
|
||||
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED, refreshTransactionsFunction, true);
|
||||
HandoffSettings.isHandoffUseEnabled().then(setIsHandOffUseEnabled);
|
||||
const interval = setInterval(() => setTimeElapsed(prev => prev + 1), 60000);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
navigate('DrawerRoot', { selectedWallet: '' });
|
||||
|
|
Loading…
Add table
Reference in a new issue