BlueWallet/screen/transactions/TransactionStatus.tsx

633 lines
19 KiB
TypeScript
Raw Normal View History

2024-04-21 19:55:26 +02:00
import React, { useEffect, useLayoutEffect, useReducer, useRef } from 'react';
2023-10-20 04:28:49 +02:00
import { View, ActivityIndicator, Text, TouchableOpacity, StyleSheet, BackHandler } from 'react-native';
2020-12-25 17:09:53 +01:00
import { Icon } from 'react-native-elements';
2024-04-21 19:55:26 +02:00
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import { BlueCard, BlueLoading, BlueSpacing10, BlueSpacing20, BlueText } from '../../BlueComponents';
2023-03-04 18:51:11 +01:00
import TransactionIncomingIcon from '../../components/icons/TransactionIncomingIcon';
import TransactionOutgoingIcon from '../../components/icons/TransactionOutgoingIcon';
import TransactionPendingIcon from '../../components/icons/TransactionPendingIcon';
import HandOffComponent from '../../components/HandOffComponent';
2024-04-21 19:55:26 +02:00
import { HDSegwitBech32Transaction, HDSegwitBech32Wallet } from '../../class';
2019-08-10 08:57:55 +02:00
import { BitcoinUnit } from '../../models/bitcoinUnits';
2020-07-20 15:38:46 +02:00
import loc, { formatBalanceWithoutSuffix } from '../../loc';
2024-04-21 19:55:26 +02:00
import { useStorage } from '../../blue_modules/storage-context';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
2023-11-15 09:40:22 +01:00
import { useTheme } from '../../components/themes';
import Button from '../../components/Button';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import SafeArea from '../../components/SafeArea';
2024-04-21 19:55:26 +02:00
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
2024-04-22 01:48:00 +02:00
import { Transaction } from '../../class/wallets/types';
2024-04-21 19:55:26 +02:00
enum ButtonStatus {
Possible,
Unknown,
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,
};
2020-12-25 17:09:53 +01:00
2024-04-21 19:55:26 +02:00
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;
}
};
2019-08-10 08:57:55 +02:00
const TransactionsStatus = () => {
2024-04-21 19:55:26 +02:00
const [state, dispatch] = useReducer(reducer, initialState);
const { isCPFPPossible, isRBFBumpFeePossible, isRBFCancelPossible, tx, isLoading, eta, intervalMs } = state;
const { setSelectedWalletID, wallets, txMetadata, fetchAndSaveWalletTransactions } = useStorage();
const { hash, walletID } = useRoute<TransactionStatusProps['route']>().params;
const { navigate, setOptions, goBack } = useNavigation<TransactionStatusProps['navigation']>();
2020-11-30 04:26:25 +01:00
const { colors } = useTheme();
const wallet = useRef(wallets.find(w => w.getID() === walletID));
2024-04-21 19:55:26 +02:00
const fetchTxInterval = useRef<NodeJS.Timeout>();
2020-11-30 04:26:25 +01:00
const stylesHook = StyleSheet.create({
value: {
color: colors.alternativeTextColor2,
},
valueUnit: {
color: colors.alternativeTextColor2,
},
iconRoot: {
backgroundColor: colors.success,
},
2021-09-08 01:46:09 +02:00
detailsText: {
color: colors.buttonTextColor,
},
details: {
backgroundColor: colors.lightButton,
},
2020-11-30 04:26:25 +01:00
});
2024-04-21 19:55:26 +02:00
// Dispatch Calls
const setTX = (value: any) => {
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 });
};
//
2020-12-02 06:24:01 +01:00
useLayoutEffect(() => {
2020-11-30 04:26:25 +01:00
setOptions({
// eslint-disable-next-line react/no-unstable-nested-components
2021-09-05 19:35:08 +02:00
headerRight: () => (
2021-09-06 19:21:09 +02:00
<TouchableOpacity
accessibilityRole="button"
testID="TransactionDetailsButton"
2021-09-08 01:46:09 +02:00
style={[styles.details, stylesHook.details]}
2021-09-06 19:21:09 +02:00
onPress={navigateToTransactionDetials}
>
2021-09-08 01:46:09 +02:00
<Text style={[styles.detailsText, stylesHook.detailsText]}>{loc.send.create_details}</Text>
2021-09-05 19:35:08 +02:00
</TouchableOpacity>
),
2020-11-30 04:26:25 +01:00
});
// eslint-disable-next-line react-hooks/exhaustive-deps
2021-09-07 00:35:54 +02:00
}, [colors, tx]);
2020-11-30 04:26:25 +01:00
useEffect(() => {
2024-04-22 01:48:00 +02:00
if (wallet.current) {
const transactions = wallet.current.getTransactions();
const newTx = transactions.find((t: Transaction) => t.hash === hash);
if (newTx) {
setTX(newTx);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hash, wallet.current]);
useEffect(() => {
wallet.current = wallets.find(w => w.getID() === walletID);
}, [walletID, wallets]);
// 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], true, 10);
const txFromElectrum = transactions[hash];
2024-04-22 01:48:00 +02:00
if (!txFromElectrum) return;
console.log('got txFromElectrum=', txFromElectrum);
const address = (txFromElectrum?.vout[0]?.scriptPubKey?.addresses || []).pop();
2024-04-22 01:48:00 +02:00
if (!address) return;
if (txFromElectrum && !txFromElectrum.confirmations && txFromElectrum.vsize && address) {
const txsM = await BlueElectrum.getMempoolTransactionsByAddress(address);
let txFromMempool;
2024-04-22 01:48:00 +02:00
// searching 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;
2024-04-22 01:48:00 +02:00
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));
}
2024-04-21 19:55:26 +02:00
} else if (txFromElectrum.confirmations && txFromElectrum.confirmations > 0) {
// now, handling a case when tx became confirmed!
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
setEta('');
2024-04-21 19:55:26 +02:00
setTX((prevState: any) => {
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 = () => {
2024-04-21 19:55:26 +02:00
goBack();
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
}, []);
2024-04-21 19:55:26 +02:00
const initialButtonsState = async () => {
2020-11-30 17:28:50 +01:00
try {
await checkPossibilityOfCPFP();
await checkPossibilityOfRBFBumpFee();
await checkPossibilityOfRBFCancel();
} catch (e) {
2024-04-21 19:55:26 +02:00
setAllButtonStatus(ButtonStatus.NotPossible);
2020-11-30 17:28:50 +01:00
}
setIsLoading(false);
};
useEffect(() => {
2024-04-21 19:55:26 +02:00
initialButtonsState();
2020-11-30 17:28:50 +01:00
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tx, wallets]);
2020-11-30 17:28:50 +01:00
2020-11-30 04:26:25 +01:00
useEffect(() => {
const wID = wallet.current?.getID();
if (wID) {
setSelectedWalletID(wallet.current?.getID());
2020-11-30 04:26:25 +01:00
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [wallet.current]);
2020-11-30 04:26:25 +01:00
useEffect(() => {
console.log('transactionStatus - useEffect');
2020-11-30 04:26:25 +01:00
}, []);
const checkPossibilityOfCPFP = async () => {
2024-04-21 19:55:26 +02:00
if (!wallet.current?.allowRBF()) {
return setIsCPFPPossible(ButtonStatus.NotPossible);
2020-11-30 04:26:25 +01:00
}
2024-04-21 19:55:26 +02:00
if (wallet.current) {
const cpfbTx = new HDSegwitBech32Transaction(null, tx.hash, wallet.current as HDSegwitBech32Wallet);
if ((await cpfbTx.isToUsTransaction()) && (await cpfbTx.getRemoteConfirmationsNum()) === 0) {
return setIsCPFPPossible(ButtonStatus.Possible);
} else {
return setIsCPFPPossible(ButtonStatus.NotPossible);
}
2020-11-30 04:26:25 +01:00
}
2024-04-21 19:55:26 +02:00
return setIsCPFPPossible(ButtonStatus.NotPossible);
2020-11-30 04:26:25 +01:00
};
const checkPossibilityOfRBFBumpFee = async () => {
2024-04-21 19:55:26 +02:00
if (!wallet.current?.allowRBF()) {
return setIsRBFBumpFeePossible(ButtonStatus.NotPossible);
2020-11-30 04:26:25 +01:00
}
2024-04-21 19:55:26 +02:00
const rbfTx = new HDSegwitBech32Transaction(null, tx.hash, wallet.current as HDSegwitBech32Wallet);
2020-11-30 04:26:25 +01:00
if (
(await rbfTx.isOurTransaction()) &&
(await rbfTx.getRemoteConfirmationsNum()) === 0 &&
(await rbfTx.isSequenceReplaceable()) &&
(await rbfTx.canBumpTx())
) {
2024-04-21 19:55:26 +02:00
return setIsRBFBumpFeePossible(ButtonStatus.Possible);
2020-11-30 04:26:25 +01:00
} else {
2024-04-21 19:55:26 +02:00
return setIsRBFBumpFeePossible(ButtonStatus.NotPossible);
2020-11-30 04:26:25 +01:00
}
};
const checkPossibilityOfRBFCancel = async () => {
2024-04-21 19:55:26 +02:00
if (!wallet.current?.allowRBF()) {
return setIsRBFCancelPossible(ButtonStatus.NotPossible);
2020-11-30 04:26:25 +01:00
}
2024-04-21 19:55:26 +02:00
const rbfTx = new HDSegwitBech32Transaction(null, tx.hash, wallet.current as HDSegwitBech32Wallet);
2020-11-30 04:26:25 +01:00
if (
(await rbfTx.isOurTransaction()) &&
(await rbfTx.getRemoteConfirmationsNum()) === 0 &&
(await rbfTx.isSequenceReplaceable()) &&
(await rbfTx.canCancelTx())
) {
2024-04-21 19:55:26 +02:00
return setIsRBFCancelPossible(ButtonStatus.Possible);
2020-11-30 04:26:25 +01:00
} else {
2024-04-21 19:55:26 +02:00
return setIsRBFCancelPossible(ButtonStatus.NotPossible);
2020-11-30 04:26:25 +01:00
}
};
const navigateToRBFBumpFee = () => {
navigate('RBFBumpFee', {
txid: tx.hash,
wallet: wallet.current,
2020-11-30 04:26:25 +01:00
});
};
const navigateToRBFCancel = () => {
navigate('RBFCancel', {
txid: tx.hash,
wallet: wallet.current,
2020-11-30 04:26:25 +01:00
});
};
const navigateToCPFP = () => {
navigate('CPFP', {
txid: tx.hash,
wallet: wallet.current,
2020-11-30 04:26:25 +01:00
});
};
const navigateToTransactionDetials = () => {
navigate('TransactionDetails', { hash: tx.hash });
};
const renderCPFP = () => {
2024-04-21 19:55:26 +02:00
if (isCPFPPossible === ButtonStatus.Unknown) {
2020-11-30 04:26:25 +01:00
return (
<>
<ActivityIndicator />
<BlueSpacing20 />
</>
);
2024-04-21 19:55:26 +02:00
} else if (isCPFPPossible === ButtonStatus.Possible) {
2020-11-30 04:26:25 +01:00
return (
<>
2023-11-15 09:40:22 +01:00
<Button onPress={navigateToCPFP} title={loc.transactions.status_bump} />
2020-12-02 06:24:01 +01:00
<BlueSpacing10 />
2020-11-30 04:26:25 +01:00
</>
);
}
};
const renderRBFCancel = () => {
2024-04-21 19:55:26 +02:00
if (isRBFCancelPossible === ButtonStatus.Unknown) {
2020-11-30 04:26:25 +01:00
return (
<>
<ActivityIndicator />
</>
);
2024-04-21 19:55:26 +02:00
} else if (isRBFCancelPossible === ButtonStatus.Possible) {
2020-11-30 04:26:25 +01:00
return (
<>
<TouchableOpacity accessibilityRole="button" style={styles.cancel}>
2020-11-30 04:26:25 +01:00
<Text onPress={navigateToRBFCancel} style={styles.cancelText}>
{loc.transactions.status_cancel}
</Text>
</TouchableOpacity>
2020-12-02 06:24:01 +01:00
<BlueSpacing10 />
2020-11-30 04:26:25 +01:00
</>
);
}
};
const renderRBFBumpFee = () => {
2024-04-21 19:55:26 +02:00
if (isRBFBumpFeePossible === ButtonStatus.Unknown) {
2020-11-30 04:26:25 +01:00
return (
<>
<ActivityIndicator />
<BlueSpacing20 />
</>
);
2024-04-21 19:55:26 +02:00
} else if (isRBFBumpFeePossible === ButtonStatus.Possible) {
2020-11-30 04:26:25 +01:00
return (
<>
2023-11-15 09:40:22 +01:00
<Button onPress={navigateToRBFBumpFee} title={loc.transactions.status_bump} />
2020-12-02 06:24:01 +01:00
<BlueSpacing10 />
2020-11-30 04:26:25 +01:00
</>
);
}
};
const renderTXMetadata = () => {
if (txMetadata[tx.hash]) {
if (txMetadata[tx.hash].memo) {
return (
<View style={styles.memo}>
<Text selectable style={styles.memoText}>
{txMetadata[tx.hash].memo}
</Text>
2020-11-30 04:26:25 +01:00
<BlueSpacing20 />
</View>
);
}
}
};
2024-04-21 19:55:26 +02:00
if (isLoading || !tx || wallet.current === undefined) {
2020-11-30 04:26:25 +01:00
return (
<SafeArea>
2020-11-30 04:26:25 +01:00
<BlueLoading />
</SafeArea>
2020-11-30 04:26:25 +01:00
);
}
return (
<SafeArea>
<HandOffComponent
title={loc.transactions.details_title}
type={HandOffComponent.activityTypes.ViewInBlockExplorer}
url={`https://mempool.space/tx/${tx.hash}`}
2021-01-19 04:40:11 +01:00
/>
2020-11-30 04:26:25 +01:00
<View style={styles.container}>
<BlueCard>
<View style={styles.center}>
<Text style={[styles.value, stylesHook.value]} selectable>
{formatBalanceWithoutSuffix(tx.value, wallet.current.preferredBalanceUnit, true)}{' '}
2024-04-21 19:55:26 +02:00
{wallet.current?.preferredBalanceUnit !== BitcoinUnit.LOCAL_CURRENCY && (
<Text style={[styles.valueUnit, stylesHook.valueUnit]}>{loc.units[wallet.current.preferredBalanceUnit]}</Text>
2020-11-30 04:26:25 +01:00
)}
</Text>
</View>
{renderTXMetadata()}
<View style={[styles.iconRoot, stylesHook.iconRoot]}>
<View>
<Icon name="check" size={50} type="font-awesome" color={colors.successCheck} />
</View>
<View style={[styles.iconWrap, styles.margin]}>
{(() => {
if (!tx.confirmations) {
return (
<View style={styles.icon}>
2022-01-28 12:13:53 +01:00
<TransactionPendingIcon />
2020-11-30 04:26:25 +01:00
</View>
);
} else if (tx.value < 0) {
return (
<View style={styles.icon}>
2022-01-28 12:13:53 +01:00
<TransactionOutgoingIcon />
2020-11-30 04:26:25 +01:00
</View>
);
} else {
return (
<View style={styles.icon}>
2022-01-28 12:13:53 +01:00
<TransactionIncomingIcon />
2020-11-30 04:26:25 +01:00
</View>
);
}
})()}
</View>
</View>
{tx.fee && (
<View style={styles.fee}>
<BlueText style={styles.feeText}>
{loc.send.create_fee.toLowerCase()} {formatBalanceWithoutSuffix(tx.fee, wallet.current.preferredBalanceUnit, true)}{' '}
2024-04-21 19:55:26 +02:00
{wallet.current?.preferredBalanceUnit !== BitcoinUnit.LOCAL_CURRENCY && wallet.current?.preferredBalanceUnit}
2020-11-30 04:26:25 +01:00
</BlueText>
</View>
)}
2021-09-05 19:35:08 +02:00
<View style={styles.confirmations}>
2020-12-13 11:07:11 +01:00
<Text style={styles.confirmationsText}>
2020-12-13 18:12:18 +01:00
{loc.formatString(loc.transactions.confirmations_lowercase, {
confirmations: tx.confirmations > 6 ? '6+' : tx.confirmations,
})}
2020-12-13 11:07:11 +01:00
</Text>
2020-11-30 04:26:25 +01:00
</View>
{eta ? (
<View style={styles.eta}>
<BlueSpacing10 />
<Text style={styles.confirmationsText}>{eta}</Text>
</View>
) : null}
2020-11-30 04:26:25 +01:00
</BlueCard>
<View style={styles.actions}>
{renderCPFP()}
{renderRBFBumpFee()}
{renderRBFCancel()}
</View>
</View>
</SafeArea>
2020-11-30 04:26:25 +01:00
);
};
export default TransactionsStatus;
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'space-between',
},
center: {
alignItems: 'center',
},
value: {
fontSize: 36,
fontWeight: '600',
},
valueUnit: {
fontSize: 16,
fontWeight: '600',
},
memo: {
alignItems: 'center',
marginVertical: 8,
},
memoText: {
color: '#9aa0aa',
fontSize: 14,
},
iconRoot: {
width: 120,
height: 120,
borderRadius: 60,
alignSelf: 'center',
justifyContent: 'center',
marginTop: 43,
marginBottom: 53,
},
iconWrap: {
minWidth: 30,
minHeight: 30,
alignItems: 'center',
justifyContent: 'center',
alignSelf: 'flex-end',
borderRadius: 15,
},
margin: {
marginBottom: -40,
},
icon: {
width: 25,
},
fee: {
marginTop: 15,
marginBottom: 13,
},
feeText: {
fontSize: 11,
fontWeight: '500',
marginBottom: 4,
color: '#00c49f',
alignSelf: 'center',
},
confirmations: {
alignSelf: 'center',
alignItems: 'center',
justifyContent: 'center',
},
confirmationsText: {
color: '#9aa0aa',
2021-09-05 19:35:08 +02:00
fontSize: 13,
},
eta: {
alignSelf: 'center',
alignItems: 'center',
justifyContent: 'center',
},
actions: {
alignSelf: 'center',
justifyContent: 'center',
},
cancel: {
marginVertical: 16,
},
cancelText: {
color: '#d0021b',
fontSize: 15,
fontWeight: '500',
textAlign: 'center',
},
details: {
alignItems: 'center',
justifyContent: 'center',
2021-09-05 19:35:08 +02:00
width: 80,
borderRadius: 8,
2021-09-14 07:36:00 +02:00
height: 34,
},
detailsText: {
2021-09-05 19:35:08 +02:00
fontSize: 15,
fontWeight: '600',
},
});