mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-02-23 23:27:26 +01:00
Merge pull request #3683 from BlueWallet/add-onchain-receive-address-eta
ADD: onchain receive address now shows ETA on incomming transaction
This commit is contained in:
commit
29e6b74d88
10 changed files with 361 additions and 12 deletions
12
blue_modules/BlueElectrum.d.ts
vendored
12
blue_modules/BlueElectrum.d.ts
vendored
|
@ -34,11 +34,17 @@ type Transaction = {
|
||||||
};
|
};
|
||||||
}[];
|
}[];
|
||||||
blockhash: string;
|
blockhash: string;
|
||||||
confirmations: number;
|
confirmations?: number;
|
||||||
time: number;
|
time: number;
|
||||||
blocktime: number;
|
blocktime: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MempoolTransaction = {
|
||||||
|
height: 0;
|
||||||
|
tx_hash: string; // eslint-disable-line camelcase
|
||||||
|
fee: number;
|
||||||
|
};
|
||||||
|
|
||||||
export async function connectMain(): Promise<void>;
|
export async function connectMain(): Promise<void>;
|
||||||
|
|
||||||
export async function waitTillConnected(): Promise<boolean>;
|
export async function waitTillConnected(): Promise<boolean>;
|
||||||
|
@ -59,6 +65,8 @@ export function multiGetTransactionByTxid(txIds: string[], batchsize: number, ve
|
||||||
|
|
||||||
export function getTransactionsByAddress(address: string): Transaction[];
|
export function getTransactionsByAddress(address: string): Transaction[];
|
||||||
|
|
||||||
|
export function getMempoolTransactionsByAddress(address: string): Promise<MempoolTransaction[]>;
|
||||||
|
|
||||||
export function estimateCurrentBlockheight(): number;
|
export function estimateCurrentBlockheight(): number;
|
||||||
|
|
||||||
export function multiGetHistoryByAddress(
|
export function multiGetHistoryByAddress(
|
||||||
|
@ -74,4 +82,6 @@ export function multiGetHistoryByAddress(
|
||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export function estimateFees(): Promise<{ fast: number; medium: number; slow: number }>;
|
||||||
|
|
||||||
export function broadcastV2(txhex: string): Promise<string>;
|
export function broadcastV2(txhex: string): Promise<string>;
|
||||||
|
|
|
@ -354,6 +354,19 @@ module.exports.getTransactionsByAddress = async function (address) {
|
||||||
return history;
|
return history;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param address {String}
|
||||||
|
* @returns {Promise<Array>}
|
||||||
|
*/
|
||||||
|
module.exports.getMempoolTransactionsByAddress = async function (address) {
|
||||||
|
if (!mainClient) throw new Error('Electrum client is not connected');
|
||||||
|
const script = bitcoin.address.toOutputScript(address);
|
||||||
|
const hash = bitcoin.crypto.sha256(script);
|
||||||
|
const reversedHash = Buffer.from(reverse(hash));
|
||||||
|
return mainClient.blockchainScripthash_getMempool(reversedHash.toString('hex'));
|
||||||
|
};
|
||||||
|
|
||||||
module.exports.ping = async function () {
|
module.exports.ping = async function () {
|
||||||
try {
|
try {
|
||||||
await mainClient.server_ping();
|
await mainClient.server_ping();
|
||||||
|
|
|
@ -429,6 +429,7 @@ class DeeplinkSchemaMatch {
|
||||||
}
|
}
|
||||||
|
|
||||||
static bip21decode(uri) {
|
static bip21decode(uri) {
|
||||||
|
if (!uri) return {};
|
||||||
return bip21.decode(uri.replace('BITCOIN:', 'bitcoin:'));
|
return bip21.decode(uri.replace('BITCOIN:', 'bitcoin:'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ export class LegacyWallet extends AbstractWallet {
|
||||||
*/
|
*/
|
||||||
timeToRefreshTransaction(): boolean {
|
timeToRefreshTransaction(): boolean {
|
||||||
for (const tx of this.getTransactions()) {
|
for (const tx of this.getTransactions()) {
|
||||||
if (tx.confirmations < 7 && this._lastTxFetch < +new Date() - 5 * 60 * 1000) {
|
if ((tx.confirmations ?? 0) < 7 && this._lastTxFetch < +new Date() - 5 * 60 * 1000) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,7 +219,7 @@ export class LegacyWallet extends AbstractWallet {
|
||||||
amount: value,
|
amount: value,
|
||||||
confirmations: tx.confirmations,
|
confirmations: tx.confirmations,
|
||||||
wif: false,
|
wif: false,
|
||||||
height: BlueElectrum.estimateCurrentBlockheight() - tx.confirmations,
|
height: BlueElectrum.estimateCurrentBlockheight() - (tx.confirmations ?? 0),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,7 @@ export type Transaction = {
|
||||||
inputs: TransactionInput[];
|
inputs: TransactionInput[];
|
||||||
outputs: TransactionOutput[];
|
outputs: TransactionOutput[];
|
||||||
blockhash: string;
|
blockhash: string;
|
||||||
confirmations: number;
|
confirmations?: number;
|
||||||
time: number;
|
time: number;
|
||||||
blocktime: number;
|
blocktime: number;
|
||||||
received?: number;
|
received?: number;
|
||||||
|
|
39
components/TransactionPendingIconBig.js
Normal file
39
components/TransactionPendingIconBig.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/* eslint react/prop-types: "off", react-native/no-inline-styles: "off" */
|
||||||
|
import { useTheme } from '@react-navigation/native';
|
||||||
|
import { StyleSheet, View } from 'react-native';
|
||||||
|
import { Icon } from 'react-native-elements';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const TransactionPendingIconBig = props => {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
const stylesBlueIconHooks = StyleSheet.create({
|
||||||
|
ball: {
|
||||||
|
backgroundColor: colors.buttonBackgroundColor,
|
||||||
|
},
|
||||||
|
ball2: {
|
||||||
|
width: 150,
|
||||||
|
height: 150,
|
||||||
|
borderRadius: 75,
|
||||||
|
},
|
||||||
|
boxIncoming: {
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<View {...props}>
|
||||||
|
<View style={stylesBlueIconHooks.boxIncoming}>
|
||||||
|
<View style={[stylesBlueIconHooks.ball2, stylesBlueIconHooks.ball]}>
|
||||||
|
<Icon
|
||||||
|
{...props}
|
||||||
|
name="kebab-horizontal"
|
||||||
|
size={100}
|
||||||
|
type="octicon"
|
||||||
|
color={colors.foregroundColor}
|
||||||
|
iconStyle={{ left: 0, top: 25 }}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
|
@ -375,6 +375,11 @@
|
||||||
"enable_offline_signing": "This wallet is not being used in conjunction with an offline signing. Would you wish to enable it now?",
|
"enable_offline_signing": "This wallet is not being used in conjunction with an offline signing. Would you wish to enable it now?",
|
||||||
"list_conf": "Conf: {number}",
|
"list_conf": "Conf: {number}",
|
||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
|
"pending_with_amount": "Pending {amt1} ({amt2})",
|
||||||
|
"received_with_amount": "+{amt1} ({amt2})",
|
||||||
|
"eta_10m": "ETA: In ~10 minutes",
|
||||||
|
"eta_3h": "ETA: In ~3 hours",
|
||||||
|
"eta_1d": "ETA: In ~1 day",
|
||||||
"list_title": "Transactions",
|
"list_title": "Transactions",
|
||||||
"rbf_explain": "We will replace this transaction with one with a higher fee, so it should be mined faster. This is called RBF—Replace by Fee.",
|
"rbf_explain": "We will replace this transaction with one with a higher fee, so it should be mined faster. This is called RBF—Replace by Fee.",
|
||||||
"rbf_title": "Bump Fee (RBF)",
|
"rbf_title": "Bump Fee (RBF)",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useCallback, useContext, useState } from 'react';
|
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
|
BackHandler,
|
||||||
InteractionManager,
|
InteractionManager,
|
||||||
Keyboard,
|
Keyboard,
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
|
@ -23,6 +24,8 @@ import {
|
||||||
BlueSpacing20,
|
BlueSpacing20,
|
||||||
BlueAlertWalletExportReminder,
|
BlueAlertWalletExportReminder,
|
||||||
BlueCard,
|
BlueCard,
|
||||||
|
BlueSpacing40,
|
||||||
|
BlueBigCheckmark,
|
||||||
} from '../../BlueComponents';
|
} from '../../BlueComponents';
|
||||||
import navigationStyle from '../../components/navigationStyle';
|
import navigationStyle from '../../components/navigationStyle';
|
||||||
import BottomModal from '../../components/BottomModal';
|
import BottomModal from '../../components/BottomModal';
|
||||||
|
@ -31,14 +34,17 @@ import { Chain, BitcoinUnit } from '../../models/bitcoinUnits';
|
||||||
import HandoffComponent from '../../components/handoff';
|
import HandoffComponent from '../../components/handoff';
|
||||||
import AmountInput from '../../components/AmountInput';
|
import AmountInput from '../../components/AmountInput';
|
||||||
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
|
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
|
||||||
import loc from '../../loc';
|
import loc, { formatBalance } from '../../loc';
|
||||||
import { BlueStorageContext } from '../../blue_modules/storage-context';
|
import { BlueStorageContext } from '../../blue_modules/storage-context';
|
||||||
import Notifications from '../../blue_modules/notifications';
|
import Notifications from '../../blue_modules/notifications';
|
||||||
|
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||||
|
import { TransactionPendingIconBig } from '../../components/TransactionPendingIconBig';
|
||||||
|
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
|
||||||
const currency = require('../../blue_modules/currency');
|
const currency = require('../../blue_modules/currency');
|
||||||
|
|
||||||
const ReceiveDetails = () => {
|
const ReceiveDetails = () => {
|
||||||
const { walletID, address } = useRoute().params;
|
const { walletID, address } = useRoute().params;
|
||||||
const { wallets, saveToDisk, sleep, isElectrumDisabled } = useContext(BlueStorageContext);
|
const { wallets, saveToDisk, sleep, isElectrumDisabled, fetchAndSaveWalletTransactions } = useContext(BlueStorageContext);
|
||||||
const wallet = wallets.find(w => w.getID() === walletID);
|
const wallet = wallets.find(w => w.getID() === walletID);
|
||||||
const [customLabel, setCustomLabel] = useState();
|
const [customLabel, setCustomLabel] = useState();
|
||||||
const [customAmount, setCustomAmount] = useState();
|
const [customAmount, setCustomAmount] = useState();
|
||||||
|
@ -46,9 +52,17 @@ const ReceiveDetails = () => {
|
||||||
const [bip21encoded, setBip21encoded] = useState();
|
const [bip21encoded, setBip21encoded] = useState();
|
||||||
const [isCustom, setIsCustom] = useState(false);
|
const [isCustom, setIsCustom] = useState(false);
|
||||||
const [isCustomModalVisible, setIsCustomModalVisible] = useState(false);
|
const [isCustomModalVisible, setIsCustomModalVisible] = useState(false);
|
||||||
|
const [showPendingBalance, setShowPendingBalance] = useState(false);
|
||||||
|
const [showConfirmedBalance, setShowConfirmedBalance] = useState(false);
|
||||||
const [showAddress, setShowAddress] = useState(false);
|
const [showAddress, setShowAddress] = useState(false);
|
||||||
const { navigate, goBack, setParams } = useNavigation();
|
const { navigate, goBack, setParams } = useNavigation();
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
|
const [intervalMs, setIntervalMs] = useState(5000);
|
||||||
|
const [eta, setEta] = useState('');
|
||||||
|
const [initialConfirmed, setInitialConfirmed] = useState(0);
|
||||||
|
const [initialUnconfirmed, setInitialUnconfirmed] = useState(0);
|
||||||
|
const [displayBalance, setDisplayBalance] = useState('');
|
||||||
|
const fetchAddressInterval = useRef();
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
modalContent: {
|
modalContent: {
|
||||||
backgroundColor: colors.modal,
|
backgroundColor: colors.modal,
|
||||||
|
@ -131,6 +145,163 @@ const ReceiveDetails = () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// re-fetching address balance periodically
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('receive/defails - useEffect');
|
||||||
|
|
||||||
|
if (fetchAddressInterval.current) {
|
||||||
|
// interval already exists, lets cleanup it and recreate, so theres no duplicate intervals
|
||||||
|
clearInterval(fetchAddressInterval.current);
|
||||||
|
fetchAddressInterval.current = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAddressInterval.current = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const decoded = DeeplinkSchemaMatch.bip21decode(bip21encoded);
|
||||||
|
const address2use = address || decoded.address;
|
||||||
|
if (!address2use) return;
|
||||||
|
|
||||||
|
console.log('checking address', address2use, 'for balance...');
|
||||||
|
const balance = await BlueElectrum.getBalanceByAddress(address2use);
|
||||||
|
console.log('...got', balance);
|
||||||
|
|
||||||
|
if (balance.unconfirmed > 0) {
|
||||||
|
if (initialConfirmed === 0 && initialUnconfirmed === 0) {
|
||||||
|
// saving initial values for later (when tx gets confirmed)
|
||||||
|
setInitialConfirmed(balance.confirmed);
|
||||||
|
setInitialUnconfirmed(balance.unconfirmed);
|
||||||
|
setIntervalMs(25000);
|
||||||
|
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
const txs = await BlueElectrum.getMempoolTransactionsByAddress(address2use);
|
||||||
|
const tx = txs.pop();
|
||||||
|
if (tx) {
|
||||||
|
const rez = await BlueElectrum.multiGetTransactionByTxid([tx.tx_hash], 10, true);
|
||||||
|
if (rez && rez[tx.tx_hash] && rez[tx.tx_hash].vsize) {
|
||||||
|
const satPerVbyte = Math.round(tx.fee / rez[tx.tx_hash].vsize);
|
||||||
|
const fees = await BlueElectrum.estimateFees();
|
||||||
|
if (satPerVbyte >= fees.fast) {
|
||||||
|
setEta(loc.formatString(loc.transactions.eta_10m));
|
||||||
|
}
|
||||||
|
if (satPerVbyte >= fees.medium && satPerVbyte < fees.fast) {
|
||||||
|
setEta(loc.formatString(loc.transactions.eta_3h));
|
||||||
|
}
|
||||||
|
if (satPerVbyte < fees.medium) {
|
||||||
|
setEta(loc.formatString(loc.transactions.eta_1d));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisplayBalance(
|
||||||
|
loc.formatString(loc.transactions.pending_with_amount, {
|
||||||
|
amt1: formatBalance(balance.unconfirmed, BitcoinUnit.LOCAL_CURRENCY, true).toString(),
|
||||||
|
amt2: formatBalance(balance.unconfirmed, BitcoinUnit.BTC, true).toString(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
setShowPendingBalance(true);
|
||||||
|
setShowAddress(false);
|
||||||
|
} else if (balance.unconfirmed === 0 && initialUnconfirmed !== 0) {
|
||||||
|
// now, handling a case when unconfirmed == 0, but in past it wasnt (i.e. it changed while user was
|
||||||
|
// staring at the screen)
|
||||||
|
|
||||||
|
const balanceToShow = balance.confirmed - initialConfirmed;
|
||||||
|
|
||||||
|
if (balanceToShow > 0) {
|
||||||
|
// address has actually more coins then initially, so we definately gained something
|
||||||
|
setShowConfirmedBalance(true);
|
||||||
|
setShowPendingBalance(false);
|
||||||
|
setShowAddress(false);
|
||||||
|
|
||||||
|
clearInterval(fetchAddressInterval.current);
|
||||||
|
fetchAddressInterval.current = undefined;
|
||||||
|
|
||||||
|
setDisplayBalance(
|
||||||
|
loc.formatString(loc.transactions.received_with_amount, {
|
||||||
|
amt1: formatBalance(balanceToShow, BitcoinUnit.LOCAL_CURRENCY, true).toString(),
|
||||||
|
amt2: formatBalance(balanceToShow, BitcoinUnit.BTC, true).toString(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
fetchAndSaveWalletTransactions(walletID);
|
||||||
|
} else {
|
||||||
|
// rare case, but probable. transaction evicted from mempool (maybe cancelled by the sender)
|
||||||
|
setShowConfirmedBalance(false);
|
||||||
|
setShowPendingBalance(false);
|
||||||
|
setShowAddress(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}, intervalMs);
|
||||||
|
}, [bip21encoded, address, initialConfirmed, initialUnconfirmed, intervalMs, fetchAndSaveWalletTransactions, walletID]);
|
||||||
|
|
||||||
|
const renderConfirmedBalance = () => {
|
||||||
|
return (
|
||||||
|
<ScrollView contentContainerStyle={styles.root} keyboardShouldPersistTaps="always">
|
||||||
|
<View style={styles.scrollBody}>
|
||||||
|
{isCustom && (
|
||||||
|
<>
|
||||||
|
<BlueText style={styles.label} numberOfLines={1}>
|
||||||
|
{customLabel}
|
||||||
|
</BlueText>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<View style={styles.qrCodeContainer}>
|
||||||
|
<BlueBigCheckmark />
|
||||||
|
</View>
|
||||||
|
<BlueSpacing40 />
|
||||||
|
<BlueText style={styles.label} numberOfLines={1}>
|
||||||
|
{displayBalance}
|
||||||
|
</BlueText>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderPendingBalance = () => {
|
||||||
|
return (
|
||||||
|
<ScrollView contentContainerStyle={styles.root} keyboardShouldPersistTaps="always">
|
||||||
|
<View style={styles.scrollBody}>
|
||||||
|
{isCustom && (
|
||||||
|
<>
|
||||||
|
<BlueText style={styles.label} numberOfLines={1}>
|
||||||
|
{customLabel}
|
||||||
|
</BlueText>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<View style={styles.qrCodeContainer}>
|
||||||
|
<TransactionPendingIconBig />
|
||||||
|
</View>
|
||||||
|
<BlueSpacing40 />
|
||||||
|
<BlueText style={styles.label} numberOfLines={1}>
|
||||||
|
{displayBalance}
|
||||||
|
</BlueText>
|
||||||
|
<BlueText style={styles.label} numberOfLines={1}>
|
||||||
|
{eta}
|
||||||
|
</BlueText>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackButton = () => {
|
||||||
|
goBack(null);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
BackHandler.addEventListener('hardwareBackPress', handleBackButton);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
BackHandler.removeEventListener('hardwareBackPress', handleBackButton);
|
||||||
|
clearInterval(fetchAddressInterval.current);
|
||||||
|
fetchAddressInterval.current = undefined;
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const renderReceiveDetails = () => {
|
const renderReceiveDetails = () => {
|
||||||
return (
|
return (
|
||||||
<ScrollView contentContainerStyle={styles.root} keyboardShouldPersistTaps="always">
|
<ScrollView contentContainerStyle={styles.root} keyboardShouldPersistTaps="always">
|
||||||
|
@ -340,7 +511,10 @@ const ReceiveDetails = () => {
|
||||||
url={`https://blockstream.info/address/${address}`}
|
url={`https://blockstream.info/address/${address}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showAddress ? renderReceiveDetails() : <BlueLoading />}
|
{showConfirmedBalance ? renderConfirmedBalance() : null}
|
||||||
|
{showPendingBalance ? renderPendingBalance() : null}
|
||||||
|
{showAddress ? renderReceiveDetails() : null}
|
||||||
|
{!showAddress && !showPendingBalance && !showConfirmedBalance ? <BlueLoading /> : null}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||||
import { View, ActivityIndicator, Text, TouchableOpacity, StyleSheet, StatusBar, I18nManager } from 'react-native';
|
import { View, ActivityIndicator, Text, TouchableOpacity, StyleSheet, StatusBar, I18nManager, BackHandler } from 'react-native';
|
||||||
import { Icon } from 'react-native-elements';
|
import { Icon } from 'react-native-elements';
|
||||||
import { useNavigation, useRoute, useTheme } from '@react-navigation/native';
|
import { useNavigation, useRoute, useTheme } from '@react-navigation/native';
|
||||||
|
|
||||||
|
@ -21,6 +21,8 @@ import { BitcoinUnit } from '../../models/bitcoinUnits';
|
||||||
import HandoffComponent from '../../components/handoff';
|
import HandoffComponent from '../../components/handoff';
|
||||||
import loc, { formatBalanceWithoutSuffix } from '../../loc';
|
import loc, { formatBalanceWithoutSuffix } from '../../loc';
|
||||||
import { BlueStorageContext } from '../../blue_modules/storage-context';
|
import { BlueStorageContext } from '../../blue_modules/storage-context';
|
||||||
|
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
|
||||||
|
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||||
|
|
||||||
const buttonStatus = Object.freeze({
|
const buttonStatus = Object.freeze({
|
||||||
possible: 1,
|
possible: 1,
|
||||||
|
@ -29,9 +31,9 @@ const buttonStatus = Object.freeze({
|
||||||
});
|
});
|
||||||
|
|
||||||
const TransactionsStatus = () => {
|
const TransactionsStatus = () => {
|
||||||
const { setSelectedWallet, wallets, txMetadata, getTransactions } = useContext(BlueStorageContext);
|
const { setSelectedWallet, wallets, txMetadata, getTransactions, fetchAndSaveWalletTransactions } = useContext(BlueStorageContext);
|
||||||
const { hash } = useRoute().params;
|
const { hash } = useRoute().params;
|
||||||
const { navigate, setOptions } = useNavigation();
|
const { navigate, setOptions, goBack } = useNavigation();
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const wallet = useRef();
|
const wallet = useRef();
|
||||||
const [isCPFPPossible, setIsCPFPPossible] = useState();
|
const [isCPFPPossible, setIsCPFPPossible] = useState();
|
||||||
|
@ -39,6 +41,9 @@ const TransactionsStatus = () => {
|
||||||
const [isRBFCancelPossible, setIsRBFCancelPossible] = useState();
|
const [isRBFCancelPossible, setIsRBFCancelPossible] = useState();
|
||||||
const [tx, setTX] = useState();
|
const [tx, setTX] = useState();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const fetchTxInterval = useRef();
|
||||||
|
const [intervalMs, setIntervalMs] = useState(1000);
|
||||||
|
const [eta, setEta] = useState('');
|
||||||
const stylesHook = StyleSheet.create({
|
const stylesHook = StyleSheet.create({
|
||||||
value: {
|
value: {
|
||||||
color: colors.alternativeTextColor2,
|
color: colors.alternativeTextColor2,
|
||||||
|
@ -94,6 +99,86 @@ const TransactionsStatus = () => {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [hash]);
|
}, [hash]);
|
||||||
|
|
||||||
|
// re-fetching tx status periodically
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('transactionStatus - useEffect');
|
||||||
|
|
||||||
|
if (!tx || tx?.confirmations) return;
|
||||||
|
if (!hash) return;
|
||||||
|
|
||||||
|
if (fetchTxInterval.current) {
|
||||||
|
// interval already exists, lets cleanup it and recreate, so theres no duplicate intervals
|
||||||
|
clearInterval(fetchTxInterval.current);
|
||||||
|
fetchTxInterval.current = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('setting up interval to check tx...');
|
||||||
|
fetchTxInterval.current = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
setIntervalMs(31000); // upon first execution we increase poll interval;
|
||||||
|
|
||||||
|
console.log('checking tx', hash, 'for confirmations...');
|
||||||
|
const transactions = await BlueElectrum.multiGetTransactionByTxid([hash], 10, true);
|
||||||
|
const txFromElectrum = transactions[hash];
|
||||||
|
console.log('got txFromElectrum=', txFromElectrum);
|
||||||
|
|
||||||
|
const address = (txFromElectrum?.vout[0]?.scriptPubKey?.addresses || []).pop();
|
||||||
|
|
||||||
|
if (txFromElectrum && !txFromElectrum.confirmations && txFromElectrum.vsize && address) {
|
||||||
|
const txsM = await BlueElectrum.getMempoolTransactionsByAddress(address);
|
||||||
|
let txFromMempool;
|
||||||
|
// searhcing for a correct tx in case this address has several pending txs:
|
||||||
|
for (const tempTxM of txsM) {
|
||||||
|
if (tempTxM.tx_hash === hash) txFromMempool = tempTxM;
|
||||||
|
}
|
||||||
|
if (!txFromMempool) return;
|
||||||
|
console.log('txFromMempool=', txFromMempool);
|
||||||
|
|
||||||
|
const satPerVbyte = Math.round(txFromMempool.fee / txFromElectrum.vsize);
|
||||||
|
const fees = await BlueElectrum.estimateFees();
|
||||||
|
console.log('fees=', fees, 'satPerVbyte=', satPerVbyte);
|
||||||
|
if (satPerVbyte >= fees.fast) {
|
||||||
|
setEta(loc.formatString(loc.transactions.eta_10m));
|
||||||
|
}
|
||||||
|
if (satPerVbyte >= fees.medium && satPerVbyte < fees.fast) {
|
||||||
|
setEta(loc.formatString(loc.transactions.eta_3h));
|
||||||
|
}
|
||||||
|
if (satPerVbyte < fees.medium) {
|
||||||
|
setEta(loc.formatString(loc.transactions.eta_1d));
|
||||||
|
}
|
||||||
|
} else if (txFromElectrum.confirmations > 0) {
|
||||||
|
// now, handling a case when tx became confirmed!
|
||||||
|
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
|
||||||
|
setEta('');
|
||||||
|
setTX(prevState => {
|
||||||
|
return Object.assign({}, prevState, { confirmations: txFromElectrum.confirmations });
|
||||||
|
});
|
||||||
|
clearInterval(fetchTxInterval.current);
|
||||||
|
fetchTxInterval.current = undefined;
|
||||||
|
wallet?.current?.getID() && fetchAndSaveWalletTransactions(wallet.current.getID());
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}, intervalMs);
|
||||||
|
}, [hash, intervalMs, tx, fetchAndSaveWalletTransactions]);
|
||||||
|
|
||||||
|
const handleBackButton = () => {
|
||||||
|
goBack(null);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
BackHandler.addEventListener('hardwareBackPress', handleBackButton);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
BackHandler.removeEventListener('hardwareBackPress', handleBackButton);
|
||||||
|
clearInterval(fetchTxInterval.current);
|
||||||
|
fetchTxInterval.current = undefined;
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const initialState = async () => {
|
const initialState = async () => {
|
||||||
try {
|
try {
|
||||||
await checkPossibilityOfCPFP();
|
await checkPossibilityOfCPFP();
|
||||||
|
@ -122,7 +207,7 @@ const TransactionsStatus = () => {
|
||||||
}, [wallet.current]);
|
}, [wallet.current]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('transactions/details - useEffect');
|
console.log('transactionStatus - useEffect');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const checkPossibilityOfCPFP = async () => {
|
const checkPossibilityOfCPFP = async () => {
|
||||||
|
@ -342,6 +427,12 @@ const TransactionsStatus = () => {
|
||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
{eta ? (
|
||||||
|
<View style={[styles.eta]}>
|
||||||
|
<BlueSpacing10 />
|
||||||
|
<Text style={styles.confirmationsText}>{eta}</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
</BlueCard>
|
</BlueCard>
|
||||||
|
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
|
@ -429,6 +520,11 @@ const styles = StyleSheet.create({
|
||||||
color: '#9aa0aa',
|
color: '#9aa0aa',
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
},
|
},
|
||||||
|
eta: {
|
||||||
|
alignSelf: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|
|
@ -138,6 +138,17 @@ describe('BlueElectrum', () => {
|
||||||
assert.strictEqual(txs[0].height, 563077);
|
assert.strictEqual(txs[0].height, 563077);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// skipped because requires fresh address with pending txs every time
|
||||||
|
it.skip('BlueElectrum can do getMempoolTransactionsByAddress()', async function () {
|
||||||
|
const txs = await BlueElectrum.getMempoolTransactionsByAddress('bc1qp33en9mnw277c9vz5fz9vcu666cvervdnk02327wwph97hdjurqqxtl03c');
|
||||||
|
assert.ok(txs.length > 0);
|
||||||
|
assert.ok(txs[0].tx_hash);
|
||||||
|
assert.ok(txs[0].fee);
|
||||||
|
|
||||||
|
const rez = await BlueElectrum.multiGetTransactionByTxid([txs[0].tx_hash], 10, true);
|
||||||
|
assert.ok(rez[txs[0].tx_hash]);
|
||||||
|
});
|
||||||
|
|
||||||
it('BlueElectrum can do getTransactionsFullByAddress()', async function () {
|
it('BlueElectrum can do getTransactionsFullByAddress()', async function () {
|
||||||
const txs = await BlueElectrum.getTransactionsFullByAddress('bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh');
|
const txs = await BlueElectrum.getTransactionsFullByAddress('bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh');
|
||||||
for (const tx of txs) {
|
for (const tx of txs) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue