import React, { useEffect, useLayoutEffect, useReducer, useRef } from 'react'; import { View, ActivityIndicator, Text, TouchableOpacity, StyleSheet, BackHandler } from 'react-native'; import { Icon } from 'react-native-elements'; import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; import { BlueCard, BlueLoading, BlueSpacing10, BlueSpacing20, BlueText } from '../../BlueComponents'; import TransactionIncomingIcon from '../../components/icons/TransactionIncomingIcon'; import TransactionOutgoingIcon from '../../components/icons/TransactionOutgoingIcon'; import TransactionPendingIcon from '../../components/icons/TransactionPendingIcon'; import HandOffComponent from '../../components/HandOffComponent'; import { HDSegwitBech32Transaction, HDSegwitBech32Wallet } from '../../class'; import { BitcoinUnit } from '../../models/bitcoinUnits'; import loc, { formatBalanceWithoutSuffix } from '../../loc'; import { useStorage } from '../../blue_modules/storage-context'; import * as BlueElectrum from '../../blue_modules/BlueElectrum'; import { useTheme } from '../../components/themes'; import Button from '../../components/Button'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import SafeArea from '../../components/SafeArea'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { Transaction } from '../../class/wallets/types'; enum ButtonStatus { Possible, Unknown, NotPossible, } interface TransactionStatusProps { route: RouteProp<{ params: { hash?: string; walletID?: string } }, 'params'>; navigation: NativeStackNavigationProp; } 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 [state, dispatch] = useReducer(reducer, initialState); const { isCPFPPossible, isRBFBumpFeePossible, isRBFCancelPossible, tx, isLoading, eta, intervalMs } = state; const { setSelectedWalletID, wallets, txMetadata, fetchAndSaveWalletTransactions } = useStorage(); const { hash, walletID } = useRoute().params; const { navigate, setOptions, goBack } = useNavigation(); const { colors } = useTheme(); const wallet = useRef(wallets.find(w => w.getID() === walletID)); const fetchTxInterval = useRef(); const stylesHook = StyleSheet.create({ value: { color: colors.alternativeTextColor2, }, valueUnit: { color: colors.alternativeTextColor2, }, iconRoot: { backgroundColor: colors.success, }, detailsText: { color: colors.buttonTextColor, }, details: { backgroundColor: colors.lightButton, }, }); // 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 }); }; // useLayoutEffect(() => { setOptions({ // eslint-disable-next-line react/no-unstable-nested-components headerRight: () => ( {loc.send.create_details} ), }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [colors, tx]); useEffect(() => { 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]; if (!txFromElectrum) return; console.log('got txFromElectrum=', txFromElectrum); const address = (txFromElectrum?.vout[0]?.scriptPubKey?.addresses || []).pop(); if (!address) return; if (txFromElectrum && !txFromElectrum.confirmations && txFromElectrum.vsize && address) { const txsM = await BlueElectrum.getMempoolTransactionsByAddress(address); let txFromMempool; // 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; 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 && txFromElectrum.confirmations > 0) { // now, handling a case when tx became confirmed! triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); setEta(''); 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 = () => { 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 }, []); const initialButtonsState = async () => { try { await checkPossibilityOfCPFP(); await checkPossibilityOfRBFBumpFee(); await checkPossibilityOfRBFCancel(); } catch (e) { setAllButtonStatus(ButtonStatus.NotPossible); } setIsLoading(false); }; useEffect(() => { initialButtonsState(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [tx, wallets]); useEffect(() => { const wID = wallet.current?.getID(); if (wID) { setSelectedWalletID(wallet.current?.getID()); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [wallet.current]); useEffect(() => { console.log('transactionStatus - useEffect'); }, []); const checkPossibilityOfCPFP = async () => { if (!wallet.current?.allowRBF()) { return setIsCPFPPossible(ButtonStatus.NotPossible); } 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); } } return setIsCPFPPossible(ButtonStatus.NotPossible); }; const checkPossibilityOfRBFBumpFee = async () => { if (!wallet.current?.allowRBF()) { return setIsRBFBumpFeePossible(ButtonStatus.NotPossible); } const rbfTx = new HDSegwitBech32Transaction(null, tx.hash, wallet.current as HDSegwitBech32Wallet); if ( (await rbfTx.isOurTransaction()) && (await rbfTx.getRemoteConfirmationsNum()) === 0 && (await rbfTx.isSequenceReplaceable()) && (await rbfTx.canBumpTx()) ) { return setIsRBFBumpFeePossible(ButtonStatus.Possible); } else { return setIsRBFBumpFeePossible(ButtonStatus.NotPossible); } }; const checkPossibilityOfRBFCancel = async () => { if (!wallet.current?.allowRBF()) { return setIsRBFCancelPossible(ButtonStatus.NotPossible); } const rbfTx = new HDSegwitBech32Transaction(null, tx.hash, wallet.current as HDSegwitBech32Wallet); if ( (await rbfTx.isOurTransaction()) && (await rbfTx.getRemoteConfirmationsNum()) === 0 && (await rbfTx.isSequenceReplaceable()) && (await rbfTx.canCancelTx()) ) { return setIsRBFCancelPossible(ButtonStatus.Possible); } else { return setIsRBFCancelPossible(ButtonStatus.NotPossible); } }; const navigateToRBFBumpFee = () => { navigate('RBFBumpFee', { txid: tx.hash, wallet: wallet.current, }); }; const navigateToRBFCancel = () => { navigate('RBFCancel', { txid: tx.hash, wallet: wallet.current, }); }; const navigateToCPFP = () => { navigate('CPFP', { txid: tx.hash, wallet: wallet.current, }); }; const navigateToTransactionDetials = () => { navigate('TransactionDetails', { hash: tx.hash }); }; const renderCPFP = () => { if (isCPFPPossible === ButtonStatus.Unknown) { return ( <> ); } else if (isCPFPPossible === ButtonStatus.Possible) { return ( <>