diff --git a/packages/client-utils/CHANGELOG.md b/packages/client-utils/CHANGELOG.md index 0022fda4ec..fdf0204138 100644 --- a/packages/client-utils/CHANGELOG.md +++ b/packages/client-utils/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Map `assetActivation` and `assetDeactivation` activity types in transaction activity mappers ([#9440](https://github.com/MetaMask/core/pull/9440)) + ## [1.0.0] ### Added diff --git a/packages/client-utils/src/mappers/keyring-transaction-mapper.test.ts b/packages/client-utils/src/mappers/keyring-transaction-mapper.test.ts index 79a2678e66..e1902a92d0 100644 --- a/packages/client-utils/src/mappers/keyring-transaction-mapper.test.ts +++ b/packages/client-utils/src/mappers/keyring-transaction-mapper.test.ts @@ -214,4 +214,85 @@ describe('mapKeyringTransaction', () => { }, }); }); + + it('maps trustline approve TokenApprove to assetActivation', () => { + const item = mapKeyringTransaction( + keyringTransactionFixtures.mapArgs.trustlineApprove, + ); + + expect(item).toMatchObject({ + type: 'assetActivation', + chainId: 'stellar:pubnet', + status: 'success', + timestamp: 1716367781000, + hash: 'trustline-approve-id', + data: { + from: 'owner-address', + token: { + amount: undefined, + symbol: 'USDC', + direction: 'out', + }, + }, + }); + }); + + it('returns the trustline activation token unchanged when no amount is present', () => { + const item = mapKeyringTransaction( + keyringTransactionFixtures.mapArgs.trustlineApproveNoAmount, + ); + + expect(item).toMatchObject({ + type: 'assetActivation', + data: { from: 'owner-address', token: undefined }, + }); + }); + + it('maps trustline disapprove TokenDisapprove to assetDeactivation', () => { + const item = mapKeyringTransaction( + keyringTransactionFixtures.mapArgs.trustlineDisapprove, + ); + + expect(item).toMatchObject({ + type: 'assetDeactivation', + chainId: 'stellar:pubnet', + status: 'success', + timestamp: 1716367781000, + hash: 'trustline-disapprove-id', + data: { + from: 'owner-address', + token: { + amount: undefined, + symbol: 'USDC', + direction: 'out', + }, + }, + }); + }); + + it('returns the trustline deactivation token unchanged when no amount is present', () => { + const item = mapKeyringTransaction( + keyringTransactionFixtures.mapArgs.trustlineDisapproveNoAmount, + ); + + expect(item).toMatchObject({ + type: 'assetDeactivation', + data: { from: 'owner-address', token: undefined }, + }); + }); + + it('maps a non-trustline TokenDisapprove to a contract interaction', () => { + const item = mapKeyringTransaction( + keyringTransactionFixtures.mapArgs.disapproveNonTrustline, + ); + + expect(item).toMatchObject({ + type: 'contractInteraction', + chainId: SolScope.Mainnet, + data: { + from: 'owner-address', + to: 'spender-address', + }, + }); + }); }); diff --git a/packages/client-utils/src/mappers/keyring-transaction-mapper.ts b/packages/client-utils/src/mappers/keyring-transaction-mapper.ts index 0337f0fa66..789f31fe73 100644 --- a/packages/client-utils/src/mappers/keyring-transaction-mapper.ts +++ b/packages/client-utils/src/mappers/keyring-transaction-mapper.ts @@ -13,6 +13,30 @@ type FungibleAsset = Extract< { fungible: true } >; +/** + * Custom labels for non-EVM transactions. + * + * The labels are used to map the transaction type to the title in the activity list and dialog. + * The labels are defined in the `transaction.details.typeLabel` property. + * For details: {@link https://github.com/MetaMask/metamask-extension/pull/38040} + */ +export enum CustomTransactionTypeLabel { + // Token requires one off approve to receive + TrustlineApprove = 'trustline-approve', + // Token requires revoke the approve to stop receiving + TrustlineDisapprove = 'trustline-disapprove', +} + +function hasTrustlineTypeLabel( + details: KeyringTransaction['details'], +): boolean { + // A flag to indicate if the transaction is a trustline type. + return [ + String(CustomTransactionTypeLabel.TrustlineApprove), + String(CustomTransactionTypeLabel.TrustlineDisapprove), + ].includes(details?.typeLabel ?? ''); +} + function mapStatus(status: KeyringTransaction['status']): Status { switch (status) { case TransactionStatus.Confirmed: @@ -158,6 +182,19 @@ export function mapKeyringTransaction({ case KeyringTransactionType.TokenApprove: { const rawToken = getToken(transaction.from, 'out'); + + if (hasTrustlineTypeLabel(transaction.details)) { + return { + type: 'assetActivation', + ...common, + data: { + from, + token: rawToken ? { ...rawToken, amount: undefined } : rawToken, + fees, + }, + }; + } + const isUnlimited = rawToken?.amount !== undefined && rawToken.amount.split('.')[0].length > approveAmountMaxIntegerDigits; @@ -175,6 +212,32 @@ export function mapKeyringTransaction({ }; } + case KeyringTransactionType.TokenDisapprove: { + if (!hasTrustlineTypeLabel(transaction.details)) { + return { + type: 'contractInteraction', + ...common, + data: { + from, + to, + fees, + }, + }; + } + + const rawToken = getToken(transaction.from, 'out'); + + return { + type: 'assetDeactivation', + ...common, + data: { + from, + token: rawToken ? { ...rawToken, amount: undefined } : rawToken, + fees, + }, + }; + } + default: return { type: 'contractInteraction', diff --git a/packages/client-utils/src/types.ts b/packages/client-utils/src/types.ts index 4408c2518d..1008b4fe60 100644 --- a/packages/client-utils/src/types.ts +++ b/packages/client-utils/src/types.ts @@ -46,7 +46,9 @@ export type ActivityKind = | 'perpsCloseLongTakeProfit' | 'marketShort' | 'stopMarketCloseShort' - | 'marketCloseShort'; + | 'marketCloseShort' + | 'assetActivation' + | 'assetDeactivation'; export type Status = 'pending' | 'success' | 'failed' | 'cancelled'; @@ -89,6 +91,14 @@ export type ActivityItem = fees?: Fee[]; } > + | ActivityData< + 'assetActivation' | 'assetDeactivation', + { + from?: string; + token?: TokenAmount; + fees?: Fee[]; + } + > | ActivityData< 'send' | 'receive', { diff --git a/packages/client-utils/test/fixtures/keyring-transactions.ts b/packages/client-utils/test/fixtures/keyring-transactions.ts index b2de9576d9..d9bc457b2e 100644 --- a/packages/client-utils/test/fixtures/keyring-transactions.ts +++ b/packages/client-utils/test/fixtures/keyring-transactions.ts @@ -5,7 +5,10 @@ import { TransactionType, } from '@metamask/keyring-api'; +import { CustomTransactionTypeLabel } from '../../src/mappers/keyring-transaction-mapper'; + const accountId = '00000000-0000-4000-8000-000000000000'; +const stellarUsdcAsset = `stellar:pubnet/asset:USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN`; export const keyringTransactionFixtures = { addresses: { @@ -289,5 +292,107 @@ export const keyringTransactionFixtures = { events: [], } as never, }, + trustlineApprove: { + transaction: { + id: 'trustline-approve-id', + chain: 'stellar:pubnet', + account: accountId, + status: TransactionStatus.Confirmed, + timestamp: 1716367781, + type: TransactionType.TokenApprove, + details: { + typeLabel: CustomTransactionTypeLabel.TrustlineApprove, + }, + from: [ + { + address: 'owner-address', + asset: { + fungible: true, + type: stellarUsdcAsset, + unit: 'USDC', + amount: '0', + }, + }, + ], + to: [{ address: 'issuer-address', asset: null }], + fees: [], + events: [], + } as never, + }, + trustlineApproveNoAmount: { + transaction: { + id: 'trustline-approve-no-amount-id', + chain: 'stellar:pubnet', + account: accountId, + status: TransactionStatus.Confirmed, + timestamp: 1716367781, + type: TransactionType.TokenApprove, + details: { + typeLabel: CustomTransactionTypeLabel.TrustlineApprove, + }, + from: [{ address: 'owner-address', asset: null }], + to: [{ address: 'issuer-address', asset: null }], + fees: [], + events: [], + } as never, + }, + trustlineDisapprove: { + transaction: { + id: 'trustline-disapprove-id', + chain: 'stellar:pubnet', + account: accountId, + status: TransactionStatus.Confirmed, + timestamp: 1716367781, + type: TransactionType.TokenDisapprove, + details: { + typeLabel: CustomTransactionTypeLabel.TrustlineDisapprove, + }, + from: [ + { + address: 'owner-address', + asset: { + fungible: true, + type: stellarUsdcAsset, + unit: 'USDC', + amount: '0', + }, + }, + ], + to: [{ address: 'issuer-address', asset: null }], + fees: [], + events: [], + } as never, + }, + trustlineDisapproveNoAmount: { + transaction: { + id: 'trustline-disapprove-no-amount-id', + chain: 'stellar:pubnet', + account: accountId, + status: TransactionStatus.Confirmed, + timestamp: 1716367781, + type: TransactionType.TokenDisapprove, + details: { + typeLabel: CustomTransactionTypeLabel.TrustlineDisapprove, + }, + from: [{ address: 'owner-address', asset: null }], + to: [{ address: 'issuer-address', asset: null }], + fees: [], + events: [], + } as never, + }, + disapproveNonTrustline: { + transaction: { + id: 'plain-disapprove-id', + chain: SolScope.Mainnet, + account: accountId, + status: TransactionStatus.Confirmed, + timestamp: 1716367781, + type: TransactionType.TokenDisapprove, + from: [{ address: 'owner-address', asset: null }], + to: [{ address: 'spender-address', asset: null }], + fees: [], + events: [], + } as never, + }, }, };