Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8c0c8f7
upgrade to yarn 2 and use resolutions to block vulnerable package ver…
piyalbasu Sep 9, 2025
c5b1ff2
ensure invoke host function tx shows contract parameters (#2243)
piyalbasu Sep 11, 2025
15dbf22
Bugfix/rm auth param names (#2244)
piyalbasu Sep 12, 2025
8b2ed38
Bugfix/add issuer for changetrust (#2249)
piyalbasu Sep 12, 2025
9e0a758
cache account balances and poll for updates
piyalbasu Sep 16, 2025
ba9a7b2
fix CI tests
piyalbasu Sep 16, 2025
a88c762
rm `force:true` which was causing action to happen too fast
piyalbasu Sep 17, 2025
e7a440a
do a fresh balance fetch on account/network change
piyalbasu Sep 17, 2025
a2e7cd3
first pass at async history
piyalbasu Sep 17, 2025
6b12c5e
pr comments
piyalbasu Sep 17, 2025
e27c594
allow for history caching
piyalbasu Sep 24, 2025
08e4bb3
add more sentry tracking for Account and Wallets views (#2268)
piyalbasu Sep 25, 2025
6859fe0
gracefully degrade on errors from Blockaid (#2269)
piyalbasu Sep 25, 2025
d82ace3
add a test for persisting configurations in the send flow (#2271)
piyalbasu Sep 26, 2025
3bd7f68
rm slow loading simulation
piyalbasu Sep 26, 2025
eca68d2
handle missing scan-tx result; add disabled state for Confirm Anyway …
piyalbasu Sep 29, 2025
8324043
add cache for balances to ensure we do a fresh lookup when needed (#2…
piyalbasu Sep 29, 2025
6144ebe
adjust test to wait for UI change
piyalbasu Sep 29, 2025
36b5331
replace yarn setup with just yarn
piyalbasu Sep 29, 2025
67be1be
rm unnecessary return
piyalbasu Oct 2, 2025
dfcb486
Merge branch 'master' into feature/move-history-fetch-to-bg
piyalbasu Oct 3, 2025
a194f20
clear token details on redux clear action
piyalbasu Oct 3, 2025
6a11925
make history row construction async and check for redux state for upd…
piyalbasu Oct 3, 2025
5e91240
add tests for assetdetails
piyalbasu Oct 6, 2025
bf2292b
increase timeout for flakey test
piyalbasu Oct 6, 2025
8b64221
Merge branch 'master' into feature/move-history-fetch-to-bg
piyalbasu Oct 6, 2025
a5a3433
pr comments
piyalbasu Oct 7, 2025
f0bb85f
Merge branch 'feature/move-history-fetch-to-bg' of github.com:stellar…
piyalbasu Oct 7, 2025
69723fa
refresh account history every time account balances refresh
piyalbasu Oct 7, 2025
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
8 changes: 8 additions & 0 deletions config/jest/setupTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ jest.mock("helpers/metrics", () => ({
storeBalanceMetricData: () => {},
}));

jest.mock("popup/App", () => ({
store: {
getState: () => ({
cache: {},
}),
},
}));

jest.mock("react-i18next", () => ({
// this mock makes sure any components using the translate hook can use it without a warning being shown
useTranslation: () => ({
Expand Down
2 changes: 1 addition & 1 deletion extension/e2e-tests/loadAccount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,6 @@ test("Clears cache and fetches balances if it's been 2 minutes since the last ba

// make sure we fetch the new balance quickly rather than waiting for the 30 second interval
await expect(page.getByTestId("asset-amount")).toHaveText("999,111", {
timeout: 500,
timeout: 3000,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this test is a bit flakey - I believe just waiting a little longer for the UI to re-render should solve this

});
});
16 changes: 15 additions & 1 deletion extension/src/helpers/hooks/useGetHistory.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,40 @@
import { useReducer } from "react";
import { useDispatch, useSelector } from "react-redux";

import { historySelector, saveHistoryForAccount } from "popup/ducks/cache";
import { getAccountHistory } from "@shared/api/internal";
import { NetworkDetails } from "@shared/constants/stellar";
import { initialState, reducer } from "helpers/request";
import { HorizonOperation } from "@shared/api/types";
import { AppDispatch } from "popup/App";

export type HistoryResponse = HorizonOperation[];

function useGetHistory() {
const reduxDispatch = useDispatch<AppDispatch>();
const [state, dispatch] = useReducer(
reducer<HistoryResponse, unknown>,
initialState,
);
const cachedHistory = useSelector(historySelector);

const fetchData = async (
publicKey: string,
networkDetails: NetworkDetails,
useCache = false,
): Promise<HistoryResponse | Error> => {
dispatch({ type: "FETCH_DATA_START" });
try {
const data = await getAccountHistory(publicKey, networkDetails);
const cachedHistoryData =
cachedHistory[networkDetails.network]?.[publicKey];
const data =
useCache && cachedHistoryData
? cachedHistoryData
: await getAccountHistory(publicKey, networkDetails);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

following a similar pattern to account balance caching

dispatch({ type: "FETCH_DATA_SUCCESS", payload: data });
reduxDispatch(
saveHistoryForAccount({ publicKey, history: data, networkDetails }),
);
return data;
} catch (error) {
dispatch({ type: "FETCH_DATA_ERROR", payload: error });
Expand Down
69 changes: 69 additions & 0 deletions extension/src/helpers/hooks/useTokenDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useReducer } from "react";
import { useDispatch, useSelector } from "react-redux";

import { tokenDetailsSelector, saveTokenDetails } from "popup/ducks/cache";
import { getTokenDetails } from "@shared/api/internal";
import { initialState, reducer } from "helpers/request";
import { NetworkDetails } from "@shared/constants/stellar";
import { AppDispatch, store } from "popup/App";

export type TokenDetailsResponse = {
decimals: number;
symbol: string;
name: string;
} | null;

function useTokenDetails() {
const reduxDispatch = useDispatch<AppDispatch>();
const [state, dispatch] = useReducer(
reducer<TokenDetailsResponse, unknown>,
initialState,
);
const cachedTokenDetails = useSelector(tokenDetailsSelector);

const fetchData = async ({
contractId,
useCache = true,
publicKey,
networkDetails,
}: {
contractId: string;
useCache?: boolean;
publicKey: string;
networkDetails: NetworkDetails;
}): Promise<TokenDetailsResponse | Error> => {
dispatch({ type: "FETCH_DATA_START" });
try {
/*
Unlike the other cache hooks, this hook may be called multiple times within one render.
For example, when constructing the history rows, this hook can be called many times as we iterate over the history items.
If we have cached token details earlier in the loop, we won't have access to the update redux state until the next render.
To workaround this, we will also check the redux state manually here rather than waiting for the next render to
update the selector hook for us.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Example: In my account history, I have 10 transfers using the same custom token. The getHistoryData hook will run once on first render and then call this method 10 times as we iterate over the history items. However, because we're still in the first render, the useSelector hook won't have access to the updated redux state yet. This will result in cachedTokenDetails being empty for all 10 iterations, resulting in 10 separate API calls to get the token details.

*/
const cachedTokenDetailsData =
cachedTokenDetails[contractId] ||
store.getState().cache.tokenDetails[contractId];

const data =
useCache && cachedTokenDetailsData
? cachedTokenDetailsData
: await getTokenDetails({ contractId, publicKey, networkDetails });
if (data && Object.keys(data).length) {
reduxDispatch(saveTokenDetails({ contractId, ...data }));
}
dispatch({ type: "FETCH_DATA_SUCCESS", payload: data });
return data;
} catch (error) {
dispatch({ type: "FETCH_DATA_ERROR", payload: error });
throw new Error("Failed to fetch token details", { cause: error });
}
};

return {
state,
fetchData,
};
}

export { useTokenDetails };
36 changes: 36 additions & 0 deletions extension/src/popup/components/__tests__/AssetDetail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,38 @@ import { TESTNET_NETWORK_DETAILS } from "@shared/constants/stellar";
import { APPLICATION_STATE as ApplicationState } from "@shared/constants/applicationState";
import * as ApiInternal from "@shared/api/internal";
import { mockAccounts, Wrapper, mockBalances } from "popup/__testHelpers__";
import { AppDataType } from "helpers/hooks/useGetAppData";

const mockHistoryData = {
type: AppDataType.RESOLVED,
operationsByAsset: {
native: [
{
fetchTokenDetailsaction: "Received",
actionIcon: "received",

amount: "+0 XLM",

date: "Sep 19",
id: "253426134438383665",

metadata: {
createdAt: "2025-09-19T21:15:45Z",
feeCharged: "10000",
memo: "Buy NXR Earn Native XLM!",
type: "payment",
isDustPayment: true,
isPayment: true,
isReceiving: true,
nonLabelAmount: "0 XLM",
to: "G1",
},
rowIcon: <></>,
rowText: "XLM",
},
],
},
} as any;

describe("AssetDetail", () => {
it("renders asset detail", async () => {
Expand Down Expand Up @@ -50,6 +82,7 @@ describe("AssetDetail", () => {
setSelectedAsset: () => null,
setIsDetailViewShowing: () => null,
subentryCount: 0,
historyData: mockHistoryData,
};

render(
Expand Down Expand Up @@ -112,6 +145,7 @@ describe("AssetDetail", () => {
setSelectedAsset: () => null,
setIsDetailViewShowing: () => null,
subentryCount: 0,
historyData: mockHistoryData,
};

render(
Expand Down Expand Up @@ -175,6 +209,7 @@ describe("AssetDetail", () => {
setSelectedAsset: () => null,
setIsDetailViewShowing: () => null,
subentryCount: 0,
historyData: mockHistoryData,
};

render(
Expand Down Expand Up @@ -242,6 +277,7 @@ describe("AssetDetail", () => {
setSelectedAsset: () => null,
setIsDetailViewShowing: () => null,
subentryCount: 0,
historyData: mockHistoryData,
};

render(
Expand Down
111 changes: 75 additions & 36 deletions extension/src/popup/components/account/AssetDetail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
import { useSelector } from "react-redux";
import { BigNumber } from "bignumber.js";
import { useTranslation } from "react-i18next";
import { CopyText, Icon, Link } from "@stellar/design-system";
import { CopyText, Icon, Link, Loader } from "@stellar/design-system";

import { ApiTokenPrice, ApiTokenPrices } from "@shared/api/types";
import { NetworkDetails } from "@shared/constants/stellar";
Expand Down Expand Up @@ -40,11 +40,53 @@ import {
LiquidityPoolShareAsset,
} from "@shared/api/types/account-balance";
import { OperationDataRow } from "popup/views/AccountHistory/hooks/useGetHistoryData";
import { AccountHistoryData } from "popup/views/Account/hooks/useGetAccountHistoryData";
import { AppDataType } from "helpers/hooks/useGetAppData";

import "./styles.scss";

const AssetDetailOperations = ({
filteredAssetOperations,
accountBalances,
publicKey,
networkDetails,
setActiveAssetId,
}: {
filteredAssetOperations: OperationDataRow[];
accountBalances: AccountBalances;
publicKey: string;
networkDetails: NetworkDetails;
setActiveAssetId: (id: string) => void;
}) => {
const { t } = useTranslation();
return (
<>
{filteredAssetOperations.length ? (
<div className="AssetDetail__list" data-testid="AssetDetail__list">
<>
{filteredAssetOperations.map((operation) => (
<HistoryItem
key={operation.id}
accountBalances={accountBalances}
operation={operation}
publicKey={publicKey}
networkDetails={networkDetails}
setActiveHistoryDetailId={() => setActiveAssetId(operation.id)}
/>
))}
</>
</div>
) : (
<div className="AssetDetail__empty" data-testid="AssetDetail__empty">
{t("No transactions to show")}
</div>
)}
</>
);
};

interface AssetDetailProps {
assetOperations: OperationDataRow[];
historyData: AccountHistoryData | null;
accountBalances: AccountBalances;
networkDetails: NetworkDetails;
publicKey: string;
Expand All @@ -55,7 +97,7 @@ interface AssetDetailProps {
}

export const AssetDetail = ({
assetOperations,
historyData,
accountBalances,
networkDetails,
publicKey,
Expand All @@ -65,10 +107,10 @@ export const AssetDetail = ({
tokenPrices,
}: AssetDetailProps) => {
const { t } = useTranslation();
const { isHideDustEnabled } = useSelector(settingsSelector);
const [optionsOpen, setOptionsOpen] = React.useState(false);
const activeOptionsRef = useRef<HTMLDivElement>(null);
const isNative = selectedAsset === "native";
const { isHideDustEnabled } = useSelector(settingsSelector);

useEffect(() => {
function handleClickOutside(event: MouseEvent) {
Expand Down Expand Up @@ -129,27 +171,33 @@ export const AssetDetail = ({
assetIssuer,
});

if (!assetOperations && !isSorobanAsset) {
Copy link
Contributor Author

@piyalbasu piyalbasu Oct 7, 2025

Choose a reason for hiding this comment

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

I'm removing this !isSorobanAsset check. This was added a long time ago here: #738

This was supposed to be blocking showing history for custom tokens, but wasn't actually working. We can see history for all classic and custom tokens

if (historyData?.type === AppDataType.REROUTE) {
return null;
}

const sortedAssetOperations = assetOperations.filter((operation) => {
if (operation.metadata.isDustPayment && isHideDustEnabled) {
return false;
}
const assetOperations =
historyData?.operationsByAsset?.[selectedAsset] || null;

return true;
});
let filteredAssetOperations = null;
let activeOperation = null;

if (assetOperations) {
filteredAssetOperations = assetOperations.filter((operation) => {
if (operation.metadata.isDustPayment && isHideDustEnabled) {
return false;
}

return true;
});
activeOperation =
filteredAssetOperations.find((op) => op.id === activeAssetId) || null;
}

if (assetIssuer && !assetDomain && !assetError && !isSorobanAsset) {
// if we have an asset issuer, wait until we have the asset domain before continuing
return <Loading />;
}

const activeOperation = sortedAssetOperations.find(
(op) => op.id === activeAssetId,
);

const isStellarExpertSupported =
isMainnet(networkDetails) || isTestnet(networkDetails);
const stellarExpertAssetLinkSlug = isSorobanBalance(selectedBalance)
Expand Down Expand Up @@ -296,30 +344,21 @@ export const AssetDetail = ({
</div>
</div>
</div>
{sortedAssetOperations.length ? (
<div className="AssetDetail__list" data-testid="AssetDetail__list">
<>
{sortedAssetOperations.map((operation) => (
<HistoryItem
key={operation.id}
accountBalances={accountBalances}
operation={operation}
publicKey={publicKey}
networkDetails={networkDetails}
setActiveHistoryDetailId={() =>
setActiveAssetId(operation.id)
}
/>
))}
</>
</div>
) : (
{filteredAssetOperations === null ? (
<div
className="AssetDetail__empty"
data-testid="AssetDetail__empty"
className="AssetDetail__list AssetDetail__list--loading"
data-testid="AssetDetail__list__loader"
>
{t("No transactions to show")}
<Loader />
</div>
) : (
<AssetDetailOperations
filteredAssetOperations={filteredAssetOperations}
accountBalances={accountBalances}
publicKey={publicKey}
networkDetails={networkDetails}
setActiveAssetId={setActiveAssetId}
/>
)}
</div>
</View.Content>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@
flex-direction: column;
gap: pxToRem(24px);
margin-top: pxToRem(32px);

&--loading {
align-items: center;
display: flex;
justify-content: center;
}
}

.SubviewHeader {
Expand Down
Loading