import React, { useContext, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { View, ActivityIndicator, Text, TouchableOpacity, StyleSheet, BackHandler } from 'react-native'; import { Icon } from 'react-native-elements'; import { 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 navigationStyle from '../../components/navigationStyle'; import HandOffComponent from '../../components/HandOffComponent'; import { HDSegwitBech32Transaction } from '../../class'; import { BitcoinUnit } from '../../models/bitcoinUnits'; import loc, { formatBalanceWithoutSuffix } from '../../loc'; import { BlueStorageContext } 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'; const buttonStatus = Object.freeze({ possible: 1, unknown: 2, notPossible: 3, }); const TransactionsStatus = () => { const { setSelectedWalletID, wallets, txMetadata, fetchAndSaveWalletTransactions } = useContext(BlueStorageContext); const { hash, walletID } = useRoute().params; const { navigate, setOptions, goBack } = useNavigation(); const { colors } = useTheme(); const wallet = useRef(wallets.find(w => w.getID() === walletID)); const [isCPFPPossible, setIsCPFPPossible] = useState(); 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({ value: { color: colors.alternativeTextColor2, }, valueUnit: { color: colors.alternativeTextColor2, }, iconRoot: { backgroundColor: colors.success, }, detailsText: { color: colors.buttonTextColor, }, details: { backgroundColor: colors.lightButton, }, }); useEffect(() => { setIsCPFPPossible(buttonStatus.unknown); setIsRBFBumpFeePossible(buttonStatus.unknown); setIsRBFCancelPossible(buttonStatus.unknown); }, []); 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(() => { for (const newTx of wallet.current.getTransactions()) { if (newTx.hash === hash) { setTX(newTx); break; } } // 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], 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! triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); 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 () => { try { await checkPossibilityOfCPFP(); await checkPossibilityOfRBFBumpFee(); await checkPossibilityOfRBFCancel(); } catch (e) { setIsCPFPPossible(buttonStatus.notPossible); setIsRBFBumpFeePossible(buttonStatus.notPossible); setIsRBFCancelPossible(buttonStatus.notPossible); } setIsLoading(false); }; useEffect(() => { initialState(); // 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); } const cpfbTx = new HDSegwitBech32Transaction(null, tx.hash, wallet.current); if ((await cpfbTx.isToUsTransaction()) && (await cpfbTx.getRemoteConfirmationsNum()) === 0) { return setIsCPFPPossible(buttonStatus.possible); } else { return setIsCPFPPossible(buttonStatus.notPossible); } }; const checkPossibilityOfRBFBumpFee = async () => { if (!wallet.current.allowRBF()) { return setIsRBFBumpFeePossible(buttonStatus.notPossible); } const rbfTx = new HDSegwitBech32Transaction(null, tx.hash, wallet.current); 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); 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 ( <>