Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement RBF and CPFP with new fees package and add pending transactions view in @caravan/coordinator #116

Closed
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/coordinator/src/actions/transactionActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const SET_REQUIRED_SIGNERS = "SET_REQUIRED_SIGNERS";
export const SET_TOTAL_SIGNERS = "SET_TOTAL_SIGNERS";

export const SET_INPUTS = "SET_INPUTS";
export const SET_RBF = "SET_RBF";

export const ADD_OUTPUT = "ADD_OUTPUT";
export const SET_OUTPUT_ADDRESS = "SET_OUTPUT_ADDRESS";
Expand Down Expand Up @@ -114,6 +115,11 @@ export function setChangeAddressAction(value) {
};
}

export const setRBF = (enabled) => ({
type: SET_RBF,
value: enabled,
});

export function setChangeOutput({ value, address }) {
return (dispatch, getState) => {
const {
Expand Down
1 change: 1 addition & 0 deletions apps/coordinator/src/actions/walletActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const WALLET_MODES = {
VIEW: 0,
DEPOSIT: 1,
SPEND: 2,
PENDING: 3,
};

export function updateDepositSliceAction(value) {
Expand Down
50 changes: 44 additions & 6 deletions apps/coordinator/src/components/ScriptExplorer/OutputsForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import {
InputAdornment,
Typography,
FormHelperText,
Switch,
FormControlLabel,
} from "@mui/material";
import InfoIcon from "@mui/icons-material/Info";
import { Speed } from "@mui/icons-material";
import AddIcon from "@mui/icons-material/Add";
import {
Expand All @@ -24,6 +27,7 @@ import {
setFee as setFeeAction,
finalizeOutputs as finalizeOutputsAction,
resetOutputs as resetOutputsAction,
setRBF as setRBFAction,
} from "../../actions/transactionActions";
import { updateBlockchainClient } from "../../actions/clientActions";
import { MIN_SATS_PER_BYTE_FEE } from "../Wallet/constants";
Expand Down Expand Up @@ -231,6 +235,11 @@ class OutputsForm extends React.Component {
setOutputAmount(1, outputAmount);
}

handleRBFToggle = () => {
const { setRBF, rbfEnabled } = this.props;
setRBF(!rbfEnabled);
};

render() {
const {
feeRate,
Expand All @@ -242,6 +251,7 @@ class OutputsForm extends React.Component {
inputs,
isWallet,
autoSpend,
rbfEnabled,
} = this.props;
const { feeRateFetchError } = this.state;
const feeDisplay = inputs && inputs.length > 0 ? fee : "0.0000";
Expand Down Expand Up @@ -281,7 +291,7 @@ class OutputsForm extends React.Component {
</Button>
</Grid>
</Grid>
<Grid item container spacing={gridSpacing}>
<Grid item container spacing={gridSpacing} alignItems="flex-end">
<Grid item xs={3}>
<Box mt={feeMt}>
<Typography
Expand Down Expand Up @@ -327,9 +337,6 @@ class OutputsForm extends React.Component {
</Typography>
</Grid>

<Grid item xs={4}>
<Box mt={feeMt}>&nbsp;</Box>
</Grid>
{!isWallet || (isWallet && !autoSpend) ? (
<Grid item xs={3}>
<Box mt={feeMt}>
Expand Down Expand Up @@ -357,10 +364,37 @@ class OutputsForm extends React.Component {
</Box>
</Grid>
) : (
""
<Grid item xs={3} />
)}

<Grid item xs={2} />
<Grid item xs={1} />
<Grid item xs={3}>
<Box
mt={feeMt}
display="flex"
flexDirection="column"
alignItems="flex-end"
>
<Tooltip title="Replace-By-Fee allows you to increase the fee later if needed">
<IconButton size="small">
<InfoIcon />
</IconButton>
</Tooltip>
<FormControlLabel
control={
<Switch
checked={rbfEnabled}
onChange={this.handleRBFToggle}
name="rbfToggle"
color="primary"
disabled={finalizedOutputs}
/>
}
label="Enable RBF"
labelPlacement="start"
/>
</Box>
</Grid>
</Grid>

<Grid item container spacing={gridSpacing}>
Expand Down Expand Up @@ -488,6 +522,8 @@ OutputsForm.propTypes = {
signatureImporters: PropTypes.shape({}).isRequired,
updatesComplete: PropTypes.bool,
getBlockchainClient: PropTypes.func.isRequired,
rbfEnabled: PropTypes.bool.isRequired,
setRBF: PropTypes.func.isRequired,
};

OutputsForm.defaultProps = {
Expand All @@ -504,6 +540,7 @@ function mapStateToProps(state) {
...state.client,
signatureImporters: state.spend.signatureImporters,
change: state.wallet.change,
rbfEnabled: state.spend.transaction.rbfEnabled,
};
}

Expand All @@ -515,6 +552,7 @@ const mapDispatchToProps = {
finalizeOutputs: finalizeOutputsAction,
resetOutputs: resetOutputsAction,
getBlockchainClient: updateBlockchainClient,
setRBF: setRBFAction,
};

export default connect(mapStateToProps, mapDispatchToProps)(OutputsForm);
4 changes: 4 additions & 0 deletions apps/coordinator/src/components/Wallet/WalletControl.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { setRequiredSigners as setRequiredSignersAction } from "../../actions/tr
import { MAX_FETCH_UTXOS_ERRORS, MAX_TRAILING_EMPTY_NODES } from "./constants";
import WalletDeposit from "./WalletDeposit";
import WalletSpend from "./WalletSpend";
import WalletPendingTransactions from "./WalletPendingTransactions";
import { SlicesTableContainer } from "../Slices";

class WalletControl extends React.Component {
Expand All @@ -41,6 +42,7 @@ class WalletControl extends React.Component {
<Tab label="Addresses" value={WALLET_MODES.VIEW} key={0} />,
<Tab label="Receive" value={WALLET_MODES.DEPOSIT} key={1} />,
<Tab label="Send" value={WALLET_MODES.SPEND} key={2} />,
<Tab label="Pending" value={WALLET_MODES.PENDING} key={3} />,
]}
</Tabs>
<Box mt={2}>{this.renderModeComponent()}</Box>
Expand All @@ -55,6 +57,8 @@ class WalletControl extends React.Component {
if (walletMode === WALLET_MODES.SPEND)
return <WalletSpend addNode={addNode} updateNode={updateNode} />;
if (walletMode === WALLET_MODES.VIEW) return <SlicesTableContainer />;
if (walletMode === WALLET_MODES.PENDING)
return <WalletPendingTransactions />;
}
const progress = this.progress();
return [
Expand Down
162 changes: 162 additions & 0 deletions apps/coordinator/src/components/Wallet/WalletPendingTransactions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { Box, Paper } from "@mui/material";
import { styled } from "@mui/material/styles";
import { useGetClient } from "../../hooks";
// import { TransactionAnalyzer } from "@caravan/bitcoin";
import { AnalyzedTransaction, RootState, UTXO } from "./fee-bumping/types";
import { calculateTimeElapsed } from "./fee-bumping/utils";
import TransactionTable from "./fee-bumping/PendingTransactionTable";
import RBFOptionsDialog from "./fee-bumping/rbf/RBFOptionsDialog";
import AccelerateFeeDialog from "./fee-bumping/rbf/AccelerateFeeDialog";
import CancelTransactionDialog from "./fee-bumping/rbf/CancelTransactionDialog";

const StyledPaper = styled(Paper)(({ theme }) => ({
padding: theme.spacing(3),
marginBottom: theme.spacing(2),
}));

const WalletPendingTransactions: React.FC = () => {
const [pendingTransactions, setPendingTransactions] = useState<
AnalyzedTransaction[]
>([]);
const [selectedTx, setSelectedTx] = useState<AnalyzedTransaction | null>(
null,
);
const [showRBFOptions, setShowRBFOptions] = useState(false);
const [showIncreaseFees, setShowIncreaseFees] = useState(false);
const [showCancelTx, setShowCancelTx] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

const network = useSelector((state: RootState) => state.settings.network);
const walletSlices = useSelector((state: RootState) => [
...Object.values(state.wallet.deposits.nodes),
...Object.values(state.wallet.change.nodes),
]);
const blockchainClient = useGetClient();

useEffect(() => {
fetchPendingTransactions();
}, [network, walletSlices, blockchainClient]);

const fetchPendingTransactions = async () => {
try {
const pendingTxs = walletSlices
.flatMap((slice) => slice.utxos)
.filter((utxo) => !utxo.confirmed);

const currentNetworkFeeRate = await getCurrentNetworkFeeRate();

const analyzedTransactions = await Promise.all(
pendingTxs.map(async (utxo) =>
analyzeTransaction(utxo, currentNetworkFeeRate),
),
);

setPendingTransactions(analyzedTransactions);
} catch (error) {
console.error("Error fetching pending transactions:", error);
setError("Failed to fetch pending transactions. Please try again later.");
} finally {
setIsLoading(false);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this could all be pulled out as a hook, something like useGetPendingTransactions. We'd need some way to set an error from inside the hook though.


const getCurrentNetworkFeeRate = async (): Promise<number> => {
try {
return await blockchainClient.getFeeEstimate();
} catch (error) {
console.error("Error fetching network fee rate:", error);
return 1; // Default to 1 sat/vB if unable to fetch
}
};

const analyzeTransaction = async (
utxo: UTXO,
currentNetworkFeeRate: number,
): Promise<AnalyzedTransaction> => {
// const analyzer = new TransactionAnalyzer({
// txHex: utxo.transactionHex,
// network,
// targetFeeRate: currentNetworkFeeRate,
// // Add other required options for TransactionAnalyzer
// });
// const analysis = analyzer.analyze();

return {
...utxo,
timeElapsed: calculateTimeElapsed(utxo.time),
currentFeeRate: currentNetworkFeeRate,
canRBF: true,
canCPFP: false,
};
};

// const estimateBlocksToMine = (feeRate: number): number => {
// TO DO (MRIGESH) : Implement new methods in client package's BlockchainClient class
// to enable use of this method ...
// };

const handleRBF = (tx: AnalyzedTransaction) => {
setSelectedTx(tx);
setShowRBFOptions(true);
};

const handleCPFP = (tx: AnalyzedTransaction) => {
console.log("CPFP initiated for transaction:", tx.txid);
//To Implement CPFP logic here
};

const handleIncreaseFees = () => {
setShowRBFOptions(false);
setShowIncreaseFees(true);
};

const handleCancelTx = () => {
setShowRBFOptions(false);
setShowCancelTx(true);
};

const closeAllModals = () => {
setShowRBFOptions(false);
setShowIncreaseFees(false);
setShowCancelTx(false);
setSelectedTx(null);
};

return (
<Box sx={{ width: "100%" }}>
<StyledPaper elevation={3}>
<TransactionTable
transactions={pendingTransactions}
onRBF={handleRBF}
onCPFP={handleCPFP}
isLoading={isLoading}
error={error}
/>
</StyledPaper>

<RBFOptionsDialog
open={showRBFOptions}
onClose={closeAllModals}
onIncreaseFees={handleIncreaseFees}
onCancelTx={handleCancelTx}
/>

<AccelerateFeeDialog
open={showIncreaseFees}
onClose={closeAllModals}
onConfirm={closeAllModals}
/>

<CancelTransactionDialog
open={showCancelTx}
onClose={closeAllModals}
onConfirm={closeAllModals}
/>
</Box>
);
};

export default WalletPendingTransactions;
Loading