From 65c376b820568f1e9a562eccd1f46f494107d8f4 Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Mon, 10 Aug 2026 15:10:19 +0100 Subject: [PATCH 1/5] refactor(tron-wallet-snap): extract shared asset utils and inject SnapAssetsAdapter Move snap-owned extraction helpers into shared utils, wire SnapAssetsAdapter from context into AssetsService, and rename getByKeyringAccountId to getAccountAssets. --- packages/tron-wallet-snap/CHANGELOG.md | 5 + packages/tron-wallet-snap/snap.manifest.json | 2 +- packages/tron-wallet-snap/src/context.ts | 12 +- .../src/handlers/keyring/keyring.test.ts | 4 +- .../src/handlers/keyring/keyring.ts | 5 +- .../src/services/assets/AssetsService.test.ts | 27 +- .../src/services/assets/AssetsService.ts | 49 +-- .../assets/adapters/SnapAssetsAdapter.ts | 347 ++---------------- .../src/services/assets/snapOwnedAssets.ts | 18 - .../src/services/assets/types.ts | 7 + .../assets/utils/buildAccountResources.ts | 15 + .../services/assets/utils/buildStakedData.ts | 28 ++ .../services/assets/utils/extractBandwidth.ts | 58 +++ .../services/assets/utils/extractEnergy.ts | 56 +++ .../assets/utils/extractInLockPeriodAsset.ts | 48 +++ .../utils/extractReadyForWithdrawalAsset.ts | 48 +++ .../assets/utils/extractStakedNativeAssets.ts | 73 ++++ .../utils/extractStakingRewardsAsset.ts | 34 ++ .../isSnapOwnedAsset.test.ts} | 27 +- .../services/assets/utils/isSnapOwnedAsset.ts | 49 +++ 20 files changed, 506 insertions(+), 406 deletions(-) delete mode 100644 packages/tron-wallet-snap/src/services/assets/snapOwnedAssets.ts create mode 100644 packages/tron-wallet-snap/src/services/assets/utils/buildAccountResources.ts create mode 100644 packages/tron-wallet-snap/src/services/assets/utils/buildStakedData.ts create mode 100644 packages/tron-wallet-snap/src/services/assets/utils/extractBandwidth.ts create mode 100644 packages/tron-wallet-snap/src/services/assets/utils/extractEnergy.ts create mode 100644 packages/tron-wallet-snap/src/services/assets/utils/extractInLockPeriodAsset.ts create mode 100644 packages/tron-wallet-snap/src/services/assets/utils/extractReadyForWithdrawalAsset.ts create mode 100644 packages/tron-wallet-snap/src/services/assets/utils/extractStakedNativeAssets.ts create mode 100644 packages/tron-wallet-snap/src/services/assets/utils/extractStakingRewardsAsset.ts rename packages/tron-wallet-snap/src/services/assets/{snapOwnedAssets.test.ts => utils/isSnapOwnedAsset.test.ts} (58%) create mode 100644 packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.ts diff --git a/packages/tron-wallet-snap/CHANGELOG.md b/packages/tron-wallet-snap/CHANGELOG.md index 4486ec75e..7c659b0f0 100644 --- a/packages/tron-wallet-snap/CHANGELOG.md +++ b/packages/tron-wallet-snap/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Extract shared asset util functions and inject `SnapAssetsAdapter` from `context` into `AssetsService` +- Rename `getByKeyringAccountId` to `getAccountAssets` (with essential-asset synthesis) and update keyring callers + ## [3.1.0] ### Added diff --git a/packages/tron-wallet-snap/snap.manifest.json b/packages/tron-wallet-snap/snap.manifest.json index 23aa33cd3..78937309b 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": "e7sZ7XY8fvO4GLcznlb5N/Fh95+ANPKS/GzYoHHz2pc=", + "shasum": "XODF4lCphkA7wyPeF0SxobKZYtVZWrPylqdLLaY0dXQ=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/tron-wallet-snap/src/context.ts b/packages/tron-wallet-snap/src/context.ts index 026a1ae13..1a4ee12d9 100644 --- a/packages/tron-wallet-snap/src/context.ts +++ b/packages/tron-wallet-snap/src/context.ts @@ -25,6 +25,7 @@ import { RpcHandler } from './handlers/rpc/rpc'; import { UserInputHandler } from './handlers/user-input/userInput'; import { AccountsRepository } from './services/accounts/AccountsRepository'; import { AccountsService } from './services/accounts/AccountsService'; +import { SnapAssetsAdapter } from './services/assets/adapters/SnapAssetsAdapter'; import { AssetsRepository } from './services/assets/AssetsRepository'; import { AssetsService } from './services/assets/AssetsService'; import { ConfigProvider } from './services/config'; @@ -113,8 +114,7 @@ const securityAlertsApiClient = new SecurityAlertsApiClient( logger, ); -// Business Services -const assetsService = new AssetsService({ +const snapAssetsAdapter = new SnapAssetsAdapter({ logger, state, assetsRepository, @@ -123,8 +123,12 @@ const assetsService = new AssetsService({ priceApiClient, tokenApiClient, snapClient, - remoteFeatureFlagsProvider, - assetsProvider, + configProvider, +}); + +// Business Services +const assetsService = new AssetsService({ + snapAdapter: snapAssetsAdapter, }); const transactionsService = new TransactionsService({ 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 47fd5452f..de310fec6 100644 --- a/packages/tron-wallet-snap/src/handlers/keyring/keyring.test.ts +++ b/packages/tron-wallet-snap/src/handlers/keyring/keyring.test.ts @@ -85,7 +85,7 @@ describe('KeyringHandler', () => { }), } as unknown as jest.Mocked; mockAssetsService = { - getByKeyringAccountId: jest.fn().mockResolvedValue([]), + getAccountAssets: jest.fn().mockResolvedValue([]), } as unknown as jest.Mocked; mockTransactionsService = { checkAddressActivity: jest.fn(), @@ -649,7 +649,7 @@ describe('KeyringHandler', () => { const result = await keyringHandler.getAccountAssets(mockAccount.id); expect(result).toStrictEqual([]); - expect(mockAssetsService.getByKeyringAccountId).toHaveBeenCalledWith( + expect(mockAssetsService.getAccountAssets).toHaveBeenCalledWith( mockAccount.id, ); }); diff --git a/packages/tron-wallet-snap/src/handlers/keyring/keyring.ts b/packages/tron-wallet-snap/src/handlers/keyring/keyring.ts index 2c9b0b431..710278d18 100644 --- a/packages/tron-wallet-snap/src/handlers/keyring/keyring.ts +++ b/packages/tron-wallet-snap/src/handlers/keyring/keyring.ts @@ -191,7 +191,7 @@ export class KeyringHandler implements KeyringSnapRpc { this.#logger.info('Listing account assets', { accountId }); const assetEntities = - await this.#assetsService.getByKeyringAccountId(accountId); + await this.#assetsService.getAccountAssets(accountId); const result = assetEntities .filter( (asset) => @@ -280,8 +280,7 @@ export class KeyringHandler implements KeyringSnapRpc { await this.#getAccountOrThrow(accountId); - const assetsList = - await this.#assetsService.getByKeyringAccountId(accountId); + const assetsList = await this.#assetsService.getAccountAssets(accountId); const assetsToUse = assetsList .filter((asset) => assets.includes(asset.assetType)) diff --git a/packages/tron-wallet-snap/src/services/assets/AssetsService.test.ts b/packages/tron-wallet-snap/src/services/assets/AssetsService.test.ts index 29e40e3a8..19f166ed6 100644 --- a/packages/tron-wallet-snap/src/services/assets/AssetsService.test.ts +++ b/packages/tron-wallet-snap/src/services/assets/AssetsService.test.ts @@ -49,8 +49,12 @@ jest.mock('@metamask/keyring-snap-sdk', () => ({ (global as any).snap = {}; +// eslint-disable-next-line @typescript-eslint/no-require-imports +const { configProvider } = require('../../context'); // eslint-disable-next-line @typescript-eslint/no-require-imports const { AssetsService } = require('./AssetsService'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +const { SnapAssetsAdapter } = require('./adapters/SnapAssetsAdapter'); const mockAccount: KeyringAccount = { id: 'test-account-id', @@ -260,7 +264,7 @@ async function withAssetsService( trackError: jest.fn().mockResolvedValue(undefined), }; - const assetsService = new AssetsService({ + const snapAdapter = new SnapAssetsAdapter({ logger: mockLogger, assetsRepository: mockAssetsRepository, state: mockState, @@ -269,7 +273,9 @@ async function withAssetsService( priceApiClient: mockPriceApiClient, tokenApiClient: mockTokenApiClient, snapClient: mockSnapClient, + configProvider, }); + const assetsService = new AssetsService({ snapAdapter }); return await testFunction({ assetsService, @@ -2871,9 +2877,15 @@ describe('AssetsService', () => { expect(AssetsService.hasChanged(asset, [])).toBe(true); expect(AssetsService.hasChanged(asset, [asset])).toBe(false); + const accountAssets = await assetsService.getAccountAssets( + mockAccount.id, + ); expect( - await assetsService.getAccountAssets(mockAccount.id), - ).toStrictEqual([asset]); + accountAssets.some( + (savedAsset: AssetEntity) => + savedAsset.assetType === KnownCaip19Id.TrxMainnet, + ), + ).toBe(true); expect( await assetsService.getAccountAssetsByIDs(mockAccount.id, [ KnownCaip19Id.TrxMainnet, @@ -2885,15 +2897,6 @@ describe('AssetsService', () => { KnownCaip19Id.TrxMainnet, ), ).toStrictEqual(asset); - const byKeyringAccountId = await assetsService.getByKeyringAccountId( - mockAccount.id, - ); - expect( - byKeyringAccountId.some( - (savedAsset: AssetEntity) => - savedAsset.assetType === KnownCaip19Id.TrxMainnet, - ), - ).toBe(true); const marketData = await assetsService.getMultipleTokensMarketData([ { asset: KnownCaip19Id.TrxMainnet, diff --git a/packages/tron-wallet-snap/src/services/assets/AssetsService.ts b/packages/tron-wallet-snap/src/services/assets/AssetsService.ts index 77f63dc87..c1ef4ba3d 100644 --- a/packages/tron-wallet-snap/src/services/assets/AssetsService.ts +++ b/packages/tron-wallet-snap/src/services/assets/AssetsService.ts @@ -1,8 +1,4 @@ import type { KeyringAccount } from '@metamask/keyring-api'; -import { - AssetsProvider, - RemoteFeatureFlagsProvider, -} from '@metamask/snap-networks-utils'; import type { AssetConversion, AssetMetadata, @@ -11,17 +7,9 @@ import type { } from '@metamask/snaps-sdk'; import type { CaipAssetType } from '@metamask/utils'; -import type { PriceApiClient } from '../../clients/price-api/PriceApiClient'; -import type { SnapClient } from '../../clients/snap/SnapClient'; -import type { TokenApiClient } from '../../clients/token-api/TokenApiClient'; -import type { TronHttpClient } from '../../clients/tron-http/TronHttpClient'; -import type { TrongridApiClient } from '../../clients/trongrid/TrongridApiClient'; import type { Network } from '../../constants'; import type { AssetEntity } from '../../entities/assets'; -import type { ILogger } from '../../utils/logger'; -import type { State, UnencryptedStateValue } from '../state/State'; import { SnapAssetsAdapter } from './adapters/SnapAssetsAdapter'; -import type { AssetsRepository } from './AssetsRepository'; /** * Assets domain facade. Currently delegates all behavior to SnapAssetsAdapter @@ -33,37 +21,8 @@ export class AssetsService { readonly cacheTtlsMilliseconds: SnapAssetsAdapter['cacheTtlsMilliseconds']; - constructor({ - logger, - assetsRepository, - state, - trongridApiClient, - tronHttpClient, - priceApiClient, - tokenApiClient, - snapClient, - }: { - logger: ILogger; - assetsRepository: AssetsRepository; - state: State; - trongridApiClient: TrongridApiClient; - tronHttpClient: TronHttpClient; - priceApiClient: PriceApiClient; - tokenApiClient: TokenApiClient; - snapClient: SnapClient; - remoteFeatureFlagsProvider: RemoteFeatureFlagsProvider; - assetsProvider: AssetsProvider; - }) { - this.#snapAdapter = new SnapAssetsAdapter({ - logger, - assetsRepository, - state, - trongridApiClient, - tronHttpClient, - priceApiClient, - tokenApiClient, - snapClient, - }); + constructor({ snapAdapter }: { snapAdapter: SnapAssetsAdapter }) { + this.#snapAdapter = snapAdapter; this.cacheTtlsMilliseconds = this.#snapAdapter.cacheTtlsMilliseconds; } @@ -114,10 +73,6 @@ export class AssetsService { return this.#snapAdapter.getAll(); } - async getByKeyringAccountId(accountId: string): Promise { - return this.#snapAdapter.getByKeyringAccountId(accountId); - } - async getMultipleTokenConversions( conversions: { from: CaipAssetType; to: CaipAssetType }[], ): Promise< 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 0590aa2ce..7a5b38d3b 100644 --- a/packages/tron-wallet-snap/src/services/assets/adapters/SnapAssetsAdapter.ts +++ b/packages/tron-wallet-snap/src/services/assets/adapters/SnapAssetsAdapter.ts @@ -34,7 +34,6 @@ import type { AccountResources } from '../../../clients/tron-http'; import type { TronHttpClient } from '../../../clients/tron-http/TronHttpClient'; import type { TrongridApiClient } from '../../../clients/trongrid/TrongridApiClient'; import type { - RawTronUnfrozenV2, Trc20Balance, TronAccount, } from '../../../clients/trongrid/types'; @@ -54,11 +53,11 @@ import { TRX_STAKED_FOR_ENERGY_METADATA, TRX_STAKING_REWARDS_METADATA, } from '../../../constants'; -import { configProvider } from '../../../context'; import type { AssetEntity } from '../../../entities/assets'; import { toUiAmount } from '../../../utils/conversion'; import { createPrefixedLogger } from '../../../utils/logger'; import type { ILogger } from '../../../utils/logger'; +import type { ConfigProvider } from '../../config'; import type { State, UnencryptedStateValue } from '../../state/State'; import type { AssetsRepository } from '../AssetsRepository'; import type { @@ -68,9 +67,18 @@ import type { ReadyForWithdrawalCaipAssetType, ResourceCaipAssetType, StakedCaipAssetType, + StakedData, StakingRewardsCaipAssetType, TokenCaipAssetType, } from '../types'; +import { buildAccountResources } from '../utils/buildAccountResources'; +import { buildStakedData } from '../utils/buildStakedData'; +import { extractBandwidth } from '../utils/extractBandwidth'; +import { extractEnergy } from '../utils/extractEnergy'; +import { extractInLockPeriodAsset } from '../utils/extractInLockPeriodAsset'; +import { extractReadyForWithdrawalAsset } from '../utils/extractReadyForWithdrawalAsset'; +import { extractStakedNativeAssets } from '../utils/extractStakedNativeAssets'; +import { extractStakingRewardsAsset } from '../utils/extractStakingRewardsAsset'; /** * Normalized account data structure that provides a consistent shape for both @@ -85,11 +93,7 @@ type NormalizedAccountData = { /** TRC20 token balances from either account info or fallback endpoint. */ trc20Balances: Trc20Balance[]; /** Staking data including frozen balances and delegated resources. */ - stakedData: { - frozenV2: TronAccount['frozenV2']; - unfrozenV2: TronAccount['unfrozenV2']; - accountResource: TronAccount['account_resource'] | undefined; - }; + stakedData: StakedData; /** Account resources (energy, bandwidth). Empty object for inactive accounts. */ resources: AccountResources | Record; /** Unclaimed staking rewards in sun (0 if no rewards). */ @@ -113,6 +117,8 @@ export class SnapAssetsAdapter { readonly #snapClient: SnapClient; + readonly #configProvider: ConfigProvider; + readonly cacheTtlsMilliseconds: { fiatExchangeRates: number; spotPrices: number; @@ -128,6 +134,7 @@ export class SnapAssetsAdapter { priceApiClient, tokenApiClient, snapClient, + configProvider, }: { logger: ILogger; assetsRepository: AssetsRepository; @@ -137,6 +144,7 @@ export class SnapAssetsAdapter { priceApiClient: PriceApiClient; tokenApiClient: TokenApiClient; snapClient: SnapClient; + configProvider: ConfigProvider; }) { this.#logger = createPrefixedLogger(logger, '[🪙 SnapAssetsAdapter]'); this.#assetsRepository = assetsRepository; @@ -146,8 +154,9 @@ export class SnapAssetsAdapter { this.#priceApiClient = priceApiClient; this.#tokenApiClient = tokenApiClient; this.#snapClient = snapClient; + this.#configProvider = configProvider; - const { cacheTtlsMilliseconds } = configProvider.get().priceApi; + const { cacheTtlsMilliseconds } = this.#configProvider.get().priceApi; this.cacheTtlsMilliseconds = cacheTtlsMilliseconds; } @@ -155,10 +164,6 @@ export class SnapAssetsAdapter { return caipAssetId.includes('swift:0/iso4217:'); } - async getAccountAssets(accountId: string): Promise { - return this.#assetsRepository.getByAccountId(accountId); - } - async getAccountAssetsByIDs( accountId: string, assetTypes: string[], @@ -316,10 +321,8 @@ export class SnapAssetsAdapter { stakingRewardsRequest: PromiseSettledResult; }): NormalizedAccountData { const isInactiveAccount = tronAccountInfoRequest.status === 'rejected'; - const resources = - tronAccountResourcesRequest.status === 'fulfilled' - ? tronAccountResourcesRequest.value - : {}; + const resources = buildAccountResources(tronAccountResourcesRequest); + const stakedData = buildStakedData(tronAccountInfoRequest); const stakingRewards = stakingRewardsRequest.status === 'fulfilled' ? Math.max(0, stakingRewardsRequest.value) @@ -330,11 +333,7 @@ export class SnapAssetsAdapter { nativeBalance: 0, trc10Balances: [], trc20Balances: trc20BalancesFallback, - stakedData: { - frozenV2: [], - unfrozenV2: [], - accountResource: undefined, - }, + stakedData, resources, stakingRewards, }; @@ -345,11 +344,7 @@ export class SnapAssetsAdapter { nativeBalance: tronAccountInfo.balance ?? 0, trc10Balances: tronAccountInfo.assetV2 ?? [], trc20Balances: tronAccountInfo.trc20 ?? [], - stakedData: { - frozenV2: tronAccountInfo.frozenV2 ?? [], - unfrozenV2: tronAccountInfo.unfrozenV2 ?? [], - accountResource: tronAccountInfo.account_resource, - }, + stakedData, resources, stakingRewards, }; @@ -371,18 +366,18 @@ export class SnapAssetsAdapter { ): AssetEntity[] { return [ this.#extractNativeAsset(account, scope, data.nativeBalance), - ...this.#extractStakedNativeAssets(account, scope, data.stakedData), - this.#extractReadyForWithdrawalAsset(account, scope, data.stakedData), - this.#extractInLockPeriodAsset(account, scope, data.stakedData), - this.#extractStakingRewardsAsset(account, scope, data.stakingRewards), + ...extractStakedNativeAssets(account, scope, data.stakedData), + extractReadyForWithdrawalAsset(account, scope, data.stakedData), + extractInLockPeriodAsset(account, scope, data.stakedData), + extractStakingRewardsAsset(account, scope, data.stakingRewards), ...this.#extractTrc10Assets(account, scope, data.trc10Balances), ...this.#extractTrc20Assets(account, scope, data.trc20Balances), - ...this.#extractBandwidth({ + ...extractBandwidth({ account, scope, tronAccountResources: data.resources, }), - ...this.#extractEnergy({ + ...extractEnergy({ account, scope, tronAccountResources: data.resources, @@ -484,285 +479,6 @@ export class SnapAssetsAdapter { }; } - /** - * Extracts staked TRX assets (for bandwidth and energy). - * - * @param account - The keyring account. - * @param scope - The network. - * @param stakedData - Staking data including frozen balances and delegated resources. - * @returns AssetEntity[] - Array of staked assets (always 2: bandwidth and energy, amounts may be 0). - */ - #extractStakedNativeAssets( - account: KeyringAccount, - scope: Network, - stakedData: NormalizedAccountData['stakedData'], - ): AssetEntity[] { - const assets: AssetEntity[] = []; - - let stakedBandwidthAmount = 0; - let stakedEnergyAmount = 0; - - stakedData.frozenV2?.forEach((frozen) => { - const amount = frozen.amount ?? 0; - - if (frozen.type === 'ENERGY') { - stakedEnergyAmount += amount; - } else if (!frozen.type) { - // Item without type is for bandwidth - stakedBandwidthAmount += amount; - } - }); - - const delegatedBandwidth = - stakedData.accountResource?.delegated_frozenV2_balance_for_bandwidth ?? 0; - const delegatedEnergy = - stakedData.accountResource?.delegated_frozenV2_balance_for_energy ?? 0; - - stakedBandwidthAmount += delegatedBandwidth; - stakedEnergyAmount += delegatedEnergy; - - assets.push({ - assetType: Networks[scope].stakedForBandwidth.id, - keyringAccountId: account.id, - network: scope, - symbol: Networks[scope].stakedForBandwidth.symbol, - decimals: Networks[scope].stakedForBandwidth.decimals, - rawAmount: stakedBandwidthAmount.toString(), - uiAmount: toUiAmount( - stakedBandwidthAmount, - Networks[scope].stakedForBandwidth.decimals, - ).toString(), - iconUrl: Networks[scope].stakedForBandwidth.iconUrl, - }); - - assets.push({ - assetType: Networks[scope].stakedForEnergy.id, - keyringAccountId: account.id, - network: scope, - symbol: Networks[scope].stakedForEnergy.symbol, - decimals: Networks[scope].stakedForEnergy.decimals, - rawAmount: stakedEnergyAmount.toString(), - uiAmount: toUiAmount( - stakedEnergyAmount, - Networks[scope].stakedForEnergy.decimals, - ).toString(), - iconUrl: Networks[scope].stakedForEnergy.iconUrl, - }); - - return assets; - } - - /** - * Extracts TRX ready for withdrawal (unstaked TRX that has completed the withdrawal period). - * - * @param account - The keyring account. - * @param scope - The network. - * @param stakedData - Staking data including unfrozen balances. - * @returns AssetEntity - The ready-for-withdrawal asset (amount may be 0). - */ - #extractReadyForWithdrawalAsset( - account: KeyringAccount, - scope: Network, - stakedData: NormalizedAccountData['stakedData'], - ): AssetEntity { - const currentTimestamp = Date.now(); - let readyForWithdrawalAmount = 0; - - stakedData.unfrozenV2?.forEach((unfrozen: RawTronUnfrozenV2) => { - const expireTime = unfrozen.unfreeze_expire_time ?? 0; - const amount = unfrozen.unfreeze_amount ?? 0; - - if (expireTime <= currentTimestamp && amount > 0) { - readyForWithdrawalAmount += amount; - } - }); - - const { id, symbol, decimals, iconUrl } = - Networks[scope].readyForWithdrawal; - - return { - assetType: id, - keyringAccountId: account.id, - network: scope, - symbol, - decimals, - rawAmount: readyForWithdrawalAmount.toString(), - uiAmount: toUiAmount(readyForWithdrawalAmount, decimals).toString(), - iconUrl, - }; - } - - /** - * Extracts staking rewards asset (unclaimed voting rewards). - * - * @param account - The keyring account. - * @param scope - The network. - * @param stakingRewards - Unclaimed staking rewards in sun. - * @returns AssetEntity - The staking rewards asset. - */ - #extractStakingRewardsAsset( - account: KeyringAccount, - scope: Network, - stakingRewards: number, - ): AssetEntity { - return { - assetType: Networks[scope].stakingRewards.id, - keyringAccountId: account.id, - network: scope, - symbol: Networks[scope].stakingRewards.symbol, - decimals: Networks[scope].stakingRewards.decimals, - rawAmount: stakingRewards.toString(), - uiAmount: toUiAmount( - stakingRewards, - Networks[scope].stakingRewards.decimals, - ).toString(), - iconUrl: Networks[scope].stakingRewards.iconUrl, - }; - } - - /** - * Extracts TRX that is in the lock period (unstaked but lock period not yet ended). - * This represents TRX that the user has initiated unstaking for but must wait - * the 14-day lock period before they can withdraw. - * - * @param account - The keyring account. - * @param scope - The network. - * @param stakedData - Staking data including unfrozen balances. - * @returns AssetEntity - The in-lock-period asset (amount may be 0). - */ - #extractInLockPeriodAsset( - account: KeyringAccount, - scope: Network, - stakedData: NormalizedAccountData['stakedData'], - ): AssetEntity { - const currentTimestamp = Date.now(); - let inLockPeriodAmount = 0; - - stakedData.unfrozenV2?.forEach((unfrozen: RawTronUnfrozenV2) => { - const expireTime = unfrozen.unfreeze_expire_time ?? 0; - const amount = unfrozen.unfreeze_amount ?? 0; - - if (expireTime > currentTimestamp && amount > 0) { - inLockPeriodAmount += amount; - } - }); - - const { id, symbol, decimals, iconUrl } = Networks[scope].inLockPeriod; - - return { - assetType: id, - keyringAccountId: account.id, - network: scope, - symbol, - decimals, - rawAmount: inLockPeriodAmount.toString(), - uiAmount: toUiAmount(inLockPeriodAmount, decimals).toString(), - iconUrl, - }; - } - - /** - * Extracts current and maximum bandwidth from the account resources. - * - * @param options - Options object. - * @param options.account - The account to extract bandwidth for. - * @param options.scope - The network to extract bandwidth for. - * @param options.tronAccountResources - The account resources to extract bandwidth for. - * @returns The bandwidth assets. - */ - #extractBandwidth({ - account, - scope, - tronAccountResources, - }: { - account: KeyringAccount; - scope: Network; - tronAccountResources: AccountResources | Record; - }): AssetEntity[] { - const freeBandwidth = tronAccountResources?.freeNetLimit ?? 0; - const stakingBandwidth = tronAccountResources?.NetLimit ?? 0; - const maximumBandwidth = freeBandwidth + stakingBandwidth; - - const usedFreeBandwidth = tronAccountResources?.freeNetUsed ?? 0; - const usedStakingBandwidth = tronAccountResources?.NetUsed ?? 0; - const usedBandwidth = usedFreeBandwidth + usedStakingBandwidth; - - const availableBandwidth = Math.max(0, maximumBandwidth - usedBandwidth); - - return [ - { - assetType: Networks[scope].bandwidth.id, - keyringAccountId: account.id, - network: scope, - symbol: Networks[scope].bandwidth.symbol, - decimals: Networks[scope].bandwidth.decimals, - rawAmount: availableBandwidth.toString(), - uiAmount: availableBandwidth.toString(), - iconUrl: Networks[scope].bandwidth.iconUrl, - }, - { - assetType: Networks[scope].maximumBandwidth.id, - keyringAccountId: account.id, - network: scope, - symbol: Networks[scope].maximumBandwidth.symbol, - decimals: Networks[scope].maximumBandwidth.decimals, - rawAmount: maximumBandwidth.toString(), - uiAmount: maximumBandwidth.toString(), - iconUrl: Networks[scope].maximumBandwidth.iconUrl, - }, - ]; - } - - /** - * Extracts current and maximum energy from the account resources. - * - * @param options - Options object. - * @param options.account - The keyring account. - * @param options.scope - The network. - * @param options.tronAccountResources - Account resources (energy, bandwidth). - * @returns AssetEntity[] - Array containing energy and maximum energy assets. - */ - #extractEnergy({ - account, - scope, - tronAccountResources, - }: { - account: KeyringAccount; - scope: Network; - tronAccountResources: AccountResources | Record; - }): AssetEntity[] { - const maximumEnergy = tronAccountResources?.EnergyLimit ?? 0; - const usedEnergy = tronAccountResources?.EnergyUsed ?? 0; - - /** - * We might have used more Energy than the maximum allocated - */ - const availableEnergy = Math.max(0, maximumEnergy - usedEnergy); - - return [ - { - assetType: Networks[scope].energy.id, - keyringAccountId: account.id, - network: scope, - symbol: Networks[scope].energy.symbol, - decimals: Networks[scope].energy.decimals, - rawAmount: availableEnergy.toString(), - uiAmount: availableEnergy.toString(), - iconUrl: Networks[scope].energy.iconUrl, - }, - { - assetType: Networks[scope].maximumEnergy.id, - keyringAccountId: account.id, - network: scope, - symbol: Networks[scope].maximumEnergy.symbol, - decimals: Networks[scope].maximumEnergy.decimals, - rawAmount: maximumEnergy.toString(), - uiAmount: maximumEnergy.toString(), - iconUrl: Networks[scope].maximumEnergy.iconUrl, - }, - ]; - } - /** * Extracts TRC10 assets from the balances array. * @@ -1442,11 +1158,8 @@ export class SnapAssetsAdapter { } as AssetEntity; } - async getByKeyringAccountId( - keyringAccountId: string, - ): Promise { - const savedAssets = - await this.#assetsRepository.getByAccountId(keyringAccountId); + async getAccountAssets(accountId: string): Promise { + const savedAssets = await this.#assetsRepository.getByAccountId(accountId); /** * Ensure the special assets are always present whether they have been synced or not. @@ -1462,7 +1175,7 @@ export class SnapAssetsAdapter { if (!savedAsset) { const zeroBalanceAsset = this.#createZeroBalanceAsset( essentialAssetId as KnownCaip19Id, - keyringAccountId, + accountId, ); missingEssentialAssets.push(zeroBalanceAsset); } diff --git a/packages/tron-wallet-snap/src/services/assets/snapOwnedAssets.ts b/packages/tron-wallet-snap/src/services/assets/snapOwnedAssets.ts deleted file mode 100644 index c8d44a17f..000000000 --- a/packages/tron-wallet-snap/src/services/assets/snapOwnedAssets.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { SNAP_OWNED_ASSETS } from '../../constants'; - -const SNAP_OWNED_ASSET_IDS = new Set(SNAP_OWNED_ASSETS); - -/** - * Returns whether an asset remains exclusively managed by the Snap. - * - * AssetsController does not persist certain Tron protocol assets, including - * staking positions and account resources. These assets must always be read, - * synchronized, persisted, and published by the Snap, regardless of the - * assets migration stage. - * - * @param assetId - CAIP-19 asset ID. - * @returns Whether the asset is exclusively managed by the Snap. - */ -export function isSnapOwnedAsset(assetId: string): boolean { - return SNAP_OWNED_ASSET_IDS.has(assetId); -} diff --git a/packages/tron-wallet-snap/src/services/assets/types.ts b/packages/tron-wallet-snap/src/services/assets/types.ts index 19791f839..f8635428a 100644 --- a/packages/tron-wallet-snap/src/services/assets/types.ts +++ b/packages/tron-wallet-snap/src/services/assets/types.ts @@ -2,8 +2,15 @@ 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'; import type { Network } from '../../constants'; +export type StakedData = { + frozenV2: TronAccount['frozenV2']; + unfrozenV2?: TronAccount['unfrozenV2']; + accountResource: TronAccount['account_resource'] | undefined; +}; + export type NativeCaipAssetType = `${Network}/slip44:195`; export type StakedCaipAssetType = `${TrxScope}/slip44:195-staked-for-${'energy' | 'bandwidth'}`; diff --git a/packages/tron-wallet-snap/src/services/assets/utils/buildAccountResources.ts b/packages/tron-wallet-snap/src/services/assets/utils/buildAccountResources.ts new file mode 100644 index 000000000..80bf79df7 --- /dev/null +++ b/packages/tron-wallet-snap/src/services/assets/utils/buildAccountResources.ts @@ -0,0 +1,15 @@ +import type { AccountResources } from '../../../clients/tron-http'; + +/** + * Builds account resources from a settled getAccountResources request. + * + * @param tronAccountResourcesRequest - The settled promise result from getAccountResources. + * @returns Account resources, or an empty object when the request failed. + */ +export function buildAccountResources( + tronAccountResourcesRequest: PromiseSettledResult, +): AccountResources | Record { + return tronAccountResourcesRequest.status === 'fulfilled' + ? tronAccountResourcesRequest.value + : {}; +} diff --git a/packages/tron-wallet-snap/src/services/assets/utils/buildStakedData.ts b/packages/tron-wallet-snap/src/services/assets/utils/buildStakedData.ts new file mode 100644 index 000000000..9743f58cb --- /dev/null +++ b/packages/tron-wallet-snap/src/services/assets/utils/buildStakedData.ts @@ -0,0 +1,28 @@ +import type { TronAccount } from '../../../clients/trongrid/types'; + +import type { StakedData } from '../types'; + +/** + * Builds staking data from a settled account info request. + * + * @param tronAccountInfoRequest - The settled promise result from getAccountInfoByAddress. + * @returns Staking data with empty defaults for inactive accounts. + */ +export function buildStakedData( + tronAccountInfoRequest: PromiseSettledResult, +): StakedData { + if (tronAccountInfoRequest.status === 'rejected') { + return { + frozenV2: [], + unfrozenV2: [], + accountResource: undefined, + }; + } + + const tronAccountInfo = tronAccountInfoRequest.value; + return { + frozenV2: tronAccountInfo.frozenV2 ?? [], + unfrozenV2: tronAccountInfo.unfrozenV2 ?? [], + accountResource: tronAccountInfo.account_resource, + }; +} diff --git a/packages/tron-wallet-snap/src/services/assets/utils/extractBandwidth.ts b/packages/tron-wallet-snap/src/services/assets/utils/extractBandwidth.ts new file mode 100644 index 000000000..a61437ead --- /dev/null +++ b/packages/tron-wallet-snap/src/services/assets/utils/extractBandwidth.ts @@ -0,0 +1,58 @@ +import type { KeyringAccount } from '@metamask/keyring-api'; + +import type { AccountResources } from '../../../clients/tron-http'; +import type { Network } from '../../../constants'; +import { Networks } from '../../../constants'; +import type { AssetEntity } from '../../../entities/assets'; + +/** + * Extracts current and maximum bandwidth from the account resources. + * + * @param options - Options object. + * @param options.account - The account to extract bandwidth for. + * @param options.scope - The network to extract bandwidth for. + * @param options.tronAccountResources - The account resources to extract bandwidth for. + * @returns The bandwidth assets. + */ +export function extractBandwidth({ + account, + scope, + tronAccountResources, +}: { + account: KeyringAccount; + scope: Network; + tronAccountResources: AccountResources | Record; +}): AssetEntity[] { + const freeBandwidth = tronAccountResources?.freeNetLimit ?? 0; + const stakingBandwidth = tronAccountResources?.NetLimit ?? 0; + const maximumBandwidth = freeBandwidth + stakingBandwidth; + + const usedFreeBandwidth = tronAccountResources?.freeNetUsed ?? 0; + const usedStakingBandwidth = tronAccountResources?.NetUsed ?? 0; + const usedBandwidth = usedFreeBandwidth + usedStakingBandwidth; + + const availableBandwidth = Math.max(0, maximumBandwidth - usedBandwidth); + + return [ + { + assetType: Networks[scope].bandwidth.id, + keyringAccountId: account.id, + network: scope, + symbol: Networks[scope].bandwidth.symbol, + decimals: Networks[scope].bandwidth.decimals, + rawAmount: availableBandwidth.toString(), + uiAmount: availableBandwidth.toString(), + iconUrl: Networks[scope].bandwidth.iconUrl, + }, + { + assetType: Networks[scope].maximumBandwidth.id, + keyringAccountId: account.id, + network: scope, + symbol: Networks[scope].maximumBandwidth.symbol, + decimals: Networks[scope].maximumBandwidth.decimals, + rawAmount: maximumBandwidth.toString(), + uiAmount: maximumBandwidth.toString(), + iconUrl: Networks[scope].maximumBandwidth.iconUrl, + }, + ]; +} diff --git a/packages/tron-wallet-snap/src/services/assets/utils/extractEnergy.ts b/packages/tron-wallet-snap/src/services/assets/utils/extractEnergy.ts new file mode 100644 index 000000000..17e26cb39 --- /dev/null +++ b/packages/tron-wallet-snap/src/services/assets/utils/extractEnergy.ts @@ -0,0 +1,56 @@ +import type { KeyringAccount } from '@metamask/keyring-api'; + +import type { AccountResources } from '../../../clients/tron-http'; +import type { Network } from '../../../constants'; +import { Networks } from '../../../constants'; +import type { AssetEntity } from '../../../entities/assets'; + +/** + * Extracts current and maximum energy from the account resources. + * + * @param options - Options object. + * @param options.account - The keyring account. + * @param options.scope - The network. + * @param options.tronAccountResources - Account resources (energy, bandwidth). + * @returns Array containing energy and maximum energy assets. + */ +export function extractEnergy({ + account, + scope, + tronAccountResources, +}: { + account: KeyringAccount; + scope: Network; + tronAccountResources: AccountResources | Record; +}): AssetEntity[] { + const maximumEnergy = tronAccountResources?.EnergyLimit ?? 0; + const usedEnergy = tronAccountResources?.EnergyUsed ?? 0; + + /** + * We might have used more Energy than the maximum allocated + */ + const availableEnergy = Math.max(0, maximumEnergy - usedEnergy); + + return [ + { + assetType: Networks[scope].energy.id, + keyringAccountId: account.id, + network: scope, + symbol: Networks[scope].energy.symbol, + decimals: Networks[scope].energy.decimals, + rawAmount: availableEnergy.toString(), + uiAmount: availableEnergy.toString(), + iconUrl: Networks[scope].energy.iconUrl, + }, + { + assetType: Networks[scope].maximumEnergy.id, + keyringAccountId: account.id, + network: scope, + symbol: Networks[scope].maximumEnergy.symbol, + decimals: Networks[scope].maximumEnergy.decimals, + rawAmount: maximumEnergy.toString(), + uiAmount: maximumEnergy.toString(), + iconUrl: Networks[scope].maximumEnergy.iconUrl, + }, + ]; +} diff --git a/packages/tron-wallet-snap/src/services/assets/utils/extractInLockPeriodAsset.ts b/packages/tron-wallet-snap/src/services/assets/utils/extractInLockPeriodAsset.ts new file mode 100644 index 000000000..fc34e4489 --- /dev/null +++ b/packages/tron-wallet-snap/src/services/assets/utils/extractInLockPeriodAsset.ts @@ -0,0 +1,48 @@ +import type { KeyringAccount } from '@metamask/keyring-api'; + +import type { RawTronUnfrozenV2 } from '../../../clients/trongrid/types'; +import type { Network } from '../../../constants'; +import { Networks } from '../../../constants'; +import type { AssetEntity } from '../../../entities/assets'; +import { toUiAmount } from '../../../utils/conversion'; + +import type { StakedData } from '../types'; + +/** + * Extracts TRX that is in the lock period (unstaked but lock period not yet ended). + * + * @param account - The keyring account. + * @param scope - The network. + * @param stakedData - Staking data including unfrozen balances. + * @returns The in-lock-period asset (amount may be 0). + */ +export function extractInLockPeriodAsset( + account: KeyringAccount, + scope: Network, + stakedData: StakedData, +): AssetEntity { + const currentTimestamp = Date.now(); + let inLockPeriodAmount = 0; + + stakedData.unfrozenV2?.forEach((unfrozen: RawTronUnfrozenV2) => { + const expireTime = unfrozen.unfreeze_expire_time ?? 0; + const amount = unfrozen.unfreeze_amount ?? 0; + + if (expireTime > currentTimestamp && amount > 0) { + inLockPeriodAmount += amount; + } + }); + + const { id, symbol, decimals, iconUrl } = Networks[scope].inLockPeriod; + + return { + assetType: id, + keyringAccountId: account.id, + network: scope, + symbol, + decimals, + rawAmount: inLockPeriodAmount.toString(), + uiAmount: toUiAmount(inLockPeriodAmount, decimals).toString(), + iconUrl, + }; +} diff --git a/packages/tron-wallet-snap/src/services/assets/utils/extractReadyForWithdrawalAsset.ts b/packages/tron-wallet-snap/src/services/assets/utils/extractReadyForWithdrawalAsset.ts new file mode 100644 index 000000000..c55762ea4 --- /dev/null +++ b/packages/tron-wallet-snap/src/services/assets/utils/extractReadyForWithdrawalAsset.ts @@ -0,0 +1,48 @@ +import type { KeyringAccount } from '@metamask/keyring-api'; + +import type { RawTronUnfrozenV2 } from '../../../clients/trongrid/types'; +import type { Network } from '../../../constants'; +import { Networks } from '../../../constants'; +import type { AssetEntity } from '../../../entities/assets'; +import { toUiAmount } from '../../../utils/conversion'; + +import type { StakedData } from '../types'; + +/** + * Extracts TRX ready for withdrawal (unstaked TRX that has completed the withdrawal period). + * + * @param account - The keyring account. + * @param scope - The network. + * @param stakedData - Staking data including unfrozen balances. + * @returns The ready-for-withdrawal asset (amount may be 0). + */ +export function extractReadyForWithdrawalAsset( + account: KeyringAccount, + scope: Network, + stakedData: StakedData, +): AssetEntity { + const currentTimestamp = Date.now(); + let readyForWithdrawalAmount = 0; + + stakedData.unfrozenV2?.forEach((unfrozen: RawTronUnfrozenV2) => { + const expireTime = unfrozen.unfreeze_expire_time ?? 0; + const amount = unfrozen.unfreeze_amount ?? 0; + + if (expireTime <= currentTimestamp && amount > 0) { + readyForWithdrawalAmount += amount; + } + }); + + const { id, symbol, decimals, iconUrl } = Networks[scope].readyForWithdrawal; + + return { + assetType: id, + keyringAccountId: account.id, + network: scope, + symbol, + decimals, + rawAmount: readyForWithdrawalAmount.toString(), + uiAmount: toUiAmount(readyForWithdrawalAmount, decimals).toString(), + iconUrl, + }; +} diff --git a/packages/tron-wallet-snap/src/services/assets/utils/extractStakedNativeAssets.ts b/packages/tron-wallet-snap/src/services/assets/utils/extractStakedNativeAssets.ts new file mode 100644 index 000000000..f9678cfa2 --- /dev/null +++ b/packages/tron-wallet-snap/src/services/assets/utils/extractStakedNativeAssets.ts @@ -0,0 +1,73 @@ +import type { KeyringAccount } from '@metamask/keyring-api'; + +import type { Network } from '../../../constants'; +import { Networks } from '../../../constants'; +import type { AssetEntity } from '../../../entities/assets'; +import { toUiAmount } from '../../../utils/conversion'; + +import type { StakedData } from '../types'; + +/** + * Extracts staked TRX assets (for bandwidth and energy). + * + * @param account - The keyring account. + * @param scope - The network. + * @param stakedData - Staking data including frozen balances and delegated resources. + * @returns Array of staked assets (always 2: bandwidth and energy, amounts may be 0). + */ +export function extractStakedNativeAssets( + account: KeyringAccount, + scope: Network, + stakedData: StakedData, +): AssetEntity[] { + let stakedBandwidthAmount = 0; + let stakedEnergyAmount = 0; + + stakedData.frozenV2?.forEach((frozen) => { + const amount = frozen.amount ?? 0; + + if (frozen.type === 'ENERGY') { + stakedEnergyAmount += amount; + } else if (!frozen.type) { + // Item without type is for bandwidth + stakedBandwidthAmount += amount; + } + }); + + const delegatedBandwidth = + stakedData.accountResource?.delegated_frozenV2_balance_for_bandwidth ?? 0; + const delegatedEnergy = + stakedData.accountResource?.delegated_frozenV2_balance_for_energy ?? 0; + + stakedBandwidthAmount += delegatedBandwidth; + stakedEnergyAmount += delegatedEnergy; + + return [ + { + assetType: Networks[scope].stakedForBandwidth.id, + keyringAccountId: account.id, + network: scope, + symbol: Networks[scope].stakedForBandwidth.symbol, + decimals: Networks[scope].stakedForBandwidth.decimals, + rawAmount: stakedBandwidthAmount.toString(), + uiAmount: toUiAmount( + stakedBandwidthAmount, + Networks[scope].stakedForBandwidth.decimals, + ).toString(), + iconUrl: Networks[scope].stakedForBandwidth.iconUrl, + }, + { + assetType: Networks[scope].stakedForEnergy.id, + keyringAccountId: account.id, + network: scope, + symbol: Networks[scope].stakedForEnergy.symbol, + decimals: Networks[scope].stakedForEnergy.decimals, + rawAmount: stakedEnergyAmount.toString(), + uiAmount: toUiAmount( + stakedEnergyAmount, + Networks[scope].stakedForEnergy.decimals, + ).toString(), + iconUrl: Networks[scope].stakedForEnergy.iconUrl, + }, + ]; +} diff --git a/packages/tron-wallet-snap/src/services/assets/utils/extractStakingRewardsAsset.ts b/packages/tron-wallet-snap/src/services/assets/utils/extractStakingRewardsAsset.ts new file mode 100644 index 000000000..af7125d64 --- /dev/null +++ b/packages/tron-wallet-snap/src/services/assets/utils/extractStakingRewardsAsset.ts @@ -0,0 +1,34 @@ +import type { KeyringAccount } from '@metamask/keyring-api'; + +import type { Network } from '../../../constants'; +import { Networks } from '../../../constants'; +import type { AssetEntity } from '../../../entities/assets'; +import { toUiAmount } from '../../../utils/conversion'; + +/** + * Extracts staking rewards asset (unclaimed voting rewards). + * + * @param account - The keyring account. + * @param scope - The network. + * @param stakingRewards - Unclaimed staking rewards in sun. + * @returns The staking rewards asset. + */ +export function extractStakingRewardsAsset( + account: KeyringAccount, + scope: Network, + stakingRewards: number, +): AssetEntity { + return { + assetType: Networks[scope].stakingRewards.id, + keyringAccountId: account.id, + network: scope, + symbol: Networks[scope].stakingRewards.symbol, + decimals: Networks[scope].stakingRewards.decimals, + rawAmount: stakingRewards.toString(), + uiAmount: toUiAmount( + stakingRewards, + Networks[scope].stakingRewards.decimals, + ).toString(), + iconUrl: Networks[scope].stakingRewards.iconUrl, + }; +} diff --git a/packages/tron-wallet-snap/src/services/assets/snapOwnedAssets.test.ts b/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.test.ts similarity index 58% rename from packages/tron-wallet-snap/src/services/assets/snapOwnedAssets.test.ts rename to packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.test.ts index 84a8b4eee..e11d23ee7 100644 --- a/packages/tron-wallet-snap/src/services/assets/snapOwnedAssets.test.ts +++ b/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.test.ts @@ -1,5 +1,12 @@ -import { KnownCaip19Id, SNAP_OWNED_ASSETS } from '../../constants'; -import { isSnapOwnedAsset } from './snapOwnedAssets'; +import { + KnownCaip19Id, + Network, + SNAP_OWNED_ASSETS, +} from '../../../constants'; +import { + getSnapOwnedAssetIdsForScope, + isSnapOwnedAsset, +} from './isSnapOwnedAsset'; describe('isSnapOwnedAsset', () => { it.each(SNAP_OWNED_ASSETS)( @@ -32,3 +39,19 @@ describe('isSnapOwnedAsset', () => { ).toBe(false); }); }); + +describe('getSnapOwnedAssetIdsForScope', () => { + it.each([ + Network.Mainnet, + Network.Nile, + Network.Shasta, + ] as const)('returns only snap-owned assets for %s', (scope) => { + const assetIds = getSnapOwnedAssetIdsForScope(scope); + + expect(assetIds).toHaveLength(9); + expect(assetIds.every((assetId) => isSnapOwnedAsset(assetId))).toBe(true); + expect( + assetIds.every((assetId) => assetId.startsWith(`${scope}/`)), + ).toBe(true); + }); +}); diff --git a/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.ts b/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.ts new file mode 100644 index 000000000..a8c42184d --- /dev/null +++ b/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.ts @@ -0,0 +1,49 @@ +import type { CaipAssetType } from '@metamask/utils'; + +import { + Networks, + SNAP_OWNED_ASSETS, + type Network, +} from '../../../constants'; + +const SNAP_OWNED_ASSET_IDS = new Set(SNAP_OWNED_ASSETS); + +/** + * Returns the full snap-owned asset ID set for a network scope. + * + * Matches the assets produced by `fetchAssetsAndBalancesForAccount` for that + * scope (staking positions and account resources, including zero balances). + * + * @param scope - The network to query. + * @returns CAIP-19 asset IDs exclusively managed by the Snap on that network. + */ +export function getSnapOwnedAssetIdsForScope(scope: Network): CaipAssetType[] { + const network = Networks[scope]; + + return [ + network.stakedForBandwidth.id, + network.stakedForEnergy.id, + network.readyForWithdrawal.id, + network.stakingRewards.id, + network.inLockPeriod.id, + network.bandwidth.id, + network.maximumBandwidth.id, + network.energy.id, + network.maximumEnergy.id, + ]; +} + +/** + * Returns whether an asset remains exclusively managed by the Snap. + * + * AssetsController does not persist certain Tron protocol assets, including + * staking positions and account resources. These assets must always be read, + * synchronized, persisted, and published by the Snap, regardless of the + * assets migration stage. + * + * @param assetId - CAIP-19 asset ID. + * @returns Whether the asset is exclusively managed by the Snap. + */ +export function isSnapOwnedAsset(assetId: string): boolean { + return SNAP_OWNED_ASSET_IDS.has(assetId); +} From 82f1e438a4986b2c415d77efa1d3774ebd1e6447 Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Mon, 10 Aug 2026 15:13:45 +0100 Subject: [PATCH 2/5] chore(tron-wallet-snap): link changelog entries to #143 --- packages/tron-wallet-snap/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tron-wallet-snap/CHANGELOG.md b/packages/tron-wallet-snap/CHANGELOG.md index 7c659b0f0..a091cac27 100644 --- a/packages/tron-wallet-snap/CHANGELOG.md +++ b/packages/tron-wallet-snap/CHANGELOG.md @@ -9,8 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Extract shared asset util functions and inject `SnapAssetsAdapter` from `context` into `AssetsService` -- Rename `getByKeyringAccountId` to `getAccountAssets` (with essential-asset synthesis) and update keyring callers +- Extract shared asset util functions and inject `SnapAssetsAdapter` from `context` into `AssetsService` ([#143](https://github.com/MetaMask/internal-snaps/pull/143)) +- Rename `getByKeyringAccountId` to `getAccountAssets` (with essential-asset synthesis) and update keyring callers ([#143](https://github.com/MetaMask/internal-snaps/pull/143)) ## [3.1.0] From f86b1f17c5d4a95d2684d3075a4962fc526b7d92 Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Mon, 10 Aug 2026 15:27:08 +0100 Subject: [PATCH 3/5] fix(tron-wallet-snap): format asset utils and sync manifest shasum from CI Apply oxfmt to extracted asset utils and set snap.manifest.json shasum to the Linux CI build output so the build job's clean-working-tree check passes. --- packages/tron-wallet-snap/snap.manifest.json | 2 +- .../services/assets/utils/buildStakedData.ts | 1 - .../assets/utils/extractInLockPeriodAsset.ts | 1 - .../utils/extractReadyForWithdrawalAsset.ts | 1 - .../assets/utils/extractStakedNativeAssets.ts | 1 - .../assets/utils/isSnapOwnedAsset.test.ts | 29 ++++++++----------- .../services/assets/utils/isSnapOwnedAsset.ts | 6 +--- 7 files changed, 14 insertions(+), 27 deletions(-) diff --git a/packages/tron-wallet-snap/snap.manifest.json b/packages/tron-wallet-snap/snap.manifest.json index 78937309b..0f47ebb3f 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": "XODF4lCphkA7wyPeF0SxobKZYtVZWrPylqdLLaY0dXQ=", + "shasum": "NpxOo6DkB0sBh8xpisBu3o+7G6MJriDNsADcHVb/Qp8=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/tron-wallet-snap/src/services/assets/utils/buildStakedData.ts b/packages/tron-wallet-snap/src/services/assets/utils/buildStakedData.ts index 9743f58cb..7a1f75c15 100644 --- a/packages/tron-wallet-snap/src/services/assets/utils/buildStakedData.ts +++ b/packages/tron-wallet-snap/src/services/assets/utils/buildStakedData.ts @@ -1,5 +1,4 @@ import type { TronAccount } from '../../../clients/trongrid/types'; - import type { StakedData } from '../types'; /** diff --git a/packages/tron-wallet-snap/src/services/assets/utils/extractInLockPeriodAsset.ts b/packages/tron-wallet-snap/src/services/assets/utils/extractInLockPeriodAsset.ts index fc34e4489..cfcc41b05 100644 --- a/packages/tron-wallet-snap/src/services/assets/utils/extractInLockPeriodAsset.ts +++ b/packages/tron-wallet-snap/src/services/assets/utils/extractInLockPeriodAsset.ts @@ -5,7 +5,6 @@ import type { Network } from '../../../constants'; import { Networks } from '../../../constants'; import type { AssetEntity } from '../../../entities/assets'; import { toUiAmount } from '../../../utils/conversion'; - import type { StakedData } from '../types'; /** diff --git a/packages/tron-wallet-snap/src/services/assets/utils/extractReadyForWithdrawalAsset.ts b/packages/tron-wallet-snap/src/services/assets/utils/extractReadyForWithdrawalAsset.ts index c55762ea4..ab839c267 100644 --- a/packages/tron-wallet-snap/src/services/assets/utils/extractReadyForWithdrawalAsset.ts +++ b/packages/tron-wallet-snap/src/services/assets/utils/extractReadyForWithdrawalAsset.ts @@ -5,7 +5,6 @@ import type { Network } from '../../../constants'; import { Networks } from '../../../constants'; import type { AssetEntity } from '../../../entities/assets'; import { toUiAmount } from '../../../utils/conversion'; - import type { StakedData } from '../types'; /** diff --git a/packages/tron-wallet-snap/src/services/assets/utils/extractStakedNativeAssets.ts b/packages/tron-wallet-snap/src/services/assets/utils/extractStakedNativeAssets.ts index f9678cfa2..6af69d448 100644 --- a/packages/tron-wallet-snap/src/services/assets/utils/extractStakedNativeAssets.ts +++ b/packages/tron-wallet-snap/src/services/assets/utils/extractStakedNativeAssets.ts @@ -4,7 +4,6 @@ import type { Network } from '../../../constants'; import { Networks } from '../../../constants'; import type { AssetEntity } from '../../../entities/assets'; import { toUiAmount } from '../../../utils/conversion'; - import type { StakedData } from '../types'; /** diff --git a/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.test.ts b/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.test.ts index e11d23ee7..21ccddec6 100644 --- a/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.test.ts +++ b/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.test.ts @@ -1,8 +1,4 @@ -import { - KnownCaip19Id, - Network, - SNAP_OWNED_ASSETS, -} from '../../../constants'; +import { KnownCaip19Id, Network, SNAP_OWNED_ASSETS } from '../../../constants'; import { getSnapOwnedAssetIdsForScope, isSnapOwnedAsset, @@ -41,17 +37,16 @@ describe('isSnapOwnedAsset', () => { }); describe('getSnapOwnedAssetIdsForScope', () => { - it.each([ - Network.Mainnet, - Network.Nile, - Network.Shasta, - ] as const)('returns only snap-owned assets for %s', (scope) => { - const assetIds = getSnapOwnedAssetIdsForScope(scope); + it.each([Network.Mainnet, Network.Nile, Network.Shasta] as const)( + 'returns only snap-owned assets for %s', + (scope) => { + const assetIds = getSnapOwnedAssetIdsForScope(scope); - expect(assetIds).toHaveLength(9); - expect(assetIds.every((assetId) => isSnapOwnedAsset(assetId))).toBe(true); - expect( - assetIds.every((assetId) => assetId.startsWith(`${scope}/`)), - ).toBe(true); - }); + expect(assetIds).toHaveLength(9); + expect(assetIds.every((assetId) => isSnapOwnedAsset(assetId))).toBe(true); + expect(assetIds.every((assetId) => assetId.startsWith(`${scope}/`))).toBe( + true, + ); + }, + ); }); diff --git a/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.ts b/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.ts index a8c42184d..83d7ba101 100644 --- a/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.ts +++ b/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.ts @@ -1,10 +1,6 @@ import type { CaipAssetType } from '@metamask/utils'; -import { - Networks, - SNAP_OWNED_ASSETS, - type Network, -} from '../../../constants'; +import { Networks, SNAP_OWNED_ASSETS, type Network } from '../../../constants'; const SNAP_OWNED_ASSET_IDS = new Set(SNAP_OWNED_ASSETS); From 32875882dbe02c9373f97e7bbb444031b97afd1c Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Mon, 10 Aug 2026 15:30:53 +0100 Subject: [PATCH 4/5] fix(tron-wallet-snap): split type import in isSnapOwnedAsset Satisfy import-x/consistent-type-specifier-style for CI eslint. --- .../src/services/assets/utils/isSnapOwnedAsset.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.ts b/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.ts index 83d7ba101..e8cb9b2f6 100644 --- a/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.ts +++ b/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.ts @@ -1,6 +1,7 @@ import type { CaipAssetType } from '@metamask/utils'; -import { Networks, SNAP_OWNED_ASSETS, type Network } from '../../../constants'; +import { Networks, SNAP_OWNED_ASSETS } from '../../../constants'; +import type { Network } from '../../../constants'; const SNAP_OWNED_ASSET_IDS = new Set(SNAP_OWNED_ASSETS); From 66312202e7d39959ea4db221ae8f022336dc6a99 Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Mon, 10 Aug 2026 17:03:26 +0100 Subject: [PATCH 5/5] refactor(tron-wallet-snap): type isSnapOwnedAsset with CaipAssetType Use CaipAssetType for the snap-owned asset set and lookup parameter per review feedback. --- .../src/services/assets/utils/isSnapOwnedAsset.test.ts | 8 +++++--- .../src/services/assets/utils/isSnapOwnedAsset.ts | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.test.ts b/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.test.ts index 21ccddec6..c495abcdb 100644 --- a/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.test.ts +++ b/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.test.ts @@ -1,3 +1,5 @@ +import type { CaipAssetType } from '@metamask/utils'; + import { KnownCaip19Id, Network, SNAP_OWNED_ASSETS } from '../../../constants'; import { getSnapOwnedAssetIdsForScope, @@ -8,7 +10,7 @@ describe('isSnapOwnedAsset', () => { it.each(SNAP_OWNED_ASSETS)( 'returns true for snap-owned asset %s', (assetId) => { - expect(isSnapOwnedAsset(assetId)).toBe(true); + expect(isSnapOwnedAsset(assetId as CaipAssetType)).toBe(true); }, ); @@ -22,7 +24,7 @@ describe('isSnapOwnedAsset', () => { expect(isSnapOwnedAsset(KnownCaip19Id.UsdtMainnet)).toBe(false); expect( isSnapOwnedAsset( - `${KnownCaip19Id.TrxMainnet.split('/')[0]}/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t`, + `${KnownCaip19Id.TrxMainnet.split('/')[0]}/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` as CaipAssetType, ), ).toBe(false); }); @@ -30,7 +32,7 @@ describe('isSnapOwnedAsset', () => { it('returns false for TRC10 tokens', () => { expect( isSnapOwnedAsset( - `${KnownCaip19Id.TrxMainnet.split('/')[0]}/trc10:1002000`, + `${KnownCaip19Id.TrxMainnet.split('/')[0]}/trc10:1002000` as CaipAssetType, ), ).toBe(false); }); diff --git a/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.ts b/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.ts index e8cb9b2f6..1bde3e6bf 100644 --- a/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.ts +++ b/packages/tron-wallet-snap/src/services/assets/utils/isSnapOwnedAsset.ts @@ -3,7 +3,9 @@ import type { CaipAssetType } from '@metamask/utils'; import { Networks, SNAP_OWNED_ASSETS } from '../../../constants'; import type { Network } from '../../../constants'; -const SNAP_OWNED_ASSET_IDS = new Set(SNAP_OWNED_ASSETS); +const SNAP_OWNED_ASSET_IDS = new Set( + SNAP_OWNED_ASSETS as CaipAssetType[], +); /** * Returns the full snap-owned asset ID set for a network scope. @@ -41,6 +43,6 @@ export function getSnapOwnedAssetIdsForScope(scope: Network): CaipAssetType[] { * @param assetId - CAIP-19 asset ID. * @returns Whether the asset is exclusively managed by the Snap. */ -export function isSnapOwnedAsset(assetId: string): boolean { +export function isSnapOwnedAsset(assetId: CaipAssetType): boolean { return SNAP_OWNED_ASSET_IDS.has(assetId); }