diff --git a/app/components/UI/ActivityListItemRow/ActivityListItemRow.test.tsx b/app/components/UI/ActivityListItemRow/ActivityListItemRow.test.tsx index 0068f5bd8238..9fd089fd06ac 100644 --- a/app/components/UI/ActivityListItemRow/ActivityListItemRow.test.tsx +++ b/app/components/UI/ActivityListItemRow/ActivityListItemRow.test.tsx @@ -1494,6 +1494,7 @@ const ALL_KINDS: ActivityListItem['type'][] = [ 'claim', 'claimMusdBonus', 'deposit', + 'stake', 'unstake', 'convert', 'wrap', @@ -1546,7 +1547,8 @@ const EXPECTED_TITLES = { claim: 'Claimed', claimMusdBonus: strings('transactions.activity_claim_musd_bonus'), deposit: 'Deposited', - unstake: 'Unstaked', + stake: 'Staked Ethereum', + unstake: 'Unstaked Ethereum', convert: 'Converted', wrap: strings('transactions.activity_wrap'), unwrap: strings('transactions.activity_unwrap'), diff --git a/app/components/UI/ActivityListItemRow/resolveIconType.ts b/app/components/UI/ActivityListItemRow/resolveIconType.ts index daf403c60ee6..f772fc514b1d 100644 --- a/app/components/UI/ActivityListItemRow/resolveIconType.ts +++ b/app/components/UI/ActivityListItemRow/resolveIconType.ts @@ -12,6 +12,7 @@ export function resolveIconType(type: ActivityKind): string { case 'nftSell': case 'lendingDeposit': case 'deposit': + case 'stake': case 'wrap': case 'perpsAddFunds': case 'predictionsAddFunds': diff --git a/app/components/UI/ActivityListItemRow/titleLabels.ts b/app/components/UI/ActivityListItemRow/titleLabels.ts new file mode 100644 index 000000000000..cca074255a88 --- /dev/null +++ b/app/components/UI/ActivityListItemRow/titleLabels.ts @@ -0,0 +1,72 @@ +import { strings } from '../../../../locales/i18n'; +import type { ActivityKind } from '../../../util/activity-adapters'; + +/** + * Fallback title resolvers for activity kinds whose title comes straight from an + * i18n string (perps/predict domain rows). Consumed by `resolveFallbackTitle` in + * `useActivityListItemRowContent`. + */ +export const ACTIVITY_FALLBACK_TITLE_RESOLVERS: Partial< + Record string> +> = { + predictionsAddFunds: () => + strings('transactions.activity_prediction_account_funded'), + predictionsWithdrawFunds: () => + strings('transactions.activity_prediction_withdrawal'), + predictionClaimWinnings: () => strings('predict.transactions.claim_title'), + predictionCashedOut: () => strings('predict.transactions.sell_title'), + // Design board copy: "Prediction placed" (not the legacy "Predicted"). + predictionPlaced: () => strings('transactions.activity_prediction_placed'), + perpsAddFunds: () => strings('transactions.activity_perps_account_funded'), + perpsWithdraw: () => strings('transactions.activity_perps_withdrawal'), + perpsOpenLong: () => strings('transactions.activity_perps_open_long'), + perpsCloseLong: () => strings('transactions.activity_perps_close_long'), + perpsCloseLongLiquidated: () => + strings('transactions.activity_perps_close_long_liquidated'), + perpsCloseLongStopLoss: () => + strings('transactions.activity_perps_close_long_stop_loss'), + perpsOpenShort: () => strings('transactions.activity_perps_open_short'), + perpsCloseShort: () => strings('transactions.activity_perps_close_short'), + perpsCloseShortLiquidated: () => + strings('transactions.activity_perps_close_short_liquidated'), + perpsCloseShortStopLoss: () => + strings('transactions.activity_perps_close_short_stop_loss'), + perpsPaidFundingFees: () => + strings('transactions.activity_perps_paid_funding_fees'), + perpsReceivedFundingFees: () => + strings('transactions.activity_perps_received_funding_fees'), + perpsCloseShortTakeProfit: () => + strings('transactions.activity_perps_close_short_take_profit'), + perpsCloseLongTakeProfit: () => + strings('transactions.activity_perps_close_long_take_profit'), + marketShort: () => strings('transactions.activity_market_short'), + stopMarketCloseShort: () => + strings('transactions.activity_stop_market_close_short'), + marketCloseShort: () => strings('transactions.activity_market_close_short'), + limitShort: () => strings('transactions.activity_limit_short'), + limitCloseShort: () => strings('transactions.activity_limit_close_short'), +}; + +/** + * Status verbs for single-token value activities (buy/sell/claim/stake/unstake/ + * deposit). Keyed by kind so the resolver is a lookup instead of a ternary chain. + */ +export const TOKEN_ACTION_LABELS: Record< + 'buy' | 'sell' | 'claim' | 'stake' | 'unstake' | 'deposit', + { success: string; pending: string; failed: string } +> = { + buy: { success: 'Bought', pending: 'Buying', failed: 'Buy failed' }, + sell: { success: 'Sold', pending: 'Selling', failed: 'Sell failed' }, + claim: { success: 'Claimed', pending: 'Claiming', failed: 'Claim failed' }, + stake: { success: 'Staked', pending: 'Staking', failed: 'Stake failed' }, + unstake: { + success: 'Unstaked', + pending: 'Unstaking', + failed: 'Unstake failed', + }, + deposit: { + success: 'Deposited', + pending: 'Depositing', + failed: 'Deposit failed', + }, +}; diff --git a/app/components/UI/ActivityListItemRow/useActivityListItemRowContent.ts b/app/components/UI/ActivityListItemRow/useActivityListItemRowContent.ts index 676f718125fa..b17f0560a019 100644 --- a/app/components/UI/ActivityListItemRow/useActivityListItemRowContent.ts +++ b/app/components/UI/ActivityListItemRow/useActivityListItemRowContent.ts @@ -41,6 +41,10 @@ import { // eslint-disable-next-line import-x/no-restricted-paths -- TODO(ADR-0020): route-isolation backlog import { getPerpsDisplaySymbol } from '@metamask/perps-controller'; import type { ActivityListItemRowContent } from './ActivityListItemRow.types'; +import { + ACTIVITY_FALLBACK_TITLE_RESOLVERS, + TOKEN_ACTION_LABELS, +} from './titleLabels'; function isPerpsFundsKind(type: ActivityKind): boolean { return type === 'perpsAddFunds' || type === 'perpsWithdraw'; @@ -263,47 +267,6 @@ function statusTitle( return titles.success; } -const ACTIVITY_FALLBACK_TITLE_RESOLVERS: Partial< - Record string> -> = { - predictionsAddFunds: () => - strings('transactions.activity_prediction_account_funded'), - predictionsWithdrawFunds: () => - strings('transactions.activity_prediction_withdrawal'), - predictionClaimWinnings: () => strings('predict.transactions.claim_title'), - predictionCashedOut: () => strings('predict.transactions.sell_title'), - // Design board copy: "Prediction placed" (not the legacy "Predicted"). - predictionPlaced: () => strings('transactions.activity_prediction_placed'), - perpsAddFunds: () => strings('transactions.activity_perps_account_funded'), - perpsWithdraw: () => strings('transactions.activity_perps_withdrawal'), - perpsOpenLong: () => strings('transactions.activity_perps_open_long'), - perpsCloseLong: () => strings('transactions.activity_perps_close_long'), - perpsCloseLongLiquidated: () => - strings('transactions.activity_perps_close_long_liquidated'), - perpsCloseLongStopLoss: () => - strings('transactions.activity_perps_close_long_stop_loss'), - perpsOpenShort: () => strings('transactions.activity_perps_open_short'), - perpsCloseShort: () => strings('transactions.activity_perps_close_short'), - perpsCloseShortLiquidated: () => - strings('transactions.activity_perps_close_short_liquidated'), - perpsCloseShortStopLoss: () => - strings('transactions.activity_perps_close_short_stop_loss'), - perpsPaidFundingFees: () => - strings('transactions.activity_perps_paid_funding_fees'), - perpsReceivedFundingFees: () => - strings('transactions.activity_perps_received_funding_fees'), - perpsCloseShortTakeProfit: () => - strings('transactions.activity_perps_close_short_take_profit'), - perpsCloseLongTakeProfit: () => - strings('transactions.activity_perps_close_long_take_profit'), - marketShort: () => strings('transactions.activity_market_short'), - stopMarketCloseShort: () => - strings('transactions.activity_stop_market_close_short'), - marketCloseShort: () => strings('transactions.activity_market_close_short'), - limitShort: () => strings('transactions.activity_limit_short'), - limitCloseShort: () => strings('transactions.activity_limit_close_short'), -}; - // Domain (perps/predict) rows have no bespoke failed copy, so mark a // failed/cancelled status with an em-dash "—Failed" suffix, mirroring the perps // severity suffix style (e.g. "Closed short—liquidated"). The failed color is @@ -623,44 +586,27 @@ function resolveCoreContent( case 'buy': case 'sell': case 'claim': + case 'stake': case 'unstake': case 'deposit': { const token = item.data.token; const symbol = token?.symbol; const isNamelessNftBuy = item.type === 'buy' && isNamelessNftToken(token); - const labels = - item.type === 'buy' - ? { success: 'Bought', pending: 'Buying', failed: 'Buy failed' } - : item.type === 'sell' - ? { success: 'Sold', pending: 'Selling', failed: 'Sell failed' } - : item.type === 'claim' - ? { - success: 'Claimed', - pending: 'Claiming', - failed: 'Claim failed', - } - : item.type === 'unstake' - ? { - success: 'Unstaked', - pending: 'Unstaking', - failed: 'Unstake failed', - } - : { - success: 'Deposited', - pending: 'Depositing', - failed: 'Deposit failed', - }; + // Pooled staking is ETH-only, so stake/unstake read the full asset name + // ("Staked Ethereum" / "Unstaked Ethereum") rather than the "ETH" symbol. + const isStakingKind = item.type === 'stake' || item.type === 'unstake'; + let displayNoun = symbol; + if (isStakingKind) { + displayNoun = 'Ethereum'; + } else if (isNamelessNftBuy) { + displayNoun = 'NFT'; + } + const labels = TOKEN_ACTION_LABELS[item.type]; return { title: statusTitle(item, { - success: withOptionalSymbol( - labels.success, - isNamelessNftBuy ? 'NFT' : symbol, - ), - pending: withOptionalSymbol( - labels.pending, - isNamelessNftBuy ? 'NFT' : symbol, - ), + success: withOptionalSymbol(labels.success, displayNoun), + pending: withOptionalSymbol(labels.pending, displayNoun), failed: labels.failed, }), subtitle: protocolSubtitle(item), diff --git a/app/components/Views/ActivityDetails/templates/DepositDetails.tsx b/app/components/Views/ActivityDetails/templates/DepositDetails.tsx index 44875301553d..28213fd97960 100644 --- a/app/components/Views/ActivityDetails/templates/DepositDetails.tsx +++ b/app/components/Views/ActivityDetails/templates/DepositDetails.tsx @@ -13,7 +13,7 @@ export function DepositDetails({ }: { item: Extract< ActivityListItem, - { type: 'buy' | 'claim' | 'deposit' | 'unstake' } + { type: 'buy' | 'claim' | 'deposit' | 'stake' | 'unstake' } >; }) { return ( diff --git a/app/components/Views/ActivityDetails/templates/TemplateLoader.test.tsx b/app/components/Views/ActivityDetails/templates/TemplateLoader.test.tsx index 7afbb2873cd8..2d50d06c1907 100644 --- a/app/components/Views/ActivityDetails/templates/TemplateLoader.test.tsx +++ b/app/components/Views/ActivityDetails/templates/TemplateLoader.test.tsx @@ -323,6 +323,22 @@ const unstakeItem: ActivityListItem = { }, } as ActivityListItem; +const stakeItem: ActivityListItem = { + type: 'stake', + chainId: 'eip155:1', + status: 'success', + timestamp: 1, + hash: '0xstake', + data: { + token: { + amount: '1000000000000000000', + decimals: 18, + symbol: 'ETH', + direction: 'out', + }, + }, +} as ActivityListItem; + const smartAccountUpgradeItem: ActivityListItem = { type: 'smartAccountUpgrade', chainId: 'eip155:1', @@ -433,6 +449,7 @@ describe('TemplateLoader', () => { ['contract interaction', contractItem], ['claim mUSD bonus', claimMusdBonusItem], ['earn/staking deposit', depositItem], + ['earn/staking stake', stakeItem], ['earn/staking claim', claimItem], ['earn/staking unstake', unstakeItem], ])('renders the %s details template', (_type, item) => { diff --git a/app/components/Views/ActivityDetails/templates/TemplateLoader.tsx b/app/components/Views/ActivityDetails/templates/TemplateLoader.tsx index 8f9da051963a..af8c0189f464 100644 --- a/app/components/Views/ActivityDetails/templates/TemplateLoader.tsx +++ b/app/components/Views/ActivityDetails/templates/TemplateLoader.tsx @@ -54,6 +54,7 @@ export function TemplateLoader({ case 'claimMusdBonus': return ; case 'claim': + case 'stake': case 'unstake': return ; case 'smartAccountUpgrade': diff --git a/app/components/Views/ActivityScreen/types.test.ts b/app/components/Views/ActivityScreen/types.test.ts index a113e7c23a32..2c19286c3261 100644 --- a/app/components/Views/ActivityScreen/types.test.ts +++ b/app/components/Views/ActivityScreen/types.test.ts @@ -100,6 +100,19 @@ describe('activityKindMatchesTypeFilter', () => { ).toBe(true); } }); + + it('groups earn/staking kinds (incl. stake) under Transactions', () => { + for (const kind of [ + 'stake', + 'deposit', + 'claim', + 'unstake', + ] as ActivityKind[]) { + expect( + activityKindMatchesTypeFilter(kind, ActivityTypeFilter.Transactions), + ).toBe(true); + } + }); }); describe('resolveInitialActivityTypeFilter', () => { diff --git a/app/components/Views/ActivityScreen/types.ts b/app/components/Views/ActivityScreen/types.ts index b0f1463b3e12..a510d5a844b9 100644 --- a/app/components/Views/ActivityScreen/types.ts +++ b/app/components/Views/ActivityScreen/types.ts @@ -119,6 +119,7 @@ export const ACTIVITY_TYPE_FILTER_KINDS: Record< 'nftMint', 'nftSell', 'deposit', + 'stake', 'claim', 'unstake', 'lendingDeposit', diff --git a/app/util/activity-adapters/adapters/local-transaction.test.ts b/app/util/activity-adapters/adapters/local-transaction.test.ts index 92f2a0e284e7..d3a5f058ae66 100644 --- a/app/util/activity-adapters/adapters/local-transaction.test.ts +++ b/app/util/activity-adapters/adapters/local-transaction.test.ts @@ -1002,7 +1002,7 @@ describe('mapLocalTransaction', () => { ); }); - it('maps a staking deposit to a deposit activity with the network fee', () => { + it('maps a staking deposit to a stake activity with the network fee', () => { const transaction = { chainId: mainnet, hash: '0xstakedeposit', @@ -1015,7 +1015,7 @@ describe('mapLocalTransaction', () => { expect(mapLocalTransaction(makeGroup(transaction))).toEqual( expect.objectContaining({ - type: 'deposit', + type: 'stake', data: expect.objectContaining({ fees: expect.any(Array) }), }), ); @@ -1145,7 +1145,7 @@ describe('mapLocalTransaction', () => { const result = mapLocalTransaction( makeGroup(transaction, { nativeAssetSymbol: undefined }), ); - expect(result.type).toBe('deposit'); + expect(result.type).toBe('stake'); const { token } = result.data as { token?: unknown }; expect(token).toBeUndefined(); }); diff --git a/app/util/activity-adapters/adapters/local-transaction.ts b/app/util/activity-adapters/adapters/local-transaction.ts index b953747c6958..977028061c58 100644 --- a/app/util/activity-adapters/adapters/local-transaction.ts +++ b/app/util/activity-adapters/adapters/local-transaction.ts @@ -719,7 +719,7 @@ export function mapLocalTransaction( case TransactionType.stakingDeposit: return { - type: 'deposit', + type: 'stake', chainId, status, timestamp, diff --git a/app/util/activity-adapters/types.ts b/app/util/activity-adapters/types.ts index a36694cdb16f..3c0e83b50f22 100644 --- a/app/util/activity-adapters/types.ts +++ b/app/util/activity-adapters/types.ts @@ -21,6 +21,7 @@ export type ActivityKind = | 'sell' | 'buy' | 'deposit' + | 'stake' | 'unstake' | 'swap' | 'swapIncomplete' @@ -146,7 +147,7 @@ export type ActivityListItem = } > | ActivityData< - 'buy' | 'claim' | 'deposit' | 'unstake', + 'buy' | 'claim' | 'deposit' | 'stake' | 'unstake', { from?: string; to?: string;