diff --git a/frontend/src/components/SendTip.jsx b/frontend/src/components/SendTip.jsx
index 6eef6f54..ecb28986 100644
--- a/frontend/src/components/SendTip.jsx
+++ b/frontend/src/components/SendTip.jsx
@@ -20,6 +20,7 @@ import { useBlockCheck } from '../hooks/useBlockCheck';
import { useStxPrice } from '../hooks/useStxPrice';
import { useSenderAddress } from '../hooks/useSenderAddress';
import { useContractFee } from '../hooks/useContractFee';
+import { useTransactionFeeEstimate } from '../hooks/useTransactionFeeEstimate';
import { analytics } from '../lib/analytics';
import ConfirmDialog from './ui/confirm-dialog';
import TxStatus from './ui/tx-status';
@@ -80,6 +81,16 @@ export default function SendTip({ addToast }) {
}, [balance]);
const { feeBasisPoints, feePercent } = useContractFee();
+ const {
+ feeEstimateMicroSTX,
+ feeEstimateSTX,
+ feeEstimateUsd,
+ highFeeWarning,
+ feeLevel,
+ setFeeLevel,
+ speedEstimates,
+ refresh: refreshTransactionFee,
+ } = useTransactionFeeEstimate();
const isRecipientHighRisk = !canProceedWithRecipient(recipient, blockedWarning);
@@ -149,10 +160,10 @@ export default function SendTip({ addToast }) {
} else if (parsed > MAX_TIP_STX) {
setAmountError(`Maximum tip is ${MAX_TIP_STX.toLocaleString()} STX`);
} else if (balanceSTX !== null) {
- // Account for the platform fee when checking balance
+ // Account for the platform fee and estimated network fee when checking balance
const microSTX = toMicroSTX(parsed.toString());
- if (!hasSufficientMicroStx(balance, totalDeduction(microSTX, feeBasisPoints))) {
- setAmountError('Insufficient balance (tip + fee exceeds balance)');
+ if (!hasSufficientMicroStx(balance, totalDeduction(microSTX, feeBasisPoints) + feeEstimateMicroSTX)) {
+ setAmountError('Insufficient balance (tip + platform fee + gas exceeds balance)');
} else {
setAmountError('');
}
@@ -179,8 +190,8 @@ export default function SendTip({ addToast }) {
if (parsedAmount > MAX_TIP_STX) { addToast(`Maximum tip is ${MAX_TIP_STX.toLocaleString()} STX`, 'warning'); return; }
if (balanceSTX !== null) {
const microSTX = toMicroSTX(amount);
- if (!hasSufficientMicroStx(balance, totalDeduction(microSTX, feeBasisPoints))) {
- addToast('Insufficient balance to cover tip plus platform fee', 'warning');
+ if (!hasSufficientMicroStx(balance, totalDeduction(microSTX, feeBasisPoints) + feeEstimateMicroSTX)) {
+ addToast('Insufficient balance to cover tip, platform fee, and gas fee', 'warning');
return;
}
}
@@ -372,7 +383,38 @@ export default function SendTip({ addToast }) {
{/* Breakdown with fee preview and post-condition ceiling */}
{amount && parseFloat(amount) > 0 && (
+ {highFeeWarning && (
+
+
+
+
High Transaction Fee Warning
+
Estimated gas fees are currently high ({feeEstimateSTX.toFixed(4)} STX). You can select a lower fee speed option to minimize costs.
+
+
+ )}
Fee Preview
+
+ {['low', 'medium', 'high'].map((level) => {
+ const isActive = feeLevel === level;
+ return (
+
+ );
+ })}
+
Tip amount
@@ -385,10 +427,17 @@ export default function SendTip({ addToast }) {
Platform fee ({feePercent.toFixed(2)}%)
{formatSTX(feeForTip(toMicroSTX(amount), feeBasisPoints), 6)} STX
+
+ Estimated gas fee ({feeLevel})
+
+ {feeEstimateSTX.toFixed(6)} STX
+ {feeEstimateUsd && (~${feeEstimateUsd})}
+
+
Total from wallet
- {formatSTX(totalDeduction(toMicroSTX(amount), feeBasisPoints), 6)} STX
+ {formatSTX(Number(totalDeduction(toMicroSTX(amount), feeBasisPoints)) + feeEstimateMicroSTX, 6)} STX
@@ -447,9 +496,13 @@ export default function SendTip({ addToast }) {
Platform fee ({feePercent.toFixed(2)}%)
{formatSTX(feeForTip(toMicroSTX(amount), feeBasisPoints), 6)} STX
+
+ Estimated gas fee ({feeLevel})
+ {feeEstimateSTX.toFixed(6)} STX
+
Total from your wallet
- {formatSTX(totalDeduction(toMicroSTX(amount), feeBasisPoints), 6)} STX
+ {formatSTX(Number(totalDeduction(toMicroSTX(amount), feeBasisPoints)) + feeEstimateMicroSTX, 6)} STX
)}
diff --git a/frontend/src/hooks/useTransactionFeeEstimate.js b/frontend/src/hooks/useTransactionFeeEstimate.js
new file mode 100644
index 00000000..1c558abe
--- /dev/null
+++ b/frontend/src/hooks/useTransactionFeeEstimate.js
@@ -0,0 +1,192 @@
+import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
+import { STACKS_API_BASE } from '../config/contracts';
+import { useStxPrice } from './useStxPrice';
+import { useDemoMode } from '../context/DemoContext';
+
+export const DEFAULT_FEE_MICROSTX = 5_000;
+export const DEFAULT_LOW_FEE_MICROSTX = 3_000;
+export const DEFAULT_HIGH_FEE_MICROSTX = 15_000;
+
+export const HIGH_FEE_THRESHOLD_MICROSTX = 50_000;
+const REFRESH_INTERVAL_MS = 30_000;
+
+const DUMMY_PAYLOAD_HEX = '0000000000000000000000000000000000000000';
+
+function uint8ArrayToHex(uint8Array) {
+ return Array.from(uint8Array)
+ .map(b => b.toString(16).padStart(2, '0'))
+ .join('');
+}
+
+async function generateDummyTransactionPayloadHex() {
+ try {
+ const { makeContractCall, principalCV, uintCV, stringUtf8CV, PostConditionMode } = await import('@stacks/transactions');
+ const { network } = await import('../utils/stacks');
+ const { CONTRACT_ADDRESS, CONTRACT_NAME, FN_SEND_CATEGORIZED_TIP } = await import('../config/contracts');
+
+ const dummyRecipient = 'SP1W6XQZ6XVYGTVW32SJW2ZG48ZJBW9BATRD19N60';
+ const dummyAmount = 1000000;
+
+ const functionArgs = [
+ principalCV(dummyRecipient),
+ uintCV(dummyAmount),
+ stringUtf8CV('Thanks!'),
+ uintCV(0)
+ ];
+
+ const tx = await makeContractCall({
+ contractAddress: CONTRACT_ADDRESS,
+ contractName: CONTRACT_NAME,
+ functionName: FN_SEND_CATEGORIZED_TIP,
+ functionArgs,
+ senderKey: '0000000000000000000000000000000000000000000000000000000000000001',
+ network,
+ postConditionMode: PostConditionMode.Deny,
+ postConditions: [],
+ });
+
+ const serialized = tx.serialize();
+ return uint8ArrayToHex(serialized);
+ } catch (e) {
+ return DUMMY_PAYLOAD_HEX;
+ }
+}
+
+export function useTransactionFeeEstimate({ pollInterval = REFRESH_INTERVAL_MS } = {}) {
+ const { demoEnabled } = useDemoMode();
+ const { toUsd } = useStxPrice();
+
+ const [feeLevel, setFeeLevel] = useState('medium');
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const [speedEstimates, setSpeedEstimates] = useState({
+ low: { microSTX: DEFAULT_LOW_FEE_MICROSTX, STX: DEFAULT_LOW_FEE_MICROSTX / 1_000_000 },
+ medium: { microSTX: DEFAULT_FEE_MICROSTX, STX: DEFAULT_FEE_MICROSTX / 1_000_000 },
+ high: { microSTX: DEFAULT_HIGH_FEE_MICROSTX, STX: DEFAULT_HIGH_FEE_MICROSTX / 1_000_000 },
+ });
+
+ const isMountedRef = useRef(true);
+ const intervalRef = useRef(null);
+
+ useEffect(() => {
+ isMountedRef.current = true;
+ return () => {
+ isMountedRef.current = false;
+ };
+ }, []);
+
+ const speedEstimatesWithUsd = useMemo(() => {
+ const updated = {};
+ Object.keys(speedEstimates).forEach((level) => {
+ const est = speedEstimates[level];
+ const usdVal = toUsd ? toUsd(est.STX) : null;
+ updated[level] = {
+ ...est,
+ Usd: usdVal,
+ };
+ });
+ return updated;
+ }, [speedEstimates, toUsd]);
+
+ const estimate = useCallback(async () => {
+ if (!isMountedRef.current) return;
+ setLoading(true);
+ if (demoEnabled) {
+ if (isMountedRef.current) {
+ setLoading(false);
+ setError(null);
+ }
+ return;
+ }
+
+ let baseEstimates = null;
+ const payloadHex = await generateDummyTransactionPayloadHex();
+
+ try {
+ const response = await fetch(`${STACKS_API_BASE}/v2/fees/transaction`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ transaction_payload: payloadHex,
+ estimated_len: 200,
+ }),
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ if (data && Array.isArray(data.estimations) && data.estimations.length >= 3) {
+ baseEstimates = {
+ low: { microSTX: data.estimations[0].fee, STX: data.estimations[0].fee / 1_000_000 },
+ medium: { microSTX: data.estimations[1].fee, STX: data.estimations[1].fee / 1_000_000 },
+ high: { microSTX: data.estimations[2].fee, STX: data.estimations[2].fee / 1_000_000 },
+ };
+ }
+ }
+ } catch (e) {
+ // Silence Stacks Node API error
+ }
+
+ if (!baseEstimates) {
+ try {
+ const response = await fetch(`${STACKS_API_BASE}/extended/v1/fee_rate`);
+ if (response.ok) {
+ const data = await response.json();
+ if (data && typeof data.fee_rate === 'number') {
+ const standardFee = Math.max(DEFAULT_FEE_MICROSTX, data.fee_rate * 250);
+ baseEstimates = {
+ low: { microSTX: Math.floor(standardFee * 0.6), STX: (standardFee * 0.6) / 1_000_000 },
+ medium: { microSTX: standardFee, STX: standardFee / 1_000_000 },
+ high: { microSTX: Math.floor(standardFee * 2), STX: (standardFee * 2) / 1_000_000 },
+ };
+ }
+ }
+ } catch (e) {
+ // Silence secondary fallback error
+ }
+ }
+
+ if (!baseEstimates) {
+ baseEstimates = {
+ low: { microSTX: DEFAULT_LOW_FEE_MICROSTX, STX: DEFAULT_LOW_FEE_MICROSTX / 1_000_000 },
+ medium: { microSTX: DEFAULT_FEE_MICROSTX, STX: DEFAULT_FEE_MICROSTX / 1_000_000 },
+ high: { microSTX: DEFAULT_HIGH_FEE_MICROSTX, STX: DEFAULT_HIGH_FEE_MICROSTX / 1_000_000 },
+ };
+ }
+
+ if (isMountedRef.current) {
+ setSpeedEstimates(baseEstimates);
+ setLoading(false);
+ setError(null);
+ }
+ }, [demoEnabled]);
+
+ useEffect(() => {
+ estimate();
+
+ if (pollInterval > 0) {
+ intervalRef.current = setInterval(estimate, pollInterval);
+ }
+
+ return () => {
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ }
+ };
+ }, [estimate, pollInterval]);
+
+ const activeEstimate = speedEstimatesWithUsd[feeLevel];
+
+ return {
+ feeEstimateMicroSTX: activeEstimate.microSTX,
+ feeEstimateSTX: activeEstimate.STX,
+ feeEstimateUsd: activeEstimate.Usd,
+ loading,
+ error,
+ highFeeWarning: activeEstimate.microSTX >= HIGH_FEE_THRESHOLD_MICROSTX,
+ feeLevel,
+ setFeeLevel,
+ speedEstimates,
+ refresh: estimate,
+ };
+}
diff --git a/frontend/src/hooks/useTransactionFeeEstimate.test.js b/frontend/src/hooks/useTransactionFeeEstimate.test.js
new file mode 100644
index 00000000..2e559adf
--- /dev/null
+++ b/frontend/src/hooks/useTransactionFeeEstimate.test.js
@@ -0,0 +1,166 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
+import { renderHook, waitFor, act } from '@testing-library/react';
+
+// Mock heavy Stacks dependencies to prevent slow cryptography execution in tests
+vi.mock('@stacks/transactions', () => ({
+ PostConditionMode: { Deny: 0x02 },
+ uintCV: vi.fn((v) => ({ type: 'uint', value: v })),
+ principalCV: vi.fn((v) => ({ type: 'principal', value: v })),
+ stringUtf8CV: vi.fn((v) => ({ type: 'string-utf8', value: v })),
+ makeContractCall: vi.fn().mockResolvedValue({
+ serialize: () => new Uint8Array([0, 1, 2, 3])
+ }),
+}));
+
+vi.mock('@stacks/network', () => ({
+ STACKS_MAINNET: 'mainnet',
+ STACKS_TESTNET: 'testnet',
+ STACKS_DEVNET: 'devnet',
+}));
+
+vi.mock('../utils/stacks', () => ({
+ network: 'mainnet',
+ appDetails: { name: 'TipStream', icon: '/logo.svg' },
+ userSession: { isUserSignedIn: () => false },
+}));
+
+vi.mock('../config/contracts', () => ({
+ CONTRACT_ADDRESS: 'SP1W6XQZ6XVYGTVW32SJW2ZG48ZJBW9BATRD19N60',
+ CONTRACT_NAME: 'tipstream',
+ STACKS_API_BASE: 'https://api.hiro.so',
+ FN_SEND_CATEGORIZED_TIP: 'send-categorized-tip',
+}));
+
+vi.mock('../context/DemoContext', () => ({
+ useDemoMode: () => ({ demoEnabled: false }),
+}));
+
+vi.mock('./useStxPrice', () => ({
+ useStxPrice: () => ({
+ price: 2.5,
+ loading: false,
+ error: null,
+ toUsd: (stx) => (Number(stx) * 2.5).toFixed(2),
+ }),
+}));
+
+import { useTransactionFeeEstimate } from './useTransactionFeeEstimate';
+
+describe('useTransactionFeeEstimate', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ global.fetch = vi.fn();
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it('starts with fallback values when loading', () => {
+ global.fetch.mockReturnValue(new Promise(() => {}));
+ const { result } = renderHook(() => useTransactionFeeEstimate({ pollInterval: 0 }));
+
+ expect(result.current.loading).toBe(true);
+ expect(result.current.feeEstimateMicroSTX).toBe(5000);
+ expect(result.current.feeEstimateSTX).toBe(0.005);
+ });
+
+ it('updates USD conversion based on STX price hook', async () => {
+ global.fetch.mockResolvedValue({
+ ok: true,
+ json: async () => ({
+ estimations: [
+ { fee: 3000, fee_rate: 1 },
+ { fee: 6000, fee_rate: 2 },
+ { fee: 12000, fee_rate: 3 }
+ ]
+ })
+ });
+
+ const { result } = renderHook(() => useTransactionFeeEstimate({ pollInterval: 0 }));
+ await waitFor(() => expect(result.current.loading).toBe(false));
+
+ expect(result.current.feeEstimateMicroSTX).toBe(6000);
+ expect(result.current.feeEstimateSTX).toBe(0.006);
+ expect(result.current.feeEstimateUsd).toBe('0.01');
+ });
+
+ it('falls back to extended/v1/fee_rate when v2 POST fails', async () => {
+ global.fetch
+ .mockRejectedValueOnce(new Error('Cost estimation disabled'))
+ .mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({ fee_rate: 40 }),
+ });
+
+ const { result } = renderHook(() => useTransactionFeeEstimate({ pollInterval: 0 }));
+ await waitFor(() => expect(result.current.loading).toBe(false));
+
+ expect(result.current.feeEstimateMicroSTX).toBe(10000);
+ expect(result.current.feeEstimateSTX).toBe(0.01);
+ });
+
+ it('falls back to default parameters when both network calls fail', async () => {
+ global.fetch
+ .mockRejectedValueOnce(new Error('V2 Failed'))
+ .mockRejectedValueOnce(new Error('Extended Failed'));
+
+ const { result } = renderHook(() => useTransactionFeeEstimate({ pollInterval: 0 }));
+ await waitFor(() => expect(result.current.loading).toBe(false));
+
+ expect(result.current.feeEstimateMicroSTX).toBe(5000);
+ expect(result.current.error).toBeNull();
+ });
+
+ it('supports selecting different fee levels (low, medium, high)', async () => {
+ global.fetch.mockResolvedValue({
+ ok: true,
+ json: async () => ({
+ estimations: [
+ { fee: 3000, fee_rate: 1 },
+ { fee: 6000, fee_rate: 2 },
+ { fee: 12000, fee_rate: 3 }
+ ]
+ })
+ });
+
+ const { result } = renderHook(() => useTransactionFeeEstimate({ pollInterval: 0 }));
+ await waitFor(() => expect(result.current.loading).toBe(false));
+
+ expect(result.current.feeLevel).toBe('medium');
+ expect(result.current.feeEstimateMicroSTX).toBe(6000);
+
+ act(() => {
+ result.current.setFeeLevel('low');
+ });
+ expect(result.current.feeEstimateMicroSTX).toBe(3000);
+
+ act(() => {
+ result.current.setFeeLevel('high');
+ });
+ expect(result.current.feeEstimateMicroSTX).toBe(12000);
+ });
+
+ it('raises high fee warning when active level estimate meets or exceeds 50000 microSTX', async () => {
+ global.fetch.mockResolvedValue({
+ ok: true,
+ json: async () => ({
+ estimations: [
+ { fee: 10000, fee_rate: 1 },
+ { fee: 20000, fee_rate: 2 },
+ { fee: 55000, fee_rate: 3 }
+ ]
+ })
+ });
+
+ const { result } = renderHook(() => useTransactionFeeEstimate({ pollInterval: 0 }));
+ await waitFor(() => expect(result.current.loading).toBe(false));
+
+ expect(result.current.highFeeWarning).toBe(false);
+
+ act(() => {
+ result.current.setFeeLevel('high');
+ });
+ expect(result.current.highFeeWarning).toBe(true);
+ });
+});
diff --git a/frontend/src/test/SendTip.fee-preview.test.jsx b/frontend/src/test/SendTip.fee-preview.test.jsx
new file mode 100644
index 00000000..ce7d3096
--- /dev/null
+++ b/frontend/src/test/SendTip.fee-preview.test.jsx
@@ -0,0 +1,157 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
+import SendTip from '../components/SendTip';
+import { TipProvider } from '../context/TipContext';
+import * as useBlockCheckModule from '../hooks/useBlockCheck';
+import * as useBalanceModule from '../hooks/useBalance';
+import * as useStxPriceModule from '../hooks/useStxPrice';
+import * as stacksModule from '../utils/stacks';
+import * as analyticsModule from '../lib/analytics';
+import * as useTransactionFeeEstimateModule from '../hooks/useTransactionFeeEstimate';
+
+vi.mock('../hooks/useBlockCheck');
+vi.mock('../hooks/useBalance');
+vi.mock('../hooks/useStxPrice');
+vi.mock('../utils/stacks');
+vi.mock('../lib/analytics');
+vi.mock('../hooks/useTransactionFeeEstimate');
+vi.mock('@stacks/connect', () => ({
+ openContractCall: vi.fn(),
+}));
+
+vi.mock('../lib/contractEvents', () => ({
+ contractEvents: {
+ fetchAll: vi.fn().mockResolvedValue([]),
+ subscribe: vi.fn(),
+ },
+ POLL_INTERVAL_MS: 30000,
+ fetchAllContractEvents: vi.fn().mockResolvedValue([]),
+}));
+
+describe('SendTip - Transaction Fee Preview & Toggles', () => {
+ const mockSenderAddress = 'SP31PKQVQZVZCK3FM3NH67CGD6G1FMR17VQVS2W5T';
+ const mockRecipient = 'SP2RDS2YKXMFSP4H9Q5D1FXF5K5J91TH1P5KH3HVP';
+ const mockSetFeeLevel = vi.fn();
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+
+ vi.spyOn(stacksModule, 'getSenderAddress').mockReturnValue(mockSenderAddress);
+ vi.spyOn(useStxPriceModule, 'useStxPrice').mockReturnValue({
+ toUsd: (stx) => (Number(stx) * 2.5).toFixed(2),
+ });
+ vi.spyOn(analyticsModule, 'analytics', 'get').mockReturnValue({
+ trackTipStarted: vi.fn(),
+ trackTipSubmitted: vi.fn(),
+ trackTipConfirmed: vi.fn(),
+ trackTipCancelled: vi.fn(),
+ trackTipFailed: vi.fn(),
+ });
+
+ vi.spyOn(useBlockCheckModule, 'useBlockCheck').mockReturnValue({
+ blocked: false,
+ checking: false,
+ checkBlocked: vi.fn(),
+ reset: vi.fn(),
+ });
+
+ vi.spyOn(useBalanceModule, 'useBalance').mockReturnValue({
+ balance: '100000000', // 100 STX
+ balanceStx: 100,
+ loading: false,
+ refetch: vi.fn(),
+ });
+ });
+
+ it('renders estimated gas fee and USD amount in breakdown', async () => {
+ vi.spyOn(useTransactionFeeEstimateModule, 'useTransactionFeeEstimate').mockReturnValue({
+ feeEstimateMicroSTX: 5000,
+ feeEstimateSTX: 0.005,
+ feeEstimateUsd: '0.01',
+ highFeeWarning: false,
+ feeLevel: 'medium',
+ setFeeLevel: mockSetFeeLevel,
+ speedEstimates: {},
+ refresh: vi.fn(),
+ });
+
+ render(
+
+
+
+ );
+
+ // Set recipient and tip amount using fast fireEvent to trigger breakdown synchronously
+ await act(async () => {
+ fireEvent.change(screen.getByPlaceholderText('SP2...'), { target: { value: mockRecipient } });
+ fireEvent.change(screen.getByPlaceholderText('0.5'), { target: { value: '1.0' } });
+ });
+
+ // Confirm gas fee preview is displayed with correct values
+ const gasFeePreview = screen.getByTestId('gas-fee-preview');
+ expect(gasFeePreview).toBeInTheDocument();
+ expect(gasFeePreview).toHaveTextContent('0.005000 STX');
+ expect(gasFeePreview).toHaveTextContent('~$0.01');
+ });
+
+ it('allows switching fee levels via speed toggle pills', async () => {
+ vi.spyOn(useTransactionFeeEstimateModule, 'useTransactionFeeEstimate').mockReturnValue({
+ feeEstimateMicroSTX: 5000,
+ feeEstimateSTX: 0.005,
+ feeEstimateUsd: '0.01',
+ highFeeWarning: false,
+ feeLevel: 'medium',
+ setFeeLevel: mockSetFeeLevel,
+ speedEstimates: {},
+ refresh: vi.fn(),
+ });
+
+ render(
+
+
+
+ );
+
+ await act(async () => {
+ fireEvent.change(screen.getByPlaceholderText('SP2...'), { target: { value: mockRecipient } });
+ fireEvent.change(screen.getByPlaceholderText('0.5'), { target: { value: '1.0' } });
+ });
+
+ // Click "low" fee speed pill
+ const lowPill = screen.getByTestId('fee-level-low');
+ await act(async () => {
+ fireEvent.click(lowPill);
+ });
+
+ expect(mockSetFeeLevel).toHaveBeenCalledWith('low');
+ });
+
+ it('renders high-fee warning banner when fee matches or exceeds threshold', async () => {
+ vi.spyOn(useTransactionFeeEstimateModule, 'useTransactionFeeEstimate').mockReturnValue({
+ feeEstimateMicroSTX: 60000,
+ feeEstimateSTX: 0.06,
+ feeEstimateUsd: '0.15',
+ highFeeWarning: true,
+ feeLevel: 'high',
+ setFeeLevel: mockSetFeeLevel,
+ speedEstimates: {},
+ refresh: vi.fn(),
+ });
+
+ render(
+
+
+
+ );
+
+ await act(async () => {
+ fireEvent.change(screen.getByPlaceholderText('SP2...'), { target: { value: mockRecipient } });
+ fireEvent.change(screen.getByPlaceholderText('0.5'), { target: { value: '1.0' } });
+ });
+
+ // Verify warning is displayed
+ const warningAlert = screen.getByTestId('high-fee-warning');
+ expect(warningAlert).toBeInTheDocument();
+ expect(warningAlert).toHaveTextContent('High Transaction Fee Warning');
+ });
+});