Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions packages/client-utils/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `mapKeyringTransaction` for mapping keyring transactions to activity items
- `mapLocalTransaction` for mapping local transaction groups to activity items
- Shared activity types (`ActivityItem`, `ActivityKind`, `Status`, etc.)
- Map `assetActivation` and `assetDeactivation` activity types in transaction activity mappers ([#9440](https://github.com/MetaMask/core/pull/9440))

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
});
});
});
63 changes: 63 additions & 0 deletions packages/client-utils/src/mappers/keyring-transaction-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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;
Expand All @@ -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',
Expand Down
12 changes: 11 additions & 1 deletion packages/client-utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ export type ActivityKind =
| 'perpsCloseLongTakeProfit'
| 'marketShort'
| 'stopMarketCloseShort'
| 'marketCloseShort';
| 'marketCloseShort'
| 'assetActivation'
| 'assetDeactivation';

export type Status = 'pending' | 'success' | 'failed' | 'cancelled';

Expand Down Expand Up @@ -89,6 +91,14 @@ export type ActivityItem =
fees?: Fee[];
}
>
| ActivityData<
'assetActivation' | 'assetDeactivation',
{
from?: string;
token?: TokenAmount;
fees?: Fee[];
}
>
| ActivityData<
'send' | 'receive',
{
Expand Down
105 changes: 105 additions & 0 deletions packages/client-utils/test/fixtures/keyring-transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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,
},
},
};
Loading