From 31ad607fa5776b9e0f3ffe1a7f1a601369db62ff Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Wed, 30 Sep 2026 16:22:12 +0100 Subject: [PATCH 1/7] refactor(tron-wallet-snap): assert supported networks at handler boundaries instead of casting --- packages/tron-wallet-snap/jest.config.js | 11 ++-- packages/tron-wallet-snap/snap.manifest.json | 2 +- .../tron-wallet-snap/src/constants/index.ts | 14 ++++ .../clientRequest/clientRequest.test.ts | 64 ++++++++++++++++++- .../handlers/clientRequest/clientRequest.ts | 9 +-- .../src/handlers/keyring/keyring.test.ts | 36 +++++++++++ .../src/handlers/keyring/keyring.ts | 7 +- .../src/validation/structs.ts | 4 ++ 8 files changed, 135 insertions(+), 12 deletions(-) diff --git a/packages/tron-wallet-snap/jest.config.js b/packages/tron-wallet-snap/jest.config.js index 947d0368e..aa1cf8010 100644 --- a/packages/tron-wallet-snap/jest.config.js +++ b/packages/tron-wallet-snap/jest.config.js @@ -16,13 +16,16 @@ module.exports = { '/jest.setup.ts', ], + // Measure index.ts barrels (e.g. src/constants/index.ts) so SonarCloud new-code coverage sees them; the shared base config's blanket index.ts exclusion zeroes them in lcov. + coveragePathIgnorePatterns: ['jest\\.setup\\.ts$'], + // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 72.49, - functions: 79.91, - lines: 85.79, - statements: 85.79, +branches: 72.66, + functions: 80.17, + lines: 86.15, + statements: 86.15, }, }, }; diff --git a/packages/tron-wallet-snap/snap.manifest.json b/packages/tron-wallet-snap/snap.manifest.json index 770eb56b0..d41d5e22a 100644 --- a/packages/tron-wallet-snap/snap.manifest.json +++ b/packages/tron-wallet-snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/internal-snaps.git" }, "source": { - "shasum": "1IlkNeNaW28YQoE0wK3OdDJctxoEBDqK6cxLQ0GyifY=", +"shasum": "1IlkNeNaW28YQoE0wK3OdDJctxoEBDqK6cxLQ0GyifY=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/tron-wallet-snap/src/constants/index.ts b/packages/tron-wallet-snap/src/constants/index.ts index b288f05be..37ebc57f7 100644 --- a/packages/tron-wallet-snap/src/constants/index.ts +++ b/packages/tron-wallet-snap/src/constants/index.ts @@ -1,3 +1,4 @@ +import { InvalidParamsError } from '@metamask/snaps-sdk'; import { BigNumber } from 'bignumber.js'; export const ZERO = BigNumber(0); @@ -66,6 +67,19 @@ export const Network = { export type Network = (typeof Network)[keyof typeof Network]; +const NETWORK_SCOPES: readonly string[] = Object.values(Network); + +export const isSupportedNetwork = (scope: string): scope is Network => + NETWORK_SCOPES.includes(scope); + +export const assertSupportedNetwork = (scope: string): Network => { + if (!isSupportedNetwork(scope)) { + throw new InvalidParamsError(`Unsupported network: "${scope}"`) as Error; + } + + return scope; +}; + export const KnownCaip19Id = { TrxMainnet: `${Network.Mainnet}/slip44:195`, TrxNile: `${Network.Nile}/slip44:195`, 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 f79b71121..27199e199 100644 --- a/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.test.ts +++ b/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.test.ts @@ -1869,7 +1869,9 @@ describe('ClientRequestHandler - signAndSendTransaction', () => { await expect( clientRequestHandler.handle(request as JsonRpcRequest), ).rejects.toThrow( - `Transaction owner_address (${TronWeb.address.fromHex(WRONG_OWNER_ADDRESS_HEX)}) does not match derived signer address (${CORRECT_OWNER_ADDRESS_BASE58})`, + `Transaction owner_address (${TronWeb.address.fromHex( + WRONG_OWNER_ADDRESS_HEX, + )}) does not match derived signer address (${CORRECT_OWNER_ADDRESS_BASE58})`, ); }); @@ -2124,6 +2126,30 @@ describe('ClientRequestHandler - onAmountInput', () => { ); }); + it('returns invalid for an asset from a non-Tron chain', async () => { + await withClientRequestHandler(async ({ handler, mockAccountsService }) => { + mockAccountsService.findById.mockResolvedValue(mockAccount); + + const request: OnAmountInputRequest = { + jsonrpc: '2.0' as const, + id: '1', + method: ClientRequestMethod.OnAmountInput, + params: { + accountId: TEST_ACCOUNT_ID, + assetId: 'eip155:1/slip44:60', + value: '10', + }, + }; + + const result = await handler.handle(request); + + expect(result).toStrictEqual({ + valid: false, + errors: [{ code: SendErrorCodes.Invalid }], + }); + }); + }); + it('uses provided toAddress when building the transaction for fee estimation', async () => { await withClientRequestHandler( async ({ @@ -2943,6 +2969,24 @@ describe('ClientRequestHandler - claimUnstakedTrx', () => { ); }); + it('throws InvalidParamsError for a non-Tron asset chain', async () => { + await withClientRequestHandler(async ({ handler }) => { + const request = { + jsonrpc: '2.0' as const, + id: '1', + method: ClientRequestMethod.ClaimUnstakedTrx, + params: { + fromAccountId: TEST_ACCOUNT_ID, + assetId: 'eip155:1/slip44:60', + }, + }; + + await expect(handler.handle(request)).rejects.toThrow( + 'Invalid method parameter(s)', + ); + }); + }); + it('throws when user rejects the confirmation', async () => { await withClientRequestHandler( async ({ @@ -3049,6 +3093,24 @@ describe('ClientRequestHandler - claimTrxStakingRewards', () => { ); }); }); + + it('throws InvalidParamsError for a non-Tron asset chain', async () => { + await withClientRequestHandler(async ({ handler }) => { + const request = { + jsonrpc: '2.0' as const, + id: '1', + method: 'claimTrxStakingRewards', + params: { + fromAccountId: TEST_ACCOUNT_ID, + assetId: 'eip155:1/slip44:60', + }, + }; + + await expect(handler.handle(request)).rejects.toThrow( + 'Invalid method parameter(s)', + ); + }); + }); }); describe('ClientRequestHandler - onAddressInput', () => { diff --git a/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts b/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts index 898e04599..c9f958de1 100644 --- a/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts +++ b/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts @@ -24,6 +24,7 @@ import type { TronWeb, Types as TronwebTypes } from 'tronweb'; import type { SnapClient } from '../../clients/snap/SnapClient'; import type { TronWebFactory } from '../../clients/tronweb/TronWebFactory'; import { + assertSupportedNetwork, FALLBACK_FEE, FEE_LIMIT, METAMASK_ORIGIN, @@ -498,7 +499,7 @@ export class ClientRequestHandler { * Check if we have enough of the asset we want to send... */ const { chainId } = parseCaipAssetType(assetId); - const scope = chainId as Network; + const scope = assertSupportedNetwork(chainId); const [asset, nativeTokenAsset, bandwidthAsset, energyAsset] = await this.#assetsService.getAccountAssetsByIDs(accountId, [ @@ -627,7 +628,7 @@ export class ClientRequestHandler { } const { chainId } = parseCaipAssetType(assetId); - const scope = chainId as Network; + const scope = assertSupportedNetwork(chainId); const amountBN = new BigNumber(amount); @@ -1154,7 +1155,7 @@ export class ClientRequestHandler { const account = await this.#accountsService.findByIdOrThrow(fromAccountId); const { chainId } = parseCaipAssetType(assetId); - const scope = chainId as Network; + const scope = assertSupportedNetwork(chainId); const confirmed = await this.#confirmationHandler.confirmClaimUnstakedTrx({ account, @@ -1192,7 +1193,7 @@ export class ClientRequestHandler { const account = await this.#accountsService.findByIdOrThrow(fromAccountId); const { chainId } = parseCaipAssetType(assetId); - const scope = chainId as Network; + const scope = assertSupportedNetwork(chainId); await this.#stakingService.claimTrxStakingRewards({ account, scope }); diff --git a/packages/tron-wallet-snap/src/handlers/keyring/keyring.test.ts b/packages/tron-wallet-snap/src/handlers/keyring/keyring.test.ts index ba66a5481..f37aed5d3 100644 --- a/packages/tron-wallet-snap/src/handlers/keyring/keyring.test.ts +++ b/packages/tron-wallet-snap/src/handlers/keyring/keyring.test.ts @@ -5,10 +5,12 @@ import type { } from '@metamask/keyring-api'; import type { ExportAccountOptions } from '@metamask/keyring-api/v2'; import type { ExtendedKeyringAccount } from '@metamask/snap-networks-utils'; +import type { JsonRpcRequest } from '@metamask/snaps-sdk'; import { InvalidParamsError, UserRejectedRequestError, } from '@metamask/snaps-sdk'; +import type { CaipChainId } from '@metamask/utils'; import type { SnapClient } from '../../clients/snap/SnapClient'; import { Network } from '../../constants'; @@ -94,6 +96,7 @@ describe('KeyringHandler', () => { handleKeyringRequest: jest .fn() .mockResolvedValue({ signature: '0xsignature123' }), + resolveAccountAddress: jest.fn().mockResolvedValue('tron:TB...caip10'), } as unknown as jest.Mocked; mockConfirmationHandler = { handleKeyringRequest: jest.fn().mockResolvedValue(true), @@ -779,4 +782,37 @@ describe('KeyringHandler', () => { ).rejects.toThrow('Error exporting account'); }); }); + + describe('resolveAccountAddress', () => { + const request = { + jsonrpc: '2.0' as const, + id: '1', + method: 'resolveAccountAddress', + params: [], + } as unknown as JsonRpcRequest; + + it('resolves the account address for a supported scope', async () => { + const result = await keyringHandler.resolveAccountAddress( + Network.Mainnet, + request, + ); + + expect(result).toBe('tron:TB...caip10'); + expect(mockWalletService.resolveAccountAddress).toHaveBeenCalledWith( + [mockAccount], + Network.Mainnet, + request, + ); + }); + + it('throws InvalidParamsError for a scope not controlled by the snap', async () => { + await expect( + keyringHandler.resolveAccountAddress( + 'eip155:1' as CaipChainId, + request, + ), + ).rejects.toThrow(InvalidParamsError); + expect(mockWalletService.resolveAccountAddress).not.toHaveBeenCalled(); + }); + }); }); diff --git a/packages/tron-wallet-snap/src/handlers/keyring/keyring.ts b/packages/tron-wallet-snap/src/handlers/keyring/keyring.ts index 01507b548..323d0ab6e 100644 --- a/packages/tron-wallet-snap/src/handlers/keyring/keyring.ts +++ b/packages/tron-wallet-snap/src/handlers/keyring/keyring.ts @@ -39,7 +39,6 @@ import { sortBy } from 'lodash'; import type { SnapClient } from '../../clients/snap/SnapClient'; import { ESSENTIAL_ASSETS } from '../../constants'; -import type { Network } from '../../constants'; import { originPermissions } from '../../permissions'; import type { AccountsService } from '../../services/accounts/AccountsService'; import type { AssetsService } from '../../services/assets/AssetsService'; @@ -56,6 +55,7 @@ import { ListAccountAssetsStruct, ListAccountTransactionsStruct, PrivateKeyHexStruct, + ResolveAccountAddressStruct, SignTransactionRequestStruct, TronKeyringRequestStruct, } from '../../validation/structs'; @@ -321,6 +321,9 @@ export class KeyringHandler implements KeyringSnapRpc { scope: CaipChainId, request: JsonRpcRequest, ): Promise { + const params = { scope }; + validateRequest(params, ResolveAccountAddressStruct); + this.#logger.info('Resolving account address', { scope, request }); // Get all keyring accounts @@ -329,7 +332,7 @@ export class KeyringHandler implements KeyringSnapRpc { // Resolve the address using the wallet service const caip10Address = await this.#walletService.resolveAccountAddress( keyringAccounts, - scope as Network, + params.scope, request, ); diff --git a/packages/tron-wallet-snap/src/validation/structs.ts b/packages/tron-wallet-snap/src/validation/structs.ts index adb05c4ec..e14d97168 100644 --- a/packages/tron-wallet-snap/src/validation/structs.ts +++ b/packages/tron-wallet-snap/src/validation/structs.ts @@ -72,6 +72,10 @@ export const ExportAccountRequestStruct = object({ export const NetworkStruct = enums(Object.values(Network)); +export const ResolveAccountAddressStruct = object({ + scope: NetworkStruct, +}); + /** * Validates createAccount options. * - entropySource: Optional string for the entropy source (UUID or ULID format) From f803d0b23fc61882b6a412d14b5627494fd4fe95 Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Thu, 1 Oct 2026 15:13:45 +0100 Subject: [PATCH 2/7] refactor(tron-wallet-snap): remove remaining Network and scope casts - Assert supported networks where data enters the snap: manifest scopes, AssetsController assets, Token API filtering and TrackTransaction background event params. - Carry the validated `Network` type instead of re-casting: sign renderers take `TronWalletKeyringRequest`, send uses `asset.network`, staking and zero-balance assets use `getAssetNetwork`. - Add `TronKeyringAccount` with `scopes: Network[]` for stored accounts; it remains assignable to the emitted `KeyringAccount`. - Build CAIP asset types from `Network` instead of `TrxScope`. - Add a lint rule banning casts to `Network` or `TrxScope` in the Tron snap. --- eslint.config.mjs | 43 +++++++++-- packages/tron-wallet-snap/CHANGELOG.md | 3 + packages/tron-wallet-snap/jest.config.js | 8 +- packages/tron-wallet-snap/snap.manifest.json | 2 +- .../src/clients/token-api/TokenApiClient.ts | 15 +++- .../src/clients/tron-http/TronHttpClient.ts | 7 +- .../src/clients/trongrid/TrongridApiClient.ts | 7 +- .../src/entities/keyringAccount.ts | 11 +++ .../clientRequest/clientRequest.test.ts | 18 ++--- .../handlers/clientRequest/clientRequest.ts | 19 ++--- .../src/handlers/cronjob/cronjob.test.tsx | 49 ++++++++++++ .../src/handlers/cronjob/cronjob.tsx | 13 +--- .../src/handlers/keyring/keyring.test.ts | 12 +-- .../src/handlers/keyring/keyring.ts | 12 ++- .../accounts/AccountsRepository.test.ts | 8 +- .../services/accounts/AccountsRepository.ts | 34 ++++----- .../services/accounts/AccountsService.test.ts | 74 +++++++++---------- .../src/services/accounts/AccountsService.ts | 48 ++++++------ .../assets/adapters/SnapAssetsAdapter.ts | 5 +- .../src/services/assets/types.ts | 21 +++--- .../assets/utils/mapControllerAsset.test.ts | 15 ++++ .../assets/utils/mapControllerAsset.ts | 5 +- .../src/services/config/ConfigProvider.ts | 2 +- .../src/services/send/SendService.ts | 13 ++-- .../src/services/staking/StakingService.ts | 18 +++-- .../src/services/state/stateTypes.ts | 4 +- .../src/services/wallet/WalletService.test.ts | 4 +- .../src/services/wallet/WalletService.ts | 14 ++-- .../views/ConfirmSignMessage/render.test.tsx | 16 ++-- .../views/ConfirmSignMessage/render.tsx | 7 +- .../ConfirmSignTransaction/render.test.tsx | 27 +++---- .../views/ConfirmSignTransaction/render.tsx | 23 +++--- .../src/utils/getAssetNetwork.test.ts | 20 +++++ .../src/utils/getAssetNetwork.ts | 17 +++++ .../src/validation/structs.ts | 9 +++ 35 files changed, 380 insertions(+), 223 deletions(-) create mode 100644 packages/tron-wallet-snap/src/entities/keyringAccount.ts create mode 100644 packages/tron-wallet-snap/src/utils/getAssetNetwork.test.ts create mode 100644 packages/tron-wallet-snap/src/utils/getAssetNetwork.ts diff --git a/eslint.config.mjs b/eslint.config.mjs index 5b0b91a48..f354d271b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -43,6 +43,27 @@ const NO_MESSENGER_ACTIONS_IN_CONSTRUCTORS_SELECTOR_OBJECTS = [ }, ]; +/** + * Argument to the `no-restricted-syntax` rule that prohibits enums. + */ +const NO_ENUMS_SELECTOR_OBJECT = { + selector: 'TSEnumDeclaration', + message: + "Don't use enums. There are a number of reasons why they are problematic, but the most important is that TypeScript treats them nominally, not structurally, and this can cause unexpected breaking changes. Instead, use an object + type, an array + type, or just a type. Learn more here: https://github.com/MetaMask/eslint-config/issues/417", +}; + +/** + * Arguments to the `no-restricted-syntax` rule that prevent the Tron snap from + * casting values to its supported network types. + */ +const TRON_NETWORK_TYPE_NAMES = '/^(Network|TrxScope)$/u'; +const NO_TRON_NETWORK_CASTS_MESSAGE = + 'Do not cast to `Network` or `TrxScope`. Narrow the value with `assertSupportedNetwork` or `isSupportedNetwork`, or carry the `Network` type from where it was validated.'; +const NO_TRON_NETWORK_CASTS_SELECTOR_OBJECTS = [ + `TSAsExpression > TSTypeReference.typeAnnotation[typeName.name=${TRON_NETWORK_TYPE_NAMES}]`, + `TSAsExpression > .typeAnnotation TSTypeReference[typeName.name=${TRON_NETWORK_TYPE_NAMES}]`, +].map((selector) => ({ selector, message: NO_TRON_NETWORK_CASTS_MESSAGE })); + /** * Collects all options for a given array-valued rule across one or more flat * config arrays, excluding the leading severity element. @@ -162,11 +183,7 @@ const config = createConfig([ typescript, ]), ...NO_CONTROLLER_STATE_CHANGE_SELECTOR_OBJECTS, - { - selector: 'TSEnumDeclaration', - message: - "Don't use enums. There are a number of reasons why they are problematic, but the most important is that TypeScript treats them nominally, not structurally, and this can cause unexpected breaking changes. Instead, use an object + type, an array + type, or just a type. Learn more here: https://github.com/MetaMask/eslint-config/issues/417", - }, + NO_ENUMS_SELECTOR_OBJECT, ], }, }, @@ -256,6 +273,22 @@ const config = createConfig([ ], }, }, + { + files: ['packages/tron-wallet-snap/src/**/*.{ts,tsx}'], + rules: { + 'no-restricted-syntax': [ + 'error', + ...collectExistingRuleOptions('no-restricted-syntax', [ + base, + typescript, + ]), + ...NO_CONTROLLER_STATE_CHANGE_SELECTOR_OBJECTS, + ...NO_MESSENGER_ACTIONS_IN_CONSTRUCTORS_SELECTOR_OBJECTS, + NO_ENUMS_SELECTOR_OBJECT, + ...NO_TRON_NETWORK_CASTS_SELECTOR_OBJECTS, + ], + }, + }, { // Prohibit exporting *AllowedActions, *AllowedEvents, and *MethodActions // from package index files diff --git a/packages/tron-wallet-snap/CHANGELOG.md b/packages/tron-wallet-snap/CHANGELOG.md index 807c39d53..eca9a4b9a 100644 --- a/packages/tron-wallet-snap/CHANGELOG.md +++ b/packages/tron-wallet-snap/CHANGELOG.md @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Reject scopes that are not supported Tron networks instead of accepting them unchecked ([#394](https://github.com/MetaMask/internal-snaps/pull/394)) + - `onAmountInput`, `confirmSend`, `claimUnstakedTrx`, `claimTrxStakingRewards` and `resolveAccountAddress` now throw an `InvalidParamsError` for an asset or scope on an unsupported chain. + - Assets returned by the AssetsController on an unsupported chain now throw instead of being mapped. - 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)) - Estimated changes from the previous scan now stay visible while a confirmation re-scans, instead of showing a loading skeleton diff --git a/packages/tron-wallet-snap/jest.config.js b/packages/tron-wallet-snap/jest.config.js index aa1cf8010..846c1cea8 100644 --- a/packages/tron-wallet-snap/jest.config.js +++ b/packages/tron-wallet-snap/jest.config.js @@ -22,10 +22,10 @@ module.exports = { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { -branches: 72.66, - functions: 80.17, - lines: 86.15, - statements: 86.15, + branches: 72.94, + functions: 80.38, + lines: 86.29, + statements: 86.28, }, }, }; diff --git a/packages/tron-wallet-snap/snap.manifest.json b/packages/tron-wallet-snap/snap.manifest.json index d41d5e22a..4922a9d5a 100644 --- a/packages/tron-wallet-snap/snap.manifest.json +++ b/packages/tron-wallet-snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/internal-snaps.git" }, "source": { -"shasum": "1IlkNeNaW28YQoE0wK3OdDJctxoEBDqK6cxLQ0GyifY=", + "shasum": "dhCsUtl4QWsR6OfdTycMFQoDNQIPXQHTXQdTch9hjDM=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/tron-wallet-snap/src/clients/token-api/TokenApiClient.ts b/packages/tron-wallet-snap/src/clients/token-api/TokenApiClient.ts index 9d4119803..5399901aa 100644 --- a/packages/tron-wallet-snap/src/clients/token-api/TokenApiClient.ts +++ b/packages/tron-wallet-snap/src/clients/token-api/TokenApiClient.ts @@ -5,7 +5,11 @@ import { array, assert } from '@metamask/superstruct'; import type { Infer } from '@metamask/superstruct'; import { CaipAssetTypeStruct, parseCaipAssetType } from '@metamask/utils'; -import { Network, SNAP_OWNED_ASSETS } from '../../constants'; +import { + isSupportedNetwork, + Network, + SNAP_OWNED_ASSETS, +} from '../../constants'; import type { TokenCaipAssetType } from '../../services/assets/types'; import { TokenCaipAssetTypeStruct } from '../../services/assets/types'; import type { ConfigProvider } from '../../services/config'; @@ -97,12 +101,17 @@ export class TokenApiClient { return false; } const { chainId } = parseCaipAssetType(assetType); - return TokenApiClient.supportedNetworks.includes(chainId as Network); + return ( + isSupportedNetwork(chainId) && + TokenApiClient.supportedNetworks.includes(chainId) + ); }); if (supportedAssetTypes.length !== assetTypes.length) { this.#logger.warn( - `[TokenApiClient] Received some asset types that are either not supported by the Token API or are excluded resource/staked tokens. They will be ignored. Supported networks: ${TokenApiClient.supportedNetworks.join(', ')}`, + `[TokenApiClient] Received some asset types that are either not supported by the Token API or are excluded resource/staked tokens. They will be ignored. Supported networks: ${TokenApiClient.supportedNetworks.join( + ', ', + )}`, ); } diff --git a/packages/tron-wallet-snap/src/clients/tron-http/TronHttpClient.ts b/packages/tron-wallet-snap/src/clients/tron-http/TronHttpClient.ts index 7a4bfbed3..0057cba31 100644 --- a/packages/tron-wallet-snap/src/clients/tron-http/TronHttpClient.ts +++ b/packages/tron-wallet-snap/src/clients/tron-http/TronHttpClient.ts @@ -2,6 +2,7 @@ import { buildUrl } from '@metamask/snap-networks-utils'; import { assert } from '@metamask/superstruct'; import type { Network } from '../../constants'; +import { isSupportedNetwork } from '../../constants'; import type { ConfigProvider } from '../../services/config'; import { hexToString } from '../../utils/hex'; import { @@ -44,13 +45,17 @@ export class TronHttpClient { // Initialize clients for all networks Object.entries(baseUrls).forEach(([network, baseUrl]) => { + if (!isSupportedNetwork(network)) { + return; + } + const headers: Record = { 'Content-Type': 'application/json', 'Access-Control-Allow-Headers': '*', 'Access-Control-Allow-Origin': '*', }; - this.#clients.set(network as Network, { baseUrl, headers }); + this.#clients.set(network, { baseUrl, headers }); }); } diff --git a/packages/tron-wallet-snap/src/clients/trongrid/TrongridApiClient.ts b/packages/tron-wallet-snap/src/clients/trongrid/TrongridApiClient.ts index fb852a9a0..b077c5e78 100644 --- a/packages/tron-wallet-snap/src/clients/trongrid/TrongridApiClient.ts +++ b/packages/tron-wallet-snap/src/clients/trongrid/TrongridApiClient.ts @@ -7,6 +7,7 @@ import type { import { assert } from '@metamask/superstruct'; import type { Network } from '../../constants'; +import { isSupportedNetwork } from '../../constants'; import type { ConfigProvider } from '../../services/config'; import type { TronHttpClient } from '../tron-http/TronHttpClient'; import type { ChainParameter } from '../tron-http/types'; @@ -81,13 +82,17 @@ export class TrongridApiClient { // Initialize clients for all networks Object.entries(baseUrls).forEach(([network, baseUrl]) => { + if (!isSupportedNetwork(network)) { + return; + } + const headers: Record = { 'Content-Type': 'application/json', 'Access-Control-Allow-Headers': '*', 'Access-Control-Allow-Origin': '*', }; - this.#clients.set(network as Network, { baseUrl, headers }); + this.#clients.set(network, { baseUrl, headers }); }); this.#tronHttpClient = tronHttpClient; diff --git a/packages/tron-wallet-snap/src/entities/keyringAccount.ts b/packages/tron-wallet-snap/src/entities/keyringAccount.ts new file mode 100644 index 000000000..f62ad2148 --- /dev/null +++ b/packages/tron-wallet-snap/src/entities/keyringAccount.ts @@ -0,0 +1,11 @@ +import type { ExtendedKeyringAccount } from '@metamask/snap-networks-utils'; + +import type { Network } from '../constants'; + +/** + * A keyring account as stored by the snap, with its scopes narrowed to the + * supported networks. The Keyring API receives it as a `KeyringAccount`. + */ +export type TronKeyringAccount = Omit & { + scopes: Network[]; +}; 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 27199e199..f304110da 100644 --- a/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.test.ts +++ b/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.test.ts @@ -3,10 +3,7 @@ import { TransactionType, TrxAccountType, } from '@metamask/keyring-api'; -import type { - AnalyticsService, - ExtendedKeyringAccount, -} from '@metamask/snap-networks-utils'; +import type { AnalyticsService } from '@metamask/snap-networks-utils'; import type { JsonRpcRequest } from '@metamask/snaps-sdk'; import type { Infer } from '@metamask/superstruct'; import { BigNumber } from 'bignumber.js'; @@ -28,6 +25,7 @@ import type { NativeAsset, ResourceAsset, } from '../../entities/assets'; +import type { TronKeyringAccount } from '../../entities/keyringAccount'; import type { AccountsService } from '../../services/accounts/AccountsService'; import type { AssetsService } from '../../services/assets/AssetsService'; import type { ConfirmationHandler } from '../../services/confirmation/ConfirmationHandler'; @@ -60,22 +58,22 @@ const createPassThroughTransactionExpirationRefresherService = () => }) as unknown as TransactionExpirationRefresherService; /** - * Creates a minimal ExtendedKeyringAccount fixture for tests that only need + * Creates a minimal TronKeyringAccount fixture for tests that only need * account identity and derivation metadata. * * @param overrides - Account fields to override on the default fixture. * @returns A Tron keyring account test fixture. */ const createMockExtendedKeyringAccount = ( - overrides: Partial = {}, -): ExtendedKeyringAccount => + overrides: Partial = {}, +): TronKeyringAccount => ({ id: TEST_ACCOUNT_ID, address: 'TGJn1wnUYHJbvN88cynZbsAz2EMeZq73yx', entropySource: 'test-entropy', derivationPath: "m/44'/195'/0'/0/0", ...overrides, - }) as ExtendedKeyringAccount; + }) as TronKeyringAccount; type MockTronWeb = { trx: { @@ -1501,7 +1499,7 @@ describe('ClientRequestHandler', () => { params: { items }, }); - const account1: ExtendedKeyringAccount = { + const account1: TronKeyringAccount = { id: TEST_ACCOUNT_ID, address: TEST_ADDRESS, entropySource: 'test-entropy', @@ -1512,7 +1510,7 @@ describe('ClientRequestHandler', () => { options: {}, methods: ['signMessage', 'signTransaction'], }; - const account2: ExtendedKeyringAccount = { + const account2: TronKeyringAccount = { id: TEST_ACCOUNT_ID_2, address: TEST_ADDRESS_2, entropySource: 'test-entropy', diff --git a/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts b/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts index c9f958de1..9a355f88a 100644 --- a/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts +++ b/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts @@ -1,10 +1,6 @@ import { TransactionStatus } from '@metamask/keyring-api'; import { normalizeError } from '@metamask/snap-networks-utils'; -import type { - AnalyticsService, - ExtendedKeyringAccount, - Logger, -} from '@metamask/snap-networks-utils'; +import type { AnalyticsService, Logger } from '@metamask/snap-networks-utils'; import type { Json, JsonRpcRequest } from '@metamask/snaps-sdk'; import { InvalidParamsError, @@ -33,6 +29,7 @@ import { TRACK_TX_INTERVAL, ZERO, } from '../../constants'; +import type { TronKeyringAccount } from '../../entities/keyringAccount'; import { isDerivedTronKeypair } from '../../services/accounts/AccountsService'; import type { AccountsService } from '../../services/accounts/AccountsService'; import type { AssetsService } from '../../services/assets/AssetsService'; @@ -89,7 +86,7 @@ type TransactionRawData = TronwebTypes.Transaction['raw_data'] & { type SigningRequest = { index: number; accountId: string; - account: ExtendedKeyringAccount; + account: TronKeyringAccount; message: string; }; @@ -119,9 +116,9 @@ function getUniqueAccountIds(items: { accountId: string }[]): string[] { } function getAccountsByNormalizedId( - accounts: ExtendedKeyringAccount[], -): Map { - const accountsByNormalizedId = new Map(); + accounts: TronKeyringAccount[], +): Map { + const accountsByNormalizedId = new Map(); accounts.forEach((account) => { accountsByNormalizedId.set(account.id.toLowerCase(), account); @@ -139,7 +136,7 @@ function validateSigningRequest( message: string; }, index: number, - accountsById: Map, + accountsById: Map, ): SigningRequestValidation { const account = accountsById.get(accountId.toLowerCase()); if (account === undefined) { @@ -1339,7 +1336,7 @@ export class ClientRequestHandler { items.length, ); const signingRequests: SigningRequest[] = []; - const accountsToDerive: ExtendedKeyringAccount[] = []; + const accountsToDerive: TronKeyringAccount[] = []; items.forEach((item, index) => { const validationResult = validateSigningRequest( 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 366f5ba25..99287f068 100644 --- a/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.test.tsx +++ b/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.test.tsx @@ -3,6 +3,7 @@ import type { AnalyticsService, IStateManager, } from '@metamask/snap-networks-utils'; +import { InvalidParamsError } from '@metamask/snaps-sdk'; import type { PriceApiClient } from '../../clients/price-api/PriceApiClient'; import type { SnapClient } from '../../clients/snap/SnapClient'; @@ -976,6 +977,54 @@ describe('CronHandler', () => { }); } + it('tracks the transaction when the background event params are valid', async () => { + await withTrackTransactionCronHandler( + async ({ cronHandler, mockSnapClient, mockTronHttpClient }) => { + mockTronHttpClient.getTransactionInfoById.mockResolvedValue(null); + + await cronHandler.handle({ + jsonrpc: '2.0', + id: 1, + method: BackgroundEventMethod.TrackTransaction, + params: { + txId: TX_ID, + scope: Network.Mainnet, + accountIds: ACCOUNT_IDS, + attempt: 0, + }, + }); + + expect( + mockTronHttpClient.getTransactionInfoById, + ).toHaveBeenCalledWith(Network.Mainnet, TX_ID); + expect(mockSnapClient.scheduleBackgroundEvent).toHaveBeenCalled(); + }, + ); + }); + + it('rejects background event params with an unsupported scope', async () => { + await withTrackTransactionCronHandler( + async ({ cronHandler, mockTronHttpClient }) => { + await expect( + cronHandler.handle({ + jsonrpc: '2.0', + id: 1, + method: BackgroundEventMethod.TrackTransaction, + params: { + txId: TX_ID, + scope: 'eip155:1', + accountIds: ACCOUNT_IDS, + attempt: 0, + }, + }), + ).rejects.toThrow(InvalidParamsError); + expect( + mockTronHttpClient.getTransactionInfoById, + ).not.toHaveBeenCalled(); + }, + ); + }); + it('schedules next attempt when transaction is not yet confirmed', async () => { await withTrackTransactionCronHandler( async ({ cronHandler, mockSnapClient, mockTronHttpClient }) => { diff --git a/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.tsx b/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.tsx index 65ab9b813..668a75bfb 100644 --- a/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.tsx +++ b/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.tsx @@ -32,6 +32,8 @@ import type { ConfirmSignTransactionContext } from '../../ui/confirmation/views/ import { ConfirmTransactionRequest } from '../../ui/confirmation/views/ConfirmTransactionRequest/ConfirmTransactionRequest'; import { CONFIRM_TRANSACTION_INTERFACE_NAME } from '../../ui/confirmation/views/ConfirmTransactionRequest/types'; import type { ConfirmTransactionRequestContext } from '../../ui/confirmation/views/ConfirmTransactionRequest/types'; +import { TrackTransactionParamsStruct } from '../../validation/structs'; +import { validateRequest } from '../../validation/validators'; export const CronjobMethod = { ContinuouslySynchronizeSelectedAccounts: @@ -142,15 +144,8 @@ export class CronHandler { await this.refreshSignTransaction(); break; case BackgroundEventMethod.TrackTransaction: - await this.trackTransaction( - params as { - txId: string; - scope: Network; - accountIds: string[]; - attempt: number; - transactionType?: TransactionType; - }, - ); + validateRequest(params, TrackTransactionParamsStruct); + await this.trackTransaction(params); break; default: throw new Error(`Unknown cronjob method: ${method}`); diff --git a/packages/tron-wallet-snap/src/handlers/keyring/keyring.test.ts b/packages/tron-wallet-snap/src/handlers/keyring/keyring.test.ts index f37aed5d3..f225aeaa5 100644 --- a/packages/tron-wallet-snap/src/handlers/keyring/keyring.test.ts +++ b/packages/tron-wallet-snap/src/handlers/keyring/keyring.test.ts @@ -4,16 +4,15 @@ import type { KeyringRequest, } from '@metamask/keyring-api'; import type { ExportAccountOptions } from '@metamask/keyring-api/v2'; -import type { ExtendedKeyringAccount } from '@metamask/snap-networks-utils'; import type { JsonRpcRequest } from '@metamask/snaps-sdk'; import { InvalidParamsError, UserRejectedRequestError, } from '@metamask/snaps-sdk'; -import type { CaipChainId } from '@metamask/utils'; import type { SnapClient } from '../../clients/snap/SnapClient'; import { Network } from '../../constants'; +import type { TronKeyringAccount } from '../../entities/keyringAccount'; import type { AccountsService } from '../../services/accounts/AccountsService'; import type { AssetsService } from '../../services/assets/AssetsService'; import type { ConfirmationHandler } from '../../services/confirmation/ConfirmationHandler'; @@ -44,7 +43,7 @@ function toHex(str: string): string { } describe('KeyringHandler', () => { - const mockAccount: ExtendedKeyringAccount = { + const mockAccount: TronKeyringAccount = { id: '123e4567-e89b-42d3-a456-426614174000', address: 'TJRabPrwbZy45sbavfcjinPJC18kjpRTv8', options: {}, @@ -420,7 +419,7 @@ describe('KeyringHandler', () => { describe('multiple accounts', () => { it('handles different accounts correctly', async () => { - const account2: ExtendedKeyringAccount = { + const account2: TronKeyringAccount = { ...mockAccount, id: '987e6543-e89b-42d3-a456-426614174999', address: 'TGehVcNhud84JDCGrNHKVz9jEAVKUpbuiv', @@ -807,10 +806,7 @@ describe('KeyringHandler', () => { it('throws InvalidParamsError for a scope not controlled by the snap', async () => { await expect( - keyringHandler.resolveAccountAddress( - 'eip155:1' as CaipChainId, - request, - ), + keyringHandler.resolveAccountAddress('eip155:1', request), ).rejects.toThrow(InvalidParamsError); expect(mockWalletService.resolveAccountAddress).not.toHaveBeenCalled(); }); diff --git a/packages/tron-wallet-snap/src/handlers/keyring/keyring.ts b/packages/tron-wallet-snap/src/handlers/keyring/keyring.ts index 323d0ab6e..ca3d2ece1 100644 --- a/packages/tron-wallet-snap/src/handlers/keyring/keyring.ts +++ b/packages/tron-wallet-snap/src/handlers/keyring/keyring.ts @@ -19,10 +19,7 @@ import { asStrictKeyringAccount, validateOrigin, } from '@metamask/snap-networks-utils'; -import type { - ExtendedKeyringAccount, - Logger, -} from '@metamask/snap-networks-utils'; +import type { Logger } from '@metamask/snap-networks-utils'; import { InvalidParamsError, SnapError, @@ -39,6 +36,7 @@ import { sortBy } from 'lodash'; import type { SnapClient } from '../../clients/snap/SnapClient'; import { ESSENTIAL_ASSETS } from '../../constants'; +import type { TronKeyringAccount } from '../../entities/keyringAccount'; import { originPermissions } from '../../permissions'; import type { AccountsService } from '../../services/accounts/AccountsService'; import type { AssetsService } from '../../services/assets/AssetsService'; @@ -111,7 +109,7 @@ export class KeyringHandler implements KeyringSnapRpc { return result ?? null; } - async #listAccounts(): Promise { + async #listAccounts(): Promise { try { const keyringAccounts = await this.#accountsService.getAll(); @@ -133,7 +131,7 @@ export class KeyringHandler implements KeyringSnapRpc { async #getAccount( accountId: string, - ): Promise { + ): Promise { try { const account = (await this.#accountsService.findById(accountId)) ?? undefined; @@ -158,7 +156,7 @@ export class KeyringHandler implements KeyringSnapRpc { } } - async #getAccountOrThrow(accountId: string): Promise { + async #getAccountOrThrow(accountId: string): Promise { const account = await this.#getAccount(accountId); if (!account) { diff --git a/packages/tron-wallet-snap/src/services/accounts/AccountsRepository.test.ts b/packages/tron-wallet-snap/src/services/accounts/AccountsRepository.test.ts index 96a580367..847b77ec6 100644 --- a/packages/tron-wallet-snap/src/services/accounts/AccountsRepository.test.ts +++ b/packages/tron-wallet-snap/src/services/accounts/AccountsRepository.test.ts @@ -1,7 +1,7 @@ import { TrxAccountType, TrxScope } from '@metamask/keyring-api'; import { InMemoryState } from '@metamask/snap-networks-utils'; -import type { ExtendedKeyringAccount } from '@metamask/snap-networks-utils'; +import type { TronKeyringAccount } from '../../entities/keyringAccount'; import type { UnencryptedStateValue } from '../state/stateTypes'; import { AccountsRepository } from './AccountsRepository'; @@ -12,7 +12,7 @@ import { AccountsRepository } from './AccountsRepository'; * @returns An in-memory state seeded with the provided accounts. */ function createEmptyState( - keyringAccounts: Record = {}, + keyringAccounts: Record = {}, ): InMemoryState { return new InMemoryState({ keyringAccounts, @@ -29,8 +29,8 @@ function createEmptyState( * @returns A complete Tron keyring account for tests. */ function createTestAccount( - overrides: Partial = {}, -): ExtendedKeyringAccount { + overrides: Partial = {}, +): TronKeyringAccount { return { id: 'account-0', entropySource: 'test-entropy', diff --git a/packages/tron-wallet-snap/src/services/accounts/AccountsRepository.ts b/packages/tron-wallet-snap/src/services/accounts/AccountsRepository.ts index fb5b386ff..8f2dcdc15 100644 --- a/packages/tron-wallet-snap/src/services/accounts/AccountsRepository.ts +++ b/packages/tron-wallet-snap/src/services/accounts/AccountsRepository.ts @@ -1,9 +1,7 @@ import type { EntropySourceId } from '@metamask/keyring-api'; -import type { - ExtendedKeyringAccount, - IStateManager, -} from '@metamask/snap-networks-utils'; +import type { IStateManager } from '@metamask/snap-networks-utils'; +import type { TronKeyringAccount } from '../../entities/keyringAccount'; import type { UnencryptedStateValue } from '../state/stateTypes'; /** @@ -17,7 +15,7 @@ type AccountCreationRange = { to: number; }; -type KeyringAccountsState = Record; +type KeyringAccountsState = Record; /** * Result of merging accounts into `keyringAccounts`. @@ -27,8 +25,8 @@ type KeyringAccountsState = Record; * conflict losers are omitted (their winners are present in `merged`). */ export type KeyringAccountsMergeResult = { - merged: Record; - added: Record; + merged: Record; + added: Record; }; /** @@ -37,7 +35,7 @@ export type KeyringAccountsMergeResult = { * @param account - The account to key. * @returns A stable conflict key for the account. */ -function getAccountIndexKey(account: ExtendedKeyringAccount): string { +function getAccountIndexKey(account: TronKeyringAccount): string { return `${account.entropySource}:${account.index}`; } @@ -51,7 +49,7 @@ function getAccountIndexKey(account: ExtendedKeyringAccount): string { function findAccountByIndexKey( accounts: KeyringAccountsState, indexKey: string, -): ExtendedKeyringAccount | undefined { +): TronKeyringAccount | undefined { return Object.values(accounts).find( (account) => getAccountIndexKey(account) === indexKey, ); @@ -104,7 +102,7 @@ export class AccountsRepository { * * @returns All accounts from the state. */ - async getAll(): Promise { + async getAll(): Promise { const accounts = await this.#state.getKey< UnencryptedStateValue['keyringAccounts'] >(this.#storageKey); @@ -122,7 +120,7 @@ export class AccountsRepository { async findByEntropySourceAndRange( entropySource: EntropySourceId, range: AccountCreationRange, - ): Promise { + ): Promise { const accounts = await this.getAll(); return accounts @@ -135,7 +133,7 @@ export class AccountsRepository { .sort((first, second) => first.index - second.index); } - async findById(id: string): Promise { + async findById(id: string): Promise { const accounts = await this.getAll(); return accounts.find((account) => account.id === id) ?? null; } @@ -147,13 +145,13 @@ export class AccountsRepository { * @returns The matching accounts. Result ordering follows stored account * ordering, not input ordering. */ - async findByIds(ids: string[]): Promise { + async findByIds(ids: string[]): Promise { const accounts = await this.getAll(); const normalizedIds = new Set(); ids.forEach((id) => normalizedIds.add(id.toLowerCase())); - const matchedAccounts: ExtendedKeyringAccount[] = []; + const matchedAccounts: TronKeyringAccount[] = []; accounts.forEach((account) => { if (normalizedIds.has(account.id.toLowerCase())) { matchedAccounts.push(account); @@ -163,15 +161,13 @@ export class AccountsRepository { return matchedAccounts; } - async findByAddress(address: string): Promise { + async findByAddress(address: string): Promise { const accounts = await this.getAll(); return accounts.find((account) => account.address === address) ?? null; } - async create( - account: ExtendedKeyringAccount, - ): Promise { + async create(account: TronKeyringAccount): Promise { let persistedAccount = account; await this.#state.setKeyWith( @@ -204,7 +200,7 @@ export class AccountsRepository { * without re-reading state. */ async mergeKeyringAccounts( - newAccounts: Record, + newAccounts: Record, ): Promise { let result: KeyringAccountsMergeResult = { merged: {}, added: {} }; diff --git a/packages/tron-wallet-snap/src/services/accounts/AccountsService.test.ts b/packages/tron-wallet-snap/src/services/accounts/AccountsService.test.ts index 8c7b4c15f..76e967482 100644 --- a/packages/tron-wallet-snap/src/services/accounts/AccountsService.test.ts +++ b/packages/tron-wallet-snap/src/services/accounts/AccountsService.test.ts @@ -10,15 +10,13 @@ import type { } from '@metamask/keyring-api'; import { AccountCreationType, TrxAccountType } from '@metamask/keyring-api'; import { getSelectedAccounts } from '@metamask/keyring-snap-sdk'; -import type { - ExtendedKeyringAccount, - Logger, -} from '@metamask/snap-networks-utils'; +import type { Logger } from '@metamask/snap-networks-utils'; import { LogLevel } from '@metamask/snap-networks-utils'; import type { SnapClient } from '../../clients/snap/SnapClient'; import { Network } from '../../constants'; import type { NativeAsset } from '../../entities/assets'; +import type { TronKeyringAccount } from '../../entities/keyringAccount'; import { createTronBip44KeypairDeriver } from '../../utils/deriveTronFromCoinTypeNode'; import { trackError } from '../../utils/errors'; import { mockLogger } from '../../utils/mockLogger'; @@ -141,9 +139,9 @@ async function withAccountsService( configurable: true, }); - const keyringAccounts: ExtendedKeyringAccount[] = []; + const keyringAccounts: TronKeyringAccount[] = []; - const getAccountIndexKey = (account: ExtendedKeyringAccount): string => + const getAccountIndexKey = (account: TronKeyringAccount): string => `${account.entropySource}:${account.index}`; const mockAccountsRepository: jest.Mocked< @@ -187,7 +185,7 @@ async function withAccountsService( ), create: jest .fn() - .mockImplementation(async (account: ExtendedKeyringAccount) => { + .mockImplementation(async (account: TronKeyringAccount) => { const conflicting = keyringAccounts.find( (existing) => getAccountIndexKey(existing) === getAccountIndexKey(account), @@ -203,9 +201,9 @@ async function withAccountsService( mergeKeyringAccounts: jest .fn() .mockImplementation( - async (newAccounts: Record) => { + async (newAccounts: Record) => { const occupied = new Set(keyringAccounts.map(getAccountIndexKey)); - const added: Record = {}; + const added: Record = {}; for (const [id, account] of Object.entries(newAccounts)) { const indexKey = getAccountIndexKey(account); @@ -339,7 +337,7 @@ describe('AccountsService', () => { }); describe('deriveTronKeypairs', () => { - const createAccount = (index: number): ExtendedKeyringAccount => + const createAccount = (index: number): TronKeyringAccount => ({ id: `account-${index}`, entropySource: 'test-entropy', @@ -347,10 +345,10 @@ describe('AccountsService', () => { index, address: `TAccount${index}`, type: TrxAccountType.Eoa, - scopes: SUPPORTED_SCOPES as unknown as Network[], + scopes: [...SUPPORTED_SCOPES], options: {}, methods: ['signMessage', 'signTransaction'], - }) as unknown as ExtendedKeyringAccount; + }) as unknown as TronKeyringAccount; it('derives multiple keypairs with one entropy fetch per entropy source', async () => { const coinJson = await getTronTestCoinTypeJson(); @@ -444,7 +442,7 @@ describe('AccountsService', () => { mockAccountsRepository.mergeKeyringAccounts, ).toHaveBeenCalledTimes(1); const merged = mockAccountsRepository.mergeKeyringAccounts.mock - .calls[0]?.[0] as Record; + .calls[0]?.[0] as Record; expect(Object.keys(merged)).toHaveLength(2); }, coinJson, @@ -476,7 +474,7 @@ describe('AccountsService', () => { ).toHaveBeenCalledTimes(1); const mergedAccounts = mockAccountsRepository.mergeKeyringAccounts - .mock.calls[0]?.[0] as Record; + .mock.calls[0]?.[0] as Record; expect(Object.keys(mergedAccounts)).toHaveLength(101); expect( @@ -491,14 +489,14 @@ describe('AccountsService', () => { it('returns persisted accounts when merge skips indices taken concurrently', async () => { const coinJson = await getTronTestCoinTypeJson(); - const concurrentAccount: ExtendedKeyringAccount = { + const concurrentAccount: TronKeyringAccount = { id: 'concurrent-0', entropySource: 'test-entropy', derivationPath: "m/44'/195'/0'/0/0", index: 0, type: TrxAccountType.Eoa, address: 'TConcurrent0', - scopes: SUPPORTED_SCOPES as unknown as Network[], + scopes: [...SUPPORTED_SCOPES], options: {}, methods: ['signMessage', 'signTransaction'], }; @@ -541,25 +539,25 @@ describe('AccountsService', () => { it('does not merge when accounts already exist for the range', async () => { const coinJson = await getTronTestCoinTypeJson(); - const existing0: ExtendedKeyringAccount = { + const existing0: TronKeyringAccount = { id: 'existing-0', entropySource: 'test-entropy', derivationPath: "m/44'/195'/0'/0/0", index: 0, type: TrxAccountType.Eoa, address: 'TExisting0', - scopes: SUPPORTED_SCOPES as unknown as Network[], + scopes: [...SUPPORTED_SCOPES], options: {}, methods: ['signMessage', 'signTransaction'], }; - const existing1: ExtendedKeyringAccount = { + const existing1: TronKeyringAccount = { id: 'existing-1', entropySource: 'test-entropy', derivationPath: "m/44'/195'/0'/0/1", index: 1, type: TrxAccountType.Eoa, address: 'TExisting1', - scopes: SUPPORTED_SCOPES as unknown as Network[], + scopes: [...SUPPORTED_SCOPES], options: {}, methods: ['signMessage', 'signTransaction'], }; @@ -784,7 +782,7 @@ describe('AccountsService', () => { describe('getAll', () => { it('delegates to repository and returns result', async () => { - const accounts: ExtendedKeyringAccount[] = [ + const accounts: TronKeyringAccount[] = [ { id: 'a1', address: 'TAddr1', @@ -813,7 +811,7 @@ describe('AccountsService', () => { describe('getAllSelected', () => { it('returns only accounts whose IDs are in getSelectedAccounts', async () => { - const account1: ExtendedKeyringAccount = { + const account1: TronKeyringAccount = { id: 'selected-1', address: 'TAddr1', type: TrxAccountType.Eoa, @@ -824,7 +822,7 @@ describe('AccountsService', () => { derivationPath: "m/44'/195'/0'/0/0", index: 0, }; - const account2: ExtendedKeyringAccount = { + const account2: TronKeyringAccount = { id: 'not-selected', address: 'TAddr2', type: TrxAccountType.Eoa, @@ -865,7 +863,7 @@ describe('AccountsService', () => { describe('findById', () => { it('delegates to repository', async () => { - const account: ExtendedKeyringAccount = { + const account: TronKeyringAccount = { id: 'find-id', address: 'TFind', type: TrxAccountType.Eoa, @@ -894,7 +892,7 @@ describe('AccountsService', () => { describe('findByIdOrThrow', () => { it('returns account when found', async () => { - const account: ExtendedKeyringAccount = { + const account: TronKeyringAccount = { id: 'throw-found', address: 'TFound', type: TrxAccountType.Eoa, @@ -932,7 +930,7 @@ describe('AccountsService', () => { describe('findByIds', () => { it('returns accounts from repository', async () => { - const accounts: ExtendedKeyringAccount[] = [ + const accounts: TronKeyringAccount[] = [ { id: 'id1', address: 'T1', @@ -978,7 +976,7 @@ describe('AccountsService', () => { describe('findByAddress', () => { it('delegates to repository', async () => { - const account: ExtendedKeyringAccount = { + const account: TronKeyringAccount = { id: 'addr-id', address: 'TByAddress123456789012345678', type: TrxAccountType.Eoa, @@ -1023,7 +1021,7 @@ describe('AccountsService', () => { describe('synchronize (assets)', () => { it('fetches assets for each account and scope, then saves', async () => { - const account: ExtendedKeyringAccount = { + const account: TronKeyringAccount = { id: 'sync-asset-id', address: 'TSyncAsset12345678901234567', type: TrxAccountType.Eoa, @@ -1077,7 +1075,7 @@ describe('AccountsService', () => { }); it('tracks save failures standalone, without failing', async () => { - const account: ExtendedKeyringAccount = { + const account: TronKeyringAccount = { id: 'sync-asset-fail-id', address: 'TSyncFail123456789012345678', type: TrxAccountType.Eoa, @@ -1113,7 +1111,7 @@ describe('AccountsService', () => { }); it('points at the specific account whose fetch failed', async () => { - const failingAccount: ExtendedKeyringAccount = { + const failingAccount: TronKeyringAccount = { id: 'sync-fail-id', address: 'TSyncFail123456789012345678', type: TrxAccountType.Eoa, @@ -1124,7 +1122,7 @@ describe('AccountsService', () => { derivationPath: "m/44'/195'/0'/0/0", index: 0, }; - const healthyAccount: ExtendedKeyringAccount = { + const healthyAccount: TronKeyringAccount = { id: 'sync-healthy-id', address: 'TSyncHealthy123456789012345', type: TrxAccountType.Eoa, @@ -1176,7 +1174,7 @@ describe('AccountsService', () => { }); it('survives a hostile rejection reason that cannot be stringified', async () => { - const account: ExtendedKeyringAccount = { + const account: TronKeyringAccount = { id: 'sync-hostile-id', address: 'TSyncHostile12345678901234567', type: TrxAccountType.Eoa, @@ -1223,7 +1221,7 @@ describe('AccountsService', () => { async ({ accountsService, mockConfigProvider, mockAssetsService }) => { mockConfigProvider.config = MOCK_CONFIG; - const account: ExtendedKeyringAccount = { + const account: TronKeyringAccount = { id: 'empty-id', address: 'TEmpty12345678901234567890', type: TrxAccountType.Eoa, @@ -1248,7 +1246,7 @@ describe('AccountsService', () => { describe('synchronizeTransactions', () => { it('calls fetch for each account and scope, then saveMany', async () => { - const account: ExtendedKeyringAccount = { + const account: TronKeyringAccount = { id: 'sync-tx-id', address: 'TSyncTx123456789012345678', type: TrxAccountType.Eoa, @@ -1302,7 +1300,7 @@ describe('AccountsService', () => { }); it('tracks the failing account when a fetch fails, without failing', async () => { - const account: ExtendedKeyringAccount = { + const account: TronKeyringAccount = { id: 'sync-tx-fail-id', address: 'TSyncTxFail12345678901234567', type: TrxAccountType.Eoa, @@ -1346,7 +1344,7 @@ describe('AccountsService', () => { }); it('tracks save failures standalone and propagates them to synchronize', async () => { - const account: ExtendedKeyringAccount = { + const account: TronKeyringAccount = { id: 'sync-tx-save-fail-id', address: 'TSyncTxSaveFail12345678901234', type: TrxAccountType.Eoa, @@ -1390,7 +1388,7 @@ describe('AccountsService', () => { describe('synchronize', () => { it('fetches both assets and transactions', async () => { - const account: ExtendedKeyringAccount = { + const account: TronKeyringAccount = { id: 'sync-id', address: 'TSync12345678901234567890', type: TrxAccountType.Eoa, @@ -1429,7 +1427,7 @@ describe('AccountsService', () => { const makeSyncAccount = ( id: string, index: number, - ): ExtendedKeyringAccount => ({ + ): TronKeyringAccount => ({ id, address: `TCoalesce${index}2345678901234567890`, type: TrxAccountType.Eoa, diff --git a/packages/tron-wallet-snap/src/services/accounts/AccountsService.ts b/packages/tron-wallet-snap/src/services/accounts/AccountsService.ts index d5bcfe83a..86b9805e1 100644 --- a/packages/tron-wallet-snap/src/services/accounts/AccountsService.ts +++ b/packages/tron-wallet-snap/src/services/accounts/AccountsService.ts @@ -16,10 +16,7 @@ import { asStrictKeyringAccount, getSyncFailuresFromSettledResult, } from '@metamask/snap-networks-utils'; -import type { - ExtendedKeyringAccount, - Logger, -} from '@metamask/snap-networks-utils'; +import type { Logger } from '@metamask/snap-networks-utils'; import { assert } from '@metamask/superstruct'; import { hexToBytes } from '@metamask/utils'; import { computeAddress } from 'ethers'; @@ -27,7 +24,9 @@ import { TronWeb } from 'tronweb'; import snapManifest from '../../../snap.manifest.json'; import type { SnapClient } from '../../clients/snap/SnapClient'; -import { Network } from '../../constants'; +import { assertSupportedNetwork } from '../../constants'; +import type { Network } from '../../constants'; +import type { TronKeyringAccount } from '../../entities/keyringAccount'; import type { DerivedTronKeypair } from '../../utils/deriveTronFromCoinTypeNode'; import { createTronBip44AddressDeriver, @@ -50,9 +49,10 @@ const CURVE = 'secp256k1' as const; */ const MAX_BIP44_ACCOUNT_INDEX = 0x7fffffff; -export const SUPPORTED_SCOPES = snapManifest.initialPermissions[ - 'endowment:keyring' -].capabilities.scopes as readonly Network[]; +export const SUPPORTED_SCOPES: readonly Network[] = + snapManifest.initialPermissions['endowment:keyring'].capabilities.scopes.map( + assertSupportedNetwork, + ); /** * Range of inclusive account indices to create. @@ -238,12 +238,12 @@ export class AccountsService { * @returns One derivation result per account, in input order. */ async deriveTronKeypairs( - accounts: ExtendedKeyringAccount[], + accounts: TronKeyringAccount[], ): Promise { const results: DerivedTronKeypairBatchResult[] = new Array(accounts.length); const accountsByEntropySource = new Map< EntropySourceId, - { index: number; account: ExtendedKeyringAccount }[] + { index: number; account: TronKeyringAccount }[] >(); accounts.forEach((account, index) => { @@ -345,7 +345,7 @@ export class AccountsService { ]); const readAndEntropyMs = Date.now() - startMs; - const allAccounts = new Map(); + const allAccounts = new Map(); for (const account of existingAccounts) { allAccounts.set(account.index, account); } @@ -357,7 +357,7 @@ export class AccountsService { } } - const newAccounts: Record = {}; + const newAccounts: Record = {}; let created = 0; let deriveMs = 0; let mergeMs = 0; @@ -371,14 +371,14 @@ export class AccountsService { AccountsService.getDefaultDerivationPath(groupIndex); const { address } = await tronAddressDeriver(groupIndex); - const tronKeyringAccount: ExtendedKeyringAccount = { + const tronKeyringAccount: TronKeyringAccount = { id, entropySource, derivationPath, index: groupIndex, type: TrxAccountType.Eoa, address, - scopes: SUPPORTED_SCOPES as unknown as Network[], + scopes: [...SUPPORTED_SCOPES], options: { entropy: { type: 'mnemonic', @@ -441,11 +441,11 @@ export class AccountsService { return result; } - async getAll(): Promise { + async getAll(): Promise { return this.#accountsRepository.getAll(); } - async getAllSelected(): Promise { + async getAllSelected(): Promise { const [allAccounts, selectedAccountIds] = await Promise.all([ this.#accountsRepository.getAll(), getSelectedAccounts(snap), @@ -456,7 +456,7 @@ export class AccountsService { ); } - async findById(id: string): Promise { + async findById(id: string): Promise { return this.#accountsRepository.findById(id); } @@ -468,7 +468,7 @@ export class AccountsService { * @returns The account if found. * @throws {Error} If the account is not found. */ - async findByIdOrThrow(id: string): Promise { + async findByIdOrThrow(id: string): Promise { const account = await this.#accountsRepository.findById(id); if (!account) { @@ -487,7 +487,7 @@ export class AccountsService { * @param ids - Account IDs to resolve. * @returns The matching accounts. */ - async findByIds(ids: string[]): Promise { + async findByIds(ids: string[]): Promise { const accounts = await this.#accountsRepository.findByIds(ids); if (ids.length !== accounts.length) { @@ -497,7 +497,7 @@ export class AccountsService { return accounts; } - async findByAddress(address: string): Promise { + async findByAddress(address: string): Promise { return this.#accountsRepository.findByAddress(address); } @@ -514,7 +514,7 @@ export class AccountsService { * * @param accounts - The accounts to synchronize assets for. */ - async synchronizeAssets(accounts: ExtendedKeyringAccount[]): Promise { + async synchronizeAssets(accounts: TronKeyringAccount[]): Promise { const scopes = this.#configProvider.config.activeNetworks; const combinations = accounts.flatMap((account) => scopes.map((scope) => ({ account, scope })), @@ -558,9 +558,7 @@ export class AccountsService { * * @param accounts - The accounts to synchronize transactions for. */ - async synchronizeTransactions( - accounts: ExtendedKeyringAccount[], - ): Promise { + async synchronizeTransactions(accounts: TronKeyringAccount[]): Promise { const scopes = this.#configProvider.config.activeNetworks; const combinations = accounts.flatMap((account) => scopes.map((scope) => ({ account, scope })), @@ -604,7 +602,7 @@ export class AccountsService { * * @param accounts - The accounts to synchronize. */ - async synchronize(accounts: ExtendedKeyringAccount[]): Promise { + async synchronize(accounts: TronKeyringAccount[]): Promise { // Sync triggers stack up (60s cronjob, a background event scheduled by // every `setSelectedAccounts` call, post-transaction refreshes), so // concurrent invocations for the same accounts share one run instead of diff --git a/packages/tron-wallet-snap/src/services/assets/adapters/SnapAssetsAdapter.ts b/packages/tron-wallet-snap/src/services/assets/adapters/SnapAssetsAdapter.ts index 56757ebc3..cb9e97a52 100644 --- a/packages/tron-wallet-snap/src/services/assets/adapters/SnapAssetsAdapter.ts +++ b/packages/tron-wallet-snap/src/services/assets/adapters/SnapAssetsAdapter.ts @@ -8,7 +8,6 @@ import { emitSnapKeyringEvent } from '@metamask/keyring-snap-sdk'; import type { IStateManager, Logger } from '@metamask/snap-networks-utils'; import type { AssetMetadata, FungibleAssetMetadata } from '@metamask/snaps-sdk'; import type { CaipAssetType } from '@metamask/utils'; -import { parseCaipAssetType } from '@metamask/utils'; import type { PriceApiClient } from '../../../clients/price-api/PriceApiClient'; import type { SpotPrices } from '../../../clients/price-api/types'; @@ -40,6 +39,7 @@ import { } from '../../../constants'; import type { AssetEntity } from '../../../entities/assets'; import { toUiAmount } from '../../../utils/conversion'; +import { getAssetNetwork } from '../../../utils/getAssetNetwork'; import type { UnencryptedStateValue } from '../../state/stateTypes'; import type { AssetsRepository } from '../AssetsRepository'; import type { @@ -1118,12 +1118,11 @@ export class SnapAssetsAdapter { keyringAccountId: string, ): AssetEntity { const metadata = TokenMetadata[assetId as keyof typeof TokenMetadata]; - const { chainId } = parseCaipAssetType(assetId); return { assetType: metadata.id, keyringAccountId, - network: chainId as Network, + network: getAssetNetwork(assetId), symbol: metadata.symbol, decimals: metadata.decimals, rawAmount: '0', diff --git a/packages/tron-wallet-snap/src/services/assets/types.ts b/packages/tron-wallet-snap/src/services/assets/types.ts index d03b20a4e..ebbe158dc 100644 --- a/packages/tron-wallet-snap/src/services/assets/types.ts +++ b/packages/tron-wallet-snap/src/services/assets/types.ts @@ -1,5 +1,4 @@ import { CaipAssetTypeStruct } from '@metamask/keyring-api'; -import type { TrxScope } from '@metamask/keyring-api'; import { pattern } from '@metamask/superstruct'; import type { TronAccount } from '../../clients/trongrid/types'; @@ -12,17 +11,19 @@ export type StakedData = { }; export type NativeCaipAssetType = `${Network}/slip44:195`; -export type StakedCaipAssetType = - `${TrxScope}/slip44:195-staked-for-${'energy' | 'bandwidth'}`; +export type StakedCaipAssetType = `${Network}/slip44:195-staked-for-${ + | 'energy' + | 'bandwidth'}`; export type ReadyForWithdrawalCaipAssetType = - `${TrxScope}/slip44:195-ready-for-withdrawal`; + `${Network}/slip44:195-ready-for-withdrawal`; export type StakingRewardsCaipAssetType = - `${TrxScope}/slip44:195-staking-rewards`; -export type InLockPeriodCaipAssetType = `${TrxScope}/slip44:195-in-lock-period`; -export type ResourceCaipAssetType = - `${TrxScope}/slip44:${'energy' | 'bandwidth'}`; -export type TokenCaipAssetType = `${TrxScope}/${'trc10' | 'trc20'}:${string}`; -export type NftCaipAssetType = `${TrxScope}/trc721:${string}`; + `${Network}/slip44:195-staking-rewards`; +export type InLockPeriodCaipAssetType = `${Network}/slip44:195-in-lock-period`; +export type ResourceCaipAssetType = `${Network}/slip44:${ + | 'energy' + | 'bandwidth'}`; +export type TokenCaipAssetType = `${Network}/${'trc10' | 'trc20'}:${string}`; +export type NftCaipAssetType = `${Network}/trc721:${string}`; /** * Validates a TRON native CAIP-19 ID (e.g., "tron:728126428/slip44:195") diff --git a/packages/tron-wallet-snap/src/services/assets/utils/mapControllerAsset.test.ts b/packages/tron-wallet-snap/src/services/assets/utils/mapControllerAsset.test.ts index 7e28a2656..f0b5ff721 100644 --- a/packages/tron-wallet-snap/src/services/assets/utils/mapControllerAsset.test.ts +++ b/packages/tron-wallet-snap/src/services/assets/utils/mapControllerAsset.test.ts @@ -58,4 +58,19 @@ describe('mapControllerAsset', () => { iconUrl: '', }); }); + + it('throws when the asset is not on a supported network', () => { + const asset = { + id: 'eip155:1/slip44:60', + chainId: 'eip155:1', + balance: { amount: '1' }, + metadata: { type: 'native', symbol: 'ETH', name: 'Ether', decimals: 18 }, + price: { price: 0, lastUpdated: 0 }, + fiatValue: 0, + } as unknown as Asset; + + expect(() => mapControllerAsset(accountId, asset)).toThrow( + 'Unsupported network: "eip155:1"', + ); + }); }); diff --git a/packages/tron-wallet-snap/src/services/assets/utils/mapControllerAsset.ts b/packages/tron-wallet-snap/src/services/assets/utils/mapControllerAsset.ts index d55786adb..6f2ac6472 100644 --- a/packages/tron-wallet-snap/src/services/assets/utils/mapControllerAsset.ts +++ b/packages/tron-wallet-snap/src/services/assets/utils/mapControllerAsset.ts @@ -1,6 +1,6 @@ import type { Asset } from '@metamask/assets-controller'; -import { Network } from '../../../constants'; +import { assertSupportedNetwork } from '../../../constants'; import type { AssetEntity } from '../../../entities/assets'; import { toRawAmount } from '../../../utils/conversion'; @@ -15,6 +15,7 @@ import { toRawAmount } from '../../../utils/conversion'; * @param accountId - Keyring account ID. * @param asset - Asset returned by AssetsController. * @returns Mapped asset entity. + * @throws If the asset is not on a supported network. */ export function mapControllerAsset( accountId: string, @@ -29,7 +30,7 @@ export function mapControllerAsset( return { assetType: assetId, keyringAccountId: accountId, - network: asset.chainId as Network, + network: assertSupportedNetwork(asset.chainId), symbol, decimals, rawAmount: toRawAmount(amount, decimals), diff --git a/packages/tron-wallet-snap/src/services/config/ConfigProvider.ts b/packages/tron-wallet-snap/src/services/config/ConfigProvider.ts index d78e5dd8d..f8ea6fb07 100644 --- a/packages/tron-wallet-snap/src/services/config/ConfigProvider.ts +++ b/packages/tron-wallet-snap/src/services/config/ConfigProvider.ts @@ -12,7 +12,7 @@ import { Duration } from '@metamask/utils'; import { Network } from '../../constants'; -const NetworkStruct = enums(Object.values(Network) as [Network, ...Network[]]); +const NetworkStruct = enums(Object.values(Network)); /** * The default base URLs for the price, token, and static APIs, used when diff --git a/packages/tron-wallet-snap/src/services/send/SendService.ts b/packages/tron-wallet-snap/src/services/send/SendService.ts index e8c5eb8c8..1e6c6a326 100644 --- a/packages/tron-wallet-snap/src/services/send/SendService.ts +++ b/packages/tron-wallet-snap/src/services/send/SendService.ts @@ -233,16 +233,17 @@ export class SendService { | TronwebTypes.Transaction | TronwebTypes.Transaction > { - const { chainId, assetNamespace, assetReference } = parseCaipAssetType( + const { assetNamespace, assetReference } = parseCaipAssetType( asset.assetType, ); + const scope = asset.network; try { switch (assetNamespace) { case 'slip44': this.#logger.log('Sending TRX transaction'); return this.buildSendTrxTransaction({ - scope: chainId as Network, + scope, fromAccountId, toAddress, amount, @@ -252,7 +253,7 @@ export class SendService { case 'trc10': this.#logger.log(`Sending TRC10 token: ${assetReference}`); return this.buildSendTrc10Transaction({ - scope: chainId as Network, + scope, fromAccountId, toAddress, amount, @@ -264,7 +265,7 @@ export class SendService { case 'trc20': this.#logger.log(`Sending TRC20 token: ${assetReference}`); return this.buildSendTrc20Transaction({ - scope: chainId as Network, + scope, fromAccountId, toAddress, contractAddress: assetReference, @@ -279,7 +280,9 @@ export class SendService { } catch (error) { this.#logger.error({ error }, 'Failed to send asset'); throw new Error( - `Failed to send asset: ${error instanceof Error ? error.message : 'Unknown error'}`, + `Failed to send asset: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, ); } } diff --git a/packages/tron-wallet-snap/src/services/staking/StakingService.ts b/packages/tron-wallet-snap/src/services/staking/StakingService.ts index 8281308e4..cbb7a9a42 100644 --- a/packages/tron-wallet-snap/src/services/staking/StakingService.ts +++ b/packages/tron-wallet-snap/src/services/staking/StakingService.ts @@ -2,7 +2,6 @@ import type { ExtendedKeyringAccount, Logger, } from '@metamask/snap-networks-utils'; -import { parseCaipAssetType } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; import type { Types as TronwebTypes } from 'tronweb'; @@ -12,6 +11,7 @@ import type { Network } from '../../constants'; import { CONSENSYS_SR_NODE_ADDRESS, KnownCaip19Id } from '../../constants'; import { trxToSun } from '../../utils/conversion'; import { executeOnChainActions } from '../../utils/executeOnChainActions'; +import { getAssetNetwork } from '../../utils/getAssetNetwork'; import type { AccountsService } from '../accounts/AccountsService'; import type { NativeCaipAssetType, StakedCaipAssetType } from '../assets/types'; @@ -58,13 +58,15 @@ export class StakingService { */ srNodeAddress?: string; }): Promise { - const { chainId } = parseCaipAssetType(assetId); + const scope = getAssetNetwork(assetId); const amountInSun = Number(trxToSun(amount)); const availableVotes = amount.integerValue(BigNumber.ROUND_DOWN).toNumber(); const voteRecipient = srNodeAddress ?? CONSENSYS_SR_NODE_ADDRESS; this.#logger.info( - `Staking ${amount.toString()} ${assetId} for ${purpose} for ${account.address} on ${chainId}...`, + `Staking ${amount.toString()} ${assetId} for ${purpose} for ${ + account.address + } on ${scope}...`, ); await executeOnChainActions({ @@ -72,7 +74,7 @@ export class StakingService { tronWebFactory: this.#tronWebFactory, snapClient: this.#snapClient, account, - scope: chainId as Network, + scope, buildTransactions: async (tronWeb) => [ await tronWeb.transactionBuilder.freezeBalanceV2( amountInSun, @@ -96,7 +98,7 @@ export class StakingService { assetId: StakedCaipAssetType; amount: BigNumber; }): Promise { - const { chainId } = parseCaipAssetType(assetId); + const scope = getAssetNetwork(assetId); /** * Check which resource we are unstaking. @@ -134,7 +136,9 @@ export class StakingService { const amountInSun = Number(trxToSun(amount)); this.#logger.info( - `Unstaking ${amount.toString()} ${assetId} for ${account.address} on ${chainId}...`, + `Unstaking ${amount.toString()} ${assetId} for ${ + account.address + } on ${scope}...`, ); await executeOnChainActions({ @@ -142,7 +146,7 @@ export class StakingService { tronWebFactory: this.#tronWebFactory, snapClient: this.#snapClient, account, - scope: chainId as Network, + scope, buildTransactions: async (tronWeb) => [ await tronWeb.transactionBuilder.unfreezeBalanceV2( amountInSun, diff --git a/packages/tron-wallet-snap/src/services/state/stateTypes.ts b/packages/tron-wallet-snap/src/services/state/stateTypes.ts index f6648e56a..49eeea5de 100644 --- a/packages/tron-wallet-snap/src/services/state/stateTypes.ts +++ b/packages/tron-wallet-snap/src/services/state/stateTypes.ts @@ -1,10 +1,10 @@ import type { Transaction } from '@metamask/keyring-api'; -import type { ExtendedKeyringAccount } from '@metamask/snap-networks-utils'; import type { AssetEntity } from '../../entities/assets'; +import type { TronKeyringAccount } from '../../entities/keyringAccount'; export type UnencryptedStateValue = { - keyringAccounts: Record; + keyringAccounts: Record; assets: Record; transactions: Record; mapInterfaceNameToId: Record; diff --git a/packages/tron-wallet-snap/src/services/wallet/WalletService.test.ts b/packages/tron-wallet-snap/src/services/wallet/WalletService.test.ts index 3f5bafd25..e1b71fed3 100644 --- a/packages/tron-wallet-snap/src/services/wallet/WalletService.test.ts +++ b/packages/tron-wallet-snap/src/services/wallet/WalletService.test.ts @@ -1,10 +1,10 @@ -import type { ExtendedKeyringAccount } from '@metamask/snap-networks-utils'; import { SnapError } from '@metamask/snaps-sdk'; import { bytesToBase64, bytesToHex, stringToBytes } from '@metamask/utils'; import { TronWeb } from 'tronweb'; import type { TronWebFactory } from '../../clients/tronweb/TronWebFactory'; import { Network } from '../../constants'; +import type { TronKeyringAccount } from '../../entities/keyringAccount'; import { TronMultichainErrors, TronMultichainMethod, @@ -79,7 +79,7 @@ describe('WalletService', () => { address: TEST_ADDRESS, }; - const mockAccount: ExtendedKeyringAccount = { + const mockAccount: TronKeyringAccount = { id: '123e4567-e89b-12d3-a456-426614174000', address: TEST_ADDRESS, options: {}, diff --git a/packages/tron-wallet-snap/src/services/wallet/WalletService.ts b/packages/tron-wallet-snap/src/services/wallet/WalletService.ts index 3f7fbab41..def24f382 100644 --- a/packages/tron-wallet-snap/src/services/wallet/WalletService.ts +++ b/packages/tron-wallet-snap/src/services/wallet/WalletService.ts @@ -1,14 +1,12 @@ import type { ResolvedAccountAddress } from '@metamask/keyring-api'; -import type { - ExtendedKeyringAccount, - Logger, -} from '@metamask/snap-networks-utils'; +import type { Logger } from '@metamask/snap-networks-utils'; import { SnapError } from '@metamask/snaps-sdk'; import type { Json, JsonRpcRequest } from '@metamask/snaps-sdk'; import { bytesToHex, hexToBytes, sha256 } from '@metamask/utils'; import type { TronWebFactory } from '../../clients/tronweb/TronWebFactory'; import type { Network } from '../../constants'; +import type { TronKeyringAccount } from '../../entities/keyringAccount'; import { TronMultichainErrors, TronMultichainMethod, @@ -67,7 +65,7 @@ export class WalletService { method, params, }: { - account: ExtendedKeyringAccount; + account: TronKeyringAccount; scope: Network; method: TronMultichainMethod; params: Json; @@ -135,7 +133,7 @@ export class WalletService { scope, params, }: { - account: ExtendedKeyringAccount; + account: TronKeyringAccount; scope: Network; params: Json; }): Promise<{ signature: string }> { @@ -197,7 +195,7 @@ export class WalletService { scope, params, }: { - account: ExtendedKeyringAccount; + account: TronKeyringAccount; scope: Network; params: Json; }): Promise<{ signature: string }> { @@ -292,7 +290,7 @@ export class WalletService { * @throws If the request is invalid or no matching account is found. */ async resolveAccountAddress( - keyringAccounts: ExtendedKeyringAccount[], + keyringAccounts: TronKeyringAccount[], scope: Network, request: JsonRpcRequest, ): Promise { diff --git a/packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmSignMessage/render.test.tsx b/packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmSignMessage/render.test.tsx index 70e732995..6b65d0c74 100644 --- a/packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmSignMessage/render.test.tsx +++ b/packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmSignMessage/render.test.tsx @@ -1,4 +1,3 @@ -import type { KeyringRequest } from '@metamask/keyring-api'; import type { ExtendedKeyringAccount } from '@metamask/snap-networks-utils'; import { bytesToBase64, stringToBytes } from '@metamask/utils'; @@ -6,6 +5,7 @@ import type { SnapClient } from '../../../../clients/snap/SnapClient'; import { Network } from '../../../../constants'; import { TronMultichainMethod } from '../../../../handlers/keyring/keyring-types'; import type { Preferences } from '../../../../types/snap'; +import type { TronWalletKeyringRequest } from '../../../../validation/structs'; import { render } from './render'; /** @@ -66,7 +66,7 @@ describe('ConfirmSignMessage render', () => { const testOrigin = 'https://example.com'; const testMessage = 'Hello, Tron!'; - const request: KeyringRequest = { + const request: TronWalletKeyringRequest = { id: '00000000-0000-4000-8000-000000000001', origin: testOrigin, account: mockAccount.id, @@ -97,7 +97,7 @@ describe('ConfirmSignMessage render', () => { it('decodes base64 message correctly', async () => { const testMessage = 'Test message with special chars: 你好 🚀'; - const request: KeyringRequest = { + const request: TronWalletKeyringRequest = { id: '00000000-0000-4000-8000-000000000002', origin: 'https://test.com', account: mockAccount.id, @@ -122,7 +122,7 @@ describe('ConfirmSignMessage render', () => { new Error('Failed to load'), ); - const request: KeyringRequest = { + const request: TronWalletKeyringRequest = { id: '00000000-0000-4000-8000-000000000003', origin: 'https://test.com', account: mockAccount.id, @@ -144,7 +144,7 @@ describe('ConfirmSignMessage render', () => { }); it('handles missing origin gracefully', async () => { - const request: KeyringRequest = { + const request: TronWalletKeyringRequest = { id: '00000000-0000-4000-8000-000000000004', origin: undefined as any, account: mockAccount.id, @@ -165,7 +165,7 @@ describe('ConfirmSignMessage render', () => { }); it('uses correct network scope', async () => { - const request: KeyringRequest = { + const request: TronWalletKeyringRequest = { id: '00000000-0000-4000-8000-000000000005', origin: 'https://test.com', account: mockAccount.id, @@ -189,7 +189,7 @@ describe('ConfirmSignMessage render', () => { const expectedResult = true; mockSnapClient.showDialog.mockResolvedValue(expectedResult); - const request: KeyringRequest = { + const request: TronWalletKeyringRequest = { id: '00000000-0000-4000-8000-000000000006', origin: 'https://test.com', account: mockAccount.id, @@ -209,7 +209,7 @@ describe('ConfirmSignMessage render', () => { }); it('passes TRX_IMAGE_SVG as network image', async () => { - const request: KeyringRequest = { + const request: TronWalletKeyringRequest = { id: '00000000-0000-4000-8000-000000000007', origin: 'https://test.com', account: mockAccount.id, diff --git a/packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmSignMessage/render.tsx b/packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmSignMessage/render.tsx index 865d9b89c..4515d0ee8 100644 --- a/packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmSignMessage/render.tsx +++ b/packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmSignMessage/render.tsx @@ -1,14 +1,13 @@ -import type { KeyringRequest } from '@metamask/keyring-api'; import type { ExtendedKeyringAccount } from '@metamask/snap-networks-utils'; import type { DialogResult } from '@metamask/snaps-sdk'; import { assert } from '@metamask/superstruct'; -import type { Network } from '../../../../constants'; import snapContext from '../../../../context'; import { TRX_IMAGE_SVG } from '../../../../static/tron-logo'; import { formatOrigin } from '../../../../utils/formatOrigin'; import { FALLBACK_LANGUAGE } from '../../../../utils/i18n'; import { SignMessageRequestStruct } from '../../../../validation/structs'; +import type { TronWalletKeyringRequest } from '../../../../validation/structs'; import { ConfirmSignMessage } from './ConfirmSignMessage'; /** @@ -19,7 +18,7 @@ import { ConfirmSignMessage } from './ConfirmSignMessage'; * @returns The confirmation dialog result. */ export async function render( - request: KeyringRequest, + request: TronWalletKeyringRequest, account: ExtendedKeyringAccount, ): Promise { assert(request.request.params, SignMessageRequestStruct); @@ -45,7 +44,7 @@ export async function render( { const testOrigin = 'https://example.com'; const testTransaction = toHex('mock-transaction-data'); - const request: KeyringRequest = { + const request: TronWalletKeyringRequest = { id: '00000000-0000-4000-8000-000000000001', origin: testOrigin, account: mockAccount.id, @@ -313,7 +314,7 @@ describe('ConfirmSignTransaction render', () => { async ({ snapContext, mockSnapClient, mockFeeCalculatorService }) => { mockFeeCalculatorService.computeFee.mockResolvedValue([]); - const request: KeyringRequest = { + const request: TronWalletKeyringRequest = { id: '00000000-0000-4000-8000-000000000002', origin: 'https://test.com', account: mockAccount.id, @@ -358,7 +359,7 @@ describe('ConfirmSignTransaction render', () => { new Error('Scan failed'), ); - const request: KeyringRequest = { + const request: TronWalletKeyringRequest = { id: '00000000-0000-4000-8000-000000000003', origin: 'https://test.com', account: mockAccount.id, @@ -397,7 +398,7 @@ describe('ConfirmSignTransaction render', () => { simulateOnChainActions: false, }); - const request: KeyringRequest = { + const request: TronWalletKeyringRequest = { id: '00000000-0000-4000-8000-000000000004', origin: 'https://test.com', account: mockAccount.id, @@ -434,7 +435,7 @@ describe('ConfirmSignTransaction render', () => { new Error('Failed to load'), ); - const request: KeyringRequest = { + const request: TronWalletKeyringRequest = { id: '00000000-0000-4000-8000-000000000005', origin: 'https://test.com', account: mockAccount.id, @@ -462,7 +463,7 @@ describe('ConfirmSignTransaction render', () => { it('handles missing origin gracefully', async () => { await withSnapContext(async ({ mockSnapClient }) => { - const request: KeyringRequest = { + const request: TronWalletKeyringRequest = { id: '00000000-0000-4000-8000-000000000006', origin: undefined as any, account: mockAccount.id, @@ -492,7 +493,7 @@ describe('ConfirmSignTransaction render', () => { it('returns the dialog promise result', async () => { await withSnapContext(async () => { - const request: KeyringRequest = { + const request: TronWalletKeyringRequest = { id: '00000000-0000-4000-8000-000000000007', origin: 'https://test.com', account: mockAccount.id, @@ -526,7 +527,7 @@ describe('ConfirmSignTransaction render', () => { true, ); - const request: KeyringRequest = { + const request: TronWalletKeyringRequest = { id: '00000000-0000-4000-8000-000000000008', origin: 'https://test.com', account: mockAccount.id, @@ -570,7 +571,7 @@ describe('ConfirmSignTransaction render', () => { new Error('schedule failed'), ); - const request: KeyringRequest = { + const request: TronWalletKeyringRequest = { id: '00000000-0000-4000-8000-000000000009', origin: 'https://test.com', account: mockAccount.id, @@ -616,7 +617,7 @@ describe('ConfirmSignTransaction render', () => { throw new Error('update failed'); }); - const request: KeyringRequest = { + const request: TronWalletKeyringRequest = { id: '00000000-0000-4000-8000-000000000010', origin: 'https://test.com', account: mockAccount.id, @@ -658,7 +659,7 @@ describe('ConfirmSignTransaction render', () => { new Error('tapos check failed'), ); - const request: KeyringRequest = { + const request: TronWalletKeyringRequest = { id: '00000000-0000-4000-8000-000000000011', origin: 'https://test.com', account: mockAccount.id, @@ -711,7 +712,7 @@ describe('ConfirmSignTransaction render', () => { ...mockAssets.slice(1), ]); - const request: KeyringRequest = { + const request: TronWalletKeyringRequest = { id: '00000000-0000-4000-8000-000000000012', origin: 'https://example.com', account: mockAccount.id, diff --git a/packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmSignTransaction/render.tsx b/packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmSignTransaction/render.tsx index 59ea9fe40..dce56dbcb 100644 --- a/packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmSignTransaction/render.tsx +++ b/packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmSignTransaction/render.tsx @@ -1,4 +1,3 @@ -import type { KeyringRequest } from '@metamask/keyring-api'; import type { ExtendedKeyringAccount } from '@metamask/snap-networks-utils'; import type { DialogResult } from '@metamask/snaps-sdk'; import { assert } from '@metamask/superstruct'; @@ -16,6 +15,7 @@ import { FetchStatus } from '../../../../types/snap'; import { sunToTrx } from '../../../../utils/conversion'; import logger from '../../../../utils/logger'; import { SignTransactionRequestStruct } from '../../../../validation/structs'; +import type { TronWalletKeyringRequest } from '../../../../validation/structs'; import { getTransactionTrxValue } from '../../../../validation/transaction'; import { getIconUrlForKnownAsset } from '../../utils/getIconUrlForKnownAsset'; import { ConfirmSignTransaction } from './ConfirmSignTransaction'; @@ -61,7 +61,7 @@ export const DEFAULT_CONTEXT: ConfirmSignTransactionContext = { * @returns The confirmation dialog result. */ export async function render( - request: KeyringRequest, + request: TronWalletKeyringRequest, account: ExtendedKeyringAccount, rawData: TronwebTypes.Transaction['raw_data'], ): Promise { @@ -79,7 +79,7 @@ export async function render( // Build initial context const context: ConfirmSignTransactionContext = { ...DEFAULT_CONTEXT, - scope: scope as Network, + scope, account, transaction, origin: origin ?? 'Unknown', @@ -94,9 +94,9 @@ export async function render( const [preferences, accountAssets] = await Promise.all([ snapClient.getPreferences().catch(() => DEFAULT_CONTEXT.preferences), assetsService.getAccountAssetsByIDs(account.id, [ - Networks[scope as Network].nativeToken.id, - Networks[scope as Network].bandwidth.id, - Networks[scope as Network].energy.id, + Networks[scope].nativeToken.id, + Networks[scope].bandwidth.id, + Networks[scope].energy.id, ]), ]); @@ -131,7 +131,7 @@ export async function render( }; const fees = await feeCalculatorService.computeFee({ - scope: scope as Network, + scope, transaction: transactionObj, availableEnergy, availableBandwidth, @@ -157,9 +157,8 @@ export async function render( context.fees = fees; context.feesFetchStatus = FetchStatus.Fetched; const trxFee = new BigNumber( - fees.find( - (fee) => fee.asset.type === Networks[scope as Network].nativeToken.id, - )?.asset.amount ?? '0', + fees.find((fee) => fee.asset.type === Networks[scope].nativeToken.id) + ?.asset.amount ?? '0', ); const trxBalance = nativeTokenAsset ? sunToTrx(nativeTokenAsset.rawAmount) @@ -214,7 +213,7 @@ export async function render( accountAddress: account.address, transactionRawData: rawData, origin, - scope: scope as Network, + scope, options, }); } catch { @@ -231,7 +230,7 @@ export async function render( const expired = await snapContext.transactionExpirationRefresherService.isTransactionExpired( { - scope: scope as Network, + scope, rawData, }, ); diff --git a/packages/tron-wallet-snap/src/utils/getAssetNetwork.test.ts b/packages/tron-wallet-snap/src/utils/getAssetNetwork.test.ts new file mode 100644 index 000000000..2b16c9e99 --- /dev/null +++ b/packages/tron-wallet-snap/src/utils/getAssetNetwork.test.ts @@ -0,0 +1,20 @@ +import { InvalidParamsError } from '@metamask/snaps-sdk'; + +import { KnownCaip19Id, Network } from '../constants'; +import { getAssetNetwork } from './getAssetNetwork'; + +describe('getAssetNetwork', () => { + it.each([ + [KnownCaip19Id.TrxMainnet, Network.Mainnet], + [KnownCaip19Id.TrxStakedForEnergyNile, Network.Nile], + [KnownCaip19Id.BandwidthShasta, Network.Shasta], + ])('returns the network of %s', (assetType, expected) => { + expect(getAssetNetwork(assetType)).toBe(expected); + }); + + it('throws for an asset on an unsupported chain', () => { + expect(() => getAssetNetwork('eip155:1/slip44:60')).toThrow( + InvalidParamsError, + ); + }); +}); diff --git a/packages/tron-wallet-snap/src/utils/getAssetNetwork.ts b/packages/tron-wallet-snap/src/utils/getAssetNetwork.ts new file mode 100644 index 000000000..e6a61379b --- /dev/null +++ b/packages/tron-wallet-snap/src/utils/getAssetNetwork.ts @@ -0,0 +1,17 @@ +import { parseCaipAssetType } from '@metamask/utils'; +import type { CaipAssetType } from '@metamask/utils'; + +import { assertSupportedNetwork } from '../constants'; +import type { Network } from '../constants'; + +/** + * Returns the supported network a CAIP-19 asset type belongs to. + * + * @param assetType - The CAIP-19 asset type. + * @returns The network of the asset. + * @throws If the asset type's chain is not a supported network. + */ +export function getAssetNetwork(assetType: CaipAssetType): Network { + const { chainId } = parseCaipAssetType(assetType); + return assertSupportedNetwork(chainId); +} diff --git a/packages/tron-wallet-snap/src/validation/structs.ts b/packages/tron-wallet-snap/src/validation/structs.ts index e14d97168..41456726b 100644 --- a/packages/tron-wallet-snap/src/validation/structs.ts +++ b/packages/tron-wallet-snap/src/validation/structs.ts @@ -2,6 +2,7 @@ import { CaipAssetTypeStruct, KeyringRequestStruct, SolMethod, + TransactionType, TrxAccountType, } from '@metamask/keyring-api'; import { ExportAccountOptionsStruct } from '@metamask/keyring-api/v2'; @@ -76,6 +77,14 @@ export const ResolveAccountAddressStruct = object({ scope: NetworkStruct, }); +export const TrackTransactionParamsStruct = object({ + txId: string(), + scope: NetworkStruct, + accountIds: array(string()), + attempt: integer(), + transactionType: optional(enums(Object.values(TransactionType))), +}); + /** * Validates createAccount options. * - entropySource: Optional string for the entropy source (UUID or ULID format) From e354bc271fa77bf5ef12848ee93838ac57e7fcba Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Thu, 1 Oct 2026 16:22:24 +0100 Subject: [PATCH 3/7] chore: revert eslint.config.mjs changes Per review, keep the root ESLint config untouched: the no-tron-network-casts rule block and the shared no-enums selector extraction are dropped. --- eslint.config.mjs | 43 +++++-------------------------------------- 1 file changed, 5 insertions(+), 38 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index f354d271b..5b0b91a48 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -43,27 +43,6 @@ const NO_MESSENGER_ACTIONS_IN_CONSTRUCTORS_SELECTOR_OBJECTS = [ }, ]; -/** - * Argument to the `no-restricted-syntax` rule that prohibits enums. - */ -const NO_ENUMS_SELECTOR_OBJECT = { - selector: 'TSEnumDeclaration', - message: - "Don't use enums. There are a number of reasons why they are problematic, but the most important is that TypeScript treats them nominally, not structurally, and this can cause unexpected breaking changes. Instead, use an object + type, an array + type, or just a type. Learn more here: https://github.com/MetaMask/eslint-config/issues/417", -}; - -/** - * Arguments to the `no-restricted-syntax` rule that prevent the Tron snap from - * casting values to its supported network types. - */ -const TRON_NETWORK_TYPE_NAMES = '/^(Network|TrxScope)$/u'; -const NO_TRON_NETWORK_CASTS_MESSAGE = - 'Do not cast to `Network` or `TrxScope`. Narrow the value with `assertSupportedNetwork` or `isSupportedNetwork`, or carry the `Network` type from where it was validated.'; -const NO_TRON_NETWORK_CASTS_SELECTOR_OBJECTS = [ - `TSAsExpression > TSTypeReference.typeAnnotation[typeName.name=${TRON_NETWORK_TYPE_NAMES}]`, - `TSAsExpression > .typeAnnotation TSTypeReference[typeName.name=${TRON_NETWORK_TYPE_NAMES}]`, -].map((selector) => ({ selector, message: NO_TRON_NETWORK_CASTS_MESSAGE })); - /** * Collects all options for a given array-valued rule across one or more flat * config arrays, excluding the leading severity element. @@ -183,7 +162,11 @@ const config = createConfig([ typescript, ]), ...NO_CONTROLLER_STATE_CHANGE_SELECTOR_OBJECTS, - NO_ENUMS_SELECTOR_OBJECT, + { + selector: 'TSEnumDeclaration', + message: + "Don't use enums. There are a number of reasons why they are problematic, but the most important is that TypeScript treats them nominally, not structurally, and this can cause unexpected breaking changes. Instead, use an object + type, an array + type, or just a type. Learn more here: https://github.com/MetaMask/eslint-config/issues/417", + }, ], }, }, @@ -273,22 +256,6 @@ const config = createConfig([ ], }, }, - { - files: ['packages/tron-wallet-snap/src/**/*.{ts,tsx}'], - rules: { - 'no-restricted-syntax': [ - 'error', - ...collectExistingRuleOptions('no-restricted-syntax', [ - base, - typescript, - ]), - ...NO_CONTROLLER_STATE_CHANGE_SELECTOR_OBJECTS, - ...NO_MESSENGER_ACTIONS_IN_CONSTRUCTORS_SELECTOR_OBJECTS, - NO_ENUMS_SELECTOR_OBJECT, - ...NO_TRON_NETWORK_CASTS_SELECTOR_OBJECTS, - ], - }, - }, { // Prohibit exporting *AllowedActions, *AllowedEvents, and *MethodActions // from package index files From 33731fa3545342bfdbed292e14099c3050bd17ee Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Thu, 1 Oct 2026 16:22:24 +0100 Subject: [PATCH 4/7] refactor(tron-wallet-snap): derive client network map from Object.values(Network) The config structs validate the clients' baseUrls as record(NetworkStruct, UrlStruct), so keys are guaranteed to be supported networks. Iterating Object.values(Network) keeps that typing end to end, removing the Object.entries key-widening that previously forced a runtime isSupportedNetwork guard (and, before this PR, an 'as Network' cast). --- packages/tron-wallet-snap/jest.config.js | 8 ++++---- packages/tron-wallet-snap/snap.manifest.json | 2 +- .../src/clients/tron-http/TronHttpClient.ts | 11 +++-------- .../src/clients/trongrid/TrongridApiClient.ts | 11 +++-------- 4 files changed, 11 insertions(+), 21 deletions(-) diff --git a/packages/tron-wallet-snap/jest.config.js b/packages/tron-wallet-snap/jest.config.js index 846c1cea8..af0db6b55 100644 --- a/packages/tron-wallet-snap/jest.config.js +++ b/packages/tron-wallet-snap/jest.config.js @@ -22,10 +22,10 @@ module.exports = { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 72.94, - functions: 80.38, - lines: 86.29, - statements: 86.28, + branches: 73.09, + functions: 80.43, + lines: 86.39, + statements: 86.38, }, }, }; diff --git a/packages/tron-wallet-snap/snap.manifest.json b/packages/tron-wallet-snap/snap.manifest.json index 4922a9d5a..6e0d19e93 100644 --- a/packages/tron-wallet-snap/snap.manifest.json +++ b/packages/tron-wallet-snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/internal-snaps.git" }, "source": { - "shasum": "dhCsUtl4QWsR6OfdTycMFQoDNQIPXQHTXQdTch9hjDM=", + "shasum": "GcP7rORiqUXpGXqVE/nwnQWbPbhES3wWTv3iz/vR7Rs=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/tron-wallet-snap/src/clients/tron-http/TronHttpClient.ts b/packages/tron-wallet-snap/src/clients/tron-http/TronHttpClient.ts index 0057cba31..d372ad31a 100644 --- a/packages/tron-wallet-snap/src/clients/tron-http/TronHttpClient.ts +++ b/packages/tron-wallet-snap/src/clients/tron-http/TronHttpClient.ts @@ -1,8 +1,7 @@ import { buildUrl } from '@metamask/snap-networks-utils'; import { assert } from '@metamask/superstruct'; -import type { Network } from '../../constants'; -import { isSupportedNetwork } from '../../constants'; +import { Network } from '../../constants'; import type { ConfigProvider } from '../../services/config'; import { hexToString } from '../../utils/hex'; import { @@ -44,18 +43,14 @@ export class TronHttpClient { const { baseUrls } = configProvider.config.tronHttpApi; // Initialize clients for all networks - Object.entries(baseUrls).forEach(([network, baseUrl]) => { - if (!isSupportedNetwork(network)) { - return; - } - + Object.values(Network).forEach((network) => { const headers: Record = { 'Content-Type': 'application/json', 'Access-Control-Allow-Headers': '*', 'Access-Control-Allow-Origin': '*', }; - this.#clients.set(network, { baseUrl, headers }); + this.#clients.set(network, { baseUrl: baseUrls[network], headers }); }); } diff --git a/packages/tron-wallet-snap/src/clients/trongrid/TrongridApiClient.ts b/packages/tron-wallet-snap/src/clients/trongrid/TrongridApiClient.ts index b077c5e78..73b3034ca 100644 --- a/packages/tron-wallet-snap/src/clients/trongrid/TrongridApiClient.ts +++ b/packages/tron-wallet-snap/src/clients/trongrid/TrongridApiClient.ts @@ -6,8 +6,7 @@ import type { } from '@metamask/snap-networks-utils'; import { assert } from '@metamask/superstruct'; -import type { Network } from '../../constants'; -import { isSupportedNetwork } from '../../constants'; +import { Network } from '../../constants'; import type { ConfigProvider } from '../../services/config'; import type { TronHttpClient } from '../tron-http/TronHttpClient'; import type { ChainParameter } from '../tron-http/types'; @@ -81,18 +80,14 @@ export class TrongridApiClient { const { baseUrls } = configProvider.config.trongridApi; // Initialize clients for all networks - Object.entries(baseUrls).forEach(([network, baseUrl]) => { - if (!isSupportedNetwork(network)) { - return; - } - + Object.values(Network).forEach((network) => { const headers: Record = { 'Content-Type': 'application/json', 'Access-Control-Allow-Headers': '*', 'Access-Control-Allow-Origin': '*', }; - this.#clients.set(network, { baseUrl, headers }); + this.#clients.set(network, { baseUrl: baseUrls[network], headers }); }); this.#tronHttpClient = tronHttpClient; From 9fdc0918b8c884ac335fd899cf261ba2a4376955 Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Thu, 1 Oct 2026 16:51:33 +0100 Subject: [PATCH 5/7] fix(tron-wallet-snap): validate tron caip asset ids --- packages/tron-wallet-snap/snap.manifest.json | 5 +- .../src/clients/token-api/TokenApiClient.ts | 20 +- .../clientRequest/clientRequest.test.ts | 215 +++++++++--------- .../handlers/clientRequest/clientRequest.ts | 147 +++++------- .../src/services/assets/AssetsService.ts | 69 ++++++ .../assets/adapters/CoreAssetsAdapter.ts | 104 +++++++++ .../assets/adapters/SnapAssetsAdapter.ts | 5 +- .../assets/utils/mapControllerAsset.ts | 5 +- .../src/services/send/SendService.ts | 21 +- .../services/staking/StakingService.test.ts | 2 +- .../src/services/staking/StakingService.ts | 14 +- packages/tron-wallet-snap/src/utils/caip.ts | 40 ++++ .../src/validation/structs.test.ts | 13 ++ 13 files changed, 421 insertions(+), 239 deletions(-) create mode 100644 packages/tron-wallet-snap/src/utils/caip.ts diff --git a/packages/tron-wallet-snap/snap.manifest.json b/packages/tron-wallet-snap/snap.manifest.json index 6e0d19e93..005de45a3 100644 --- a/packages/tron-wallet-snap/snap.manifest.json +++ b/packages/tron-wallet-snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/internal-snaps.git" }, "source": { - "shasum": "GcP7rORiqUXpGXqVE/nwnQWbPbhES3wWTv3iz/vR7Rs=", + "shasum": "R0LTwZZs0pys5NIQLXqxoG1g3q0Bllj+ginLZD78rMk=" "location": { "npm": { "filePath": "dist/bundle.js", @@ -68,7 +68,8 @@ "RemoteFeatureFlagController:getState", "AssetsController:getAccountAssetByID", "AssetsController:getAccountAssetsByIDs", - "AssetsController:getAccountAssetsByScope" + "AssetsController:getAccountAssetsByScope", + "AssetsController:getAssets" ] } }, diff --git a/packages/tron-wallet-snap/src/clients/token-api/TokenApiClient.ts b/packages/tron-wallet-snap/src/clients/token-api/TokenApiClient.ts index 5399901aa..70ff9aa57 100644 --- a/packages/tron-wallet-snap/src/clients/token-api/TokenApiClient.ts +++ b/packages/tron-wallet-snap/src/clients/token-api/TokenApiClient.ts @@ -3,13 +3,10 @@ import type { Logger } from '@metamask/snap-networks-utils'; import type { FungibleAssetMetadata } from '@metamask/snaps-sdk'; import { array, assert } from '@metamask/superstruct'; import type { Infer } from '@metamask/superstruct'; -import { CaipAssetTypeStruct, parseCaipAssetType } from '@metamask/utils'; +import { CaipAssetTypeStruct } from '@metamask/utils'; +import { parseTronCaipAssetType } from '../../utils/caip'; -import { - isSupportedNetwork, - Network, - SNAP_OWNED_ASSETS, -} from '../../constants'; +import { Network, SNAP_OWNED_ASSETS } from '../../constants'; import type { TokenCaipAssetType } from '../../services/assets/types'; import { TokenCaipAssetTypeStruct } from '../../services/assets/types'; import type { ConfigProvider } from '../../services/config'; @@ -100,18 +97,13 @@ export class TokenApiClient { if (SNAP_OWNED_ASSETS.includes(assetType)) { return false; } - const { chainId } = parseCaipAssetType(assetType); - return ( - isSupportedNetwork(chainId) && - TokenApiClient.supportedNetworks.includes(chainId) - ); + const { chainId } = parseTronCaipAssetType(assetType); + return TokenApiClient.supportedNetworks.includes(chainId); }); if (supportedAssetTypes.length !== assetTypes.length) { this.#logger.warn( - `[TokenApiClient] Received some asset types that are either not supported by the Token API or are excluded resource/staked tokens. They will be ignored. Supported networks: ${TokenApiClient.supportedNetworks.join( - ', ', - )}`, + `[TokenApiClient] Received some asset types that are either not supported by the Token API or are excluded resource/staked tokens. They will be ignored. Supported networks: ${TokenApiClient.supportedNetworks.join(', ')}`, ); } 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 f304110da..5e1740b7c 100644 --- a/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.test.ts +++ b/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.test.ts @@ -1,9 +1,17 @@ +<<<<<<< HEAD import { FeeType, TransactionType, TrxAccountType, } from '@metamask/keyring-api'; import type { AnalyticsService } from '@metamask/snap-networks-utils'; +======= +import { FeeType, TrxAccountType } from '@metamask/keyring-api'; +import type { + AnalyticsService, + ExtendedKeyringAccount, +} from '@metamask/snap-networks-utils'; +>>>>>>> 53b044eb (fix(tron-wallet-snap): validate tron caip asset ids) import type { JsonRpcRequest } from '@metamask/snaps-sdk'; import type { Infer } from '@metamask/superstruct'; import { BigNumber } from 'bignumber.js'; @@ -15,7 +23,6 @@ import type { TronWebFactory } from '../../clients/tronweb/TronWebFactory'; import { FALLBACK_FEE, FEE_LIMIT, - METAMASK_ORIGIN, Network, Networks, TRACK_TX_INTERVAL, @@ -25,7 +32,6 @@ import type { NativeAsset, ResourceAsset, } from '../../entities/assets'; -import type { TronKeyringAccount } from '../../entities/keyringAccount'; import type { AccountsService } from '../../services/accounts/AccountsService'; import type { AssetsService } from '../../services/assets/AssetsService'; import type { ConfirmationHandler } from '../../services/confirmation/ConfirmationHandler'; @@ -58,22 +64,22 @@ const createPassThroughTransactionExpirationRefresherService = () => }) as unknown as TransactionExpirationRefresherService; /** - * Creates a minimal TronKeyringAccount fixture for tests that only need + * Creates a minimal ExtendedKeyringAccount fixture for tests that only need * account identity and derivation metadata. * * @param overrides - Account fields to override on the default fixture. * @returns A Tron keyring account test fixture. */ const createMockExtendedKeyringAccount = ( - overrides: Partial = {}, -): TronKeyringAccount => + overrides: Partial = {}, +): ExtendedKeyringAccount => ({ id: TEST_ACCOUNT_ID, address: 'TGJn1wnUYHJbvN88cynZbsAz2EMeZq73yx', entropySource: 'test-entropy', derivationPath: "m/44'/195'/0'/0/0", ...overrides, - }) as TronKeyringAccount; + }) as ExtendedKeyringAccount; type MockTronWeb = { trx: { @@ -115,7 +121,13 @@ type WithClientRequestHandlerCallback = (payload: { Pick >; mockAssetsService: jest.Mocked< - Pick + Pick< + AssetsService, + | 'getAccountAssetsByIDs' + | 'getAccountAssetByID' + | 'getFreshAccountAssetsByIDs' + | 'getFreshAccountAssetByID' + > >; mockSendService: jest.Mocked< Pick< @@ -165,10 +177,18 @@ async function withClientRequestHandler( }; const mockAssetsService: jest.Mocked< - Pick + Pick< + AssetsService, + | 'getAccountAssetsByIDs' + | 'getAccountAssetByID' + | 'getFreshAccountAssetsByIDs' + | 'getFreshAccountAssetByID' + > > = { getAccountAssetsByIDs: jest.fn(), getAccountAssetByID: jest.fn(), + getFreshAccountAssetsByIDs: jest.fn(), + getFreshAccountAssetByID: jest.fn(), }; const mockSendService: jest.Mocked< @@ -321,6 +341,8 @@ describe('ClientRequestHandler', () => { mockAssetsService = { getAccountAssetsByIDs: jest.fn(), + getFreshAccountAssetsByIDs: jest.fn().mockResolvedValue([]), + getFreshAccountAssetByID: jest.fn().mockResolvedValue(null), } as unknown as jest.Mocked; mockSendService = {} as unknown as jest.Mocked; @@ -756,7 +778,7 @@ describe('ClientRequestHandler', () => { mockTronWeb.trx.sign.mockResolvedValue(signedTransaction); // Mock available resources - mockAssetsService.getAccountAssetsByIDs.mockResolvedValue([ + mockAssetsService.getFreshAccountAssetsByIDs.mockResolvedValue([ { rawAmount: '5000' }, // Bandwidth { rawAmount: '100000' }, // Energy ] as any); @@ -801,8 +823,10 @@ describe('ClientRequestHandler', () => { ).toHaveBeenCalledWith('TriggerSmartContract', expect.any(String)); // trx.sign is NOT called - fee computation uses unsigned transactions expect(mockTronWeb.trx.sign).not.toHaveBeenCalled(); - expect(mockAssetsService.getAccountAssetsByIDs).toHaveBeenCalledWith( - TEST_ACCOUNT_ID, + expect( + mockAssetsService.getFreshAccountAssetsByIDs, + ).toHaveBeenCalledWith( + expect.objectContaining({ id: TEST_ACCOUNT_ID }), [Networks[scope].bandwidth.id, Networks[scope].energy.id], ); // computeFee receives unsigned transaction (no signature field) @@ -893,7 +917,7 @@ describe('ClientRequestHandler', () => { }; mockTronWeb.trx.sign.mockResolvedValue(signedTransaction); - mockAssetsService.getAccountAssetsByIDs.mockResolvedValue([ + mockAssetsService.getFreshAccountAssetsByIDs.mockResolvedValue([ { rawAmount: '1000' }, // Bandwidth { rawAmount: '0' }, // Energy (not needed for native transfer) ] as any); @@ -986,7 +1010,7 @@ describe('ClientRequestHandler', () => { }); // No resources available - mockAssetsService.getAccountAssetsByIDs.mockResolvedValue([ + mockAssetsService.getFreshAccountAssetsByIDs.mockResolvedValue([ undefined, // No bandwidth asset undefined, // No energy asset ] as any); @@ -1499,7 +1523,7 @@ describe('ClientRequestHandler', () => { params: { items }, }); - const account1: TronKeyringAccount = { + const account1: ExtendedKeyringAccount = { id: TEST_ACCOUNT_ID, address: TEST_ADDRESS, entropySource: 'test-entropy', @@ -1510,7 +1534,7 @@ describe('ClientRequestHandler', () => { options: {}, methods: ['signMessage', 'signTransaction'], }; - const account2: TronKeyringAccount = { + const account2: ExtendedKeyringAccount = { id: TEST_ACCOUNT_ID_2, address: TEST_ADDRESS_2, entropySource: 'test-entropy', @@ -1758,6 +1782,8 @@ describe('ClientRequestHandler - signAndSendTransaction', () => { mockAssetsService = { getAccountAssetsByIDs: jest.fn(), + getFreshAccountAssetsByIDs: jest.fn().mockResolvedValue([]), + getFreshAccountAssetByID: jest.fn().mockResolvedValue(null), } as unknown as jest.Mocked; mockSendService = {} as unknown as jest.Mocked; @@ -1867,9 +1893,7 @@ describe('ClientRequestHandler - signAndSendTransaction', () => { await expect( clientRequestHandler.handle(request as JsonRpcRequest), ).rejects.toThrow( - `Transaction owner_address (${TronWeb.address.fromHex( - WRONG_OWNER_ADDRESS_HEX, - )}) does not match derived signer address (${CORRECT_OWNER_ADDRESS_BASE58})`, + `Transaction owner_address (${TronWeb.address.fromHex(WRONG_OWNER_ADDRESS_HEX)}) does not match derived signer address (${CORRECT_OWNER_ADDRESS_BASE58})`, ); }); @@ -1968,7 +1992,7 @@ describe('ClientRequestHandler - signAndSendTransaction', () => { expect(mockAnalyticsService.trackTransactionSubmitted).toHaveBeenCalledWith( { - origin: METAMASK_ORIGIN, + origin: 'MetaMask', accountType: 'tron:eoa', chainIdCaip: scope, transactionType: TransactionType.Unknown, @@ -2113,7 +2137,9 @@ describe('ClientRequestHandler - onAmountInput', () => { ]; mockAccountsService.findById.mockResolvedValue(mockAccount); - mockAssetsService.getAccountAssetsByIDs.mockResolvedValue(mockAssets); + mockAssetsService.getFreshAccountAssetsByIDs.mockResolvedValue( + mockAssets, + ); const result = await handler.handle(request); @@ -2124,30 +2150,6 @@ describe('ClientRequestHandler - onAmountInput', () => { ); }); - it('returns invalid for an asset from a non-Tron chain', async () => { - await withClientRequestHandler(async ({ handler, mockAccountsService }) => { - mockAccountsService.findById.mockResolvedValue(mockAccount); - - const request: OnAmountInputRequest = { - jsonrpc: '2.0' as const, - id: '1', - method: ClientRequestMethod.OnAmountInput, - params: { - accountId: TEST_ACCOUNT_ID, - assetId: 'eip155:1/slip44:60', - value: '10', - }, - }; - - const result = await handler.handle(request); - - expect(result).toStrictEqual({ - valid: false, - errors: [{ code: SendErrorCodes.Invalid }], - }); - }); - }); - it('uses provided toAddress when building the transaction for fee estimation', async () => { await withClientRequestHandler( async ({ @@ -2195,7 +2197,9 @@ describe('ClientRequestHandler - onAmountInput', () => { ]; mockAccountsService.findById.mockResolvedValue(mockAccount); - mockAssetsService.getAccountAssetsByIDs.mockResolvedValue(mockAssets); + mockAssetsService.getFreshAccountAssetsByIDs.mockResolvedValue( + mockAssets, + ); mockSendService.buildTransaction.mockResolvedValue(builtTransaction); mockFeeCalculatorService.computeFee.mockResolvedValue(mockFees); @@ -2267,7 +2271,9 @@ describe('ClientRequestHandler - onAmountInput', () => { ]; mockAccountsService.findById.mockResolvedValue(mockAccount); - mockAssetsService.getAccountAssetsByIDs.mockResolvedValue(mockAssets); + mockAssetsService.getFreshAccountAssetsByIDs.mockResolvedValue( + mockAssets, + ); mockSendService.buildTransaction.mockResolvedValue(builtTransaction); mockFeeCalculatorService.computeFee.mockResolvedValue(mockFees); @@ -2318,7 +2324,9 @@ describe('ClientRequestHandler - onAmountInput', () => { ]; mockAccountsService.findById.mockResolvedValue(mockAccount); - mockAssetsService.getAccountAssetsByIDs.mockResolvedValue(mockAssets); + mockAssetsService.getFreshAccountAssetsByIDs.mockResolvedValue( + mockAssets, + ); const result = await handler.handle(request); @@ -2379,7 +2387,9 @@ describe('ClientRequestHandler - onAmountInput', () => { ]; mockAccountsService.findById.mockResolvedValue(mockAccount); - mockAssetsService.getAccountAssetsByIDs.mockResolvedValue(mockAssets); + mockAssetsService.getFreshAccountAssetsByIDs.mockResolvedValue( + mockAssets, + ); mockSendService.buildTransaction.mockResolvedValue(builtTransaction); mockFeeCalculatorService.computeFee.mockResolvedValue(mockFees); @@ -2468,14 +2478,12 @@ describe('ClientRequestHandler - computeStakeFee', () => { // Native TRX asset for mainnet const nativeAssetId = Networks[scope].nativeToken.id; - // Mock native balance and resources - mockAssetsService.getAccountAssetByID.mockResolvedValue({ - uiAmount: '100', - } as AssetEntity); - mockAssetsService.getAccountAssetsByIDs.mockResolvedValue([ + // Mock native balance and resources (single fresh fetch) + mockAssetsService.getFreshAccountAssetsByIDs.mockResolvedValue([ + { uiAmount: '100' }, // Native TRX { rawAmount: '5000' }, // Bandwidth { rawAmount: '100000' }, // Energy - ] as AssetEntity[]); + ] as unknown as AssetEntity[]); const feeResult = [ { @@ -2505,13 +2513,15 @@ describe('ClientRequestHandler - computeStakeFee', () => { 'ENERGY', 'TGJn1wnUYHJbvN88cynZbsAz2EMeZq73yx', ); - expect(mockAssetsService.getAccountAssetByID).toHaveBeenCalledWith( - TEST_ACCOUNT_ID, - nativeAssetId, - ); - expect(mockAssetsService.getAccountAssetsByIDs).toHaveBeenCalledWith( - TEST_ACCOUNT_ID, - [Networks[scope].bandwidth.id, Networks[scope].energy.id], + expect( + mockAssetsService.getFreshAccountAssetsByIDs, + ).toHaveBeenCalledWith( + expect.objectContaining({ id: TEST_ACCOUNT_ID }), + [ + nativeAssetId, + Networks[scope].bandwidth.id, + Networks[scope].energy.id, + ], ); // computeFee receives unsigned transaction (no signature field) expect(mockFeeCalculatorService.computeFee).toHaveBeenCalledWith({ @@ -2554,9 +2564,9 @@ describe('ClientRequestHandler - computeStakeFee', () => { } as any); // Account has only 5 TRX - mockAssetsService.getAccountAssetByID.mockResolvedValue({ - uiAmount: '5', - } as AssetEntity); + mockAssetsService.getFreshAccountAssetsByIDs.mockResolvedValue([ + { uiAmount: '5' }, // Native TRX + ] as unknown as AssetEntity[]); const result = await handler.handle(request as JsonRpcRequest); @@ -2612,7 +2622,9 @@ describe('ClientRequestHandler - confirmSend validation', () => { uiAmount: '100', rawAmount: '100000000', } as NativeAsset; - mockAssetsService.getAccountAssetByID.mockResolvedValue(mockAsset); + mockAssetsService.getFreshAccountAssetsByIDs.mockResolvedValue([ + mockAsset, + ]); // validateSend returns insufficient balance mockSendService.validateSend.mockResolvedValue({ @@ -2681,7 +2693,9 @@ describe('ClientRequestHandler - confirmSend validation', () => { uiAmount: '100', rawAmount: '100000000', } as NativeAsset; - mockAssetsService.getAccountAssetByID.mockResolvedValue(mockAsset); + mockAssetsService.getFreshAccountAssetsByIDs.mockResolvedValue([ + mockAsset, + ]); // validateSend returns insufficient balance to cover fee mockSendService.validateSend.mockResolvedValue({ @@ -2743,13 +2757,12 @@ describe('ClientRequestHandler - confirmSend validation', () => { uiAmount: '100', rawAmount: '100000000', } as NativeAsset; - mockAssetsService.getAccountAssetByID.mockResolvedValue(mockAsset); - // validateSend returns valid. mockSendService.validateSend.mockResolvedValue({ valid: true }); // Mock the rest of the flow. - mockAssetsService.getAccountAssetsByIDs.mockResolvedValue([ + mockAssetsService.getFreshAccountAssetsByIDs.mockResolvedValue([ + mockAsset, { rawAmount: '1000' }, // Bandwidth { rawAmount: '50000' }, // Energy ] as any); @@ -2905,9 +2918,9 @@ describe('ClientRequestHandler - confirmSend validation', () => { } as any); // Asset not found - (mockAssetsService.getAccountAssetByID as jest.Mock).mockResolvedValue( - null, - ); + ( + mockAssetsService.getFreshAccountAssetsByIDs as jest.Mock + ).mockResolvedValue([null, null, null]); const result = await handler.handle(request); @@ -2967,24 +2980,6 @@ describe('ClientRequestHandler - claimUnstakedTrx', () => { ); }); - it('throws InvalidParamsError for a non-Tron asset chain', async () => { - await withClientRequestHandler(async ({ handler }) => { - const request = { - jsonrpc: '2.0' as const, - id: '1', - method: ClientRequestMethod.ClaimUnstakedTrx, - params: { - fromAccountId: TEST_ACCOUNT_ID, - assetId: 'eip155:1/slip44:60', - }, - }; - - await expect(handler.handle(request)).rejects.toThrow( - 'Invalid method parameter(s)', - ); - }); - }); - it('throws when user rejects the confirmation', async () => { await withClientRequestHandler( async ({ @@ -3040,6 +3035,28 @@ describe('ClientRequestHandler - claimUnstakedTrx', () => { ); }); }); + + it('rejects foreign CAIP asset IDs at the boundary', async () => { + await withClientRequestHandler(async ({ handler, mockAccountsService }) => { + mockAccountsService.findById.mockResolvedValue(mockAccount); + + const request = { + jsonrpc: '2.0' as const, + id: '1', + method: ClientRequestMethod.OnAmountInput, + params: { + accountId: TEST_ACCOUNT_ID, + assetId: 'eip155:1/slip44:60', + value: '10', + }, + }; + + await expect(handler.handle(request)).resolves.toStrictEqual({ + valid: false, + errors: [{ code: SendErrorCodes.Invalid }], + }); + }); + }); }); describe('ClientRequestHandler - claimTrxStakingRewards', () => { @@ -3091,24 +3108,6 @@ describe('ClientRequestHandler - claimTrxStakingRewards', () => { ); }); }); - - it('throws InvalidParamsError for a non-Tron asset chain', async () => { - await withClientRequestHandler(async ({ handler }) => { - const request = { - jsonrpc: '2.0' as const, - id: '1', - method: 'claimTrxStakingRewards', - params: { - fromAccountId: TEST_ACCOUNT_ID, - assetId: 'eip155:1/slip44:60', - }, - }; - - await expect(handler.handle(request)).rejects.toThrow( - 'Invalid method parameter(s)', - ); - }); - }); }); describe('ClientRequestHandler - onAddressInput', () => { diff --git a/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts b/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts index 9a355f88a..21fa43d62 100644 --- a/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts +++ b/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts @@ -1,6 +1,10 @@ import { TransactionStatus } from '@metamask/keyring-api'; import { normalizeError } from '@metamask/snap-networks-utils'; -import type { AnalyticsService, Logger } from '@metamask/snap-networks-utils'; +import type { + AnalyticsService, + ExtendedKeyringAccount, + Logger, +} from '@metamask/snap-networks-utils'; import type { Json, JsonRpcRequest } from '@metamask/snaps-sdk'; import { InvalidParamsError, @@ -8,28 +12,21 @@ import { UserRejectedRequestError, } from '@metamask/snaps-sdk'; import { assert } from '@metamask/superstruct'; -import { - bytesToHex, - hexToBytes, - parseCaipAssetType, - sha256, -} from '@metamask/utils'; +import { bytesToHex, hexToBytes, sha256 } from '@metamask/utils'; +import { parseTronCaipAssetType } from '../../utils/caip'; import { BigNumber } from 'bignumber.js'; import type { TronWeb, Types as TronwebTypes } from 'tronweb'; import type { SnapClient } from '../../clients/snap/SnapClient'; import type { TronWebFactory } from '../../clients/tronweb/TronWebFactory'; import { - assertSupportedNetwork, FALLBACK_FEE, FEE_LIMIT, - METAMASK_ORIGIN, Network, Networks, TRACK_TX_INTERVAL, ZERO, } from '../../constants'; -import type { TronKeyringAccount } from '../../entities/keyringAccount'; import { isDerivedTronKeypair } from '../../services/accounts/AccountsService'; import type { AccountsService } from '../../services/accounts/AccountsService'; import type { AssetsService } from '../../services/assets/AssetsService'; @@ -86,7 +83,7 @@ type TransactionRawData = TronwebTypes.Transaction['raw_data'] & { type SigningRequest = { index: number; accountId: string; - account: TronKeyringAccount; + account: ExtendedKeyringAccount; message: string; }; @@ -116,9 +113,9 @@ function getUniqueAccountIds(items: { accountId: string }[]): string[] { } function getAccountsByNormalizedId( - accounts: TronKeyringAccount[], -): Map { - const accountsByNormalizedId = new Map(); + accounts: ExtendedKeyringAccount[], +): Map { + const accountsByNormalizedId = new Map(); accounts.forEach((account) => { accountsByNormalizedId.set(account.id.toLowerCase(), account); @@ -136,7 +133,7 @@ function validateSigningRequest( message: string; }, index: number, - accountsById: Map, + accountsById: Map, ): SigningRequestValidation { const account = accountsById.get(accountId.toLowerCase()); if (account === undefined) { @@ -411,15 +408,14 @@ export class ClientRequestHandler { await this.#transactionsService.save(pendingTransaction); /** - * Client requests come from MetaMask's own unified send flow, matching the - * unified send path and the background transaction tracker. The origin is - * lowercased so it is recognized as MetaMask by the security alerts scan - * and stays consistent with the other non-EVM snaps. + * Origin is 'MetaMask' because client requests come from MetaMask's own + * unified send flow, matching the unified send path and the background + * transaction tracker. */ const transactionType = mapRawTransactionType(rawData); await this.#analyticsService.trackTransactionSubmitted({ - origin: METAMASK_ORIGIN, + origin: 'MetaMask', accountType: account.type, chainIdCaip: scope, transactionType, @@ -495,11 +491,10 @@ export class ClientRequestHandler { /** * Check if we have enough of the asset we want to send... */ - const { chainId } = parseCaipAssetType(assetId); - const scope = assertSupportedNetwork(chainId); + const { chainId: scope } = parseTronCaipAssetType(assetId); const [asset, nativeTokenAsset, bandwidthAsset, energyAsset] = - await this.#assetsService.getAccountAssetsByIDs(accountId, [ + await this.#assetsService.getFreshAccountAssetsByIDs(account, [ assetId, Networks[scope].nativeToken.id, Networks[scope].bandwidth.id, @@ -612,10 +607,18 @@ export class ClientRequestHandler { }; } - const asset = await this.#assetsService.getAccountAssetByID( - fromAccountId, - assetId, - ); + const { chainId: scope } = parseTronCaipAssetType(assetId); + + /** + * Fetch the sent asset and the account resources fresh, so the + * validation and fee estimation below never act on stale balances. + */ + const [asset, bandwidthAsset, energyAsset] = + await this.#assetsService.getFreshAccountAssetsByIDs(account, [ + assetId, + Networks[scope].bandwidth.id, + Networks[scope].energy.id, + ]); if (!asset) { return { @@ -624,9 +627,6 @@ export class ClientRequestHandler { }; } - const { chainId } = parseCaipAssetType(assetId); - const scope = assertSupportedNetwork(chainId); - const amountBN = new BigNumber(amount); /** @@ -652,27 +652,13 @@ export class ClientRequestHandler { }; } - const [[bandwidthAsset, energyAsset], transaction] = await Promise.all([ - /** - * Get available Energy and Bandwidth from account assets. - */ - this.#assetsService.getAccountAssetsByIDs(fromAccountId, [ - Networks[scope].bandwidth.id, - Networks[scope].energy.id, - ]), - /** - * Build the unsigned transaction. - * Fee estimation uses a constant overhead for the signature (134 bytes). - * Signing happens after user confirmation in sendTransaction(). - */ - this.#sendService.buildTransaction({ - fromAccountId, - toAddress, - asset, - amount: amountBN, - feeLimit: FEE_LIMIT, - }), - ]); + const transaction = await this.#sendService.buildTransaction({ + fromAccountId, + toAddress, + asset, + amount: amountBN, + feeLimit: FEE_LIMIT, + }); const availableEnergy = energyAsset ? new BigNumber(energyAsset.rawAmount) @@ -697,8 +683,7 @@ export class ClientRequestHandler { /** * Show the confirmation UI. - * Client requests come from MetaMask's own unified send flow, so the origin - * is reported as MetaMask. + * Origin is 'MetaMask' because client requests come from MetaMask's own unified send flow. */ const confirmed = await this.#confirmationHandler.confirmTransactionRequest( { @@ -709,7 +694,7 @@ export class ClientRequestHandler { fees, asset, accountType: account.type, - origin: METAMASK_ORIGIN, + origin: 'MetaMask', transactionRawData: freshTransactionRawData, }, ); @@ -775,7 +760,7 @@ export class ClientRequestHandler { * Recreate the transaction object from base64-encoded raw data. * No signing needed - fee calculation uses constant overhead for signature. */ - await this.#accountsService.findByIdOrThrow(accountId); + const account = await this.#accountsService.findByIdOrThrow(accountId); const tronWeb = this.#tronWebFactory.createClient(scope); @@ -800,10 +785,11 @@ export class ClientRequestHandler { }; /** - * Get available Energy and Bandwidth from account assets. + * Get available Energy and Bandwidth from account assets, fetched fresh + * so the fee breakdown reflects the latest account resources. */ const [bandwidthAsset, energyAsset] = - await this.#assetsService.getAccountAssetsByIDs(accountId, [ + await this.#assetsService.getFreshAccountAssetsByIDs(account, [ Networks[scope].bandwidth.id, Networks[scope].energy.id, ]); @@ -858,10 +844,12 @@ export class ClientRequestHandler { const scope = Network.Mainnet; - const asset = await this.#assetsService.getAccountAssetByID( - fromAccountId, - Networks[scope].nativeToken.id, - ); + const [asset, bandwidthAsset, energyAsset] = + await this.#assetsService.getFreshAccountAssetsByIDs(account, [ + Networks[scope].nativeToken.id, + Networks[scope].bandwidth.id, + Networks[scope].energy.id, + ]); const accountBalance = asset ? new BigNumber(asset.uiAmount) : ZERO; const requestBalance = BigNumber(value); @@ -885,15 +873,6 @@ export class ClientRequestHandler { account.address, ); - /** - * Get available Energy and Bandwidth from account assets. - */ - const [bandwidthAsset, energyAsset] = - await this.#assetsService.getAccountAssetsByIDs(fromAccountId, [ - Networks[scope].bandwidth.id, - Networks[scope].energy.id, - ]); - const availableEnergy = energyAsset ? BigNumber(energyAsset.rawAmount) : ZERO; @@ -933,9 +912,9 @@ export class ClientRequestHandler { const { accountId, assetId, value } = request.params; - await this.#accountsService.findByIdOrThrow(accountId); - const asset = await this.#assetsService.getAccountAssetByID( - accountId, + const account = await this.#accountsService.findByIdOrThrow(accountId); + const asset = await this.#assetsService.getFreshAccountAssetByID( + account, assetId, ); @@ -981,8 +960,8 @@ export class ClientRequestHandler { const account = await this.#accountsService.findByIdOrThrow(fromAccountId); - const asset = await this.#assetsService.getAccountAssetByID( - fromAccountId, + const asset = await this.#assetsService.getFreshAccountAssetByID( + account, assetId, ); @@ -1042,9 +1021,9 @@ export class ClientRequestHandler { */ const stakedAssetId = `${assetId}-staked-for-${purpose.toLowerCase()}`; - await this.#accountsService.findByIdOrThrow(accountId); - const asset = await this.#assetsService.getAccountAssetByID( - accountId, + const account = await this.#accountsService.findByIdOrThrow(accountId); + const asset = await this.#assetsService.getFreshAccountAssetByID( + account, stakedAssetId, ); @@ -1097,8 +1076,8 @@ export class ClientRequestHandler { const account = await this.#accountsService.findByIdOrThrow(accountId); - const asset = await this.#assetsService.getAccountAssetByID( - accountId, + const asset = await this.#assetsService.getFreshAccountAssetByID( + account, stakedAssetId, ); @@ -1151,8 +1130,7 @@ export class ClientRequestHandler { const account = await this.#accountsService.findByIdOrThrow(fromAccountId); - const { chainId } = parseCaipAssetType(assetId); - const scope = assertSupportedNetwork(chainId); + const { chainId: scope } = parseTronCaipAssetType(assetId); const confirmed = await this.#confirmationHandler.confirmClaimUnstakedTrx({ account, @@ -1189,8 +1167,7 @@ export class ClientRequestHandler { const account = await this.#accountsService.findByIdOrThrow(fromAccountId); - const { chainId } = parseCaipAssetType(assetId); - const scope = assertSupportedNetwork(chainId); + const { chainId: scope } = parseTronCaipAssetType(assetId); await this.#stakingService.claimTrxStakingRewards({ account, scope }); @@ -1336,7 +1313,7 @@ export class ClientRequestHandler { items.length, ); const signingRequests: SigningRequest[] = []; - const accountsToDerive: TronKeyringAccount[] = []; + const accountsToDerive: ExtendedKeyringAccount[] = []; items.forEach((item, index) => { const validationResult = validateSigningRequest( diff --git a/packages/tron-wallet-snap/src/services/assets/AssetsService.ts b/packages/tron-wallet-snap/src/services/assets/AssetsService.ts index 181a0555e..dc6a30d69 100644 --- a/packages/tron-wallet-snap/src/services/assets/AssetsService.ts +++ b/packages/tron-wallet-snap/src/services/assets/AssetsService.ts @@ -8,6 +8,7 @@ import type { KeyringAccount } from '@metamask/keyring-api'; import type { RemoteFeatureFlagsProvider } from '@metamask/snap-networks-utils'; import type { Network } from '../../constants'; +import { parseTronCaipAssetType } from '../../utils/caip'; import type { AssetEntity } from '../../entities/assets'; import type { CoreAssetsAdapter } from './adapters/CoreAssetsAdapter'; import { SnapAssetsAdapter } from './adapters/SnapAssetsAdapter'; @@ -57,6 +58,10 @@ export class AssetsService { return []; } + assetIds.forEach((assetId) => { + parseTronCaipAssetType(assetId); + }); + if (await this.#shouldReturnAssetsFromCore()) { const assets = await this.#coreAdapter.getAccountAssetsByIDs( accountId, @@ -72,6 +77,8 @@ export class AssetsService { accountId: string, assetId: string, ): Promise { + parseTronCaipAssetType(assetId); + if (await this.#shouldReturnAssetsFromCore()) { const asset = await this.#coreAdapter.getAccountAssetByID( accountId, @@ -109,4 +116,66 @@ export class AssetsService { return this.#snapAdapter.getAccountAssets(accountId); } + + /** + * Fetches fresh (up-to-date) assets for the requested asset IDs, for flows + * that must not act on stale balances. + * + * When the assets migration is active, the Core adapter combines the + * controller's one-time fetch with a direct Tron RPC sync of snap-owned + * assets. Otherwise, the Snap's own sync flow refreshes and persists the + * latest values before reading them. + * + * @param account - The keyring account to fetch assets for. + * @param assetIds - CAIP-19 asset IDs to resolve fresh values for. + * @returns Assets in the same order as the requested asset IDs, with `null` + * for asset IDs that could not be resolved. + */ + async getFreshAccountAssetsByIDs( + account: KeyringAccount, + assetIds: string[], + ): Promise<(AssetEntity | null)[]> { + if (assetIds.length === 0) { + return []; + } + + if (await this.#shouldReturnAssetsFromCore()) { + return this.#coreAdapter.getFreshAccountAssetsByIDs( + account, + assetIds as Caip19AssetId[], + ); + } + + const scopes = [ + ...new Set( + assetIds.map((assetId) => parseTronCaipAssetType(assetId).chainId), + ), + ] as Network[]; + + const freshAssets = ( + await Promise.all( + scopes.map((scope) => + this.#snapAdapter.fetchAssetsAndBalancesForAccount(scope, account), + ), + ) + ).flat(); + await this.#snapAdapter.saveMany(freshAssets); + + return this.#snapAdapter.getAccountAssetsByIDs(account.id, assetIds); + } + + /** + * Fetches a single fresh asset for the given asset ID. + * + * @param account - The keyring account to fetch the asset for. + * @param assetId - CAIP-19 asset ID to resolve a fresh value for. + * @returns The fresh asset, or `null` if it could not be resolved. + */ + async getFreshAccountAssetByID( + account: KeyringAccount, + assetId: string, + ): Promise { + const [asset] = await this.getFreshAccountAssetsByIDs(account, [assetId]); + return asset ?? null; + } } diff --git a/packages/tron-wallet-snap/src/services/assets/adapters/CoreAssetsAdapter.ts b/packages/tron-wallet-snap/src/services/assets/adapters/CoreAssetsAdapter.ts index 5a2382909..bdc1e6952 100644 --- a/packages/tron-wallet-snap/src/services/assets/adapters/CoreAssetsAdapter.ts +++ b/packages/tron-wallet-snap/src/services/assets/adapters/CoreAssetsAdapter.ts @@ -5,9 +5,13 @@ import type { AccountBalancesUpdatedEvent, KeyringAccount, } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import { emitSnapKeyringEvent } from '@metamask/keyring-snap-sdk'; import type { AssetsProvider } from '@metamask/snap-networks-utils'; import { Logger } from '@metamask/snap-networks-utils'; +import type { CaipChainId } from '@metamask/utils'; + +import { parseTronCaipAssetType } from '../../../utils/caip'; import type { TronHttpClient } from '../../../clients/tron-http/TronHttpClient'; import { TrongridAccountNotFoundError } from '../../../clients/trongrid/errors'; @@ -29,11 +33,39 @@ export type CoreAssetsAdapterOptions = { getAccountAssetByID: AssetsProvider['getAccountAssetByID']; getAccountAssetsByIDs: AssetsProvider['getAccountAssetsByIDs']; getAccountAssetsByScope: AssetsProvider['getAccountAssetsByScope']; + getAssets: AssetsProvider['getAssets']; getAddressInfo: TrongridApiClient['getAccountInfoByAddress']; getAddressResources: TronHttpClient['getAccountResources']; getAddressStakingRewards: TronHttpClient['getReward']; }; +/** + * Builds the account descriptor the controller's one-time fetch pipeline + * expects. The controller only consumes `id`, `address`, and `scopes` from it, + * so the keyring account's shape is sufficient and the metadata is a stub. + * + * @param account - The keyring account. + * @param scopes - The CAIP-2 chain IDs to scope the fetch to. + * @returns An InternalAccount-compatible descriptor. + */ +function toInternalAccount( + account: KeyringAccount, + scopes: CaipChainId[], +): InternalAccount { + return { + id: account.id, + address: account.address, + type: account.type, + scopes, + methods: account.methods, + options: account.options, + metadata: { + name: 'Tron Account', + keyring: { type: 'Snap Keyring' }, + }, + } as unknown as InternalAccount; +} + /** * Uses the AssetsController for fungible reads. Snap-owned (special) assets are * published via keyring events without local persistence when migration is active. @@ -47,6 +79,8 @@ export class CoreAssetsAdapter { readonly #getAccountAssetsByScope: AssetsProvider['getAccountAssetsByScope']; + readonly #getAssets: AssetsProvider['getAssets']; + readonly #getAddressInfo: TrongridApiClient['getAccountInfoByAddress']; readonly #getAddressResources: TronHttpClient['getAccountResources']; @@ -58,6 +92,7 @@ export class CoreAssetsAdapter { getAccountAssetByID, getAccountAssetsByIDs, getAccountAssetsByScope, + getAssets, getAddressInfo, getAddressResources, getAddressStakingRewards, @@ -67,6 +102,7 @@ export class CoreAssetsAdapter { this.#getAccountAssetByID = getAccountAssetByID; this.#getAccountAssetsByIDs = getAccountAssetsByIDs; this.#getAccountAssetsByScope = getAccountAssetsByScope; + this.#getAssets = getAssets; this.#getAddressInfo = getAddressInfo; this.#getAddressResources = getAddressResources; this.#getAddressStakingRewards = getAddressStakingRewards; @@ -136,6 +172,74 @@ export class CoreAssetsAdapter { return allAssets; } + /** + * Fetches fresh assets for the requested asset IDs, guaranteeing up-to-date + * data for flows that act on-chain or display actionable values. + * + * Combines both freshness strategies: + * - Controller-tracked assets (native TRX, TRC20) come from the controller's + * one-time fetch pipeline (`AssetsController:getAssets` with + * `forceUpdate` and `bypassServerCache`), reading the fresh fetch result + * directly. + * - Snap-owned assets (staking positions, energy, bandwidth) are fetched + * directly from Tron RPC via the Snap's own sync flow, since the + * controller only sees them through asynchronously published updates. + * + * @param account - The keyring account to fetch assets for. + * @param assetIds - CAIP-19 asset IDs to resolve fresh values for. + * @returns Assets keyed in the same order as the requested asset IDs, with + * `null` for asset IDs that could not be resolved. + */ + async getFreshAccountAssetsByIDs( + account: KeyringAccount, + assetIds: Caip19AssetId[], + ): Promise<(AssetEntity | null)[]> { + const scopes = Array.from( + new Set(assetIds.map((assetId) => parseTronCaipAssetType(assetId).chainId)), + ) as Network[]; + + const [snapOwnedFetches, controllerAssets] = await Promise.all([ + Promise.all( + scopes.map((scope) => + this.fetchAssetsAndBalancesForAccount(scope as Network, account), + ), + ), + this.#getAssets([toInternalAccount(account, scopes)], { + chainIds: scopes, + forceUpdate: true, + bypassServerCache: true, + }), + ]); + + const snapOwnedAssets = snapOwnedFetches.flat(); + + return assetIds.map((assetId) => { + if (isSnapOwnedAsset(assetId)) { + return ( + snapOwnedAssets.find((asset) => asset.assetType === assetId) ?? null + ); + } + + const asset = controllerAssets[account.id]?.[assetId]; + return asset ? mapControllerAsset(account.id, asset) : null; + }); + } + + /** + * Fetches a single fresh asset for the given asset ID. + * + * @param account - The keyring account to fetch the asset for. + * @param assetId - CAIP-19 asset ID to resolve a fresh value for. + * @returns The fresh asset, or `null` if it could not be resolved. + */ + async getFreshAccountAssetByID( + account: KeyringAccount, + assetId: Caip19AssetId, + ): Promise { + const [asset] = await this.getFreshAccountAssetsByIDs(account, [assetId]); + return asset ?? null; + } + /** * We used to fetch all assets and balances but now the Snap is only responsible for fetching snap-owned assets. * - Energy & Bandwidth diff --git a/packages/tron-wallet-snap/src/services/assets/adapters/SnapAssetsAdapter.ts b/packages/tron-wallet-snap/src/services/assets/adapters/SnapAssetsAdapter.ts index cb9e97a52..25b71768b 100644 --- a/packages/tron-wallet-snap/src/services/assets/adapters/SnapAssetsAdapter.ts +++ b/packages/tron-wallet-snap/src/services/assets/adapters/SnapAssetsAdapter.ts @@ -8,6 +8,7 @@ import { emitSnapKeyringEvent } from '@metamask/keyring-snap-sdk'; import type { IStateManager, Logger } from '@metamask/snap-networks-utils'; import type { AssetMetadata, FungibleAssetMetadata } from '@metamask/snaps-sdk'; import type { CaipAssetType } from '@metamask/utils'; +import { parseTronCaipAssetType } from '../../../utils/caip'; import type { PriceApiClient } from '../../../clients/price-api/PriceApiClient'; import type { SpotPrices } from '../../../clients/price-api/types'; @@ -39,7 +40,6 @@ import { } from '../../../constants'; import type { AssetEntity } from '../../../entities/assets'; import { toUiAmount } from '../../../utils/conversion'; -import { getAssetNetwork } from '../../../utils/getAssetNetwork'; import type { UnencryptedStateValue } from '../../state/stateTypes'; import type { AssetsRepository } from '../AssetsRepository'; import type { @@ -1118,11 +1118,12 @@ export class SnapAssetsAdapter { keyringAccountId: string, ): AssetEntity { const metadata = TokenMetadata[assetId as keyof typeof TokenMetadata]; + const { chainId: network } = parseTronCaipAssetType(assetId); return { assetType: metadata.id, keyringAccountId, - network: getAssetNetwork(assetId), + network, symbol: metadata.symbol, decimals: metadata.decimals, rawAmount: '0', diff --git a/packages/tron-wallet-snap/src/services/assets/utils/mapControllerAsset.ts b/packages/tron-wallet-snap/src/services/assets/utils/mapControllerAsset.ts index 6f2ac6472..ddc84c491 100644 --- a/packages/tron-wallet-snap/src/services/assets/utils/mapControllerAsset.ts +++ b/packages/tron-wallet-snap/src/services/assets/utils/mapControllerAsset.ts @@ -1,6 +1,6 @@ import type { Asset } from '@metamask/assets-controller'; -import { assertSupportedNetwork } from '../../../constants'; +import type { Network } from '../../../constants'; import type { AssetEntity } from '../../../entities/assets'; import { toRawAmount } from '../../../utils/conversion'; @@ -15,7 +15,6 @@ import { toRawAmount } from '../../../utils/conversion'; * @param accountId - Keyring account ID. * @param asset - Asset returned by AssetsController. * @returns Mapped asset entity. - * @throws If the asset is not on a supported network. */ export function mapControllerAsset( accountId: string, @@ -30,7 +29,7 @@ export function mapControllerAsset( return { assetType: assetId, keyringAccountId: accountId, - network: assertSupportedNetwork(asset.chainId), + network: asset.chainId as Network, symbol, decimals, rawAmount: toRawAmount(amount, decimals), diff --git a/packages/tron-wallet-snap/src/services/send/SendService.ts b/packages/tron-wallet-snap/src/services/send/SendService.ts index 1e6c6a326..c53bb8a4e 100644 --- a/packages/tron-wallet-snap/src/services/send/SendService.ts +++ b/packages/tron-wallet-snap/src/services/send/SendService.ts @@ -1,17 +1,12 @@ import type { AnalyticsService, Logger } from '@metamask/snap-networks-utils'; -import { parseCaipAssetType } from '@metamask/utils'; +import { parseTronCaipAssetType } from '../../utils/caip'; import { BigNumber } from 'bignumber.js'; import type { TronWeb, Types as TronwebTypes } from 'tronweb'; import type { SnapClient } from '../../clients/snap/SnapClient'; import type { TronWebFactory } from '../../clients/tronweb/TronWebFactory'; import type { Network } from '../../constants'; -import { - METAMASK_ORIGIN, - Networks, - TRACK_TX_INTERVAL, - ZERO, -} from '../../constants'; +import { Networks, TRACK_TX_INTERVAL, ZERO } from '../../constants'; import type { AssetEntity } from '../../entities/assets'; import { SendErrorCodes } from '../../handlers/clientRequest/types'; import { BackgroundEventMethod } from '../../handlers/cronjob/cronjob'; @@ -233,10 +228,8 @@ export class SendService { | TronwebTypes.Transaction | TronwebTypes.Transaction > { - const { assetNamespace, assetReference } = parseCaipAssetType( - asset.assetType, - ); - const scope = asset.network; + const { chainId: scope, assetNamespace, assetReference } = + parseTronCaipAssetType(asset.assetType); try { switch (assetNamespace) { @@ -280,9 +273,7 @@ export class SendService { } catch (error) { this.#logger.error({ error }, 'Failed to send asset'); throw new Error( - `Failed to send asset: ${ - error instanceof Error ? error.message : 'Unknown error' - }`, + `Failed to send asset: ${error instanceof Error ? error.message : 'Unknown error'}`, ); } } @@ -391,7 +382,7 @@ export class SendService { scope, fromAccountId, transaction, - origin = METAMASK_ORIGIN, + origin = 'MetaMask', }: { scope: Network; fromAccountId: string; diff --git a/packages/tron-wallet-snap/src/services/staking/StakingService.test.ts b/packages/tron-wallet-snap/src/services/staking/StakingService.test.ts index 5086ebfd3..6ea1a7254 100644 --- a/packages/tron-wallet-snap/src/services/staking/StakingService.test.ts +++ b/packages/tron-wallet-snap/src/services/staking/StakingService.test.ts @@ -452,7 +452,7 @@ describe('StakingService', () => { assetId: invalidAssetId, amount, }), - ).rejects.toThrow('Invalid asset ID'); + ).rejects.toThrow('Expected the value to satisfy a union'); }); it('correctly derives keypair for unstaking', async () => { diff --git a/packages/tron-wallet-snap/src/services/staking/StakingService.ts b/packages/tron-wallet-snap/src/services/staking/StakingService.ts index cbb7a9a42..0927f7fda 100644 --- a/packages/tron-wallet-snap/src/services/staking/StakingService.ts +++ b/packages/tron-wallet-snap/src/services/staking/StakingService.ts @@ -2,6 +2,7 @@ import type { ExtendedKeyringAccount, Logger, } from '@metamask/snap-networks-utils'; +import { parseTronCaipAssetType } from '../../utils/caip'; import { BigNumber } from 'bignumber.js'; import type { Types as TronwebTypes } from 'tronweb'; @@ -11,7 +12,6 @@ import type { Network } from '../../constants'; import { CONSENSYS_SR_NODE_ADDRESS, KnownCaip19Id } from '../../constants'; import { trxToSun } from '../../utils/conversion'; import { executeOnChainActions } from '../../utils/executeOnChainActions'; -import { getAssetNetwork } from '../../utils/getAssetNetwork'; import type { AccountsService } from '../accounts/AccountsService'; import type { NativeCaipAssetType, StakedCaipAssetType } from '../assets/types'; @@ -58,15 +58,13 @@ export class StakingService { */ srNodeAddress?: string; }): Promise { - const scope = getAssetNetwork(assetId); + const { chainId: scope } = parseTronCaipAssetType(assetId); const amountInSun = Number(trxToSun(amount)); const availableVotes = amount.integerValue(BigNumber.ROUND_DOWN).toNumber(); const voteRecipient = srNodeAddress ?? CONSENSYS_SR_NODE_ADDRESS; this.#logger.info( - `Staking ${amount.toString()} ${assetId} for ${purpose} for ${ - account.address - } on ${scope}...`, + `Staking ${amount.toString()} ${assetId} for ${purpose} for ${account.address} on ${scope}...`, ); await executeOnChainActions({ @@ -98,7 +96,7 @@ export class StakingService { assetId: StakedCaipAssetType; amount: BigNumber; }): Promise { - const scope = getAssetNetwork(assetId); + const { chainId: scope } = parseTronCaipAssetType(assetId); /** * Check which resource we are unstaking. @@ -136,9 +134,7 @@ export class StakingService { const amountInSun = Number(trxToSun(amount)); this.#logger.info( - `Unstaking ${amount.toString()} ${assetId} for ${ - account.address - } on ${scope}...`, + `Unstaking ${amount.toString()} ${assetId} for ${account.address} on ${scope}...`, ); await executeOnChainActions({ diff --git a/packages/tron-wallet-snap/src/utils/caip.ts b/packages/tron-wallet-snap/src/utils/caip.ts new file mode 100644 index 000000000..34c6fd0d9 --- /dev/null +++ b/packages/tron-wallet-snap/src/utils/caip.ts @@ -0,0 +1,40 @@ +import { assert } from '@metamask/superstruct'; +import type { CaipAssetType } from '@metamask/utils'; +import { parseCaipAssetType } from '@metamask/utils'; + +import { Network } from '../constants'; +import { TronCaipAssetTypeStruct } from '../validation/structs'; +import type { + NativeCaipAssetType, + NftCaipAssetType, + ReadyForWithdrawalCaipAssetType, + ResourceCaipAssetType, + StakedCaipAssetType, + StakingRewardsCaipAssetType, + TokenCaipAssetType, +} from '../services/assets/types'; + +export type TronCaipAssetType = + | NativeCaipAssetType + | StakedCaipAssetType + | ReadyForWithdrawalCaipAssetType + | StakingRewardsCaipAssetType + | ResourceCaipAssetType + | TokenCaipAssetType + | NftCaipAssetType; + +export type ParsedTronCaipAssetType = ReturnType & { + chainId: Network; +}; + +export function parseTronCaipAssetType( + assetId: string, +): ParsedTronCaipAssetType { + assert(assetId, TronCaipAssetTypeStruct); + const parsed = parseCaipAssetType(assetId as CaipAssetType); + + return { + ...parsed, + chainId: parsed.chainId as Network, + }; +} diff --git a/packages/tron-wallet-snap/src/validation/structs.test.ts b/packages/tron-wallet-snap/src/validation/structs.test.ts index ce5aa02a6..614c33962 100644 --- a/packages/tron-wallet-snap/src/validation/structs.test.ts +++ b/packages/tron-wallet-snap/src/validation/structs.test.ts @@ -1,9 +1,22 @@ /* eslint-disable jest/require-to-throw-message */ import { assert, is } from '@metamask/superstruct'; +import { Network, Networks } from '../constants'; +import { parseTronCaipAssetType } from '../utils/caip'; import { Base58Struct, Base64Struct } from './structs'; describe('structs', () => { + describe('parseTronCaipAssetType', () => { + it('accepts Tron asset IDs and returns a local network scope', () => { + const parsed = parseTronCaipAssetType(Networks[Network.Mainnet].nativeToken.id); + + expect(parsed.chainId).toBe(Network.Mainnet); + }); + + it('rejects foreign CAIP asset IDs', () => { + expect(() => parseTronCaipAssetType('eip155:1/slip44:60')).toThrow(); + }); + }); describe('Base58Struct', () => { it('validates valid Base58 strings', () => { const validBase58Strings = [ From eecc11aee614f49495c2be28e90ca9acb13a1b96 Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Thu, 1 Oct 2026 17:28:29 +0100 Subject: [PATCH 6/7] refactor(tron-wallet-snap): rename tron keyring account --- .../entities/{keyringAccount.ts => tronKeyringAccount.ts} | 4 ---- .../src/handlers/clientRequest/clientRequest.test.ts | 5 ----- .../tron-wallet-snap/src/handlers/keyring/keyring.test.ts | 2 +- packages/tron-wallet-snap/src/handlers/keyring/keyring.ts | 2 +- .../src/services/accounts/AccountsRepository.test.ts | 2 +- .../src/services/accounts/AccountsRepository.ts | 2 +- .../src/services/accounts/AccountsService.test.ts | 2 +- .../src/services/accounts/AccountsService.ts | 2 +- packages/tron-wallet-snap/src/services/state/stateTypes.ts | 2 +- .../src/services/wallet/WalletService.test.ts | 2 +- .../tron-wallet-snap/src/services/wallet/WalletService.ts | 2 +- 11 files changed, 9 insertions(+), 18 deletions(-) rename packages/tron-wallet-snap/src/entities/{keyringAccount.ts => tronKeyringAccount.ts} (58%) diff --git a/packages/tron-wallet-snap/src/entities/keyringAccount.ts b/packages/tron-wallet-snap/src/entities/tronKeyringAccount.ts similarity index 58% rename from packages/tron-wallet-snap/src/entities/keyringAccount.ts rename to packages/tron-wallet-snap/src/entities/tronKeyringAccount.ts index f62ad2148..2c4050d8d 100644 --- a/packages/tron-wallet-snap/src/entities/keyringAccount.ts +++ b/packages/tron-wallet-snap/src/entities/tronKeyringAccount.ts @@ -2,10 +2,6 @@ import type { ExtendedKeyringAccount } from '@metamask/snap-networks-utils'; import type { Network } from '../constants'; -/** - * A keyring account as stored by the snap, with its scopes narrowed to the - * supported networks. The Keyring API receives it as a `KeyringAccount`. - */ export type TronKeyringAccount = Omit & { scopes: Network[]; }; 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 5e1740b7c..e410fcf9a 100644 --- a/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.test.ts +++ b/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.test.ts @@ -1,17 +1,12 @@ -<<<<<<< HEAD import { FeeType, TransactionType, TrxAccountType, } from '@metamask/keyring-api'; -import type { AnalyticsService } from '@metamask/snap-networks-utils'; -======= -import { FeeType, TrxAccountType } from '@metamask/keyring-api'; import type { AnalyticsService, ExtendedKeyringAccount, } from '@metamask/snap-networks-utils'; ->>>>>>> 53b044eb (fix(tron-wallet-snap): validate tron caip asset ids) import type { JsonRpcRequest } from '@metamask/snaps-sdk'; import type { Infer } from '@metamask/superstruct'; import { BigNumber } from 'bignumber.js'; diff --git a/packages/tron-wallet-snap/src/handlers/keyring/keyring.test.ts b/packages/tron-wallet-snap/src/handlers/keyring/keyring.test.ts index f225aeaa5..8f6db4227 100644 --- a/packages/tron-wallet-snap/src/handlers/keyring/keyring.test.ts +++ b/packages/tron-wallet-snap/src/handlers/keyring/keyring.test.ts @@ -12,7 +12,7 @@ import { import type { SnapClient } from '../../clients/snap/SnapClient'; import { Network } from '../../constants'; -import type { TronKeyringAccount } from '../../entities/keyringAccount'; +import type { TronKeyringAccount } from '../../entities/tronKeyringAccount'; import type { AccountsService } from '../../services/accounts/AccountsService'; import type { AssetsService } from '../../services/assets/AssetsService'; import type { ConfirmationHandler } from '../../services/confirmation/ConfirmationHandler'; diff --git a/packages/tron-wallet-snap/src/handlers/keyring/keyring.ts b/packages/tron-wallet-snap/src/handlers/keyring/keyring.ts index ca3d2ece1..cf5a8a1f2 100644 --- a/packages/tron-wallet-snap/src/handlers/keyring/keyring.ts +++ b/packages/tron-wallet-snap/src/handlers/keyring/keyring.ts @@ -36,7 +36,7 @@ import { sortBy } from 'lodash'; import type { SnapClient } from '../../clients/snap/SnapClient'; import { ESSENTIAL_ASSETS } from '../../constants'; -import type { TronKeyringAccount } from '../../entities/keyringAccount'; +import type { TronKeyringAccount } from '../../entities/tronKeyringAccount'; import { originPermissions } from '../../permissions'; import type { AccountsService } from '../../services/accounts/AccountsService'; import type { AssetsService } from '../../services/assets/AssetsService'; diff --git a/packages/tron-wallet-snap/src/services/accounts/AccountsRepository.test.ts b/packages/tron-wallet-snap/src/services/accounts/AccountsRepository.test.ts index 847b77ec6..ba49d747f 100644 --- a/packages/tron-wallet-snap/src/services/accounts/AccountsRepository.test.ts +++ b/packages/tron-wallet-snap/src/services/accounts/AccountsRepository.test.ts @@ -1,7 +1,7 @@ import { TrxAccountType, TrxScope } from '@metamask/keyring-api'; import { InMemoryState } from '@metamask/snap-networks-utils'; -import type { TronKeyringAccount } from '../../entities/keyringAccount'; +import type { TronKeyringAccount } from '../../entities/tronKeyringAccount'; import type { UnencryptedStateValue } from '../state/stateTypes'; import { AccountsRepository } from './AccountsRepository'; diff --git a/packages/tron-wallet-snap/src/services/accounts/AccountsRepository.ts b/packages/tron-wallet-snap/src/services/accounts/AccountsRepository.ts index 8f2dcdc15..45a26a253 100644 --- a/packages/tron-wallet-snap/src/services/accounts/AccountsRepository.ts +++ b/packages/tron-wallet-snap/src/services/accounts/AccountsRepository.ts @@ -1,7 +1,7 @@ import type { EntropySourceId } from '@metamask/keyring-api'; import type { IStateManager } from '@metamask/snap-networks-utils'; -import type { TronKeyringAccount } from '../../entities/keyringAccount'; +import type { TronKeyringAccount } from '../../entities/tronKeyringAccount'; import type { UnencryptedStateValue } from '../state/stateTypes'; /** diff --git a/packages/tron-wallet-snap/src/services/accounts/AccountsService.test.ts b/packages/tron-wallet-snap/src/services/accounts/AccountsService.test.ts index 76e967482..f53a68f1e 100644 --- a/packages/tron-wallet-snap/src/services/accounts/AccountsService.test.ts +++ b/packages/tron-wallet-snap/src/services/accounts/AccountsService.test.ts @@ -16,7 +16,7 @@ import { LogLevel } from '@metamask/snap-networks-utils'; import type { SnapClient } from '../../clients/snap/SnapClient'; import { Network } from '../../constants'; import type { NativeAsset } from '../../entities/assets'; -import type { TronKeyringAccount } from '../../entities/keyringAccount'; +import type { TronKeyringAccount } from '../../entities/tronKeyringAccount'; import { createTronBip44KeypairDeriver } from '../../utils/deriveTronFromCoinTypeNode'; import { trackError } from '../../utils/errors'; import { mockLogger } from '../../utils/mockLogger'; diff --git a/packages/tron-wallet-snap/src/services/accounts/AccountsService.ts b/packages/tron-wallet-snap/src/services/accounts/AccountsService.ts index 86b9805e1..8cb39ccb5 100644 --- a/packages/tron-wallet-snap/src/services/accounts/AccountsService.ts +++ b/packages/tron-wallet-snap/src/services/accounts/AccountsService.ts @@ -26,7 +26,7 @@ import snapManifest from '../../../snap.manifest.json'; import type { SnapClient } from '../../clients/snap/SnapClient'; import { assertSupportedNetwork } from '../../constants'; import type { Network } from '../../constants'; -import type { TronKeyringAccount } from '../../entities/keyringAccount'; +import type { TronKeyringAccount } from '../../entities/tronKeyringAccount'; import type { DerivedTronKeypair } from '../../utils/deriveTronFromCoinTypeNode'; import { createTronBip44AddressDeriver, diff --git a/packages/tron-wallet-snap/src/services/state/stateTypes.ts b/packages/tron-wallet-snap/src/services/state/stateTypes.ts index 49eeea5de..b9e214b2f 100644 --- a/packages/tron-wallet-snap/src/services/state/stateTypes.ts +++ b/packages/tron-wallet-snap/src/services/state/stateTypes.ts @@ -1,7 +1,7 @@ import type { Transaction } from '@metamask/keyring-api'; import type { AssetEntity } from '../../entities/assets'; -import type { TronKeyringAccount } from '../../entities/keyringAccount'; +import type { TronKeyringAccount } from '../../entities/tronKeyringAccount'; export type UnencryptedStateValue = { keyringAccounts: Record; diff --git a/packages/tron-wallet-snap/src/services/wallet/WalletService.test.ts b/packages/tron-wallet-snap/src/services/wallet/WalletService.test.ts index e1b71fed3..eee32d166 100644 --- a/packages/tron-wallet-snap/src/services/wallet/WalletService.test.ts +++ b/packages/tron-wallet-snap/src/services/wallet/WalletService.test.ts @@ -4,7 +4,7 @@ import { TronWeb } from 'tronweb'; import type { TronWebFactory } from '../../clients/tronweb/TronWebFactory'; import { Network } from '../../constants'; -import type { TronKeyringAccount } from '../../entities/keyringAccount'; +import type { TronKeyringAccount } from '../../entities/tronKeyringAccount'; import { TronMultichainErrors, TronMultichainMethod, diff --git a/packages/tron-wallet-snap/src/services/wallet/WalletService.ts b/packages/tron-wallet-snap/src/services/wallet/WalletService.ts index def24f382..b4dc82e58 100644 --- a/packages/tron-wallet-snap/src/services/wallet/WalletService.ts +++ b/packages/tron-wallet-snap/src/services/wallet/WalletService.ts @@ -6,7 +6,7 @@ import { bytesToHex, hexToBytes, sha256 } from '@metamask/utils'; import type { TronWebFactory } from '../../clients/tronweb/TronWebFactory'; import type { Network } from '../../constants'; -import type { TronKeyringAccount } from '../../entities/keyringAccount'; +import type { TronKeyringAccount } from '../../entities/tronKeyringAccount'; import { TronMultichainErrors, TronMultichainMethod, From 746f36f1a30bdb84f53fcc301b2e138e2a46bf32 Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Thu, 1 Oct 2026 17:33:01 +0100 Subject: [PATCH 7/7] refactor(tron-wallet-snap): move cronjob validation to handler --- packages/tron-wallet-snap/snap.manifest.json | 2 +- .../clientRequest/clientRequest.test.ts | 12 +----- .../src/handlers/cronjob/cronjob.test.tsx | 43 +++++++++---------- .../src/handlers/cronjob/cronjob.tsx | 8 +++- 4 files changed, 29 insertions(+), 36 deletions(-) diff --git a/packages/tron-wallet-snap/snap.manifest.json b/packages/tron-wallet-snap/snap.manifest.json index 005de45a3..5cc9ffac3 100644 --- a/packages/tron-wallet-snap/snap.manifest.json +++ b/packages/tron-wallet-snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/internal-snaps.git" }, "source": { - "shasum": "R0LTwZZs0pys5NIQLXqxoG1g3q0Bllj+ginLZD78rMk=" + "shasum": "R0LTwZZs0pys5NIQLXqxoG1g3q0Bllj+ginLZD78rMk=", "location": { "npm": { "filePath": "dist/bundle.js", 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 e410fcf9a..4744cc773 100644 --- a/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.test.ts +++ b/packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.test.ts @@ -118,12 +118,9 @@ type WithClientRequestHandlerCallback = (payload: { mockAssetsService: jest.Mocked< Pick< AssetsService, - | 'getAccountAssetsByIDs' - | 'getAccountAssetByID' | 'getFreshAccountAssetsByIDs' | 'getFreshAccountAssetByID' - > - >; + >; mockSendService: jest.Mocked< Pick< SendService, @@ -174,14 +171,10 @@ async function withClientRequestHandler( const mockAssetsService: jest.Mocked< Pick< AssetsService, - | 'getAccountAssetsByIDs' - | 'getAccountAssetByID' | 'getFreshAccountAssetsByIDs' | 'getFreshAccountAssetByID' > > = { - getAccountAssetsByIDs: jest.fn(), - getAccountAssetByID: jest.fn(), getFreshAccountAssetsByIDs: jest.fn(), getFreshAccountAssetByID: jest.fn(), }; @@ -335,7 +328,6 @@ describe('ClientRequestHandler', () => { } as unknown as jest.Mocked; mockAssetsService = { - getAccountAssetsByIDs: jest.fn(), getFreshAccountAssetsByIDs: jest.fn().mockResolvedValue([]), getFreshAccountAssetByID: jest.fn().mockResolvedValue(null), } as unknown as jest.Mocked; @@ -1776,7 +1768,7 @@ describe('ClientRequestHandler - signAndSendTransaction', () => { } as unknown as jest.Mocked; mockAssetsService = { - getAccountAssetsByIDs: jest.fn(), + getFreshAccountAssetsByIDs: jest.fn(), getFreshAccountAssetsByIDs: jest.fn().mockResolvedValue([]), getFreshAccountAssetByID: jest.fn().mockResolvedValue(null), } as unknown as jest.Mocked; 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 99287f068..41f538e54 100644 --- a/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.test.tsx +++ b/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.test.tsx @@ -977,30 +977,7 @@ describe('CronHandler', () => { }); } - it('tracks the transaction when the background event params are valid', async () => { - await withTrackTransactionCronHandler( - async ({ cronHandler, mockSnapClient, mockTronHttpClient }) => { - mockTronHttpClient.getTransactionInfoById.mockResolvedValue(null); - await cronHandler.handle({ - jsonrpc: '2.0', - id: 1, - method: BackgroundEventMethod.TrackTransaction, - params: { - txId: TX_ID, - scope: Network.Mainnet, - accountIds: ACCOUNT_IDS, - attempt: 0, - }, - }); - - expect( - mockTronHttpClient.getTransactionInfoById, - ).toHaveBeenCalledWith(Network.Mainnet, TX_ID); - expect(mockSnapClient.scheduleBackgroundEvent).toHaveBeenCalled(); - }, - ); - }); it('rejects background event params with an unsupported scope', async () => { await withTrackTransactionCronHandler( @@ -1025,6 +1002,26 @@ describe('CronHandler', () => { ); }); + it('tracks the transaction through the shared method when params are valid', async () => { + await withTrackTransactionCronHandler( + async ({ cronHandler, mockSnapClient, mockTronHttpClient }) => { + mockTronHttpClient.getTransactionInfoById.mockResolvedValue(null); + + await cronHandler.trackTransaction({ + txId: TX_ID, + scope: Network.Mainnet, + accountIds: ACCOUNT_IDS, + attempt: 0, + }); + + expect( + mockTronHttpClient.getTransactionInfoById, + ).toHaveBeenCalledWith(Network.Mainnet, TX_ID); + expect(mockSnapClient.scheduleBackgroundEvent).toHaveBeenCalled(); + }, + ); + }); + it('schedules next attempt when transaction is not yet confirmed', async () => { await withTrackTransactionCronHandler( async ({ cronHandler, mockSnapClient, mockTronHttpClient }) => { diff --git a/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.tsx b/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.tsx index 668a75bfb..f21e4c8f4 100644 --- a/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.tsx +++ b/packages/tron-wallet-snap/src/handlers/cronjob/cronjob.tsx @@ -144,8 +144,7 @@ export class CronHandler { await this.refreshSignTransaction(); break; case BackgroundEventMethod.TrackTransaction: - validateRequest(params, TrackTransactionParamsStruct); - await this.trackTransaction(params); + await this.handleTrackTransaction(request); break; default: throw new Error(`Unknown cronjob method: ${method}`); @@ -649,6 +648,11 @@ export class CronHandler { } } + async handleTrackTransaction(request: JsonRpcRequest): Promise { + validateRequest(request.params, TrackTransactionParamsStruct); + await this.trackTransaction(request.params); + } + /** * Background job to track a transaction's confirmation status. * Continues polling until confirmed, then syncs accounts and emits finalized event.