From e901ab6016c23713359027148700ce2e8951a0cd Mon Sep 17 00:00:00 2001 From: Frederic HENG Date: Thu, 1 Oct 2026 10:47:26 +0200 Subject: [PATCH 1/3] feat(tron): populate transaction_type on lifecycle events Transaction lifecycle events could not be attributed to a flow: the payload carried `origin` but no classification, so an in-app send, a dApp `signTransaction`, and a staking operation were indistinguishable. Classify each event from the contract type with a new `mapRawTransactionType` helper, reusing the `@metamask/keyring-api` `TransactionType` vocabulary: - `ConfirmationHandler` reports the contract type for dApp `signTransaction` confirmations, and `send` for the unified send confirmation, which is only reachable from the send flow. - `SendService` and the Wallet Standard `signAndSendTransaction` report the type of the transaction they broadcast. - `CronHandler` reports the type resolved at submit time, carried through the background event, because `gettransactioninfobyid` does not return `raw_data` to re-derive it from. A smart-contract call is reported as `unknown`: it cannot be classified before confirmation without an ABI. --- packages/tron-wallet-snap/CHANGELOG.md | 4 + .../clientRequest/clientRequest.test.ts | 9 ++- .../handlers/clientRequest/clientRequest.ts | 5 ++ .../src/handlers/cronjob/cronjob.test.tsx | 3 + .../src/handlers/cronjob/cronjob.tsx | 7 ++ .../confirmation/ConfirmationHandler.test.ts | 9 ++- .../confirmation/ConfirmationHandler.ts | 8 ++ .../src/services/send/SendService.test.ts | 4 +- .../src/services/send/SendService.ts | 10 +++ .../src/utils/transactionType.test.ts | 73 +++++++++++++++++++ .../src/utils/transactionType.ts | 36 +++++++++ 11 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 packages/tron-wallet-snap/src/utils/transactionType.test.ts create mode 100644 packages/tron-wallet-snap/src/utils/transactionType.ts diff --git a/packages/tron-wallet-snap/CHANGELOG.md b/packages/tron-wallet-snap/CHANGELOG.md index 60a6fa30..97bb96d0 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` ([#393](https://github.com/MetaMask/internal-snaps/pull/393)) + ### 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..11a92a6d 100644 --- a/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.tsx +++ b/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.tsx @@ -147,6 +147,7 @@ export class CronHandler { scope: Network; accountIds: string[]; attempt: number; + transactionType?: string; }, ); break; @@ -661,17 +662,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?: string; }): Promise { this.#logger.info( `[Attempt ${attempt + 1}] Tracking transaction ${txId} on ${scope}...`, @@ -709,6 +713,7 @@ export class CronHandler { scope, accountIds, attempt: attempt + 1, + ...(transactionType === undefined ? {} : { transactionType }), }, duration: TRACK_TX_INTERVAL, }); @@ -742,6 +747,7 @@ export class CronHandler { origin: METAMASK_ORIGIN, accountType: senderAccount.type, chainIdCaip: scope, + transactionType, }); } catch (error) { this.#logger.error( @@ -766,6 +772,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; + } +} From 8f86c24d11c350e1067385635435870ce458acc3 Mon Sep 17 00:00:00 2001 From: Frederic HENG Date: Thu, 1 Oct 2026 11:03:16 +0200 Subject: [PATCH 2/3] chore(tron): point the CHANGELOG entry at this PR The entry referenced the stack base (#393) because the branch had no PR number yet. Use the actual PR. --- packages/tron-wallet-snap/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tron-wallet-snap/CHANGELOG.md b/packages/tron-wallet-snap/CHANGELOG.md index 97bb96d0..807c39d5 100644 --- a/packages/tron-wallet-snap/CHANGELOG.md +++ b/packages/tron-wallet-snap/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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` ([#393](https://github.com/MetaMask/internal-snaps/pull/393)) +- 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 From 4953702560c99f3fd48918d56232b96455d50d6c Mon Sep 17 00:00:00 2001 From: Frederic HENG Date: Thu, 1 Oct 2026 16:21:49 +0200 Subject: [PATCH 3/3] fix(tron): type the carried transaction classification The cron job stored the submit-time classification as a string, which no longer matches the lifecycle event property. --- packages/tron-wallet-snap/src/handlers/cronjob/cronjob.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.tsx b/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.tsx index 11a92a6d..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,7 +148,7 @@ export class CronHandler { scope: Network; accountIds: string[]; attempt: number; - transactionType?: string; + transactionType?: TransactionType; }, ); break; @@ -675,7 +676,7 @@ export class CronHandler { scope: Network; accountIds: string[]; attempt: number; - transactionType?: string; + transactionType?: TransactionType; }): Promise { this.#logger.info( `[Attempt ${attempt + 1}] Tracking transaction ${txId} on ${scope}...`,