Skip to content

Commit 524410d

Browse files
committed
fix(activity): show token icon and amount for lending deposit rows
Lending deposit rows derived their token from the pool contract (txParams.to) instead of the underlying deposited asset, so the Activity row had no resolvable icon, symbol, or amount. Resolve the supplied token from simulation data / the outgoing Transfer log (falling back to the pool address), and enrich lending token metadata from the tokens API so the row shows the correct avatar and amount. TMCU-1082
1 parent 5493067 commit 524410d

4 files changed

Lines changed: 296 additions & 8 deletions

File tree

app/components/UI/ActivityListItemRow/ActivityListItemRow.test.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,6 +1117,64 @@ describe('ActivityListItemRow — row content', () => {
11171117
jest.mocked(useTokensData).mockReturnValue({});
11181118
});
11191119

1120+
it('resolves a lending-deposit token symbol/decimals from the tokens API and scales the amount', () => {
1121+
const assetId =
1122+
'eip155:42161/erc20:0x0000000000000000000000000000000000000002';
1123+
jest.mocked(useTokensData).mockReturnValue({
1124+
[assetId]: {
1125+
assetId,
1126+
symbol: 'USDT',
1127+
decimals: 6,
1128+
name: 'Tether USD',
1129+
iconUrl: '',
1130+
},
1131+
});
1132+
1133+
// The adapter carries only the atomic amount + asset id (the tx targets the
1134+
// pool, so symbol/decimals aren't in local metadata). Without decimals the
1135+
// amount would render unscaled (10,000 instead of 0.01).
1136+
const item = makeItem({
1137+
type: 'lendingDeposit',
1138+
status: 'success',
1139+
chainId: 'eip155:42161',
1140+
sourceToken: { amount: '10000', direction: 'out', assetId },
1141+
});
1142+
1143+
const { getByTestId } = render(
1144+
<ActivityListItemRow item={item} index={0} />,
1145+
);
1146+
1147+
expect(getByTestId('activity-primary-amount-0xabc').props.children).toBe(
1148+
'-0.01 USDT',
1149+
);
1150+
expect(getByTestId('avatar-token-USDT')).toBeOnTheScreen();
1151+
1152+
jest.mocked(useTokensData).mockReturnValue({});
1153+
});
1154+
1155+
it('renders a lending-deposit amount from adapter-provided decimals without an API lookup', () => {
1156+
// When the adapter already resolved symbol/decimals, the row scales the
1157+
// amount without depending on the tokens API (which returns nothing here).
1158+
const item = makeItem({
1159+
type: 'lendingDeposit',
1160+
status: 'success',
1161+
sourceToken: {
1162+
amount: '10000',
1163+
decimals: 6,
1164+
symbol: 'USDC',
1165+
direction: 'out',
1166+
},
1167+
});
1168+
1169+
const { getByTestId } = render(
1170+
<ActivityListItemRow item={item} index={0} />,
1171+
);
1172+
1173+
expect(getByTestId('activity-primary-amount-0xabc').props.children).toBe(
1174+
'-0.01 USDC',
1175+
);
1176+
});
1177+
11201178
it('renders cross-token bridge as swapped with token pair subtitle', () => {
11211179
const item = makeItem({
11221180
type: 'bridge',

app/components/UI/ActivityListItemRow/useActivityListItemRowContent.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,26 @@ function enrichSpendingCapToken(
371371
};
372372
}
373373

374+
function enrichTokenFromApi(
375+
token: TokenAmount | undefined,
376+
dataByAssetId: Record<string, { symbol?: string; decimals?: number }>,
377+
): TokenAmount | undefined {
378+
if (!token?.assetId) {
379+
return token;
380+
}
381+
const listToken = dataByAssetId[token.assetId.toLowerCase()];
382+
if (!listToken) {
383+
return token;
384+
}
385+
const symbol = token.symbol ?? listToken.symbol;
386+
const decimals = token.decimals ?? listToken.decimals;
387+
return {
388+
...token,
389+
...(symbol ? { symbol } : {}),
390+
...(decimals === undefined ? {} : { decimals }),
391+
};
392+
}
393+
374394
function areSameDisplayToken(
375395
sourceToken: TokenAmount | undefined,
376396
destinationToken: TokenAmount | undefined,
@@ -1131,17 +1151,37 @@ export function useActivityListItemRowContent(
11311151
)
11321152
: undefined;
11331153

1154+
const isLending =
1155+
item.type === 'lendingDeposit' || item.type === 'lendingWithdrawal';
1156+
const lendingAssetIds: string[] = [];
1157+
if (isLending) {
1158+
if (
1159+
'destinationToken' in item.data &&
1160+
item.data.destinationToken?.assetId
1161+
) {
1162+
lendingAssetIds.push(item.data.destinationToken.assetId);
1163+
}
1164+
if ('sourceToken' in item.data && item.data.sourceToken?.assetId) {
1165+
lendingAssetIds.push(item.data.sourceToken.assetId);
1166+
}
1167+
}
1168+
const lendingTokenData = useTokensData(lendingAssetIds);
1169+
11341170
const content = resolveCoreContent(item, bridgeHistoryItem);
11351171
const primaryToken = enrichStablecoinTokenMetadata(
11361172
isSpendingCap
11371173
? spendingCapToken?.amount
11381174
? spendingCapToken
11391175
: undefined
1140-
: content.primaryToken,
1176+
: isLending
1177+
? enrichTokenFromApi(content.primaryToken, lendingTokenData)
1178+
: content.primaryToken,
11411179
networkChainId,
11421180
);
11431181
const secondaryToken = enrichStablecoinTokenMetadata(
1144-
content.secondaryToken,
1182+
isLending
1183+
? enrichTokenFromApi(content.secondaryToken, lendingTokenData)
1184+
: content.secondaryToken,
11451185
networkChainId,
11461186
);
11471187
const isPerpsFunding = isPerpsFundingKind(item.type);
@@ -1210,7 +1250,9 @@ export function useActivityListItemRowContent(
12101250
avatarTokens:
12111251
isSpendingCap && spendingCapToken
12121252
? [spendingCapToken]
1213-
: resolveAvatarTokens(item, bridgeHistoryItem),
1253+
: isLending && primaryToken
1254+
? [primaryToken]
1255+
: resolveAvatarTokens(item, bridgeHistoryItem),
12141256
avatarIconUrl: predictIconUrl,
12151257
perpsMarketSymbol,
12161258
primaryToken,

app/util/activity-adapters/adapters/local-transaction.test.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,146 @@ describe('mapLocalTransaction', () => {
802802
});
803803
});
804804

805+
it('maps a typed lendingDeposit to the underlying token from simulation data', () => {
806+
// Mobile tags lending deposits with TransactionType.lendingDeposit, whose tx
807+
// `to` is the pool — the deposited token must come from the balance change,
808+
// not the pool address (which resolves no symbol/icon).
809+
const transaction = {
810+
chainId: base,
811+
id: 'typed-lending-deposit-id',
812+
hash: '0x1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809',
813+
status: TransactionStatus.confirmed,
814+
time: 1779892154611,
815+
type: TransactionType.lendingDeposit,
816+
txParams: {
817+
from,
818+
to: baseAavePool,
819+
value: '0x0',
820+
},
821+
simulationData: {
822+
tokenBalanceChanges: [
823+
{
824+
address: baseUsdc,
825+
difference: '0x186a0',
826+
isDecrease: true,
827+
standard: 'erc20',
828+
},
829+
],
830+
},
831+
} as unknown as Partial<TransactionMeta>;
832+
833+
expect(
834+
withoutRaw(mapLocalTransaction(makeGroup(transaction))),
835+
).toStrictEqual({
836+
type: 'lendingDeposit',
837+
chainId: 'eip155:8453',
838+
status: 'success',
839+
timestamp: 1779892154611,
840+
hash: '0x1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809',
841+
data: {
842+
sourceToken: {
843+
amount: '100000',
844+
assetId: toAssetId(baseUsdc, 'eip155:8453'),
845+
decimals: 6,
846+
direction: 'out',
847+
symbol: 'USDC',
848+
},
849+
},
850+
});
851+
});
852+
853+
it('maps a typed lendingDeposit to the underlying token from the outgoing transfer log when no simulation data', () => {
854+
const transaction = {
855+
chainId: base,
856+
hash: '0x2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809a1',
857+
status: TransactionStatus.confirmed,
858+
time: 1779892154611,
859+
type: TransactionType.lendingDeposit,
860+
txParams: {
861+
from,
862+
to: baseAavePool,
863+
value: '0x0',
864+
},
865+
txReceipt: {
866+
logs: [
867+
// Transfer FROM the user (topics[1]) TO the pool (topics[2]) — the
868+
// deposited token.
869+
{
870+
address: baseUsdc,
871+
data: '0x00000000000000000000000000000000000000000000000000000000000186a0',
872+
topics: [
873+
erc20TransferTopic,
874+
addressTopic(from),
875+
addressTopic(baseAavePool),
876+
],
877+
},
878+
],
879+
},
880+
} as unknown as Partial<TransactionMeta>;
881+
882+
expect(
883+
withoutRaw(mapLocalTransaction(makeGroup(transaction))),
884+
).toStrictEqual({
885+
type: 'lendingDeposit',
886+
chainId: 'eip155:8453',
887+
status: 'success',
888+
timestamp: 1779892154611,
889+
hash: '0x2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809a1',
890+
data: {
891+
sourceToken: {
892+
amount: '100000',
893+
assetId: toAssetId(baseUsdc, 'eip155:8453'),
894+
decimals: 6,
895+
direction: 'out',
896+
symbol: 'USDC',
897+
},
898+
},
899+
});
900+
});
901+
902+
it('maps a typed lendingDeposit with neither simulation data nor a matching transfer log to the pool-address token (prior behavior)', () => {
903+
const otherSender =
904+
'0x0000000000000000000000001111111111111111111111111111111111111111';
905+
const transaction = {
906+
chainId: base,
907+
hash: '0x3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809a1b2',
908+
status: TransactionStatus.confirmed,
909+
time: 1779892154611,
910+
type: TransactionType.lendingDeposit,
911+
txParams: {
912+
from,
913+
to: baseAavePool,
914+
value: '0x0',
915+
},
916+
txReceipt: {
917+
logs: [
918+
// A Transfer whose sender isn't the user — not the deposited token.
919+
{
920+
address: baseUsdc,
921+
data: '0x00000000000000000000000000000000000000000000000000000000000186a0',
922+
topics: [erc20TransferTopic, otherSender, addressTopic(from)],
923+
},
924+
],
925+
},
926+
} as unknown as Partial<TransactionMeta>;
927+
928+
expect(
929+
withoutRaw(mapLocalTransaction(makeGroup(transaction))),
930+
).toStrictEqual({
931+
type: 'lendingDeposit',
932+
chainId: 'eip155:8453',
933+
status: 'success',
934+
timestamp: 1779892154611,
935+
hash: '0x3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809a1b2',
936+
data: {
937+
sourceToken: {
938+
assetId: toAssetId(baseAavePool, 'eip155:8453'),
939+
direction: 'out',
940+
},
941+
},
942+
});
943+
});
944+
805945
it('maps a withdraw contract interaction from the received token transfer', () => {
806946
const transaction = {
807947
chainId: base,

app/util/activity-adapters/adapters/local-transaction.ts

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,58 @@ export function mapLocalTransaction(
279279
})
280280
: undefined;
281281
};
282+
// The deposited token is the underlying asset (USDC/USDT/…), NOT the pool the
283+
// tx is sent to. Deriving the token from `txParams.to` (the pool) leaves the
284+
// row with no resolvable symbol/icon and no amount, so resolve the real token
285+
// from simulation data first, then the outgoing Transfer log, and only fall
286+
// back to the pool address to preserve the prior behavior.
287+
const getLendingDepositSourceToken = () => {
288+
const suppliedTokenBalanceChange =
289+
initialTransaction.simulationData?.tokenBalanceChanges?.find(
290+
({ isDecrease, standard }) => isDecrease && standard === 'erc20',
291+
);
292+
293+
if (suppliedTokenBalanceChange) {
294+
return getContractToken({
295+
amount: BigInt(suppliedTokenBalanceChange.difference).toString(),
296+
transaction: initialTransaction,
297+
direction: 'out',
298+
contractAddress: suppliedTokenBalanceChange.address,
299+
});
300+
}
301+
302+
// Post-confirmation fallback: the outgoing Transfer log (token sent FROM the
303+
// user). Mirrors getLendingWithdrawalDestinationToken but matches on the
304+
// sender (topics[1]) instead of the recipient (topics[2]).
305+
const fromAddress = from.toLowerCase();
306+
const sentTokenLog = (initialTransaction.txReceipt?.logs ?? []).find(
307+
({ topics: [eventTopic, logFrom] = [] }) => {
308+
const senderAddress = logFrom
309+
? `0x${logFrom.slice(-40)}`.toLowerCase()
310+
: undefined;
311+
312+
return (
313+
eventTopic?.toLowerCase() === environment.tokenTransferLogTopicHash &&
314+
senderAddress === fromAddress
315+
);
316+
},
317+
);
318+
319+
if (sentTokenLog) {
320+
return getContractToken({
321+
amount: BigInt(String(sentTokenLog.data)).toString(),
322+
transaction: initialTransaction,
323+
direction: 'out',
324+
contractAddress: sentTokenLog.address,
325+
});
326+
}
327+
328+
return getContractToken({
329+
transaction: initialTransaction,
330+
direction: 'out',
331+
contractAddress: initialTransaction.txParams.to,
332+
});
333+
};
282334
const getDirectWrappedTokenActivity = (): ActivityListItem | undefined => {
283335
if (!methodId) {
284336
return undefined;
@@ -694,11 +746,7 @@ export function mapLocalTransaction(
694746
hash,
695747
raw: { type: 'localTransaction', data: transactionGroup },
696748
data: {
697-
sourceToken: getContractToken({
698-
transaction: initialTransaction,
699-
direction: 'out',
700-
contractAddress: initialTransaction.txParams.to,
701-
}),
749+
sourceToken: getLendingDepositSourceToken(),
702750
...(fees ? { fees } : {}),
703751
},
704752
};

0 commit comments

Comments
 (0)