diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 5b13d4f15..ceb709105 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1352,15 +1352,7 @@ "count": 20 } }, - "packages/tron-wallet-snap/src/caching/InMemoryCache.ts": { - "import-x/no-extraneous-dependencies": { - "count": 1 - } - }, "packages/tron-wallet-snap/src/caching/StateCache.ts": { - "import-x/no-extraneous-dependencies": { - "count": 1 - }, "no-restricted-syntax": { "count": 1 } @@ -1375,20 +1367,7 @@ "count": 2 } }, - "packages/tron-wallet-snap/src/clients/price-api/PriceApiClient.ts": { - "import-x/no-extraneous-dependencies": { - "count": 1 - } - }, - "packages/tron-wallet-snap/src/clients/price-api/types.ts": { - "import-x/no-extraneous-dependencies": { - "count": 1 - } - }, "packages/tron-wallet-snap/src/clients/security-alerts-api/utils.ts": { - "import-x/no-extraneous-dependencies": { - "count": 1 - }, "no-restricted-syntax": { "count": 5 } @@ -1401,11 +1380,6 @@ "count": 1 } }, - "packages/tron-wallet-snap/src/clients/token-api/TokenApiClient.ts": { - "import-x/no-extraneous-dependencies": { - "count": 1 - } - }, "packages/tron-wallet-snap/src/clients/trongrid/TrongridApiClient.test.ts": { "require-atomic-updates": { "count": 1 @@ -1424,16 +1398,6 @@ "count": 42 } }, - "packages/tron-wallet-snap/src/handlers/clientRequest/clientRequest.ts": { - "import-x/no-extraneous-dependencies": { - "count": 1 - } - }, - "packages/tron-wallet-snap/src/handlers/clientRequest/validation.ts": { - "import-x/no-extraneous-dependencies": { - "count": 1 - } - }, "packages/tron-wallet-snap/src/handlers/cronjob/cronjob.test.tsx": { "@typescript-eslint/explicit-function-return-type": { "count": 2 @@ -1452,11 +1416,6 @@ "count": 1 } }, - "packages/tron-wallet-snap/src/services/accounts/AccountsService.ts": { - "import-x/no-extraneous-dependencies": { - "count": 1 - } - }, "packages/tron-wallet-snap/src/services/assets/AssetsRepository.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 2 @@ -1464,22 +1423,12 @@ }, "packages/tron-wallet-snap/src/services/assets/AssetsService.test.ts": { "@typescript-eslint/explicit-function-return-type": { - "count": 3 + "count": 2 }, "@typescript-eslint/no-explicit-any": { "count": 1 } }, - "packages/tron-wallet-snap/src/services/assets/adapters/SnapAssetsAdapter.ts": { - "import-x/no-extraneous-dependencies": { - "count": 1 - } - }, - "packages/tron-wallet-snap/src/services/config/ConfigProvider.ts": { - "import-x/no-extraneous-dependencies": { - "count": 1 - } - }, "packages/tron-wallet-snap/src/services/send/FeeCalculatorService.ts": { "no-restricted-syntax": { "count": 1 @@ -1493,11 +1442,6 @@ "count": 15 } }, - "packages/tron-wallet-snap/src/services/send/SendService.ts": { - "import-x/no-extraneous-dependencies": { - "count": 1 - } - }, "packages/tron-wallet-snap/src/services/send/TransactionExpirationRefresherService.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 5 @@ -1508,11 +1452,6 @@ "count": 7 } }, - "packages/tron-wallet-snap/src/services/staking/StakingService.ts": { - "import-x/no-extraneous-dependencies": { - "count": 1 - } - }, "packages/tron-wallet-snap/src/services/state/State.test.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 1 @@ -1521,11 +1460,6 @@ "count": 1 } }, - "packages/tron-wallet-snap/src/services/transaction-expiration-refresher/TransactionExpirationRefresherService.ts": { - "import-x/no-extraneous-dependencies": { - "count": 1 - } - }, "packages/tron-wallet-snap/src/services/transaction-scan/TransactionScanService.ts": { "no-restricted-syntax": { "count": 5 @@ -1542,14 +1476,6 @@ }, "@typescript-eslint/no-explicit-any": { "count": 7 - }, - "import-x/no-extraneous-dependencies": { - "count": 1 - } - }, - "packages/tron-wallet-snap/src/services/wallet/WalletService.ts": { - "import-x/no-extraneous-dependencies": { - "count": 1 } }, "packages/tron-wallet-snap/src/ui/confirmation/components/TransactionAlert/TransactionAlert.test.tsx": { @@ -1568,9 +1494,6 @@ "packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmSignMessage/render.test.tsx": { "@typescript-eslint/no-explicit-any": { "count": 3 - }, - "import-x/no-extraneous-dependencies": { - "count": 1 } }, "packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmSignTransaction/ConfirmSignTransaction.test.tsx": { @@ -1581,14 +1504,6 @@ "packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmSignTransaction/render.test.tsx": { "@typescript-eslint/no-explicit-any": { "count": 14 - }, - "import-x/no-extraneous-dependencies": { - "count": 1 - } - }, - "packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmSignTransaction/render.tsx": { - "import-x/no-extraneous-dependencies": { - "count": 1 } }, "packages/tron-wallet-snap/src/ui/confirmation/views/ConfirmTransactionRequest/ConfirmTransactionRequest.test.tsx": { @@ -1604,26 +1519,11 @@ "count": 2 } }, - "packages/tron-wallet-snap/src/utils/deriveTronFromCoinTypeNode.test.ts": { - "import-x/no-extraneous-dependencies": { - "count": 1 - } - }, - "packages/tron-wallet-snap/src/utils/deriveTronFromCoinTypeNode.ts": { - "import-x/no-extraneous-dependencies": { - "count": 1 - } - }, "packages/tron-wallet-snap/src/utils/getExplorerUrl.test.ts": { "n/no-process-env": { "count": 3 } }, - "packages/tron-wallet-snap/src/validation/keyring-structs.test.ts": { - "import-x/no-extraneous-dependencies": { - "count": 1 - } - }, "packages/tron-wallet-snap/src/validation/transaction.test.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 diff --git a/packages/tron-wallet-snap/CHANGELOG.md b/packages/tron-wallet-snap/CHANGELOG.md index 4486ec75e..811f44fc0 100644 --- a/packages/tron-wallet-snap/CHANGELOG.md +++ b/packages/tron-wallet-snap/CHANGELOG.md @@ -11,7 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +### Added + - Add Core messenger plumbing (`coreMessenger`, `RemoteFeatureFlagsProvider`, `AssetsProvider`) for upcoming AssetsController migration ([#95](https://github.com/MetaMask/internal-snaps/pull/95)) +- Add `CoreAssetsAdapter` and initialize it in `AssetsService` for upcoming AssetsController routing ([#127](https://github.com/MetaMask/internal-snaps/pull/127)) ### Fixed diff --git a/packages/tron-wallet-snap/package.json b/packages/tron-wallet-snap/package.json index c09483487..7debfa820 100644 --- a/packages/tron-wallet-snap/package.json +++ b/packages/tron-wallet-snap/package.json @@ -60,6 +60,7 @@ "@metamask/snaps-jest": "^10.2.0", "@metamask/snaps-sdk": "^11.2.0", "@metamask/superstruct": "^3.4.1", + "@metamask/utils": "^11.9.0", "@types/lodash": "^4.17.15", "async-mutex": "^0.5.0", "bignumber.js": "^9.3.1", diff --git a/packages/tron-wallet-snap/snap.manifest.json b/packages/tron-wallet-snap/snap.manifest.json index 23aa33cd3..8be30bf27 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": "1GetcX2q26WkRq3aUu5hrAElNLvZXEfzEK2rAZv6ddI=", "location": { "npm": { "filePath": "dist/bundle.js", @@ -66,10 +66,10 @@ }, "endowment:messenger": { "actions": [ - "RemoteFeatureFlagController:getState", "AssetsController:getAccountAssetByID", "AssetsController:getAccountAssetsByIDs", - "AssetsController:getAccountAssetsByScope" + "AssetsController:getAccountAssetsByScope", + "RemoteFeatureFlagController:getState" ] } }, diff --git a/packages/tron-wallet-snap/src/context.ts b/packages/tron-wallet-snap/src/context.ts index 026a1ae13..2f34203f2 100644 --- a/packages/tron-wallet-snap/src/context.ts +++ b/packages/tron-wallet-snap/src/context.ts @@ -25,6 +25,8 @@ 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 { CoreAssetsAdapter } from './services/assets/adapters/CoreAssetsAdapter'; +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 +115,7 @@ const securityAlertsApiClient = new SecurityAlertsApiClient( logger, ); -// Business Services -const assetsService = new AssetsService({ +const snapAssetsAdapter = new SnapAssetsAdapter({ logger, state, assetsRepository, @@ -123,10 +124,20 @@ const assetsService = new AssetsService({ priceApiClient, tokenApiClient, snapClient, - remoteFeatureFlagsProvider, + configProvider, +}); +const coreAssetsAdapter = new CoreAssetsAdapter({ + logger, assetsProvider, }); +// Business Services +const assetsService = new AssetsService({ + snapAdapter: snapAssetsAdapter, + coreAdapter: coreAssetsAdapter, + remoteFeatureFlagsProvider, +}); + const transactionsService = new TransactionsService({ logger, transactionsRepository, diff --git a/packages/tron-wallet-snap/src/services/assets/AssetsService.ts b/packages/tron-wallet-snap/src/services/assets/AssetsService.ts index 77f63dc87..114789b0d 100644 --- a/packages/tron-wallet-snap/src/services/assets/AssetsService.ts +++ b/packages/tron-wallet-snap/src/services/assets/AssetsService.ts @@ -1,5 +1,5 @@ import type { KeyringAccount } from '@metamask/keyring-api'; -import { +import type { AssetsProvider, RemoteFeatureFlagsProvider, } from '@metamask/snap-networks-utils'; @@ -16,54 +16,78 @@ 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 { configProvider } from '../../context'; 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 type { CoreAssetsAdapter } from './adapters/CoreAssetsAdapter'; +import { CoreAssetsAdapter as CoreAssetsAdapterClass } from './adapters/CoreAssetsAdapter'; import { SnapAssetsAdapter } from './adapters/SnapAssetsAdapter'; import type { AssetsRepository } from './AssetsRepository'; +type AssetsServiceDependencies = { + logger: ILogger; + assetsRepository: AssetsRepository; + state: State; + trongridApiClient: TrongridApiClient; + tronHttpClient: TronHttpClient; + priceApiClient: PriceApiClient; + tokenApiClient: TokenApiClient; + snapClient: SnapClient; + remoteFeatureFlagsProvider?: RemoteFeatureFlagsProvider; + assetsProvider?: AssetsProvider; +}; + +type AssetsServiceAdapters = { + snapAdapter: SnapAssetsAdapter; + coreAdapter: CoreAssetsAdapter; + remoteFeatureFlagsProvider: RemoteFeatureFlagsProvider; +}; + +function hasAdapterOptions( + options: AssetsServiceDependencies | AssetsServiceAdapters, +): options is AssetsServiceAdapters { + const candidate = options as AssetsServiceAdapters; + return candidate.snapAdapter !== undefined && candidate.coreAdapter !== undefined; +} + /** * Assets domain facade. Currently delegates all behavior to SnapAssetsAdapter - * (legacy snap-owned reads/writes). Core adapter routing can be introduced later - * without changing callers. + * (legacy snap-owned reads/writes). Core adapter is initialized for upcoming + * routing without changing callers. */ export class AssetsService { readonly #snapAdapter: SnapAssetsAdapter; + // Initialized for upcoming Core routing; not read until the migration PR lands. + // eslint-disable-next-line no-unused-private-class-members -- reserved adapter slot + readonly #coreAdapter: CoreAssetsAdapter; + 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(options: AssetsServiceDependencies | AssetsServiceAdapters) { + if (hasAdapterOptions(options)) { + this.#snapAdapter = options.snapAdapter; + this.#coreAdapter = options.coreAdapter; + } else { + this.#snapAdapter = new SnapAssetsAdapter({ + logger: options.logger, + assetsRepository: options.assetsRepository, + state: options.state, + trongridApiClient: options.trongridApiClient, + tronHttpClient: options.tronHttpClient, + priceApiClient: options.priceApiClient, + tokenApiClient: options.tokenApiClient, + snapClient: options.snapClient, + configProvider, + }); + this.#coreAdapter = new CoreAssetsAdapterClass({ + logger: options.logger, + assetsProvider: options.assetsProvider as AssetsProvider, + }); + } + this.cacheTtlsMilliseconds = this.#snapAdapter.cacheTtlsMilliseconds; } diff --git a/packages/tron-wallet-snap/src/services/assets/adapters/CoreAssetsAdapter.ts b/packages/tron-wallet-snap/src/services/assets/adapters/CoreAssetsAdapter.ts new file mode 100644 index 000000000..3cc4a48cf --- /dev/null +++ b/packages/tron-wallet-snap/src/services/assets/adapters/CoreAssetsAdapter.ts @@ -0,0 +1,170 @@ +import type { Caip19AssetId } from '@metamask/assets-controller'; +import { KeyringEvent } from '@metamask/keyring-api'; +import type { + AccountAssetListUpdatedEvent, + AccountBalancesUpdatedEvent, +} from '@metamask/keyring-api'; +import { emitSnapKeyringEvent } from '@metamask/keyring-snap-sdk'; +import type { AssetsProvider } from '@metamask/snap-networks-utils'; + +import { Network } from '../../../constants'; +import type { AssetEntity } from '../../../entities/assets'; +import { createPrefixedLogger } from '../../../utils/logger'; +import type { ILogger } from '../../../utils/logger'; +import { mapControllerAsset } from '../mapControllerAsset'; +import { isSnapOwnedAsset } from '../snapOwnedAssets'; + +/** + * Uses the AssetsController for fungible reads. Snap-owned (special) assets are + * published via keyring events without local persistence when migration is active. + */ +export class CoreAssetsAdapter { + readonly #logger: ILogger; + + readonly #assetsProvider: AssetsProvider; + + constructor({ + logger, + assetsProvider, + }: { + logger: ILogger; + assetsProvider: AssetsProvider; + }) { + this.#logger = createPrefixedLogger(logger, '[CoreAssetsAdapter]'); + this.#assetsProvider = assetsProvider; + } + + async getAccountAssetByID( + accountId: string, + assetId: string, + ): Promise { + this.#logger.info('Getting account asset by ID', { accountId, assetId }); + const asset = await this.#assetsProvider.getAccountAssetByID( + accountId, + assetId as Caip19AssetId, + ); + + if (!asset) { + return null; + } + + return mapControllerAsset(accountId, asset); + } + + async getAccountAssetsByIDs( + accountId: string, + assetIds: string[], + ): Promise<(AssetEntity | null)[]> { + this.#logger.info('Getting account assets by IDs', { accountId, assetIds }); + const assets = await this.#assetsProvider.getAccountAssetsByIDs( + accountId, + assetIds as Caip19AssetId[], + ); + + return assetIds.map((assetId) => { + const asset = assets[assetId as Caip19AssetId]; + return asset ? mapControllerAsset(accountId, asset) : null; + }); + } + + async getAccountAssetsByScope( + scope: Network, + keyringAccountId: string, + ): Promise { + this.#logger.info('Getting account assets by scope', { + scope, + keyringAccountId, + }); + const controllerAssets = await this.#assetsProvider.getAccountAssetsByScope( + scope, + keyringAccountId, + ); + + return Object.values(controllerAssets).map((asset) => + mapControllerAsset(keyringAccountId, asset), + ); + } + + async getAccountAssets(accountId: string): Promise { + this.#logger.info('Getting account assets', { accountId }); + const [mainnetAssets, nileAssets, shastaAssets] = await Promise.all([ + this.#assetsProvider.getAccountAssetsByScope(Network.Mainnet, accountId), + this.#assetsProvider.getAccountAssetsByScope(Network.Nile, accountId), + this.#assetsProvider.getAccountAssetsByScope(Network.Shasta, accountId), + ]); + + const allUnmappedAssets = [ + ...Object.values(mainnetAssets), + ...Object.values(nileAssets), + ...Object.values(shastaAssets), + ]; + const allAssets = allUnmappedAssets.map((asset) => + mapControllerAsset(accountId, asset), + ); + + return allAssets; + } + + /** + * Publishes snap-owned assets to the extension without persisting locally. + * Always emits the full special-asset set from the payload as `added`, plus + * balances for those assets (including zeros). + * + * @param assets - Latest asset snapshot (fungibles are ignored). + */ + async saveMany(assets: AssetEntity[]): Promise { + this.#logger.info('Publishing snap-owned assets', assets); + + const snapOwnedAssets = assets.filter((asset) => + isSnapOwnedAsset(asset.assetType), + ); + + if (snapOwnedAssets.length === 0) { + return; + } + + const assetListUpdatedPayload = snapOwnedAssets.reduce< + AccountAssetListUpdatedEvent['params']['assets'] + >((acc, asset) => { + const existing = acc[asset.keyringAccountId] ?? { + added: [], + removed: [], + }; + const added = existing.added.includes(asset.assetType) + ? existing.added + : [...existing.added, asset.assetType]; + + return { + ...acc, + [asset.keyringAccountId]: { + added, + removed: existing.removed, + }, + }; + }, {}); + + await emitSnapKeyringEvent(snap, KeyringEvent.AccountAssetListUpdated, { + assets: assetListUpdatedPayload, + }); + + const balancesUpdatedPayload = snapOwnedAssets.reduce< + AccountBalancesUpdatedEvent['params']['balances'] + >( + (acc, asset) => ({ + ...acc, + [asset.keyringAccountId]: { + ...(acc[asset.keyringAccountId] ?? {}), + [asset.assetType]: { + unit: asset.symbol, + amount: asset.uiAmount, + }, + }, + }), + {}, + ); + + await emitSnapKeyringEvent(snap, KeyringEvent.AccountBalancesUpdated, { + balances: balancesUpdatedPayload, + }); + } +} 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..4f9b406f0 100644 --- a/packages/tron-wallet-snap/src/services/assets/adapters/SnapAssetsAdapter.ts +++ b/packages/tron-wallet-snap/src/services/assets/adapters/SnapAssetsAdapter.ts @@ -54,11 +54,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 { @@ -113,6 +113,8 @@ export class SnapAssetsAdapter { readonly #snapClient: SnapClient; + readonly #configProvider: ConfigProvider; + readonly cacheTtlsMilliseconds: { fiatExchangeRates: number; spotPrices: number; @@ -128,6 +130,7 @@ export class SnapAssetsAdapter { priceApiClient, tokenApiClient, snapClient, + configProvider, }: { logger: ILogger; assetsRepository: AssetsRepository; @@ -137,6 +140,7 @@ export class SnapAssetsAdapter { priceApiClient: PriceApiClient; tokenApiClient: TokenApiClient; snapClient: SnapClient; + configProvider: ConfigProvider; }) { this.#logger = createPrefixedLogger(logger, '[🪙 SnapAssetsAdapter]'); this.#assetsRepository = assetsRepository; @@ -146,8 +150,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 +160,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[], @@ -1442,6 +1443,10 @@ export class SnapAssetsAdapter { } as AssetEntity; } + async getAccountAssets(accountId: string): Promise { + return this.#assetsRepository.getByAccountId(accountId); + } + async getByKeyringAccountId( keyringAccountId: string, ): Promise { diff --git a/packages/tron-wallet-snap/src/services/assets/mapControllerAsset.test.ts b/packages/tron-wallet-snap/src/services/assets/mapControllerAsset.test.ts new file mode 100644 index 000000000..14c59fd05 --- /dev/null +++ b/packages/tron-wallet-snap/src/services/assets/mapControllerAsset.test.ts @@ -0,0 +1,61 @@ +import type { Asset } from '@metamask/assets-controller'; +import type { CaipAssetType } from '@metamask/utils'; + +import { Network } from '../../constants'; +import { mapControllerAsset } from './mapControllerAsset'; + +describe('mapControllerAsset', () => { + const accountId = 'account-id'; + const unknownAssetId = 'tron:728126428/trc20:unknown'; + + it('maps controller metadata when present', () => { + const asset = { + id: unknownAssetId, + chainId: Network.Mainnet, + balance: { amount: '1234567' }, + metadata: { + type: 'fungible', + symbol: 'TKN', + name: 'Token', + decimals: 6, + image: 'https://example.com/token.png', + }, + price: { price: 0, lastUpdated: 0 }, + fiatValue: 0, + } as unknown as Asset; + + expect(mapControllerAsset(accountId, asset)).toStrictEqual({ + assetType: unknownAssetId, + keyringAccountId: accountId, + network: Network.Mainnet, + symbol: 'TKN', + decimals: 6, + rawAmount: '1234567', + uiAmount: '1.234567', + iconUrl: 'https://example.com/token.png', + }); + }); + + it('uses empty defaults when metadata is missing', () => { + const assetId = 'tron:728126428/trc20:missing' as CaipAssetType; + const asset = { + id: assetId, + chainId: Network.Mainnet, + balance: { amount: '42' }, + metadata: { type: 'fungible', name: 'Missing' }, + price: { price: 0, lastUpdated: 0 }, + fiatValue: 0, + } as unknown as Asset; + + expect(mapControllerAsset(accountId, asset)).toStrictEqual({ + assetType: assetId, + keyringAccountId: accountId, + network: Network.Mainnet, + symbol: '', + decimals: 0, + rawAmount: '42', + uiAmount: '42', + iconUrl: '', + }); + }); +}); diff --git a/packages/tron-wallet-snap/src/services/assets/mapControllerAsset.ts b/packages/tron-wallet-snap/src/services/assets/mapControllerAsset.ts new file mode 100644 index 000000000..fe15d57b6 --- /dev/null +++ b/packages/tron-wallet-snap/src/services/assets/mapControllerAsset.ts @@ -0,0 +1,34 @@ +import type { Asset } from '@metamask/assets-controller'; + +import { Network } from '../../constants'; +import type { AssetEntity } from '../../entities/assets'; +import { toUiAmount } from '../../utils/conversion'; + +/** + * Maps an AssetsController asset to the Snap's {@link AssetEntity} shape. + * + * @param accountId - Keyring account ID. + * @param asset - Asset returned by AssetsController. + * @returns Mapped asset entity. + */ +export function mapControllerAsset( + accountId: string, + asset: Asset, +): AssetEntity { + const assetId = asset.id; + const decimals = asset.metadata.decimals ?? 0; + const symbol = asset.metadata.symbol ?? ''; + const iconUrl = asset.metadata.image ?? ''; + const { amount } = asset.balance; + + return { + assetType: assetId, + keyringAccountId: accountId, + network: asset.chainId as Network, + symbol, + decimals, + rawAmount: amount, + uiAmount: toUiAmount(amount, decimals).toString(), + iconUrl, + } as AssetEntity; +} diff --git a/yarn.lock b/yarn.lock index 1a044bd76..b28a6fee9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3780,6 +3780,7 @@ __metadata: "@metamask/snaps-jest": "npm:^10.2.0" "@metamask/snaps-sdk": "npm:^11.2.0" "@metamask/superstruct": "npm:^3.4.1" + "@metamask/utils": "npm:^11.9.0" "@types/lodash": "npm:^4.17.15" async-mutex: "npm:^0.5.0" bignumber.js: "npm:^9.3.1"