diff --git a/extension/e2e-tests/helpers/sendPayment.ts b/extension/e2e-tests/helpers/sendPayment.ts index d74879f7eb..886ea4ae82 100644 --- a/extension/e2e-tests/helpers/sendPayment.ts +++ b/extension/e2e-tests/helpers/sendPayment.ts @@ -27,10 +27,9 @@ export const sendXlmPayment = async ({ page }) => { await expect(page.getByTestId("SendSettingsTransactionFee")).toHaveText( /[0-9]/, ); - // 100 XLM is the default, so likely a sign the fee was not set properly from Horizon - await expect( - page.getByTestId("SendSettingsTransactionFee"), - ).not.toContainText("100 XLM"); + await expect(page.getByTestId("SendSettingsTransactionFee")).toContainText( + "100 XLM", + ); await expectPageToHaveScreenshot( { page, diff --git a/extension/e2e-tests/sendPayment.test.ts b/extension/e2e-tests/sendPayment.test.ts index b87f98c5ad..feb3b00292 100644 --- a/extension/e2e-tests/sendPayment.test.ts +++ b/extension/e2e-tests/sendPayment.test.ts @@ -249,10 +249,9 @@ test("Send XLM payments to recent federated addresses", async ({ await expect(page.getByTestId("SendSettingsTransactionFee")).toHaveText( /[0-9]/, ); - // 100 XLM is the default, so likely a sign the fee was not set properly from Horizon - await expect( - page.getByTestId("SendSettingsTransactionFee"), - ).not.toContainText("100 XLM"); + await expect(page.getByTestId("SendSettingsTransactionFee")).toContainText( + "100 XLM", + ); await expect(page.getByText("Review Send")).toBeEnabled(); await page.getByText("Review Send").click(); @@ -281,10 +280,9 @@ test("Send XLM payments to recent federated addresses", async ({ await expect(page.getByTestId("SendSettingsTransactionFee")).toHaveText( /[0-9]/, ); - // 100 XLM is the default, so likely a sign the fee was not set properly from Horizon - await expect( - page.getByTestId("SendSettingsTransactionFee"), - ).not.toContainText("100 XLM"); + await expect(page.getByTestId("SendSettingsTransactionFee")).toContainText( + "100 XLM", + ); await page.getByText("Review Send").click(); await expect(page.getByText("Confirm Send")).toBeVisible(); await page.getByTestId("transaction-details-btn-send").click(); diff --git a/extension/src/popup/components/manageAssets/ManageAssetRowButton/index.tsx b/extension/src/popup/components/manageAssets/ManageAssetRowButton/index.tsx index fa92ac63bf..3649382c9c 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRowButton/index.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRowButton/index.tsx @@ -55,7 +55,6 @@ interface ManageAssetRowButtonProps { setNewAssetFlags: (flags: any) => void; setShowUnverifiedWarning: (rowButtonShowing: boolean) => void; setHandleAddToken: (func: any) => void; - recommendedFee: string; balances: AccountBalances; } @@ -78,7 +77,6 @@ export const ManageAssetRowButton = ({ setNewAssetFlags, setShowUnverifiedWarning, setHandleAddToken, - recommendedFee, balances, }: ManageAssetRowButtonProps) => { const dispatch = useDispatch(); @@ -101,7 +99,6 @@ export const ManageAssetRowButton = ({ const { changeTrustline } = useChangeTrustline({ assetCode: code, assetIssuer: issuer, - recommendedFee, setAssetSubmitting, setIsSigningWithHardwareWallet, setIsTrustlineErrorShowing, diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx b/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx index 4e40d50eb9..0885aa78c2 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx @@ -15,7 +15,6 @@ import { truncateString, } from "helpers/stellar"; import { isContractId, isSacContract } from "popup/helpers/soroban"; -import { useNetworkFees } from "popup/helpers/useNetworkFees"; import { defaultBlockaidScanAssetResult } from "@shared/helpers/stellar"; import { LoadingBackground } from "popup/basics/LoadingBackground"; @@ -100,7 +99,6 @@ export const ManageAssetRows = ({ const [assetSubmitting, setAssetSubmitting] = useState(""); const dispatch = useDispatch(); const walletType = useSelector(hardwareWalletTypeSelector); - const { recommendedFee } = useNetworkFees(); const navigate = useNavigate(); const [showBlockedDomainWarning, setShowBlockedDomainWarning] = @@ -238,7 +236,6 @@ export const ManageAssetRows = ({ setAssetSubmitting={setAssetSubmitting} setShowNewAssetWarning={setShowNewAssetWarning} setShowUnverifiedWarning={setShowUnverifiedWarning} - recommendedFee={recommendedFee} /> )} diff --git a/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/hooks/useGetTxDetailsData.tsx b/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/hooks/useGetTxDetailsData.tsx index 2de9b9d0a8..4683a4510b 100644 --- a/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/hooks/useGetTxDetailsData.tsx +++ b/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/hooks/useGetTxDetailsData.tsx @@ -184,9 +184,12 @@ const getBuiltTx = async ( return transaction; } catch (error) { - captureException({ - error, - arguments: { + const err = + error instanceof Error + ? error + : new Error(typeof error === "string" ? error : JSON.stringify(error)); + captureException(err, { + extra: { sourceAsset, destAsset, amount, @@ -200,8 +203,7 @@ const getBuiltTx = async ( publicKey, }, }); - const err = typeof error === "string" ? error : JSON.stringify(error); - throw new Error(`Failed to build operation: ${err}`); + throw new Error(`Failed to build operation: ${err.message}`); } }; diff --git a/extension/src/popup/helpers/__tests__/useNetworkFees.test.js b/extension/src/popup/helpers/__tests__/useNetworkFees.test.js new file mode 100644 index 0000000000..4ce9447ed8 --- /dev/null +++ b/extension/src/popup/helpers/__tests__/useNetworkFees.test.js @@ -0,0 +1,104 @@ +import React, { useRef, useEffect } from "react"; +import { render, act } from "@testing-library/react"; +import { useNetworkFees, NetworkCongestion } from "../useNetworkFees"; +import { BASE_FEE } from "stellar-sdk"; + +import { useSelector } from "react-redux"; +import { stellarSdkServer } from "@shared/api/helpers/stellarSdkServer"; + +jest.mock("react-redux", () => ({ + useSelector: jest.fn(), +})); + +jest.mock("@shared/api/helpers/stellarSdkServer", () => ({ + stellarSdkServer: jest.fn(), +})); + +function TestComponent({ callback }) { + const data = useNetworkFees(); + + useEffect(() => { + callback(data); + }, [data]); + + return null; +} + +describe("useNetworkFees (React 18 compatible)", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("fetches and updates fee + congestion manually", async () => { + useSelector.mockReturnValue({ + networkUrl: "https://testnet.stellar.org", + networkPassphrase: "Test SDF Network ; September 2015", + }); + + const feeStatsMock = jest.fn().mockResolvedValue({ + max_fee: { mode: "300" }, + ledger_capacity_usage: "0.6", + }); + + stellarSdkServer.mockReturnValue({ feeStats: feeStatsMock }); + + let hookResult; + await act(async () => { + render( + { + hookResult = data; + }} + />, + ); + }); + + await act(async () => { + await hookResult.fetchData(); + }); + + expect(hookResult.recommendedFee).toBe("0.00003"); + expect(hookResult.networkCongestion).toBe(NetworkCongestion.MEDIUM); + + feeStatsMock.mockResolvedValueOnce({ + max_fee: { mode: "1000" }, + ledger_capacity_usage: "0.9", + }); + + await act(async () => { + await hookResult.fetchData(); + }); + + expect(hookResult.recommendedFee).toBe("0.0001"); + expect(hookResult.networkCongestion).toBe(NetworkCongestion.HIGH); + }); + + it("falls back to BASE_FEE on error", async () => { + useSelector.mockReturnValue({ + networkUrl: "https://testnet.stellar.org", + networkPassphrase: "Test SDF Network ; September 2015", + }); + + stellarSdkServer.mockReturnValue({ + feeStats: jest.fn().mockRejectedValue(new Error("Network failure")), + }); + + let hookResult; + await act(async () => { + render( + { + hookResult = data; + }} + />, + ); + }); + + await act(async () => { + await hookResult.fetchData(); + }); + + expect(hookResult.recommendedFee).toBe(BASE_FEE); + expect(hookResult.networkCongestion).toBe(""); + }); +}); diff --git a/extension/src/popup/helpers/useChangeTrustline.ts b/extension/src/popup/helpers/useChangeTrustline.ts index c56e55e633..e7946a386e 100644 --- a/extension/src/popup/helpers/useChangeTrustline.ts +++ b/extension/src/popup/helpers/useChangeTrustline.ts @@ -25,7 +25,6 @@ import { useNetworkFees } from "./useNetworkFees"; export const useChangeTrustline = ({ assetCode, assetIssuer, - recommendedFee: inputFee, setAssetSubmitting, setIsSigningWithHardwareWallet, setIsTrustlineErrorShowing, @@ -33,7 +32,6 @@ export const useChangeTrustline = ({ }: { assetCode: string; assetIssuer: string; - recommendedFee?: string; setAssetSubmitting?: (rowButtonShowing: string) => void; setIsSigningWithHardwareWallet?: (value: boolean) => void; setIsTrustlineErrorShowing?: (value: boolean) => void; @@ -56,8 +54,7 @@ export const useChangeTrustline = ({ networkDetails.networkPassphrase, ); - const networkFees = useNetworkFees(); - const recommendedFee = inputFee || networkFees.recommendedFee; + const { fetchData: fetchFees } = useNetworkFees(); const canonicalAsset = getCanonicalFromAsset(assetCode, assetIssuer); @@ -104,14 +101,14 @@ export const useChangeTrustline = ({ successfulCallback?: () => Promise, ) => { setAssetSubmitting?.(canonicalAsset); - + const fees = await fetchFees(); const transactionXDR: string = await getManageAssetXDR({ publicKey, assetCode, assetIssuer, addTrustline, server, - recommendedFee, + recommendedFee: fees.recommendedFee, networkDetails, }); diff --git a/extension/src/popup/helpers/useNetworkFees.ts b/extension/src/popup/helpers/useNetworkFees.ts index dfa61ea8a4..7906bc8c0f 100644 --- a/extension/src/popup/helpers/useNetworkFees.ts +++ b/extension/src/popup/helpers/useNetworkFees.ts @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { useSelector } from "react-redux"; +import { BASE_FEE } from "stellar-sdk"; import { stellarSdkServer } from "@shared/api/helpers/stellarSdkServer"; import { stroopToXlm } from "helpers/stellar"; @@ -15,34 +16,39 @@ export const useNetworkFees = () => { const { networkUrl, networkPassphrase } = useSelector( settingsNetworkDetailsSelector, ); - const [recommendedFee, setRecommendedFee] = useState(""); + const [recommendedFee, setRecommendedFee] = useState(BASE_FEE); const [networkCongestion, setNetworkCongestion] = useState( "" as NetworkCongestion, ); - useEffect(() => { - (async () => { - try { - const server = stellarSdkServer(networkUrl, networkPassphrase); - const { max_fee: maxFee, ledger_capacity_usage: ledgerCapacityUsage } = - await server.feeStats(); - const ledgerCapacityUsageNum = Number(ledgerCapacityUsage); + const fetchData = async () => { + try { + const server = stellarSdkServer(networkUrl, networkPassphrase); + const { max_fee: maxFee, ledger_capacity_usage: ledgerCapacityUsage } = + await server.feeStats(); + const ledgerCapacityUsageNum = Number(ledgerCapacityUsage); - setRecommendedFee(stroopToXlm(maxFee.mode).toFixed()); - if (ledgerCapacityUsageNum > 0.5 && ledgerCapacityUsageNum <= 0.75) { - setNetworkCongestion(NetworkCongestion.MEDIUM); - } else if (ledgerCapacityUsageNum > 0.75) { - setNetworkCongestion(NetworkCongestion.HIGH); - } else { - setNetworkCongestion(NetworkCongestion.LOW); - } - } catch (e) { - // use default values - setRecommendedFee("100"); - console.error(e); + setRecommendedFee(stroopToXlm(maxFee.mode).toFixed()); + if (ledgerCapacityUsageNum > 0.5 && ledgerCapacityUsageNum <= 0.75) { + setNetworkCongestion(NetworkCongestion.MEDIUM); + } else if (ledgerCapacityUsageNum > 0.75) { + setNetworkCongestion(NetworkCongestion.HIGH); + } else { + setNetworkCongestion(NetworkCongestion.LOW); } + return { recommendedFee, networkCongestion }; + } catch (e) { + setRecommendedFee(BASE_FEE); + return { recommendedFee }; + } + }; + + useEffect(() => { + (async () => { + await fetchData(); })(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [networkUrl, networkPassphrase]); - return { recommendedFee, networkCongestion }; + return { recommendedFee, networkCongestion, fetchData }; }; diff --git a/extension/src/popup/views/__tests__/ManageAssets.test.tsx b/extension/src/popup/views/__tests__/ManageAssets.test.tsx index 4384157d0e..5964f4c8bd 100644 --- a/extension/src/popup/views/__tests__/ManageAssets.test.tsx +++ b/extension/src/popup/views/__tests__/ManageAssets.test.tsx @@ -145,6 +145,7 @@ jest.spyOn(ApiInternal, "signFreighterTransaction").mockImplementation(() => jest.spyOn(UseNetworkFees, "useNetworkFees").mockImplementation(() => ({ recommendedFee: "0.00001", networkCongestion: UseNetworkFees.NetworkCongestion.MEDIUM, + fetchData: () => Promise.resolve({ recommendedFee: "00.1" }), })); jest.spyOn(SearchAsset, "searchAsset").mockImplementation(({ asset }) => { diff --git a/extension/src/popup/views/__tests__/SendPayment.test.tsx b/extension/src/popup/views/__tests__/SendPayment.test.tsx index ed3db59ee2..c3ccaf5ac7 100644 --- a/extension/src/popup/views/__tests__/SendPayment.test.tsx +++ b/extension/src/popup/views/__tests__/SendPayment.test.tsx @@ -61,6 +61,7 @@ jest.spyOn(UseNetworkFees, "useNetworkFees").mockImplementation(() => { return { recommendedFee: ".00001", networkCongestion: UseNetworkFees.NetworkCongestion.MEDIUM, + fetchData: () => Promise.resolve({ recommendedFee: "00.1" }), }; }); diff --git a/extension/src/popup/views/__tests__/SendTokenPayment.test.tsx b/extension/src/popup/views/__tests__/SendTokenPayment.test.tsx index 074cdf8d8d..ff2abea3f2 100644 --- a/extension/src/popup/views/__tests__/SendTokenPayment.test.tsx +++ b/extension/src/popup/views/__tests__/SendTokenPayment.test.tsx @@ -50,13 +50,7 @@ jest.spyOn(UseNetworkFees, "useNetworkFees").mockImplementation(() => { return { recommendedFee: ".00001", networkCongestion: UseNetworkFees.NetworkCongestion.MEDIUM, - }; -}); - -jest.spyOn(UseNetworkFees, "useNetworkFees").mockImplementation(() => { - return { - recommendedFee: ".00001", - networkCongestion: UseNetworkFees.NetworkCongestion.MEDIUM, + fetchData: () => Promise.resolve({ recommendedFee: "00.1" }), }; }); diff --git a/extension/src/popup/views/__tests__/Swap.test.tsx b/extension/src/popup/views/__tests__/Swap.test.tsx index a2157ce89c..4c1dc9517e 100644 --- a/extension/src/popup/views/__tests__/Swap.test.tsx +++ b/extension/src/popup/views/__tests__/Swap.test.tsx @@ -131,6 +131,7 @@ jest.spyOn(ApiInternal, "signFreighterTransaction").mockImplementation(() => jest.spyOn(UseNetworkFees, "useNetworkFees").mockImplementation(() => ({ recommendedFee: "0.00001", networkCongestion: UseNetworkFees.NetworkCongestion.MEDIUM, + fetchData: () => Promise.resolve({ recommendedFee: "00.1" }), })); jest.spyOn(BlockaidHelpers, "useScanTx").mockImplementation(() => { diff --git a/extension/src/popup/views/__tests__/SwapUnfunded.test.tsx b/extension/src/popup/views/__tests__/SwapUnfunded.test.tsx index 72ac82c16e..0161d64dbd 100644 --- a/extension/src/popup/views/__tests__/SwapUnfunded.test.tsx +++ b/extension/src/popup/views/__tests__/SwapUnfunded.test.tsx @@ -33,6 +33,7 @@ jest.spyOn(ApiInternal, "signFreighterTransaction").mockImplementation(() => jest.spyOn(UseNetworkFees, "useNetworkFees").mockImplementation(() => ({ recommendedFee: "0.00001", networkCongestion: UseNetworkFees.NetworkCongestion.MEDIUM, + fetchData: () => Promise.resolve({ recommendedFee: "00.1" }), })); jest.mock("popup/helpers/horizonGetBestPath", () => ({