Merge pull request #6442 from BlueWallet/txstatus

REF: TransactionStatus to TSX
This commit is contained in:
GLaDOS 2024-04-22 08:18:30 +00:00 committed by GitHub
commit b11dc8d515
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 172 additions and 81 deletions

View file

@ -49,7 +49,7 @@ import CPFP from './screen/transactions/CPFP';
import RBFBumpFee from './screen/transactions/RBFBumpFee'; import RBFBumpFee from './screen/transactions/RBFBumpFee';
import RBFCancel from './screen/transactions/RBFCancel'; import RBFCancel from './screen/transactions/RBFCancel';
import TransactionDetails from './screen/transactions/details'; import TransactionDetails from './screen/transactions/details';
import TransactionStatus from './screen/transactions/transactionStatus'; import TransactionStatus from './screen/transactions/TransactionStatus';
import AztecoRedeem from './screen/receive/aztecoRedeem'; import AztecoRedeem from './screen/receive/aztecoRedeem';
import ReceiveDetails from './screen/receive/details'; import ReceiveDetails from './screen/receive/details';
@ -107,7 +107,21 @@ const WalletsRoot = () => {
<WalletsStack.Screen name="WalletDetails" component={WalletDetails} options={WalletDetails.navigationOptions(theme)} /> <WalletsStack.Screen name="WalletDetails" component={WalletDetails} options={WalletDetails.navigationOptions(theme)} />
<WalletsStack.Screen name="LdkViewLogs" component={LdkViewLogs} options={LdkViewLogs.navigationOptions(theme)} /> <WalletsStack.Screen name="LdkViewLogs" component={LdkViewLogs} options={LdkViewLogs.navigationOptions(theme)} />
<WalletsStack.Screen name="TransactionDetails" component={TransactionDetails} options={TransactionDetails.navigationOptions(theme)} /> <WalletsStack.Screen name="TransactionDetails" component={TransactionDetails} options={TransactionDetails.navigationOptions(theme)} />
<WalletsStack.Screen name="TransactionStatus" component={TransactionStatus} options={TransactionStatus.navigationOptions(theme)} /> <WalletsStack.Screen
name="TransactionStatus"
component={TransactionStatus}
initialParams={{
hash: undefined,
walletID: undefined,
}}
options={navigationStyle({
title: '',
statusBarStyle: 'auto',
headerStyle: {
backgroundColor: theme.colors.customHeader,
},
})(theme)}
/>
<WalletsStack.Screen name="CPFP" component={CPFP} options={CPFP.navigationOptions(theme)} /> <WalletsStack.Screen name="CPFP" component={CPFP} options={CPFP.navigationOptions(theme)} />
<WalletsStack.Screen name="RBFBumpFee" component={RBFBumpFee} options={RBFBumpFee.navigationOptions(theme)} /> <WalletsStack.Screen name="RBFBumpFee" component={RBFBumpFee} options={RBFBumpFee.navigationOptions(theme)} />
<WalletsStack.Screen name="RBFCancel" component={RBFCancel} options={RBFCancel.navigationOptions(theme)} /> <WalletsStack.Screen name="RBFCancel" component={RBFCancel} options={RBFCancel.navigationOptions(theme)} />

View file

@ -1,44 +1,98 @@
import React, { useContext, useEffect, useLayoutEffect, useRef, useState } from 'react'; import React, { useEffect, useLayoutEffect, useReducer, useRef } from 'react';
import { View, ActivityIndicator, Text, TouchableOpacity, StyleSheet, BackHandler } from 'react-native'; import { View, ActivityIndicator, Text, TouchableOpacity, StyleSheet, BackHandler } from 'react-native';
import { Icon } from 'react-native-elements'; import { Icon } from 'react-native-elements';
import { useNavigation, useRoute } from '@react-navigation/native'; import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import { BlueCard, BlueLoading, BlueSpacing10, BlueSpacing20, BlueText } from '../../BlueComponents'; import { BlueCard, BlueLoading, BlueSpacing10, BlueSpacing20, BlueText } from '../../BlueComponents';
import TransactionIncomingIcon from '../../components/icons/TransactionIncomingIcon'; import TransactionIncomingIcon from '../../components/icons/TransactionIncomingIcon';
import TransactionOutgoingIcon from '../../components/icons/TransactionOutgoingIcon'; import TransactionOutgoingIcon from '../../components/icons/TransactionOutgoingIcon';
import TransactionPendingIcon from '../../components/icons/TransactionPendingIcon'; import TransactionPendingIcon from '../../components/icons/TransactionPendingIcon';
import navigationStyle from '../../components/navigationStyle';
import HandOffComponent from '../../components/HandOffComponent'; import HandOffComponent from '../../components/HandOffComponent';
import { HDSegwitBech32Transaction } from '../../class'; import { HDSegwitBech32Transaction, HDSegwitBech32Wallet } from '../../class';
import { BitcoinUnit } from '../../models/bitcoinUnits'; import { BitcoinUnit } from '../../models/bitcoinUnits';
import loc, { formatBalanceWithoutSuffix } from '../../loc'; import loc, { formatBalanceWithoutSuffix } from '../../loc';
import { BlueStorageContext } from '../../blue_modules/storage-context'; import { useStorage } from '../../blue_modules/storage-context';
import * as BlueElectrum from '../../blue_modules/BlueElectrum'; import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import { useTheme } from '../../components/themes'; import { useTheme } from '../../components/themes';
import Button from '../../components/Button'; import Button from '../../components/Button';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import SafeArea from '../../components/SafeArea'; import SafeArea from '../../components/SafeArea';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Transaction } from '../../class/wallets/types';
const buttonStatus = Object.freeze({ enum ButtonStatus {
possible: 1, Possible,
unknown: 2, Unknown,
notPossible: 3, NotPossible,
}); }
interface TransactionStatusProps {
route: RouteProp<{ params: { hash?: string; walletID?: string } }, 'params'>;
navigation: NativeStackNavigationProp<any>;
}
enum ActionType {
SetCPFPPossible,
SetRBFBumpFeePossible,
SetRBFCancelPossible,
SetTransaction,
SetLoading,
SetEta,
SetIntervalMs,
SetAllButtonStatus,
}
interface State {
isCPFPPossible: ButtonStatus;
isRBFBumpFeePossible: ButtonStatus;
isRBFCancelPossible: ButtonStatus;
tx: any;
isLoading: boolean;
eta: string;
intervalMs: number;
}
const initialState: State = {
isCPFPPossible: ButtonStatus.Unknown,
isRBFBumpFeePossible: ButtonStatus.Unknown,
isRBFCancelPossible: ButtonStatus.Unknown,
tx: undefined,
isLoading: true,
eta: '',
intervalMs: 1000,
};
const reducer = (state: State, action: { type: ActionType; payload?: any }): State => {
switch (action.type) {
case ActionType.SetCPFPPossible:
return { ...state, isCPFPPossible: action.payload };
case ActionType.SetRBFBumpFeePossible:
return { ...state, isRBFBumpFeePossible: action.payload };
case ActionType.SetRBFCancelPossible:
return { ...state, isRBFCancelPossible: action.payload };
case ActionType.SetTransaction:
return { ...state, tx: action.payload };
case ActionType.SetLoading:
return { ...state, isLoading: action.payload };
case ActionType.SetEta:
return { ...state, eta: action.payload };
case ActionType.SetIntervalMs:
return { ...state, intervalMs: action.payload };
case ActionType.SetAllButtonStatus:
return { ...state, isCPFPPossible: action.payload, isRBFBumpFeePossible: action.payload, isRBFCancelPossible: action.payload };
default:
return state;
}
};
const TransactionsStatus = () => { const TransactionsStatus = () => {
const { setSelectedWalletID, wallets, txMetadata, fetchAndSaveWalletTransactions } = useContext(BlueStorageContext); const [state, dispatch] = useReducer(reducer, initialState);
const { hash, walletID } = useRoute().params; const { isCPFPPossible, isRBFBumpFeePossible, isRBFCancelPossible, tx, isLoading, eta, intervalMs } = state;
const { navigate, setOptions, goBack } = useNavigation(); const { setSelectedWalletID, wallets, txMetadata, fetchAndSaveWalletTransactions } = useStorage();
const { hash, walletID } = useRoute<TransactionStatusProps['route']>().params;
const { navigate, setOptions, goBack } = useNavigation<TransactionStatusProps['navigation']>();
const { colors } = useTheme(); const { colors } = useTheme();
const wallet = useRef(wallets.find(w => w.getID() === walletID)); const wallet = useRef(wallets.find(w => w.getID() === walletID));
const [isCPFPPossible, setIsCPFPPossible] = useState(); const fetchTxInterval = useRef<NodeJS.Timeout>();
const [isRBFBumpFeePossible, setIsRBFBumpFeePossible] = useState();
const [isRBFCancelPossible, setIsRBFCancelPossible] = useState();
const [tx, setTX] = useState();
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,
@ -57,11 +111,41 @@ const TransactionsStatus = () => {
}, },
}); });
useEffect(() => { // Dispatch Calls
setIsCPFPPossible(buttonStatus.unknown);
setIsRBFBumpFeePossible(buttonStatus.unknown); const setTX = (value: any) => {
setIsRBFCancelPossible(buttonStatus.unknown); dispatch({ type: ActionType.SetTransaction, payload: value });
}, []); };
const setIntervalMs = (ms: number) => {
dispatch({ type: ActionType.SetIntervalMs, payload: ms });
};
const setEta = (value: string) => {
dispatch({ type: ActionType.SetEta, payload: value });
};
const setAllButtonStatus = (status: ButtonStatus) => {
dispatch({ type: ActionType.SetAllButtonStatus, payload: status });
};
const setIsLoading = (value: boolean) => {
dispatch({ type: ActionType.SetLoading, payload: value });
};
const setIsCPFPPossible = (status: ButtonStatus) => {
dispatch({ type: ActionType.SetCPFPPossible, payload: status });
};
const setIsRBFBumpFeePossible = (status: ButtonStatus) => {
dispatch({ type: ActionType.SetRBFBumpFeePossible, payload: status });
};
const setIsRBFCancelPossible = (status: ButtonStatus) => {
dispatch({ type: ActionType.SetRBFCancelPossible, payload: status });
};
//
useLayoutEffect(() => { useLayoutEffect(() => {
setOptions({ setOptions({
@ -81,10 +165,11 @@ const TransactionsStatus = () => {
}, [colors, tx]); }, [colors, tx]);
useEffect(() => { useEffect(() => {
for (const newTx of wallet.current.getTransactions()) { if (wallet.current) {
if (newTx.hash === hash) { const transactions = wallet.current.getTransactions();
const newTx = transactions.find((t: Transaction) => t.hash === hash);
if (newTx) {
setTX(newTx); setTX(newTx);
break;
} }
} }
@ -116,18 +201,22 @@ const TransactionsStatus = () => {
console.log('checking tx', hash, 'for confirmations...'); console.log('checking tx', hash, 'for confirmations...');
const transactions = await BlueElectrum.multiGetTransactionByTxid([hash], true, 10); const transactions = await BlueElectrum.multiGetTransactionByTxid([hash], true, 10);
const txFromElectrum = transactions[hash]; const txFromElectrum = transactions[hash];
if (!txFromElectrum) return;
console.log('got txFromElectrum=', txFromElectrum); console.log('got txFromElectrum=', txFromElectrum);
const address = (txFromElectrum?.vout[0]?.scriptPubKey?.addresses || []).pop(); const address = (txFromElectrum?.vout[0]?.scriptPubKey?.addresses || []).pop();
if (!address) return;
if (txFromElectrum && !txFromElectrum.confirmations && txFromElectrum.vsize && address) { if (!txFromElectrum.confirmations && txFromElectrum.vsize) {
const txsM = await BlueElectrum.getMempoolTransactionsByAddress(address); const txsM = await BlueElectrum.getMempoolTransactionsByAddress(address);
let txFromMempool; let txFromMempool;
// searhcing for a correct tx in case this address has several pending txs: // searching for a correct tx in case this address has several pending txs:
for (const tempTxM of txsM) { for (const tempTxM of txsM) {
if (tempTxM.tx_hash === hash) txFromMempool = tempTxM; if (tempTxM.tx_hash === hash) txFromMempool = tempTxM;
} }
if (!txFromMempool) return; if (!txFromMempool) return;
console.log('txFromMempool=', txFromMempool); console.log('txFromMempool=', txFromMempool);
const satPerVbyte = Math.round(txFromMempool.fee / txFromElectrum.vsize); const satPerVbyte = Math.round(txFromMempool.fee / txFromElectrum.vsize);
@ -142,11 +231,11 @@ const TransactionsStatus = () => {
if (satPerVbyte < fees.medium) { if (satPerVbyte < fees.medium) {
setEta(loc.formatString(loc.transactions.eta_1d)); setEta(loc.formatString(loc.transactions.eta_1d));
} }
} else if (txFromElectrum.confirmations > 0) { } else if (txFromElectrum.confirmations && txFromElectrum.confirmations > 0) {
// now, handling a case when tx became confirmed! // now, handling a case when tx became confirmed!
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
setEta(''); setEta('');
setTX(prevState => { setTX((prevState: any) => {
return Object.assign({}, prevState, { confirmations: txFromElectrum.confirmations }); return Object.assign({}, prevState, { confirmations: txFromElectrum.confirmations });
}); });
clearInterval(fetchTxInterval.current); clearInterval(fetchTxInterval.current);
@ -160,7 +249,7 @@ const TransactionsStatus = () => {
}, [hash, intervalMs, tx, fetchAndSaveWalletTransactions]); }, [hash, intervalMs, tx, fetchAndSaveWalletTransactions]);
const handleBackButton = () => { const handleBackButton = () => {
goBack(null); goBack();
return true; return true;
}; };
@ -175,21 +264,19 @@ const TransactionsStatus = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const initialState = async () => { const initialButtonsState = async () => {
try { try {
await checkPossibilityOfCPFP(); await checkPossibilityOfCPFP();
await checkPossibilityOfRBFBumpFee(); await checkPossibilityOfRBFBumpFee();
await checkPossibilityOfRBFCancel(); await checkPossibilityOfRBFCancel();
} catch (e) { } catch (e) {
setIsCPFPPossible(buttonStatus.notPossible); setAllButtonStatus(ButtonStatus.NotPossible);
setIsRBFBumpFeePossible(buttonStatus.notPossible);
setIsRBFCancelPossible(buttonStatus.notPossible);
} }
setIsLoading(false); setIsLoading(false);
}; };
useEffect(() => { useEffect(() => {
initialState(); initialButtonsState();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [tx, wallets]); }, [tx, wallets]);
@ -207,51 +294,54 @@ const TransactionsStatus = () => {
}, []); }, []);
const checkPossibilityOfCPFP = async () => { const checkPossibilityOfCPFP = async () => {
if (!wallet.current.allowRBF()) { if (!wallet.current?.allowRBF()) {
return setIsCPFPPossible(buttonStatus.notPossible); return setIsCPFPPossible(ButtonStatus.NotPossible);
} }
const cpfbTx = new HDSegwitBech32Transaction(null, tx.hash, wallet.current); if (wallet.current) {
if ((await cpfbTx.isToUsTransaction()) && (await cpfbTx.getRemoteConfirmationsNum()) === 0) { const cpfbTx = new HDSegwitBech32Transaction(null, tx.hash, wallet.current as HDSegwitBech32Wallet);
return setIsCPFPPossible(buttonStatus.possible); if ((await cpfbTx.isToUsTransaction()) && (await cpfbTx.getRemoteConfirmationsNum()) === 0) {
} else { return setIsCPFPPossible(ButtonStatus.Possible);
return setIsCPFPPossible(buttonStatus.notPossible); } else {
return setIsCPFPPossible(ButtonStatus.NotPossible);
}
} }
return setIsCPFPPossible(ButtonStatus.NotPossible);
}; };
const checkPossibilityOfRBFBumpFee = async () => { const checkPossibilityOfRBFBumpFee = async () => {
if (!wallet.current.allowRBF()) { if (!wallet.current?.allowRBF()) {
return setIsRBFBumpFeePossible(buttonStatus.notPossible); return setIsRBFBumpFeePossible(ButtonStatus.NotPossible);
} }
const rbfTx = new HDSegwitBech32Transaction(null, tx.hash, wallet.current); const rbfTx = new HDSegwitBech32Transaction(null, tx.hash, wallet.current as HDSegwitBech32Wallet);
if ( if (
(await rbfTx.isOurTransaction()) && (await rbfTx.isOurTransaction()) &&
(await rbfTx.getRemoteConfirmationsNum()) === 0 && (await rbfTx.getRemoteConfirmationsNum()) === 0 &&
(await rbfTx.isSequenceReplaceable()) && (await rbfTx.isSequenceReplaceable()) &&
(await rbfTx.canBumpTx()) (await rbfTx.canBumpTx())
) { ) {
return setIsRBFBumpFeePossible(buttonStatus.possible); return setIsRBFBumpFeePossible(ButtonStatus.Possible);
} else { } else {
return setIsRBFBumpFeePossible(buttonStatus.notPossible); return setIsRBFBumpFeePossible(ButtonStatus.NotPossible);
} }
}; };
const checkPossibilityOfRBFCancel = async () => { const checkPossibilityOfRBFCancel = async () => {
if (!wallet.current.allowRBF()) { if (!wallet.current?.allowRBF()) {
return setIsRBFCancelPossible(buttonStatus.notPossible); return setIsRBFCancelPossible(ButtonStatus.NotPossible);
} }
const rbfTx = new HDSegwitBech32Transaction(null, tx.hash, wallet.current); const rbfTx = new HDSegwitBech32Transaction(null, tx.hash, wallet.current as HDSegwitBech32Wallet);
if ( if (
(await rbfTx.isOurTransaction()) && (await rbfTx.isOurTransaction()) &&
(await rbfTx.getRemoteConfirmationsNum()) === 0 && (await rbfTx.getRemoteConfirmationsNum()) === 0 &&
(await rbfTx.isSequenceReplaceable()) && (await rbfTx.isSequenceReplaceable()) &&
(await rbfTx.canCancelTx()) (await rbfTx.canCancelTx())
) { ) {
return setIsRBFCancelPossible(buttonStatus.possible); return setIsRBFCancelPossible(ButtonStatus.Possible);
} else { } else {
return setIsRBFCancelPossible(buttonStatus.notPossible); return setIsRBFCancelPossible(ButtonStatus.NotPossible);
} }
}; };
@ -280,14 +370,14 @@ const TransactionsStatus = () => {
}; };
const renderCPFP = () => { const renderCPFP = () => {
if (isCPFPPossible === buttonStatus.unknown) { if (isCPFPPossible === ButtonStatus.Unknown) {
return ( return (
<> <>
<ActivityIndicator /> <ActivityIndicator />
<BlueSpacing20 /> <BlueSpacing20 />
</> </>
); );
} else if (isCPFPPossible === buttonStatus.possible) { } else if (isCPFPPossible === ButtonStatus.Possible) {
return ( return (
<> <>
<Button onPress={navigateToCPFP} title={loc.transactions.status_bump} /> <Button onPress={navigateToCPFP} title={loc.transactions.status_bump} />
@ -298,13 +388,13 @@ const TransactionsStatus = () => {
}; };
const renderRBFCancel = () => { const renderRBFCancel = () => {
if (isRBFCancelPossible === buttonStatus.unknown) { if (isRBFCancelPossible === ButtonStatus.Unknown) {
return ( return (
<> <>
<ActivityIndicator /> <ActivityIndicator />
</> </>
); );
} else if (isRBFCancelPossible === buttonStatus.possible) { } else if (isRBFCancelPossible === ButtonStatus.Possible) {
return ( return (
<> <>
<TouchableOpacity accessibilityRole="button" style={styles.cancel}> <TouchableOpacity accessibilityRole="button" style={styles.cancel}>
@ -319,14 +409,14 @@ const TransactionsStatus = () => {
}; };
const renderRBFBumpFee = () => { const renderRBFBumpFee = () => {
if (isRBFBumpFeePossible === buttonStatus.unknown) { if (isRBFBumpFeePossible === ButtonStatus.Unknown) {
return ( return (
<> <>
<ActivityIndicator /> <ActivityIndicator />
<BlueSpacing20 /> <BlueSpacing20 />
</> </>
); );
} else if (isRBFBumpFeePossible === buttonStatus.possible) { } else if (isRBFBumpFeePossible === ButtonStatus.Possible) {
return ( return (
<> <>
<Button onPress={navigateToRBFBumpFee} title={loc.transactions.status_bump} /> <Button onPress={navigateToRBFBumpFee} title={loc.transactions.status_bump} />
@ -351,7 +441,7 @@ const TransactionsStatus = () => {
} }
}; };
if (isLoading || !tx) { if (isLoading || !tx || wallet.current === undefined) {
return ( return (
<SafeArea> <SafeArea>
<BlueLoading /> <BlueLoading />
@ -371,7 +461,7 @@ const TransactionsStatus = () => {
<View style={styles.center}> <View style={styles.center}>
<Text style={[styles.value, stylesHook.value]} selectable> <Text style={[styles.value, stylesHook.value]} selectable>
{formatBalanceWithoutSuffix(tx.value, wallet.current.preferredBalanceUnit, true)}{' '} {formatBalanceWithoutSuffix(tx.value, wallet.current.preferredBalanceUnit, true)}{' '}
{wallet.current.preferredBalanceUnit !== BitcoinUnit.LOCAL_CURRENCY && ( {wallet.current?.preferredBalanceUnit !== BitcoinUnit.LOCAL_CURRENCY && (
<Text style={[styles.valueUnit, stylesHook.valueUnit]}>{loc.units[wallet.current.preferredBalanceUnit]}</Text> <Text style={[styles.valueUnit, stylesHook.valueUnit]}>{loc.units[wallet.current.preferredBalanceUnit]}</Text>
)} )}
</Text> </Text>
@ -412,7 +502,7 @@ const TransactionsStatus = () => {
<View style={styles.fee}> <View style={styles.fee}>
<BlueText style={styles.feeText}> <BlueText style={styles.feeText}>
{loc.send.create_fee.toLowerCase()} {formatBalanceWithoutSuffix(tx.fee, wallet.current.preferredBalanceUnit, true)}{' '} {loc.send.create_fee.toLowerCase()} {formatBalanceWithoutSuffix(tx.fee, wallet.current.preferredBalanceUnit, true)}{' '}
{wallet.current.preferredBalanceUnit !== BitcoinUnit.LOCAL_CURRENCY && wallet.current.preferredBalanceUnit} {wallet.current?.preferredBalanceUnit !== BitcoinUnit.LOCAL_CURRENCY && wallet.current?.preferredBalanceUnit}
</BlueText> </BlueText>
</View> </View>
)} )}
@ -540,16 +630,3 @@ const styles = StyleSheet.create({
fontWeight: '600', fontWeight: '600',
}, },
}); });
TransactionsStatus.navigationOptions = navigationStyle(
{
headerTitle: '',
statusBarStyle: 'auto',
},
(options, { theme }) => ({
...options,
headerStyle: {
backgroundColor: theme.colors.customHeader,
},
}),
);