From 6b4014573f2c0c572e255bba5738218e1c46ddcc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 03:20:51 +0000 Subject: [PATCH 1/6] Initial plan From 1e9f220cdabc4631d393cd6696b810cdee57f2e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 03:23:01 +0000 Subject: [PATCH 2/6] Improve Bitcoin max amount fee estimation using actual UTXO count Co-authored-by: Corey-Code <37006206+Corey-Code@users.noreply.github.com> --- src/popup/components/SendModal.tsx | 51 +++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/src/popup/components/SendModal.tsx b/src/popup/components/SendModal.tsx index fd6e999..d756819 100644 --- a/src/popup/components/SendModal.tsx +++ b/src/popup/components/SendModal.tsx @@ -385,10 +385,10 @@ const SendModal: React.FC = ({ return address.startsWith(addressPrefix) && address.length >= 39; }; - const handleMaxAmount = () => { + const handleMaxAmount = async () => { if (isBitcoin) { // For Bitcoin sweep, we'll use the sweepAll mode which calculates exact fee at send time - // based on actual UTXO count. Display estimated max for UI. + // based on actual UTXO count. Fetch UTXOs to display accurate fee estimate in UI. const balanceInSats = Math.floor(availableBalance * Math.pow(10, nativeDecimals)); if (balanceInSats <= 0) { @@ -397,17 +397,44 @@ const SendModal: React.FC = ({ return; } - // Estimate fee for UI display (actual sweep will calculate exact fee) - // For SegWit P2WPKH: ~68 vbytes per input + ~31 vbytes for output + 10.5 vbytes overhead - const feeRate = estimatedFee ? Math.ceil(parseInt(estimatedFee.amount) / 140) : 10; // sats/vbyte - const estimatedVbytes = 110; // Single input estimate for display - const feeInSats = feeRate * estimatedVbytes; + try { + // Fetch actual UTXOs to calculate accurate fee estimate for UI display + const client = getBitcoinClient(chainId); + const utxos = await client.getConfirmedUTXOs(chainAddress); + const utxoCount = utxos.length; - // Show estimated max (actual sweep will send everything minus exact fee) - const maxSats = Math.max(0, balanceInSats - feeInSats); - const maxAmount = maxSats / Math.pow(10, nativeDecimals); - setAmount(maxAmount.toFixed(8)); - setIsSweepAll(true); // Enable sweep mode for exact max + // Determine if network supports SegWit for accurate size calculation + const btcNetwork = networkRegistry.getBitcoin(chainId); + const isSegWit = btcNetwork?.addressType === 'p2wpkh' || btcNetwork?.addressType === 'p2sh-p2wpkh'; + + // Calculate estimated transaction size based on actual UTXO count + // For SegWit P2WPKH: ~68 vbytes per input + ~31 vbytes for output + 10.5 vbytes overhead + // For legacy P2PKH: ~148 vbytes per input + ~34 vbytes for output + 10 vbytes overhead + const inputSize = isSegWit ? 68 : 148; + const outputSize = isSegWit ? 31 : 34; + const overhead = isSegWit ? 11 : 10; + const estimatedVbytes = overhead + (utxoCount * inputSize) + outputSize; + + // Estimate fee for UI display (actual sweep will calculate exact fee at send time) + const feeRate = estimatedFee ? Math.ceil(parseInt(estimatedFee.amount) / 140) : 10; // sats/vbyte + const feeInSats = feeRate * estimatedVbytes; + + // Show estimated max (actual sweep will send everything minus exact fee) + const maxSats = Math.max(0, balanceInSats - feeInSats); + const maxAmount = maxSats / Math.pow(10, nativeDecimals); + setAmount(maxAmount.toFixed(8)); + setIsSweepAll(true); // Enable sweep mode for exact max + } catch (error) { + console.warn('Failed to fetch UTXOs for max amount calculation, using fallback:', error); + // Fallback to single input estimate if UTXO fetch fails + const feeRate = estimatedFee ? Math.ceil(parseInt(estimatedFee.amount) / 140) : 10; + const estimatedVbytes = 110; // Single input estimate + const feeInSats = feeRate * estimatedVbytes; + const maxSats = Math.max(0, balanceInSats - feeInSats); + const maxAmount = maxSats / Math.pow(10, nativeDecimals); + setAmount(maxAmount.toFixed(8)); + setIsSweepAll(true); + } return; } From 78828612b2fd442c486c6061bd5f30c1dd74c73b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 03:23:19 +0000 Subject: [PATCH 3/6] Add PR reference link to improved fee estimation Co-authored-by: Corey-Code <37006206+Corey-Code@users.noreply.github.com> --- src/popup/components/SendModal.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/popup/components/SendModal.tsx b/src/popup/components/SendModal.tsx index d756819..6b66a13 100644 --- a/src/popup/components/SendModal.tsx +++ b/src/popup/components/SendModal.tsx @@ -389,6 +389,8 @@ const SendModal: React.FC = ({ if (isBitcoin) { // For Bitcoin sweep, we'll use the sweepAll mode which calculates exact fee at send time // based on actual UTXO count. Fetch UTXOs to display accurate fee estimate in UI. + // Improved fee estimation based on review feedback: + // https://github.com/Corey-Code/vidulum-app/pull/52#discussion_r2756997417 const balanceInSats = Math.floor(availableBalance * Math.pow(10, nativeDecimals)); if (balanceInSats <= 0) { From e92cd59570602e45467c6291993250da6360ef83 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 03:24:14 +0000 Subject: [PATCH 4/6] Refactor handleMaxAmount to reduce duplication and improve clarity Co-authored-by: Corey-Code <37006206+Corey-Code@users.noreply.github.com> --- src/popup/components/SendModal.tsx | 32 ++++++++++++++++-------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/popup/components/SendModal.tsx b/src/popup/components/SendModal.tsx index 6b66a13..cd4aa9a 100644 --- a/src/popup/components/SendModal.tsx +++ b/src/popup/components/SendModal.tsx @@ -399,6 +399,21 @@ const SendModal: React.FC = ({ return; } + // Helper function to calculate and set max amount + const calculateAndSetMaxAmount = (vbytes: number) => { + // Extract fee rate from estimated fee + // The estimatedFee.amount is the total fee in sats for a ~140 vbyte transaction + // Divide by 140 to get sats/vbyte rate + const feeRate = estimatedFee ? Math.ceil(parseInt(estimatedFee.amount) / 140) : 10; // sats/vbyte + const feeInSats = feeRate * vbytes; + + // Show estimated max (actual sweep will send everything minus exact fee) + const maxSats = Math.max(0, balanceInSats - feeInSats); + const maxAmount = maxSats / Math.pow(10, nativeDecimals); + setAmount(maxAmount.toFixed(8)); + setIsSweepAll(true); // Enable sweep mode for exact max + }; + try { // Fetch actual UTXOs to calculate accurate fee estimate for UI display const client = getBitcoinClient(chainId); @@ -417,25 +432,12 @@ const SendModal: React.FC = ({ const overhead = isSegWit ? 11 : 10; const estimatedVbytes = overhead + (utxoCount * inputSize) + outputSize; - // Estimate fee for UI display (actual sweep will calculate exact fee at send time) - const feeRate = estimatedFee ? Math.ceil(parseInt(estimatedFee.amount) / 140) : 10; // sats/vbyte - const feeInSats = feeRate * estimatedVbytes; - - // Show estimated max (actual sweep will send everything minus exact fee) - const maxSats = Math.max(0, balanceInSats - feeInSats); - const maxAmount = maxSats / Math.pow(10, nativeDecimals); - setAmount(maxAmount.toFixed(8)); - setIsSweepAll(true); // Enable sweep mode for exact max + calculateAndSetMaxAmount(estimatedVbytes); } catch (error) { console.warn('Failed to fetch UTXOs for max amount calculation, using fallback:', error); // Fallback to single input estimate if UTXO fetch fails - const feeRate = estimatedFee ? Math.ceil(parseInt(estimatedFee.amount) / 140) : 10; const estimatedVbytes = 110; // Single input estimate - const feeInSats = feeRate * estimatedVbytes; - const maxSats = Math.max(0, balanceInSats - feeInSats); - const maxAmount = maxSats / Math.pow(10, nativeDecimals); - setAmount(maxAmount.toFixed(8)); - setIsSweepAll(true); + calculateAndSetMaxAmount(estimatedVbytes); } return; } From 0579702227445e4a298091b6bef6112e272489e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 03:25:15 +0000 Subject: [PATCH 5/6] Add loading state and improve documentation for MAX button Co-authored-by: Corey-Code <37006206+Corey-Code@users.noreply.github.com> --- src/popup/components/SendModal.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/popup/components/SendModal.tsx b/src/popup/components/SendModal.tsx index cd4aa9a..5db3a6e 100644 --- a/src/popup/components/SendModal.tsx +++ b/src/popup/components/SendModal.tsx @@ -74,6 +74,7 @@ const SendModal: React.FC = ({ const [estimatedFee, setEstimatedFee] = useState(null); const [simulatingFee, setSimulatingFee] = useState(false); const [isSweepAll, setIsSweepAll] = useState(false); + const [calculatingMax, setCalculatingMax] = useState(false); const toast = useToast(); @@ -386,6 +387,9 @@ const SendModal: React.FC = ({ }; const handleMaxAmount = async () => { + // Prevent concurrent executions + if (calculatingMax) return; + if (isBitcoin) { // For Bitcoin sweep, we'll use the sweepAll mode which calculates exact fee at send time // based on actual UTXO count. Fetch UTXOs to display accurate fee estimate in UI. @@ -402,8 +406,9 @@ const SendModal: React.FC = ({ // Helper function to calculate and set max amount const calculateAndSetMaxAmount = (vbytes: number) => { // Extract fee rate from estimated fee - // The estimatedFee.amount is the total fee in sats for a ~140 vbyte transaction - // Divide by 140 to get sats/vbyte rate + // The estimatedFee.amount represents the total fee in satoshis for a baseline transaction + // of ~140 vbytes (1 SegWit P2WPKH input + 2 outputs). We divide by 140 to derive the + // sats/vbyte rate, which we then apply to our actual transaction size. const feeRate = estimatedFee ? Math.ceil(parseInt(estimatedFee.amount) / 140) : 10; // sats/vbyte const feeInSats = feeRate * vbytes; @@ -414,6 +419,7 @@ const SendModal: React.FC = ({ setIsSweepAll(true); // Enable sweep mode for exact max }; + setCalculatingMax(true); try { // Fetch actual UTXOs to calculate accurate fee estimate for UI display const client = getBitcoinClient(chainId); @@ -425,7 +431,7 @@ const SendModal: React.FC = ({ const isSegWit = btcNetwork?.addressType === 'p2wpkh' || btcNetwork?.addressType === 'p2sh-p2wpkh'; // Calculate estimated transaction size based on actual UTXO count - // For SegWit P2WPKH: ~68 vbytes per input + ~31 vbytes for output + 10.5 vbytes overhead + // For SegWit P2WPKH: ~68 vbytes per input + ~31 vbytes for output + 11 vbytes overhead // For legacy P2PKH: ~148 vbytes per input + ~34 vbytes for output + 10 vbytes overhead const inputSize = isSegWit ? 68 : 148; const outputSize = isSegWit ? 31 : 34; @@ -438,6 +444,8 @@ const SendModal: React.FC = ({ // Fallback to single input estimate if UTXO fetch fails const estimatedVbytes = 110; // Single input estimate calculateAndSetMaxAmount(estimatedVbytes); + } finally { + setCalculatingMax(false); } return; } From 2b0a60f8f0e7674dd7b7fa62ecd85799a6416041 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 03:25:54 +0000 Subject: [PATCH 6/6] Improve error messaging and clarify documentation Co-authored-by: Corey-Code <37006206+Corey-Code@users.noreply.github.com> --- src/popup/components/SendModal.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/popup/components/SendModal.tsx b/src/popup/components/SendModal.tsx index 5db3a6e..ca9c19b 100644 --- a/src/popup/components/SendModal.tsx +++ b/src/popup/components/SendModal.tsx @@ -407,8 +407,8 @@ const SendModal: React.FC = ({ const calculateAndSetMaxAmount = (vbytes: number) => { // Extract fee rate from estimated fee // The estimatedFee.amount represents the total fee in satoshis for a baseline transaction - // of ~140 vbytes (1 SegWit P2WPKH input + 2 outputs). We divide by 140 to derive the - // sats/vbyte rate, which we then apply to our actual transaction size. + // of ~140 vbytes (1 SegWit P2WPKH input + 2 P2WPKH outputs + overhead). We divide by 140 + // to derive the sats/vbyte rate, which we then apply to our actual transaction size. const feeRate = estimatedFee ? Math.ceil(parseInt(estimatedFee.amount) / 140) : 10; // sats/vbyte const feeInSats = feeRate * vbytes; @@ -440,9 +440,9 @@ const SendModal: React.FC = ({ calculateAndSetMaxAmount(estimatedVbytes); } catch (error) { - console.warn('Failed to fetch UTXOs for max amount calculation, using fallback:', error); + console.warn('Failed to fetch UTXOs for accurate fee calculation, using conservative single-input estimate instead:', error); // Fallback to single input estimate if UTXO fetch fails - const estimatedVbytes = 110; // Single input estimate + const estimatedVbytes = 110; // Conservative single SegWit input estimate calculateAndSetMaxAmount(estimatedVbytes); } finally { setCalculatingMax(false);