diff --git a/packages/tron-wallet-snap/CHANGELOG.md b/packages/tron-wallet-snap/CHANGELOG.md index 60a6fa30..807c39d5 100644 --- a/packages/tron-wallet-snap/CHANGELOG.md +++ b/packages/tron-wallet-snap/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Emit `transaction_type` on the transaction lifecycle events, derived from the contract type, so a flow can be attributed by combining `origin` and `transaction_type` ([#399](https://github.com/MetaMask/internal-snaps/pull/399)) + ### Changed - Render the confirmation "Estimated changes" section with the shared `EstimatedChanges` component from `@metamask/snap-networks-utils` ([#396](https://github.com/MetaMask/internal-snaps/pull/396)) diff --git a/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.test.ts b/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.test.ts index 78051532..f79b7112 100644 --- a/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.test.ts +++ b/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.test.ts @@ -1,4 +1,8 @@ -import { FeeType, TrxAccountType } from '@metamask/keyring-api'; +import { + FeeType, + TransactionType, + TrxAccountType, +} from '@metamask/keyring-api'; import type { AnalyticsService, ExtendedKeyringAccount, @@ -481,6 +485,7 @@ describe('ClientRequestHandler', () => { scope, accountIds: [TEST_ACCOUNT_ID], attempt: 0, + transactionType: TransactionType.Unknown, }, duration: TRACK_TX_INTERVAL, }); @@ -1966,6 +1971,7 @@ describe('ClientRequestHandler - signAndSendTransaction', () => { origin: METAMASK_ORIGIN, accountType: 'tron:eoa', chainIdCaip: scope, + transactionType: TransactionType.Unknown, }, ); @@ -1976,6 +1982,7 @@ describe('ClientRequestHandler - signAndSendTransaction', () => { scope, accountIds: [TEST_ACCOUNT_ID], attempt: 0, + transactionType: TransactionType.Unknown, }, duration: TRACK_TX_INTERVAL, }); diff --git a/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts b/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts index 0edce216..898e0459 100644 --- a/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts +++ b/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts @@ -48,6 +48,7 @@ import { TransactionMapper } from '../../services/transactions/TransactionsMappe import type { TransactionsService } from '../../services/transactions/TransactionsService'; import { assertOrThrow } from '../../utils/assertOrThrow'; import { trxToSun } from '../../utils/conversion'; +import { mapRawTransactionType } from '../../utils/transactionType'; import { assertTransactionSignerConsistency, assertTransactionStructure, @@ -417,10 +418,13 @@ export class ClientRequestHandler { * lowercased so it is recognized as MetaMask by the security alerts scan * and stays consistent with the other non-EVM snaps. */ + const transactionType = mapRawTransactionType(rawData); + await this.#analyticsService.trackTransactionSubmitted({ origin: METAMASK_ORIGIN, accountType: account.type, chainIdCaip: scope, + transactionType, }); await this.#snapClient.scheduleBackgroundEvent({ @@ -430,6 +434,7 @@ export class ClientRequestHandler { scope, accountIds: [accountId], attempt: 0, + transactionType, }, duration: TRACK_TX_INTERVAL, }); diff --git a/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.test.tsx b/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.test.tsx index 77f5c1b6..366f5ba2 100644 --- a/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.test.tsx +++ b/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.test.tsx @@ -1,3 +1,4 @@ +import { TransactionType } from '@metamask/keyring-api'; import type { AnalyticsService, IStateManager, @@ -1020,6 +1021,7 @@ describe('CronHandler', () => { scope: Network.Mainnet, accountIds: ACCOUNT_IDS, attempt: 0, + transactionType: TransactionType.Send, }); expect(mockSnapClient.scheduleBackgroundEvent).toHaveBeenCalledWith( @@ -1033,6 +1035,7 @@ describe('CronHandler', () => { origin: METAMASK_ORIGIN, accountType: mockAccount.type, chainIdCaip: Network.Mainnet, + transactionType: TransactionType.Send, }); }, ); diff --git a/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.tsx b/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.tsx index e1385f28..65ab9b81 100644 --- a/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.tsx +++ b/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.tsx @@ -1,3 +1,4 @@ +import type { TransactionType } from '@metamask/keyring-api'; import type { AnalyticsService, ExtendedKeyringAccount, @@ -147,6 +148,7 @@ export class CronHandler { scope: Network; accountIds: string[]; attempt: number; + transactionType?: TransactionType; }, ); break; @@ -661,17 +663,20 @@ export class CronHandler { * @param params.scope - The network scope (e.g., 'mainnet', 'shasta') * @param params.accountIds - Account IDs to sync after confirmation (first account is always the sender) * @param params.attempt - Current attempt number (for retry logic) + * @param params.transactionType - Classification resolved at submit time, carried so the finalized event reports the same flow */ async trackTransaction({ txId, scope, accountIds, attempt = 0, + transactionType, }: { txId: string; scope: Network; accountIds: string[]; attempt: number; + transactionType?: TransactionType; }): Promise { this.#logger.info( `[Attempt ${attempt + 1}] Tracking transaction ${txId} on ${scope}...`, @@ -709,6 +714,7 @@ export class CronHandler { scope, accountIds, attempt: attempt + 1, + ...(transactionType === undefined ? {} : { transactionType }), }, duration: TRACK_TX_INTERVAL, }); @@ -742,6 +748,7 @@ export class CronHandler { origin: METAMASK_ORIGIN, accountType: senderAccount.type, chainIdCaip: scope, + transactionType, }); } catch (error) { this.#logger.error( @@ -766,6 +773,7 @@ export class CronHandler { scope, accountIds, attempt: attempt + 1, + ...(transactionType === undefined ? {} : { transactionType }), }, duration: TRACK_TX_INTERVAL, }); diff --git a/packages/tron-wallet-snap/src/services/confirmation/ConfirmationHandler.test.ts b/packages/tron-wallet-snap/src/services/confirmation/ConfirmationHandler.test.ts index 7415b423..be977be8 100644 --- a/packages/tron-wallet-snap/src/services/confirmation/ConfirmationHandler.test.ts +++ b/packages/tron-wallet-snap/src/services/confirmation/ConfirmationHandler.test.ts @@ -1,4 +1,4 @@ -import { FeeType } from '@metamask/keyring-api'; +import { FeeType, TransactionType } from '@metamask/keyring-api'; import type { AnalyticsService, ExtendedKeyringAccount, @@ -442,6 +442,7 @@ describe('ConfirmationHandler', () => { origin: METAMASK_ORIGIN, accountType: 'tron:eoa', chainIdCaip: Network.Mainnet, + transactionType: TransactionType.Send, }, ); expect( @@ -450,6 +451,7 @@ describe('ConfirmationHandler', () => { origin: METAMASK_ORIGIN, accountType: 'tron:eoa', chainIdCaip: Network.Mainnet, + transactionType: TransactionType.Send, }); expect( mockAnalyticsService.trackTransactionRejected, @@ -470,6 +472,7 @@ describe('ConfirmationHandler', () => { origin: METAMASK_ORIGIN, accountType: 'tron:eoa', chainIdCaip: Network.Mainnet, + transactionType: TransactionType.Send, }); expect( mockAnalyticsService.trackTransactionApproved, @@ -638,6 +641,7 @@ describe('ConfirmationHandler', () => { origin: request.origin, accountType: mockAccount.type, chainIdCaip: Network.Mainnet, + transactionType: TransactionType.Send, }, ); expect( @@ -646,6 +650,7 @@ describe('ConfirmationHandler', () => { origin: request.origin, accountType: mockAccount.type, chainIdCaip: Network.Mainnet, + transactionType: TransactionType.Send, }); expect( mockAnalyticsService.trackTransactionRejected, @@ -679,6 +684,7 @@ describe('ConfirmationHandler', () => { origin: request.origin, accountType: mockAccount.type, chainIdCaip: Network.Mainnet, + transactionType: TransactionType.Send, }); expect( mockAnalyticsService.trackTransactionApproved, @@ -709,6 +715,7 @@ describe('ConfirmationHandler', () => { origin: request.origin, accountType: mockAccount.type, chainIdCaip: Network.Mainnet, + transactionType: TransactionType.Send, }); }); }, diff --git a/packages/tron-wallet-snap/src/services/confirmation/ConfirmationHandler.ts b/packages/tron-wallet-snap/src/services/confirmation/ConfirmationHandler.ts index 74127de6..54ac0c70 100644 --- a/packages/tron-wallet-snap/src/services/confirmation/ConfirmationHandler.ts +++ b/packages/tron-wallet-snap/src/services/confirmation/ConfirmationHandler.ts @@ -1,3 +1,4 @@ +import { TransactionType } from '@metamask/keyring-api'; import type { AnalyticsService, ExtendedKeyringAccount, @@ -25,6 +26,7 @@ import { CONFIRM_SIGN_TRANSACTION_INTERFACE_NAME } from '../../ui/confirmation/v import type { ConfirmSignTransactionContext } from '../../ui/confirmation/views/ConfirmSignTransaction/types'; import { render as renderConfirmTransactionRequest } from '../../ui/confirmation/views/ConfirmTransactionRequest/render'; import { CONFIRM_TRANSACTION_INTERFACE_NAME } from '../../ui/confirmation/views/ConfirmTransactionRequest/types'; +import { mapRawTransactionType } from '../../utils/transactionType'; import { SignTransactionRequestStruct } from '../../validation/structs'; import type { TronWalletKeyringRequest } from '../../validation/structs'; import { assertTransactionStructure } from '../../validation/transaction'; @@ -148,6 +150,7 @@ export class ConfirmationHandler { origin: request.origin, accountType: account.type, chainIdCaip: scope, + transactionType: mapRawTransactionType(rawData), }; await this.#analyticsService.trackTransactionAdded(trackingProperties); @@ -194,6 +197,11 @@ export class ConfirmationHandler { origin, accountType, chainIdCaip: scope, + /** + * This confirmation is only reached from the unified send flow, so the + * operation is a send regardless of the underlying contract type. + */ + transactionType: TransactionType.Send, }; // Track Transaction Added event diff --git a/packages/tron-wallet-snap/src/services/send/SendService.test.ts b/packages/tron-wallet-snap/src/services/send/SendService.test.ts index dc815d8b..fdd3a000 100644 --- a/packages/tron-wallet-snap/src/services/send/SendService.test.ts +++ b/packages/tron-wallet-snap/src/services/send/SendService.test.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { FeeType } from '@metamask/keyring-api'; +import { FeeType, TransactionType } from '@metamask/keyring-api'; import type { AnalyticsService } from '@metamask/snap-networks-utils'; import { BigNumber } from 'bignumber.js'; import { TronWeb } from 'tronweb'; @@ -168,6 +168,7 @@ describe('SendService', () => { origin: METAMASK_ORIGIN, accountType: 'tron:eoa', chainIdCaip: Network.Mainnet, + transactionType: TransactionType.Send, }); expect(mockSnapClient.scheduleBackgroundEvent).toHaveBeenCalledWith({ method: BackgroundEventMethod.TrackTransaction, @@ -176,6 +177,7 @@ describe('SendService', () => { scope: Network.Mainnet, accountIds: [TEST_ACCOUNT_ID], attempt: 0, + transactionType: TransactionType.Send, }, duration: TRACK_TX_INTERVAL, }); diff --git a/packages/tron-wallet-snap/src/services/send/SendService.ts b/packages/tron-wallet-snap/src/services/send/SendService.ts index eae3ea60..e8c5eb8c 100644 --- a/packages/tron-wallet-snap/src/services/send/SendService.ts +++ b/packages/tron-wallet-snap/src/services/send/SendService.ts @@ -16,6 +16,7 @@ import type { AssetEntity } from '../../entities/assets'; import { SendErrorCodes } from '../../handlers/clientRequest/types'; import { BackgroundEventMethod } from '../../handlers/cronjob/cronjob'; import { toRawAmount, trxToSun } from '../../utils/conversion'; +import { mapRawTransactionType } from '../../utils/transactionType'; import { assertTransactionSignerConsistency } from '../../validation/transaction'; import type { AccountsService } from '../accounts/AccountsService'; import type { AssetsService } from '../assets/AssetsService'; @@ -420,6 +421,13 @@ export class SendService { transaction, }); + /** + * Resolve the classification before signing so the submitted event and the + * background tracker both carry it. A broadcast transaction has no + * account-balance context, so it is derived from the contract type. + */ + const transactionType = mapRawTransactionType(freshTransaction.raw_data); + /** * Sign and send the transaction atomically after user confirmation */ @@ -434,6 +442,7 @@ export class SendService { origin, accountType: account.type, chainIdCaip: scope, + transactionType, }); await this.#snapClient.scheduleBackgroundEvent({ @@ -443,6 +452,7 @@ export class SendService { scope, accountIds: [fromAccountId], attempt: 0, + transactionType, }, duration: TRACK_TX_INTERVAL, }); diff --git a/packages/tron-wallet-snap/src/utils/transactionType.test.ts b/packages/tron-wallet-snap/src/utils/transactionType.test.ts new file mode 100644 index 00000000..d4b82d94 --- /dev/null +++ b/packages/tron-wallet-snap/src/utils/transactionType.test.ts @@ -0,0 +1,73 @@ +import { TransactionType } from '@metamask/keyring-api'; +import { Types as TronwebTypes } from 'tronweb'; + +import { mapRawTransactionType } from './transactionType'; + +/** + * Builds raw transaction data carrying a single contract of the given type. + * + * @param type - The Tron contract type. + * @returns Raw transaction data. + */ +function buildRawData( + type: TronwebTypes.ContractType, +): TronwebTypes.Transaction['raw_data'] { + return { + contract: [ + { + parameter: { + value: {}, + type_url: 'type.googleapis.com/protocol.Transaction.Contract', + }, + type, + }, + ], + ref_block_bytes: '', + ref_block_hash: '', + expiration: 0, + timestamp: 0, + } as TronwebTypes.Transaction['raw_data']; +} + +describe('mapRawTransactionType', () => { + it.each([ + [TronwebTypes.ContractType.TransferContract, TransactionType.Send], + [TronwebTypes.ContractType.TransferAssetContract, TransactionType.Send], + [ + TronwebTypes.ContractType.FreezeBalanceContract, + TransactionType.StakeDeposit, + ], + [ + TronwebTypes.ContractType.FreezeBalanceV2Contract, + TransactionType.StakeDeposit, + ], + [ + TronwebTypes.ContractType.UnfreezeBalanceContract, + TransactionType.StakeWithdraw, + ], + [ + TronwebTypes.ContractType.UnfreezeBalanceV2Contract, + TransactionType.StakeWithdraw, + ], + [ + TronwebTypes.ContractType.WithdrawExpireUnfreezeContract, + TransactionType.StakeWithdraw, + ], + [TronwebTypes.ContractType.TriggerSmartContract, TransactionType.Unknown], + [TronwebTypes.ContractType.VoteWitnessContract, TransactionType.Unknown], + ])('maps %s to %s', (contractType, expected) => { + expect(mapRawTransactionType(buildRawData(contractType))).toBe(expected); + }); + + it('falls back to unknown when the raw data is missing', () => { + expect(mapRawTransactionType(undefined)).toBe(TransactionType.Unknown); + }); + + it('falls back to unknown when there is no contract', () => { + expect( + mapRawTransactionType({ + contract: [], + } as unknown as TronwebTypes.Transaction['raw_data']), + ).toBe(TransactionType.Unknown); + }); +}); diff --git a/packages/tron-wallet-snap/src/utils/transactionType.ts b/packages/tron-wallet-snap/src/utils/transactionType.ts new file mode 100644 index 00000000..458e9943 --- /dev/null +++ b/packages/tron-wallet-snap/src/utils/transactionType.ts @@ -0,0 +1,36 @@ +import { TransactionType } from '@metamask/keyring-api'; +import { Types as TronwebTypes } from 'tronweb'; + +/** + * Classifies an unsigned or broadcast Tron transaction from its raw data. + * + * The contract type is the only classification signal available before a + * transaction is confirmed: the account-balance comparison that + * `TransactionMapper` performs for confirmed transactions is not possible here, + * and a smart-contract call carries no ABI at this stage. Every contract that + * the Snap itself builds for a transfer or a staking operation is still + * identifiable, and the rest fall back to `unknown` rather than guessing. + * + * @param rawData - The transaction raw data, or `undefined` when unavailable. + * @returns The keyring-api transaction type. + */ +export function mapRawTransactionType( + rawData: TronwebTypes.Transaction['raw_data'] | undefined, +): TransactionType { + const contractType = rawData?.contract?.[0]?.type; + + switch (contractType) { + case TronwebTypes.ContractType.TransferContract: + case TronwebTypes.ContractType.TransferAssetContract: + return TransactionType.Send; + case TronwebTypes.ContractType.FreezeBalanceContract: + case TronwebTypes.ContractType.FreezeBalanceV2Contract: + return TransactionType.StakeDeposit; + case TronwebTypes.ContractType.UnfreezeBalanceContract: + case TronwebTypes.ContractType.UnfreezeBalanceV2Contract: + case TronwebTypes.ContractType.WithdrawExpireUnfreezeContract: + return TransactionType.StakeWithdraw; + default: + return TransactionType.Unknown; + } +}