From c31a861fb835adb718ae2c89c018c02c718a13fc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 5 Aug 2026 14:02:45 +0000 Subject: [PATCH 1/2] feat(WPN-1476): hardcode Core assets reads and disable Snap fungible tracking Remove migration-stage and remote feature-flag routing from AssetsService. Always read fungibles via AssetsProvider/mapControllerAsset; keep Snap-owned NFT merge through SnapAssetsAdapter. Disable fetch/save persistence for fungibles and simplify AccountsSynchronizer to Core reads only. Co-authored-by: Ulisses Ferreira --- packages/solana-wallet-snap/CHANGELOG.md | 4 +- packages/solana-wallet-snap/package.json | 1 - .../solana-wallet-snap/snap.manifest.json | 1 - .../services/accounts/AccountsSynchronizer.ts | 18 +- .../services/assets/AssetsService.test.ts | 903 ++++-------------- .../src/core/services/assets/AssetsService.ts | 310 +----- .../assets/shouldTrackSnapAssets.test.ts | 14 - .../services/assets/shouldTrackSnapAssets.ts | 14 - .../registerCoreAssetsControllerHandlers.ts | 5 - .../solana-wallet-snap/src/snapContext.ts | 19 +- .../src/types/core-messenger.ts | 2 - yarn.lock | 1 - 12 files changed, 248 insertions(+), 1044 deletions(-) delete mode 100644 packages/solana-wallet-snap/src/core/services/assets/shouldTrackSnapAssets.test.ts delete mode 100644 packages/solana-wallet-snap/src/core/services/assets/shouldTrackSnapAssets.ts diff --git a/packages/solana-wallet-snap/CHANGELOG.md b/packages/solana-wallet-snap/CHANGELOG.md index 4f91b0da..7f1aadb9 100644 --- a/packages/solana-wallet-snap/CHANGELOG.md +++ b/packages/solana-wallet-snap/CHANGELOG.md @@ -9,8 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Route Solana fungible asset reads through AssetsController migration stages (`Off`, `ReadAssetsControllerWithFallback`, `ReadAssetsControllerOnly`), mapping controller assets via `mapControllerAsset` while Snap-owned NFT assets always use `SnapAssetsAdapter`. Gate fungible tracking in `fetch`/`save`/`saveMany` and account monitors via `shouldTrackSnapAssets`. -- Wire Core messenger plumbing (`endowment:messenger`, `AssetsProvider`, `RemoteFeatureFlagsProvider`) into the Solana snap. +- Hardcode Solana fungible asset reads through `AssetsProvider` / `mapControllerAsset` (no migration-stage or remote feature-flag routing). Disable Snap fungible tracking in `fetch`/`save`/`saveMany` and account sync; Snap-owned NFT assets still use `SnapAssetsAdapter`. +- Wire Core messenger plumbing (`endowment:messenger`, `AssetsProvider`) into the Solana snap. - Extract Snap-owned balance fetch/persist/read logic into `SnapAssetsAdapter`; `AssetsService` delegates account asset reads and saves through the adapter (no Core routing yet). ([#121](https://github.com/MetaMask/internal-snaps/pull/121)) - Align `AssetsService` read API with `snap-networks-utils` / AssetsController shapes by adding `getAccountAssetByID`, `getAccountAssetsByIDs`, `getAccountAssetsByScope`, and `getAccountAssetsForAllActiveScopes`, and routing Keyring, Send, send render, and `refreshSend` through them (still Snap-owned storage). ([#120](https://github.com/MetaMask/internal-snaps/pull/120)) - This package was migrated from [snap-solana-wallet](https://github.com/MetaMask/snap-solana-wallet). See the source repository for the original [changelog](https://github.com/MetaMask/snap-solana-wallet/blob/main/packages/snap/CHANGELOG.md). ([#72](https://github.com/MetaMask/internal-snaps/pull/72)) diff --git a/packages/solana-wallet-snap/package.json b/packages/solana-wallet-snap/package.json index 51f74c9f..f2376ad6 100644 --- a/packages/solana-wallet-snap/package.json +++ b/packages/solana-wallet-snap/package.json @@ -58,7 +58,6 @@ "@metamask/keyring-api": "^23.7.0", "@metamask/keyring-snap-sdk": "^9.2.1", "@metamask/messenger": "^2.0.0", - "@metamask/remote-feature-flag-controller": "^5.0.0", "@metamask/snap-networks-utils": "workspace:^", "@metamask/snaps-cli": "^8.4.1", "@metamask/snaps-jest": "^10.2.0", diff --git a/packages/solana-wallet-snap/snap.manifest.json b/packages/solana-wallet-snap/snap.manifest.json index c588fb0b..d7c3f283 100644 --- a/packages/solana-wallet-snap/snap.manifest.json +++ b/packages/solana-wallet-snap/snap.manifest.json @@ -91,7 +91,6 @@ "snap_getPreferences": {}, "endowment:messenger": { "actions": [ - "RemoteFeatureFlagController:getState", "AssetsController:getAccountAssetByID", "AssetsController:getAccountAssetsByIDs", "AssetsController:getAccountAssetsByScope" diff --git a/packages/solana-wallet-snap/src/core/services/accounts/AccountsSynchronizer.ts b/packages/solana-wallet-snap/src/core/services/accounts/AccountsSynchronizer.ts index 6212b9ef..8b859dca 100644 --- a/packages/solana-wallet-snap/src/core/services/accounts/AccountsSynchronizer.ts +++ b/packages/solana-wallet-snap/src/core/services/accounts/AccountsSynchronizer.ts @@ -33,21 +33,9 @@ export class AccountsSynchronizer { const assets = ( await Promise.allSettled( - accountsToSync.map(async (account) => { - if ( - await this.#assetsService.shouldTrackSnapAssetsForAccount( - account.id, - ) - ) { - const fetchedAssets = await this.#assetsService.fetch(account); - await this.#assetsService.saveMany(fetchedAssets); - return fetchedAssets; - } - - return this.#assetsService.getAccountAssetsForAllActiveScopes( - account.id, - ); - }), + accountsToSync.map(async (account) => + this.#assetsService.getAccountAssetsForAllActiveScopes(account.id), + ), ) ) .map((item) => (item.status === 'fulfilled' ? item.value : [])) diff --git a/packages/solana-wallet-snap/src/core/services/assets/AssetsService.test.ts b/packages/solana-wallet-snap/src/core/services/assets/AssetsService.test.ts index 000364d1..bf305a78 100644 --- a/packages/solana-wallet-snap/src/core/services/assets/AssetsService.test.ts +++ b/packages/solana-wallet-snap/src/core/services/assets/AssetsService.test.ts @@ -1,13 +1,7 @@ -import { - SNAPS_ASSETS_MIGRATION_FLAG_KEYS, - SnapsAssetsMigrationStage, -} from '@metamask/assets-controller'; -import { KeyringEvent } from '@metamask/keyring-api'; import { emitSnapKeyringEvent } from '@metamask/keyring-snap-sdk'; import { cloneDeep } from 'lodash'; import type { AssetEntity } from '../../../entities'; -import type { CoreMessengerCaller } from '../../../types/core-messenger'; import type { ICache } from '../../caching/ICache'; import { InMemoryCache } from '../../caching/InMemoryCache'; import { MOCK_NFTS_LIST_RESPONSE_MAPPED } from '../../clients/nft-api/mocks/mockNftsListResponseMapped'; @@ -28,7 +22,6 @@ import type { ConfigProvider } from '../config'; import type { SolanaConnection } from '../connection'; import { mockLogger } from '../mocks/logger'; import { createMockConnection } from '../mocks/mockConnection'; -import { MOCK_SOLANA_RPC_GET_TOKEN_ACCOUNTS_BY_OWNER_RESPONSE } from '../mocks/mockSolanaRpcResponses'; import type { TokenPricesService } from '../token-prices/TokenPrices'; import { SnapAssetsAdapter } from './adapters/SnapAssetsAdapter'; import type { AssetsRepository } from './AssetsRepository'; @@ -38,18 +31,6 @@ jest.mock('@metamask/keyring-snap-sdk', () => ({ emitSnapKeyringEvent: jest.fn(), })); -const SOLANA_FLAG_KEY = SNAPS_ASSETS_MIGRATION_FLAG_KEYS.solana; - -function createMessengerCallMock( - getState: () => unknown, -): CoreMessengerCaller['call'] { - return async (method) => { - if (method === 'RemoteFeatureFlagController:getState') { - return getState() as Awaited>; - } - return undefined; - }; -} describe('AssetsService', () => { let assetsService: AssetsService; let snapAssetsAdapter: SnapAssetsAdapter; @@ -62,13 +43,9 @@ describe('AssetsService', () => { let mockNftApiClient: NftApiClient; let mockCache: ICache; let mockAssetsProvider: import('@metamask/snap-networks-utils').AssetsProvider; - let migrationStage: SnapsAssetsMigrationStage; - let mockCoreMessenger: CoreMessengerCaller; - let setMigrationStage: (stage: SnapsAssetsMigrationStage) => void; beforeEach(() => { jest.clearAllMocks(); - migrationStage = SnapsAssetsMigrationStage.Off; mockConnection = createMockConnection(); mockConfigProvider = { @@ -129,25 +106,10 @@ describe('AssetsService', () => { getAccountAssetsByScope: jest.fn(), } as unknown as import('@metamask/snap-networks-utils').AssetsProvider; - setMigrationStage = (stage: SnapsAssetsMigrationStage) => { - migrationStage = stage; - }; - - mockCoreMessenger = { - call: jest.fn().mockImplementation( - createMessengerCallMock(() => ({ - remoteFeatureFlags: { - [SOLANA_FLAG_KEY]: { stage: migrationStage }, - }, - })), - ), - }; - assetsService = new AssetsService({ logger: mockLogger, configProvider: mockConfigProvider, snapAssetsAdapter, - coreMessenger: mockCoreMessenger, accountsService: mockAccountsService, tokenApiClient: mockTokenApiClient, tokenPricesService: mockTokenPricesService, @@ -157,392 +119,35 @@ describe('AssetsService', () => { }); describe('fetch', () => { - it('fetches native and token assets', async () => { - jest.spyOn(mockConnection, 'getRpc').mockReturnValue({ - getBalance: jest.fn().mockReturnValueOnce({ - send: jest.fn().mockResolvedValue({ - value: 1000000000, // Native balance on Mainnet - }), - }), - getTokenAccountsByOwner: jest.fn().mockReturnValue({ - send: jest - .fn() - .mockResolvedValueOnce({ - value: - MOCK_SOLANA_RPC_GET_TOKEN_ACCOUNTS_BY_OWNER_RESPONSE.result - .value, - }) - .mockResolvedValue({ - value: [], - }), - }), - } as any); - - const assets = await assetsService.fetch(MOCK_SOLANA_KEYRING_ACCOUNT_0); - - expect(assets).toStrictEqual(MOCK_ASSET_ENTITIES); - }); - - it('does not fail on individual RPC call failures to fetch native assets', async () => { - jest.spyOn(mockConnection, 'getRpc').mockReturnValue({ - getBalance: jest.fn().mockReturnValue({ - send: jest - .fn() - .mockRejectedValueOnce(new Error('Error getting balance')), - }), - getTokenAccountsByOwner: jest.fn().mockReturnValue({ - send: jest - .fn() - .mockResolvedValueOnce({ - value: - MOCK_SOLANA_RPC_GET_TOKEN_ACCOUNTS_BY_OWNER_RESPONSE.result - .value, - }) - .mockResolvedValue({ - value: [], - }), - }), - } as any); + it('returns an empty array because Snap fungible tracking is disabled', async () => { + const fetchSpy = jest.spyOn(snapAssetsAdapter, 'fetch'); const assets = await assetsService.fetch(MOCK_SOLANA_KEYRING_ACCOUNT_0); - expect(assets).toStrictEqual([MOCK_ASSET_ENTITY_1, MOCK_ASSET_ENTITY_2]); - }); - - it('does not fail on individual RPC call failures to fetch token assets', async () => { - jest.spyOn(mockConnection, 'getRpc').mockReturnValue({ - getBalance: jest.fn().mockReturnValueOnce({ - send: jest.fn().mockResolvedValue({ - value: 1000000000, // Native balance on Mainnet - }), - }), - getTokenAccountsByOwner: jest.fn().mockReturnValue({ - send: jest - .fn() - .mockRejectedValueOnce(new Error('Error getting token accounts')), - }), - } as any); - - const assets = await assetsService.fetch(MOCK_SOLANA_KEYRING_ACCOUNT_0); - - expect(assets).toStrictEqual([MOCK_ASSET_ENTITY_0]); + expect(assets).toStrictEqual([]); + expect(fetchSpy).not.toHaveBeenCalled(); }); }); describe('save', () => { - it('saves an asset', async () => { - const spy = jest - .spyOn(assetsService, 'saveMany') - .mockResolvedValueOnce(undefined); + it('is a no-op because Snap fungible tracking is disabled', async () => { + const saveManySpy = jest.spyOn(mockAssetsRepository, 'saveMany'); await assetsService.save(MOCK_ASSET_ENTITY_0); - expect(spy).toHaveBeenCalledWith([MOCK_ASSET_ENTITY_0]); + expect(saveManySpy).not.toHaveBeenCalled(); }); }); describe('saveMany', () => { - it('delegates to repository for saving assets', async () => { - const saveManySpy = jest - .spyOn(mockAssetsRepository, 'saveMany') - .mockResolvedValue(undefined); - - jest.spyOn(mockAssetsRepository, 'getAll').mockResolvedValueOnce([]); - - await assetsService.saveMany(MOCK_ASSET_ENTITIES); - - expect(saveManySpy).toHaveBeenCalledWith(MOCK_ASSET_ENTITIES); - }); - - it('emits event "AccountAssetListUpdated" with ALL assets in added list, and removed assets in removed list', async () => { - jest.spyOn(mockAssetsRepository, 'getAll').mockResolvedValueOnce([]); - - const addedAssets = [MOCK_ASSET_ENTITY_0, MOCK_ASSET_ENTITY_1]; - const removedAssets = [ - { - ...MOCK_ASSET_ENTITY_2, - rawAmount: '0', - }, - ]; - - await assetsService.saveMany([...addedAssets, ...removedAssets]); - - expect(emitSnapKeyringEvent).toHaveBeenNthCalledWith( - 1, - snap, - KeyringEvent.AccountAssetListUpdated, - { - assets: { - [MOCK_SOLANA_KEYRING_ACCOUNT_0.id]: { - added: [ - MOCK_ASSET_ENTITY_0.assetType, - MOCK_ASSET_ENTITY_1.assetType, - ], - removed: [MOCK_ASSET_ENTITY_2.assetType], - }, - }, - }, - ); - }); - - it('emits event "AccountAssetListUpdated" when an asset was saved with a zero balance and some more is added', async () => { - jest - .spyOn(mockAssetsRepository, 'getAll') - .mockResolvedValueOnce([{ ...MOCK_ASSET_ENTITY_0, rawAmount: '0' }]) - .mockResolvedValueOnce([{ ...MOCK_ASSET_ENTITY_0, rawAmount: '0' }]); - - await assetsService.saveMany([ - { ...MOCK_ASSET_ENTITY_0, rawAmount: '0' }, - ]); - - await assetsService.saveMany([ - { ...MOCK_ASSET_ENTITY_0, rawAmount: '1000000' }, - ]); - - expect(emitSnapKeyringEvent).toHaveBeenCalledWith( - snap, - KeyringEvent.AccountAssetListUpdated, - { - assets: { - [MOCK_SOLANA_KEYRING_ACCOUNT_0.id]: { - added: [MOCK_ASSET_ENTITY_0.assetType], - removed: [], - }, - }, - }, - ); - }); - - it('emits event "AccountBalancesUpdated" when balances change', async () => { - jest.spyOn(mockAssetsRepository, 'getAll').mockResolvedValueOnce([]); - - await assetsService.saveMany(MOCK_ASSET_ENTITIES); - - expect(emitSnapKeyringEvent).toHaveBeenNthCalledWith( - 2, - snap, - KeyringEvent.AccountBalancesUpdated, - { - balances: { - [MOCK_SOLANA_KEYRING_ACCOUNT_0.id]: { - [MOCK_ASSET_ENTITY_0.assetType]: { - unit: MOCK_ASSET_ENTITY_0.symbol, - amount: MOCK_ASSET_ENTITY_0.uiAmount, - }, - [MOCK_ASSET_ENTITY_1.assetType]: { - unit: MOCK_ASSET_ENTITY_1.symbol, - amount: MOCK_ASSET_ENTITY_1.uiAmount, - }, - [MOCK_ASSET_ENTITY_2.assetType]: { - unit: MOCK_ASSET_ENTITY_2.symbol, - amount: MOCK_ASSET_ENTITY_2.uiAmount, - }, - }, - }, - }, - ); - }); - - it('emits event "AccountBalancesUpdated" when native balance goes from non-zero to zero', async () => { - jest - .spyOn(mockAssetsRepository, 'getAll') - .mockResolvedValue([{ ...MOCK_ASSET_ENTITY_0, uiAmount: '1234' }]); - - await assetsService.saveMany([{ ...MOCK_ASSET_ENTITY_0, uiAmount: '0' }]); - - expect(emitSnapKeyringEvent).toHaveBeenCalledWith( - snap, - KeyringEvent.AccountBalancesUpdated, - { - balances: { - [MOCK_SOLANA_KEYRING_ACCOUNT_0.id]: { - [MOCK_ASSET_ENTITY_0.assetType]: { - unit: MOCK_ASSET_ENTITY_0.symbol, - amount: '0', - }, - }, - }, - }, - ); - }); - - // With isIncremental = false, we do emit events, even when no assets changed - it.skip('does not emit events when no assets changed', async () => { - jest - .spyOn(mockAssetsRepository, 'getAll') - .mockResolvedValue(MOCK_ASSET_ENTITIES); - - await assetsService.saveMany(MOCK_ASSET_ENTITIES); - (emitSnapKeyringEvent as jest.Mock).mockClear(); + it('is a no-op because Snap fungible tracking is disabled', async () => { + const saveManySpy = jest.spyOn(mockAssetsRepository, 'saveMany'); await assetsService.saveMany(MOCK_ASSET_ENTITIES); + expect(saveManySpy).not.toHaveBeenCalled(); expect(emitSnapKeyringEvent).not.toHaveBeenCalled(); }); - - it('fetches saved assets before saving new assets to ensure correct change detection', async () => { - const callOrder: string[] = []; - - const getAllSpy = jest - .spyOn(mockAssetsRepository, 'getAll') - .mockImplementation(async () => { - callOrder.push('getAll'); - return []; - }); - - const saveManySpy = jest - .spyOn(mockAssetsRepository, 'saveMany') - .mockImplementation(async () => { - callOrder.push('saveMany'); - }); - - await assetsService.saveMany(MOCK_ASSET_ENTITIES); - - // Verify that getAll was called before saveMany - expect(callOrder).toStrictEqual(['getAll', 'saveMany']); - expect(getAllSpy).toHaveBeenCalledTimes(1); - expect(saveManySpy).toHaveBeenCalledTimes(1); - }); - - it('correctly detects new assets when savedAssets is fetched before saving', async () => { - // Start with empty state - jest.spyOn(mockAssetsRepository, 'getAll').mockResolvedValueOnce([]); - - await assetsService.saveMany([MOCK_ASSET_ENTITY_0]); - - // Should emit AccountAssetListUpdated with the new asset - expect(emitSnapKeyringEvent).toHaveBeenCalledWith( - snap, - KeyringEvent.AccountAssetListUpdated, - { - assets: { - [MOCK_SOLANA_KEYRING_ACCOUNT_0.id]: { - added: [MOCK_ASSET_ENTITY_0.assetType], - removed: [], - }, - }, - }, - ); - }); - - it('correctly detects assets going from zero to non-zero balance when savedAssets is fetched before saving', async () => { - const assetWithZeroBalance = { ...MOCK_ASSET_ENTITY_0, rawAmount: '0' }; - const assetWithNonZeroBalance = { - ...MOCK_ASSET_ENTITY_0, - rawAmount: '1000000', - }; - - // First save with zero balance - jest.spyOn(mockAssetsRepository, 'getAll').mockResolvedValueOnce([]); - await assetsService.saveMany([assetWithZeroBalance]); - - (emitSnapKeyringEvent as jest.Mock).mockClear(); - - // Then save with non-zero balance, but getAll should return the state before this save - jest - .spyOn(mockAssetsRepository, 'getAll') - .mockResolvedValueOnce([assetWithZeroBalance]); - await assetsService.saveMany([assetWithNonZeroBalance]); - - // Should emit AccountAssetListUpdated because asset went from zero to non-zero - expect(emitSnapKeyringEvent).toHaveBeenCalledWith( - snap, - KeyringEvent.AccountAssetListUpdated, - { - assets: { - [MOCK_SOLANA_KEYRING_ACCOUNT_0.id]: { - added: [MOCK_ASSET_ENTITY_0.assetType], - removed: [], - }, - }, - }, - ); - }); - - // With isIncremental = false, we do emit events, even when no assets changed - it.skip('does not incorrectly mark assets as new when they are already in the saved state', async () => { - // Mock that the asset already exists in saved state - jest - .spyOn(mockAssetsRepository, 'getAll') - .mockResolvedValueOnce([MOCK_ASSET_ENTITY_0]); - - await assetsService.saveMany([MOCK_ASSET_ENTITY_0]); - - // Should not emit AccountAssetListUpdated since no assets were actually added/removed - expect(emitSnapKeyringEvent).not.toHaveBeenCalledWith( - snap, - KeyringEvent.AccountAssetListUpdated, - expect.any(Object), - ); - }); - - it('correctly identifies balance changes when savedAssets reflects pre-save state', async () => { - const originalAsset = { ...MOCK_ASSET_ENTITY_0, rawAmount: '1000000' }; - const updatedAsset = { - ...MOCK_ASSET_ENTITY_0, - rawAmount: '2000000', - uiAmount: '2.0', - }; - - // Mock that original asset exists in saved state - jest - .spyOn(mockAssetsRepository, 'getAll') - .mockResolvedValueOnce([originalAsset]); - - await assetsService.saveMany([updatedAsset]); - - // Should emit AccountBalancesUpdated because balance changed - expect(emitSnapKeyringEvent).toHaveBeenCalledWith( - snap, - KeyringEvent.AccountBalancesUpdated, - { - balances: { - [MOCK_SOLANA_KEYRING_ACCOUNT_0.id]: { - [MOCK_ASSET_ENTITY_0.assetType]: { - unit: updatedAsset.symbol, - amount: updatedAsset.uiAmount, - }, - }, - }, - }, - ); - }); - - it('does not include native assets in removed array even when they have zero balance', async () => { - const nativeAssetWithZeroBalance = { - ...MOCK_ASSET_ENTITY_0, - rawAmount: '0', - }; - const tokenAssetWithZeroBalance = { - ...MOCK_ASSET_ENTITY_1, - rawAmount: '0', - }; - - // Mock that both assets existed with non-zero balance - jest.spyOn(mockAssetsRepository, 'getAll').mockResolvedValueOnce([ - MOCK_ASSET_ENTITY_0, // Native asset with non-zero balance - MOCK_ASSET_ENTITY_1, // Token asset with non-zero balance - ]); - - await assetsService.saveMany([ - nativeAssetWithZeroBalance, - tokenAssetWithZeroBalance, - ]); - - // Should emit AccountAssetListUpdated with only the token asset in the removed array - expect(emitSnapKeyringEvent).toHaveBeenCalledWith( - snap, - KeyringEvent.AccountAssetListUpdated, - { - assets: { - [MOCK_SOLANA_KEYRING_ACCOUNT_0.id]: { - added: [MOCK_ASSET_ENTITY_0.assetType], - removed: [MOCK_ASSET_ENTITY_1.assetType], // Only token asset, not native - }, - }, - }, - ); - }); }); describe('hasChanged', () => { @@ -626,7 +231,7 @@ describe('AssetsService', () => { }); it('includes placeholder native assets with zero balance when no native assets exist', async () => { - const nonNativeAssets = [MOCK_ASSET_ENTITY_1, MOCK_ASSET_ENTITY_2]; // Token assets only + const nonNativeAssets = [MOCK_ASSET_ENTITY_1, MOCK_ASSET_ENTITY_2]; jest .spyOn(mockAssetsRepository, 'findByKeyringAccountId') @@ -636,7 +241,6 @@ describe('AssetsService', () => { MOCK_SOLANA_KEYRING_ACCOUNT_0, ); - // Should include the saved assets plus a placeholder native asset expect(assets).toHaveLength(nonNativeAssets.length + 1); expect(assets).toStrictEqual( expect.arrayContaining([ @@ -658,7 +262,7 @@ describe('AssetsService', () => { it('does not add placeholder native assets when they already exist', async () => { jest .spyOn(mockAssetsRepository, 'findByKeyringAccountId') - .mockResolvedValueOnce(MOCK_ASSET_ENTITIES); // Includes native asset (MOCK_ASSET_ENTITY_0) + .mockResolvedValueOnce(MOCK_ASSET_ENTITIES); const assets = await assetsService.findByAccount( MOCK_SOLANA_KEYRING_ACCOUNT_0, @@ -669,23 +273,60 @@ describe('AssetsService', () => { }); describe('getAccountAssetByID', () => { - it('returns the matching asset when present', async () => { - jest - .spyOn(mockAssetsRepository, 'findByKeyringAccountId') - .mockResolvedValueOnce(MOCK_ASSET_ENTITIES); + it('routes fungible assets through AssetsProvider', async () => { + jest.spyOn(mockAssetsProvider, 'getAccountAssetByID').mockResolvedValue({ + id: MOCK_ASSET_ENTITY_1.assetType, + chainId: Network.Mainnet, + balance: { amount: MOCK_ASSET_ENTITY_1.rawAmount }, + metadata: { + type: 'fungible', + symbol: MOCK_ASSET_ENTITY_1.symbol, + name: MOCK_ASSET_ENTITY_1.symbol, + decimals: MOCK_ASSET_ENTITY_1.decimals, + }, + price: { price: 0, lastUpdated: 0 }, + fiatValue: 0, + } as never); const asset = await assetsService.getAccountAssetByID( MOCK_SOLANA_KEYRING_ACCOUNT_0.id, MOCK_ASSET_ENTITY_1.assetType, ); - expect(asset).toStrictEqual(MOCK_ASSET_ENTITY_1); + expect(mockAssetsProvider.getAccountAssetByID).toHaveBeenCalledWith( + MOCK_SOLANA_KEYRING_ACCOUNT_0.id, + MOCK_ASSET_ENTITY_1.assetType, + ); + expect(asset).toMatchObject({ + assetType: MOCK_ASSET_ENTITY_1.assetType, + keyringAccountId: MOCK_SOLANA_KEYRING_ACCOUNT_0.id, + rawAmount: MOCK_ASSET_ENTITY_1.rawAmount, + }); }); - it('returns null when the asset is missing', async () => { + it('routes NFT assets through SnapAssetsAdapter', async () => { + const nftAssetType = + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/nft:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; + const snapSpy = jest + .spyOn(snapAssetsAdapter, 'getAccountAssetByID') + .mockResolvedValueOnce(null); + + await assetsService.getAccountAssetByID( + MOCK_SOLANA_KEYRING_ACCOUNT_0.id, + nftAssetType, + ); + + expect(snapSpy).toHaveBeenCalledWith( + MOCK_SOLANA_KEYRING_ACCOUNT_0.id, + nftAssetType, + ); + expect(mockAssetsProvider.getAccountAssetByID).not.toHaveBeenCalled(); + }); + + it('returns null when the fungible asset is missing from Core', async () => { jest - .spyOn(mockAssetsRepository, 'findByKeyringAccountId') - .mockResolvedValueOnce([]); + .spyOn(mockAssetsProvider, 'getAccountAssetByID') + .mockResolvedValueOnce(null); const asset = await assetsService.getAccountAssetByID( MOCK_SOLANA_KEYRING_ACCOUNT_0.id, @@ -697,64 +338,84 @@ describe('AssetsService', () => { }); describe('getAccountAssetsByIDs', () => { - it('returns a record keyed by asset ID', async () => { + it('routes fungible and NFT asset IDs to the correct adapters', async () => { + const nftAssetType = + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/nft:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; + + jest.spyOn(mockAssetsProvider, 'getAccountAssetsByIDs').mockResolvedValue({ + [MOCK_ASSET_ENTITY_0.assetType]: { + id: MOCK_ASSET_ENTITY_0.assetType, + chainId: Network.Mainnet, + balance: { amount: MOCK_ASSET_ENTITY_0.rawAmount }, + metadata: { + type: 'native', + symbol: 'SOL', + name: 'Solana', + decimals: 9, + }, + price: { price: 0, lastUpdated: 0 }, + fiatValue: 0, + }, + } as never); jest - .spyOn(mockAssetsRepository, 'findByKeyringAccountId') - .mockResolvedValueOnce(MOCK_ASSET_ENTITIES); + .spyOn(snapAssetsAdapter, 'getAccountAssetsByIDs') + .mockResolvedValueOnce({ + [nftAssetType]: null, + }); const assets = await assetsService.getAccountAssetsByIDs( MOCK_SOLANA_KEYRING_ACCOUNT_0.id, - [MOCK_ASSET_ENTITY_0.assetType, MOCK_ASSET_ENTITY_1.assetType], + [MOCK_ASSET_ENTITY_0.assetType, nftAssetType], ); - expect(assets).toStrictEqual({ - [MOCK_ASSET_ENTITY_0.assetType]: MOCK_ASSET_ENTITY_0, - [MOCK_ASSET_ENTITY_1.assetType]: MOCK_ASSET_ENTITY_1, - }); - }); - - it('returns null entries for missing assets', async () => { - jest - .spyOn(mockAssetsRepository, 'findByKeyringAccountId') - .mockResolvedValueOnce([MOCK_ASSET_ENTITY_0]); - - const assets = await assetsService.getAccountAssetsByIDs( + expect(mockAssetsProvider.getAccountAssetsByIDs).toHaveBeenCalledWith( MOCK_SOLANA_KEYRING_ACCOUNT_0.id, - [MOCK_ASSET_ENTITY_0.assetType, MOCK_ASSET_ENTITY_1.assetType], + [MOCK_ASSET_ENTITY_0.assetType], + ); + expect(snapAssetsAdapter.getAccountAssetsByIDs).toHaveBeenCalledWith( + MOCK_SOLANA_KEYRING_ACCOUNT_0.id, + [nftAssetType], ); - expect(assets).toStrictEqual({ - [MOCK_ASSET_ENTITY_0.assetType]: MOCK_ASSET_ENTITY_0, - [MOCK_ASSET_ENTITY_1.assetType]: null, + [MOCK_ASSET_ENTITY_0.assetType]: expect.objectContaining({ + assetType: MOCK_ASSET_ENTITY_0.assetType, + }), + [nftAssetType]: null, }); }); }); describe('getAccountAssetsByScope', () => { - it('filters account assets to the requested scope', async () => { - jest - .spyOn(mockAssetsRepository, 'findByKeyringAccountId') - .mockResolvedValueOnce(MOCK_ASSET_ENTITIES); - - const assets = await assetsService.getAccountAssetsByScope( - Network.Mainnet, - MOCK_SOLANA_KEYRING_ACCOUNT_0.id, - ); - - expect(assets).toStrictEqual(MOCK_ASSET_ENTITIES); - }); - }); - - describe('assets migration routing', () => { - beforeEach(() => { - setMigrationStage( - SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, - ); - }); - - describe('getAccountAssetByID', () => { - it('routes fungible assets through AssetsProvider', async () => { - jest.spyOn(mockAssetsProvider, 'getAccountAssetByID').mockResolvedValue({ + it('merges fungible Core assets with Snap-owned NFT assets for the scope', async () => { + const nftAssetType = + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/nft:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; + const nftAsset = { + assetType: nftAssetType, + keyringAccountId: MOCK_SOLANA_KEYRING_ACCOUNT_0.id, + network: Network.Mainnet, + mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + pubkey: '9wt9PfjPD3JCy5r7o4K1cTGiuTG7fq2pQhdDCdQALKjg', + symbol: 'NFT', + decimals: 0, + rawAmount: '1', + uiAmount: '1', + } as AssetEntity; + + jest.spyOn(mockAssetsProvider, 'getAccountAssetsByScope').mockResolvedValue({ + [MOCK_ASSET_ENTITY_0.assetType]: { + id: MOCK_ASSET_ENTITY_0.assetType, + chainId: Network.Mainnet, + balance: { amount: MOCK_ASSET_ENTITY_0.rawAmount }, + metadata: { + type: 'native', + symbol: 'SOL', + name: 'Solana', + decimals: 9, + }, + price: { price: 0, lastUpdated: 0 }, + fiatValue: 0, + }, + [MOCK_ASSET_ENTITY_1.assetType]: { id: MOCK_ASSET_ENTITY_1.assetType, chainId: Network.Mainnet, balance: { amount: MOCK_ASSET_ENTITY_1.rawAmount }, @@ -766,261 +427,87 @@ describe('AssetsService', () => { }, price: { price: 0, lastUpdated: 0 }, fiatValue: 0, - } as never); - - const asset = await assetsService.getAccountAssetByID( - MOCK_SOLANA_KEYRING_ACCOUNT_0.id, - MOCK_ASSET_ENTITY_1.assetType, - ); - - expect(mockAssetsProvider.getAccountAssetByID).toHaveBeenCalledWith( - MOCK_SOLANA_KEYRING_ACCOUNT_0.id, - MOCK_ASSET_ENTITY_1.assetType, - ); - expect(asset).toMatchObject({ - assetType: MOCK_ASSET_ENTITY_1.assetType, - keyringAccountId: MOCK_SOLANA_KEYRING_ACCOUNT_0.id, - rawAmount: MOCK_ASSET_ENTITY_1.rawAmount, - }); - }); - - it('routes NFT assets through SnapAssetsAdapter', async () => { - const nftAssetType = - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/nft:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; - const snapSpy = jest - .spyOn(snapAssetsAdapter, 'getAccountAssetByID') - .mockResolvedValueOnce(null); - - await assetsService.getAccountAssetByID( - MOCK_SOLANA_KEYRING_ACCOUNT_0.id, - nftAssetType, - ); - - expect(snapSpy).toHaveBeenCalledWith( - MOCK_SOLANA_KEYRING_ACCOUNT_0.id, - nftAssetType, - ); - expect(mockAssetsProvider.getAccountAssetByID).not.toHaveBeenCalled(); - }); - - it('returns null when the fungible asset is missing from Core', async () => { - jest - .spyOn(mockAssetsProvider, 'getAccountAssetByID') - .mockResolvedValueOnce(null); - - const asset = await assetsService.getAccountAssetByID( - MOCK_SOLANA_KEYRING_ACCOUNT_0.id, - MOCK_ASSET_ENTITY_1.assetType, - ); - - expect(asset).toBeNull(); - }); - - it('routes fungible assets through SnapAssetsAdapter when stage is Off', async () => { - setMigrationStage(SnapsAssetsMigrationStage.Off); - jest - .spyOn(mockAssetsRepository, 'findByKeyringAccountId') - .mockResolvedValueOnce(MOCK_ASSET_ENTITIES); - - const asset = await assetsService.getAccountAssetByID( - MOCK_SOLANA_KEYRING_ACCOUNT_0.id, - MOCK_ASSET_ENTITY_1.assetType, - ); + }, + } as never); + jest + .spyOn(snapAssetsAdapter, 'getAccountAssetsByScope') + .mockResolvedValueOnce([MOCK_ASSET_ENTITY_2, nftAsset]); - expect(mockAssetsProvider.getAccountAssetByID).not.toHaveBeenCalled(); - expect(asset).toStrictEqual(MOCK_ASSET_ENTITY_1); - }); + const assets = await assetsService.getAccountAssetsByScope( + Network.Mainnet, + MOCK_SOLANA_KEYRING_ACCOUNT_0.id, + ); - it('falls back to SnapAssetsAdapter when Core read fails in WithFallback stage', async () => { - setMigrationStage( - SnapsAssetsMigrationStage.ReadAssetsControllerWithFallback, - ); - jest - .spyOn(mockAssetsProvider, 'getAccountAssetByID') - .mockRejectedValueOnce(new Error('Core unavailable')); - jest - .spyOn(mockAssetsRepository, 'findByKeyringAccountId') - .mockResolvedValueOnce(MOCK_ASSET_ENTITIES); - - const asset = await assetsService.getAccountAssetByID( - MOCK_SOLANA_KEYRING_ACCOUNT_0.id, - MOCK_ASSET_ENTITY_1.assetType, - ); - - expect(asset).toStrictEqual(MOCK_ASSET_ENTITY_1); - }); + expect(mockAssetsProvider.getAccountAssetsByScope).toHaveBeenCalledWith( + Network.Mainnet, + MOCK_SOLANA_KEYRING_ACCOUNT_0.id, + ); + expect(assets).toHaveLength(3); + expect(assets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + assetType: MOCK_ASSET_ENTITY_0.assetType, + }), + expect.objectContaining({ + assetType: MOCK_ASSET_ENTITY_1.assetType, + }), + nftAsset, + ]), + ); }); + }); - describe('getAccountAssetsByIDs', () => { - it('routes fungible and NFT asset IDs to the correct adapters', async () => { - const nftAssetType = - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/nft:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; - - jest.spyOn(mockAssetsProvider, 'getAccountAssetsByIDs').mockResolvedValue({ - [MOCK_ASSET_ENTITY_0.assetType]: { - id: MOCK_ASSET_ENTITY_0.assetType, - chainId: Network.Mainnet, - balance: { amount: MOCK_ASSET_ENTITY_0.rawAmount }, - metadata: { - type: 'native', - symbol: 'SOL', - name: 'Solana', - decimals: 9, - }, - price: { price: 0, lastUpdated: 0 }, - fiatValue: 0, + describe('getAccountAssetsForAllActiveScopes', () => { + it('merges fungible Core assets with Snap-owned NFT assets across active scopes', async () => { + const nftAssetType = + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/nft:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; + const nftAsset = { + assetType: nftAssetType, + keyringAccountId: MOCK_SOLANA_KEYRING_ACCOUNT_0.id, + network: Network.Mainnet, + mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + pubkey: '9wt9PfjPD3JCy5r7o4K1cTGiuTG7fq2pQhdDCdQALKjg', + symbol: 'NFT', + decimals: 0, + rawAmount: '1', + uiAmount: '1', + } as AssetEntity; + + jest.spyOn(mockAssetsProvider, 'getAccountAssetsByScope').mockResolvedValue({ + [MOCK_ASSET_ENTITY_0.assetType]: { + id: MOCK_ASSET_ENTITY_0.assetType, + chainId: Network.Mainnet, + balance: { amount: MOCK_ASSET_ENTITY_0.rawAmount }, + metadata: { + type: 'native', + symbol: 'SOL', + name: 'Solana', + decimals: 9, }, - } as never); - jest - .spyOn(snapAssetsAdapter, 'getAccountAssetsByIDs') - .mockResolvedValueOnce({ - [nftAssetType]: null, - }); - - const assets = await assetsService.getAccountAssetsByIDs( - MOCK_SOLANA_KEYRING_ACCOUNT_0.id, - [MOCK_ASSET_ENTITY_0.assetType, nftAssetType], - ); - - expect(mockAssetsProvider.getAccountAssetsByIDs).toHaveBeenCalledWith( - MOCK_SOLANA_KEYRING_ACCOUNT_0.id, - [MOCK_ASSET_ENTITY_0.assetType], - ); - expect(snapAssetsAdapter.getAccountAssetsByIDs).toHaveBeenCalledWith( - MOCK_SOLANA_KEYRING_ACCOUNT_0.id, - [nftAssetType], - ); - expect(assets[MOCK_ASSET_ENTITY_0.assetType]).toMatchObject({ - assetType: MOCK_ASSET_ENTITY_0.assetType, - }); - expect(assets[nftAssetType]).toBeNull(); - }); - }); + price: { price: 0, lastUpdated: 0 }, + fiatValue: 0, + }, + } as never); + jest + .spyOn(snapAssetsAdapter, 'getAccountAssetsForAllActiveScopes') + .mockResolvedValueOnce([MOCK_ASSET_ENTITY_1, nftAsset]); - describe('getAccountAssetsByScope', () => { - it('merges fungible Core assets with Snap-owned NFT assets for the scope', async () => { - const nftAssetType = - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/nft:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; - const nftAsset = { - assetType: nftAssetType, - keyringAccountId: MOCK_SOLANA_KEYRING_ACCOUNT_0.id, - network: Network.Mainnet, - mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', - pubkey: '9wt9PfjPD3JCy5r7o4K1cTGiuTG7fq2pQhdDCdQALKjg', - symbol: 'NFT', - decimals: 0, - rawAmount: '1', - uiAmount: '1', - } as AssetEntity; - - jest.spyOn(mockAssetsProvider, 'getAccountAssetsByScope').mockResolvedValue({ - [MOCK_ASSET_ENTITY_0.assetType]: { - id: MOCK_ASSET_ENTITY_0.assetType, - chainId: Network.Mainnet, - balance: { amount: MOCK_ASSET_ENTITY_0.rawAmount }, - metadata: { - type: 'native', - symbol: 'SOL', - name: 'Solana', - decimals: 9, - }, - price: { price: 0, lastUpdated: 0 }, - fiatValue: 0, - }, - [MOCK_ASSET_ENTITY_1.assetType]: { - id: MOCK_ASSET_ENTITY_1.assetType, - chainId: Network.Mainnet, - balance: { amount: MOCK_ASSET_ENTITY_1.rawAmount }, - metadata: { - type: 'fungible', - symbol: MOCK_ASSET_ENTITY_1.symbol, - name: MOCK_ASSET_ENTITY_1.symbol, - decimals: MOCK_ASSET_ENTITY_1.decimals, - }, - price: { price: 0, lastUpdated: 0 }, - fiatValue: 0, - }, - } as never); - jest - .spyOn(snapAssetsAdapter, 'getAccountAssetsByScope') - .mockResolvedValueOnce([MOCK_ASSET_ENTITY_2, nftAsset]); - - const assets = await assetsService.getAccountAssetsByScope( - Network.Mainnet, - MOCK_SOLANA_KEYRING_ACCOUNT_0.id, - ); - - expect(mockAssetsProvider.getAccountAssetsByScope).toHaveBeenCalledWith( - Network.Mainnet, - MOCK_SOLANA_KEYRING_ACCOUNT_0.id, - ); - expect(assets).toHaveLength(3); - expect(assets).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - assetType: MOCK_ASSET_ENTITY_0.assetType, - }), - expect.objectContaining({ - assetType: MOCK_ASSET_ENTITY_1.assetType, - }), - nftAsset, - ]), - ); - }); - }); + const assets = await assetsService.getAccountAssetsForAllActiveScopes( + MOCK_SOLANA_KEYRING_ACCOUNT_0.id, + ); - describe('getAccountAssetsForAllActiveScopes', () => { - it('merges fungible Core assets with Snap-owned NFT assets across active scopes', async () => { - const nftAssetType = - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/nft:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; - const nftAsset = { - assetType: nftAssetType, - keyringAccountId: MOCK_SOLANA_KEYRING_ACCOUNT_0.id, - network: Network.Mainnet, - mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', - pubkey: '9wt9PfjPD3JCy5r7o4K1cTGiuTG7fq2pQhdDCdQALKjg', - symbol: 'NFT', - decimals: 0, - rawAmount: '1', - uiAmount: '1', - } as AssetEntity; - - jest.spyOn(mockAssetsProvider, 'getAccountAssetsByScope').mockResolvedValue({ - [MOCK_ASSET_ENTITY_0.assetType]: { - id: MOCK_ASSET_ENTITY_0.assetType, - chainId: Network.Mainnet, - balance: { amount: MOCK_ASSET_ENTITY_0.rawAmount }, - metadata: { - type: 'native', - symbol: 'SOL', - name: 'Solana', - decimals: 9, - }, - price: { price: 0, lastUpdated: 0 }, - fiatValue: 0, - }, - } as never); - jest - .spyOn(snapAssetsAdapter, 'getAccountAssetsByScope') - .mockResolvedValueOnce([MOCK_ASSET_ENTITY_1, nftAsset]); - - const assets = await assetsService.getAccountAssetsForAllActiveScopes( - MOCK_SOLANA_KEYRING_ACCOUNT_0.id, - ); - - expect(mockAssetsProvider.getAccountAssetsByScope).toHaveBeenCalledWith( - Network.Mainnet, - MOCK_SOLANA_KEYRING_ACCOUNT_0.id, - ); - expect(assets).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - assetType: MOCK_ASSET_ENTITY_0.assetType, - }), - nftAsset, - ]), - ); - }); + expect(mockAssetsProvider.getAccountAssetsByScope).toHaveBeenCalledWith( + Network.Mainnet, + MOCK_SOLANA_KEYRING_ACCOUNT_0.id, + ); + expect(assets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + assetType: MOCK_ASSET_ENTITY_0.assetType, + }), + nftAsset, + ]), + ); }); }); }); diff --git a/packages/solana-wallet-snap/src/core/services/assets/AssetsService.ts b/packages/solana-wallet-snap/src/core/services/assets/AssetsService.ts index f9ed3e52..4b63813a 100644 --- a/packages/solana-wallet-snap/src/core/services/assets/AssetsService.ts +++ b/packages/solana-wallet-snap/src/core/services/assets/AssetsService.ts @@ -1,21 +1,14 @@ /* eslint-disable jsdoc/require-returns */ -import { - SNAPS_ASSETS_MIGRATION_FLAG_KEYS, - SnapsAssetsMigrationStage, - getSnapsAssetsMigrationNamespace, - parseSnapsAssetsMigrationStage, -} from '@metamask/assets-controller'; import type { Caip19AssetId } from '@metamask/assets-controller'; import type { AssetsProvider } from '@metamask/snap-networks-utils'; import type { FungibleAssetMarketData, FungibleAssetMetadata, } from '@metamask/snaps-sdk'; -import type { CaipAssetType, CaipChainId, Json } from '@metamask/utils'; +import type { CaipAssetType, CaipChainId } from '@metamask/utils'; import { parseCaipAssetType } from '@metamask/utils'; import type { AssetEntity, SolanaKeyringAccount } from '../../../entities'; -import type { CoreMessengerCaller } from '../../../types/core-messenger'; import type { NftApiClient } from '../../clients/nft-api/NftApiClient'; import type { TokenApiClient } from '../../clients/token-api-client/TokenApiClient'; import { SolanaCaip19Tokens } from '../../constants/solana'; @@ -32,17 +25,9 @@ import type { ConfigProvider } from '../config'; import type { TokenPricesService } from '../token-prices/TokenPrices'; import { SnapAssetsAdapter } from './adapters/SnapAssetsAdapter'; import { mapControllerAsset } from './mapControllerAsset'; -import { shouldTrackSnapAssets } from './shouldTrackSnapAssets'; import { isSnapOwnedAsset } from './snapOwnedAssets'; import type { AssetMetadata, NonFungibleAssetMetadata } from './types'; -export { shouldTrackSnapAssets }; - -/** - * Assets migration stage used when no remote feature flag is set for the chain. - */ -const ASSETS_MIGRATION_STAGE = SnapsAssetsMigrationStage.Off; - function isFungibleProviderAsset(assetId: string): boolean { return !isSnapOwnedAsset(assetId); } @@ -56,8 +41,6 @@ export class AssetsService { readonly #assetsProvider: AssetsProvider; - readonly #coreMessenger: CoreMessengerCaller; - readonly #accountsService: AccountsService; readonly #tokenPricesService: TokenPricesService; @@ -70,7 +53,6 @@ export class AssetsService { logger, configProvider, snapAssetsAdapter, - coreMessenger, accountsService, tokenApiClient, tokenPricesService, @@ -80,7 +62,6 @@ export class AssetsService { logger: ILogger; configProvider: ConfigProvider; snapAssetsAdapter: SnapAssetsAdapter; - coreMessenger: CoreMessengerCaller; accountsService: AccountsService; tokenApiClient: TokenApiClient; tokenPricesService: TokenPricesService; @@ -90,7 +71,6 @@ export class AssetsService { this.#logger = createPrefixedLogger(logger, '[🪙 AssetsService]'); this.#configProvider = configProvider; this.#snapAdapter = snapAssetsAdapter; - this.#coreMessenger = coreMessenger; this.#accountsService = accountsService; this.#assetsProvider = assetsProvider; this.#tokenApiClient = tokenApiClient; @@ -98,73 +78,10 @@ export class AssetsService { this.#nftApiClient = nftApiClient; } - async #resolveMigrationStage( - chainId: string, - ): Promise { - const { remoteFeatureFlags } = await this.#coreMessenger.call( - 'RemoteFeatureFlagController:getState', - ); - - const namespace = getSnapsAssetsMigrationNamespace(chainId as CaipChainId); - - if (namespace) { - const flagKey = SNAPS_ASSETS_MIGRATION_FLAG_KEYS[namespace]; - - if (Object.hasOwn(remoteFeatureFlags, flagKey)) { - const remoteStage = parseSnapsAssetsMigrationStage( - remoteFeatureFlags[flagKey] as Json | undefined, - ); - - if (remoteStage !== undefined) { - return remoteStage; - } - } - } - - return ASSETS_MIGRATION_STAGE; - } - async #solanaChainIds(): Promise { return this.#configProvider.getActiveNetworks(); } - async #filterTrackableAssets(assets: AssetEntity[]): Promise { - const filtered: AssetEntity[] = []; - - for (const asset of assets) { - if (isSnapOwnedAsset(asset.assetType)) { - filtered.push(asset); - continue; - } - - if (await this.shouldTrackSnapAssetsForScope(asset.network)) { - filtered.push(asset); - } - } - - return filtered; - } - - async shouldTrackSnapAssetsForScope(scope: CaipChainId): Promise { - const stage = await this.#resolveMigrationStage(scope); - return shouldTrackSnapAssets(stage); - } - - async shouldTrackSnapAssetsForAccount(accountId: string): Promise { - const account = await this.#accountsService.findById(accountId); - if (!account) { - return false; - } - - for (const scope of account.scopes) { - if (await this.shouldTrackSnapAssetsForScope(scope)) { - return true; - } - } - - return false; - } - async #getCoreAccountAssetByID( accountId: string, assetId: CaipAssetType, @@ -318,7 +235,7 @@ export class AssetsService { imageUrl: nftMetadata.imageUrl, description: nftMetadata.description, fungible: false as const, - isPossibleSpam: false, // FIXME: The isSpam should be part of the NFT item response, not balance, otherwise we can't get it here + isPossibleSpam: false, attributes: Object.fromEntries( nftMetadata.attributes.map( (attr: { key: string; value: string | number }) => [ @@ -332,7 +249,7 @@ export class AssetsService { address: nftMetadata.onchainCollectionAddress as Caip10Address, symbol: nftMetadata.collectionSymbol, tokenCount: nftMetadata.collectionCount, - creator: '' as Caip10Address, // FIXME: There can be more than one creator + creator: '' as Caip10Address, imageUrl: nftMetadata.collectionImageUrl ?? '', }, }; @@ -348,29 +265,22 @@ export class AssetsService { ): Promise> { this.#logger.log('Fetching metadata for assets', assetTypes); - const { nativeAssetTypes, tokenAssetTypes, nftAssetTypes } = + const { nativeAssetTypes, tokenAssetTypes } = this.#splitAssetsByType(assetTypes); - const [ - nativeTokensMetadata, - tokensMetadata, - // nftMetadata, - ] = await Promise.all([ + const [nativeTokensMetadata, tokensMetadata] = await Promise.all([ this.#getNativeTokensMetadata(nativeAssetTypes), this.#tokenApiClient.getTokensMetadata(tokenAssetTypes), - // this.#getNftsMetadata(nftAssetTypes), ]); return { ...nativeTokensMetadata, ...tokensMetadata, - // ...nftMetadata, }; } - async fetch(account: SolanaKeyringAccount): Promise { - const assets = await this.#snapAdapter.fetch(account); - return this.#filterTrackableAssets(assets); + async fetch(_account: SolanaKeyringAccount): Promise { + return []; } async fetchAssetsMarketData( @@ -388,27 +298,14 @@ export class AssetsService { return marketData; } - async save(asset: AssetEntity): Promise { - await this.saveMany([asset]); + async save(_asset: AssetEntity): Promise { + // Fungible assets are tracked by Core; Snap persistence is disabled. } - async saveMany(assets: AssetEntity[]): Promise { - const trackableAssets = await this.#filterTrackableAssets(assets); - - if (trackableAssets.length === 0) { - return; - } - - await this.#snapAdapter.saveMany(trackableAssets); + async saveMany(_assets: AssetEntity[]): Promise { + // Fungible assets are tracked by Core; Snap persistence is disabled. } - /** - * Checks if the asset has changed compared to passed assets lookup. - * - * @param asset - The asset to check. - * @param assetsLookup - The lookup table to check against. - * @returns True if the asset has changed, false otherwise. - */ static hasChanged(asset: AssetEntity, assetsLookup: AssetEntity[]): boolean { return SnapAssetsAdapter.hasChanged(asset, assetsLookup); } @@ -417,12 +314,6 @@ export class AssetsService { return this.#snapAdapter.getAll(); } - /** - * Returns a single account asset by CAIP-19 ID, or `null` if missing. - * - * @param accountId - Keyring account ID. - * @param assetId - CAIP-19 asset ID. - */ async getAccountAssetByID( accountId: string, assetId: string, @@ -431,34 +322,11 @@ export class AssetsService { return this.#snapAdapter.getAccountAssetByID(accountId, assetId); } - const { chainId } = parseCaipAssetType(assetId as CaipAssetType); - const stage = await this.#resolveMigrationStage(chainId); - - if (stage === SnapsAssetsMigrationStage.Off) { - return this.#snapAdapter.getAccountAssetByID(accountId, assetId); - } - const account = await this.#accountsService.findById(accountId); if (!account) { return null; } - if (stage === SnapsAssetsMigrationStage.ReadAssetsControllerWithFallback) { - try { - const coreAsset = await this.#getCoreAccountAssetByID( - accountId, - assetId as CaipAssetType, - account.address, - ); - if (coreAsset) { - return coreAsset; - } - return this.#snapAdapter.getAccountAssetByID(accountId, assetId); - } catch { - return this.#snapAdapter.getAccountAssetByID(accountId, assetId); - } - } - return this.#getCoreAccountAssetByID( accountId, assetId as CaipAssetType, @@ -466,13 +334,6 @@ export class AssetsService { ); } - /** - * Returns account assets for the given CAIP-19 IDs, keyed by asset ID. - * Missing assets are `null`. - * - * @param accountId - Keyring account ID. - * @param assetIds - CAIP-19 asset IDs to resolve. - */ async getAccountAssetsByIDs( accountId: string, assetIds: string[], @@ -481,137 +342,53 @@ export class AssetsService { return {}; } - const result: Record = {}; - const fungibleIds: string[] = []; - const snapOwnedIds: string[] = []; - - for (const assetId of assetIds) { - if (isSnapOwnedAsset(assetId)) { - snapOwnedIds.push(assetId); - } else { - fungibleIds.push(assetId); - } - } - - if (snapOwnedIds.length > 0) { - const snapResults = await this.#snapAdapter.getAccountAssetsByIDs( - accountId, - snapOwnedIds, - ); - Object.assign(result, snapResults); - } - - if (fungibleIds.length === 0) { - return result; - } - - const { chainId } = parseCaipAssetType(fungibleIds[0] as CaipAssetType); - const stage = await this.#resolveMigrationStage(chainId); const account = await this.#accountsService.findById(accountId); - if (!account) { - fungibleIds.forEach((assetId) => { - result[assetId] = null; - }); - return result; + return Object.fromEntries(assetIds.map((assetId) => [assetId, null])); } - let fungibleResults: Record; - - if (stage === SnapsAssetsMigrationStage.Off) { - fungibleResults = await this.#snapAdapter.getAccountAssetsByIDs( - accountId, - fungibleIds, - ); - } else if ( - stage === SnapsAssetsMigrationStage.ReadAssetsControllerWithFallback - ) { - try { - fungibleResults = await this.#getCoreAccountAssetsByIDs( - accountId, - fungibleIds, - account.address, - ); - } catch { - fungibleResults = await this.#snapAdapter.getAccountAssetsByIDs( - accountId, - fungibleIds, - ); - } - } else { - fungibleResults = await this.#getCoreAccountAssetsByIDs( - accountId, - fungibleIds, - account.address, - ); - } + const snapOwnedIds = assetIds.filter(isSnapOwnedAsset); + const fungibleIds = assetIds.filter( + (assetId) => !isSnapOwnedAsset(assetId), + ); - Object.assign(result, fungibleResults); + const [fungibleResults, snapResults] = await Promise.all([ + fungibleIds.length > 0 + ? this.#getCoreAccountAssetsByIDs( + accountId, + fungibleIds, + account.address, + ) + : Promise.resolve({}), + snapOwnedIds.length > 0 + ? this.#snapAdapter.getAccountAssetsByIDs(accountId, snapOwnedIds) + : Promise.resolve({}), + ]); - return result; + return { ...snapResults, ...fungibleResults }; } - /** - * Returns controller-backed assets for an account on the given Solana scope. - * - * @param scope - CAIP-2 chain ID to filter results. - * @param accountId - Keyring account ID. - */ async getAccountAssetsByScope( scope: CaipChainId, accountId: string, ): Promise { - const stage = await this.#resolveMigrationStage(scope); - const snapAssets = await this.#snapAdapter.getAccountAssetsByScope( - scope, - accountId, - ); - const nftAssets = snapAssets.filter((asset) => - isSnapOwnedAsset(asset.assetType), - ); - - if (stage === SnapsAssetsMigrationStage.Off) { - const fungibleAssets = snapAssets.filter( - (asset) => !isSnapOwnedAsset(asset.assetType), - ); - return [...fungibleAssets, ...nftAssets]; - } - const account = await this.#accountsService.findById(accountId); if (!account) { - return nftAssets; + return []; } - let fungibleAssets: AssetEntity[]; + const [fungibleAssets, snapAssets] = await Promise.all([ + this.#getCoreAccountAssetsByScope(scope, accountId, account.address), + this.#snapAdapter.getAccountAssetsByScope(scope, accountId), + ]); - if (stage === SnapsAssetsMigrationStage.ReadAssetsControllerWithFallback) { - try { - fungibleAssets = await this.#getCoreAccountAssetsByScope( - scope, - accountId, - account.address, - ); - } catch { - fungibleAssets = snapAssets.filter( - (asset) => !isSnapOwnedAsset(asset.assetType), - ); - } - } else { - fungibleAssets = await this.#getCoreAccountAssetsByScope( - scope, - accountId, - account.address, - ); - } + const nftAssets = snapAssets.filter((asset) => + isSnapOwnedAsset(asset.assetType), + ); return [...fungibleAssets, ...nftAssets]; } - /** - * Returns assets for an account across all active Solana networks. - * - * @param accountId - Keyring account ID. - */ async getAccountAssetsForAllActiveScopes( accountId: string, ): Promise { @@ -625,18 +402,21 @@ export class AssetsService { account.scopes.includes(chainId), ); - const assetsByScope = await Promise.all( + const fungibleByScope = await Promise.all( relevantChainIds.map((scope) => - this.getAccountAssetsByScope(scope, accountId), + this.#getCoreAccountAssetsByScope(scope, accountId, account.address), ), ); + const snapAssets = + await this.#snapAdapter.getAccountAssetsForAllActiveScopes(accountId); + const nftAssets = snapAssets.filter((asset) => + isSnapOwnedAsset(asset.assetType), + ); - return assetsByScope.flat(); + return [...fungibleByScope.flat(), ...nftAssets]; } async findByAccount(account: SolanaKeyringAccount): Promise { return this.#snapAdapter.findByAccount(account); } } - -export { SnapsAssetsMigrationStage }; diff --git a/packages/solana-wallet-snap/src/core/services/assets/shouldTrackSnapAssets.test.ts b/packages/solana-wallet-snap/src/core/services/assets/shouldTrackSnapAssets.test.ts deleted file mode 100644 index 8d8110dc..00000000 --- a/packages/solana-wallet-snap/src/core/services/assets/shouldTrackSnapAssets.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { SnapsAssetsMigrationStage } from '@metamask/assets-controller'; - -import { shouldTrackSnapAssets } from './shouldTrackSnapAssets'; - -describe('shouldTrackSnapAssets', () => { - it.each([ - [SnapsAssetsMigrationStage.Off, true], - [SnapsAssetsMigrationStage.ReadAssetsControllerWithFallback, true], - [SnapsAssetsMigrationStage.ReadAssetsControllerWithoutFallback, true], - [SnapsAssetsMigrationStage.ReadAssetsControllerOnly, false], - ])('returns %s for stage %s', (stage, expected) => { - expect(shouldTrackSnapAssets(stage)).toBe(expected); - }); -}); diff --git a/packages/solana-wallet-snap/src/core/services/assets/shouldTrackSnapAssets.ts b/packages/solana-wallet-snap/src/core/services/assets/shouldTrackSnapAssets.ts deleted file mode 100644 index cd15b341..00000000 --- a/packages/solana-wallet-snap/src/core/services/assets/shouldTrackSnapAssets.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { SnapsAssetsMigrationStage } from '@metamask/assets-controller'; - -/** - * Returns whether the Snap should persist fungible asset balances for the given - * migration stage. NFT assets are always tracked by the Snap regardless of stage. - * - * @param stage - Assets migration stage for the chain. - * @returns Whether Snap-side fungible asset tracking is enabled. - */ -export function shouldTrackSnapAssets( - stage: SnapsAssetsMigrationStage, -): boolean { - return stage < SnapsAssetsMigrationStage.ReadAssetsControllerOnly; -} diff --git a/packages/solana-wallet-snap/src/core/test/helpers/registerCoreAssetsControllerHandlers.ts b/packages/solana-wallet-snap/src/core/test/helpers/registerCoreAssetsControllerHandlers.ts index 232388b4..973ffd27 100644 --- a/packages/solana-wallet-snap/src/core/test/helpers/registerCoreAssetsControllerHandlers.ts +++ b/packages/solana-wallet-snap/src/core/test/helpers/registerCoreAssetsControllerHandlers.ts @@ -107,9 +107,4 @@ export function registerCoreAssetsControllerHandlers( return result; }, ); - - controllerMessenger.registerActionHandler( - 'RemoteFeatureFlagController:getState', - () => ({ remoteFeatureFlags: {} }), - ); } diff --git a/packages/solana-wallet-snap/src/snapContext.ts b/packages/solana-wallet-snap/src/snapContext.ts index 94676b04..80242a8e 100644 --- a/packages/solana-wallet-snap/src/snapContext.ts +++ b/packages/solana-wallet-snap/src/snapContext.ts @@ -1,11 +1,5 @@ -import { - AssetsProvider, - RemoteFeatureFlagsProvider, -} from '@metamask/snap-networks-utils'; -import type { - AssetsProviderMessenger, - RemoteFeatureFlagsProviderMessenger, -} from '@metamask/snap-networks-utils'; +import { AssetsProvider } from '@metamask/snap-networks-utils'; +import type { AssetsProviderMessenger } from '@metamask/snap-networks-utils'; import { getMessenger } from '@metamask/snaps-sdk'; import type { ICache } from './core/caching/ICache'; @@ -90,10 +84,9 @@ export type SnapExecutionContext = { accountsSynchronizer: AccountsSynchronizer; tokenHelper: TokenHelper; /** - * Core messenger plumbing (routing wired in a follow-up PR). + * Core messenger plumbing for AssetsProvider reads. */ coreMessenger: CoreMessenger; - remoteFeatureFlagsProvider: RemoteFeatureFlagsProvider; assetsProvider: AssetsProvider; }; @@ -181,9 +174,6 @@ const snapAssetsAdapter = new SnapAssetsAdapter({ * Core controllers plumbing */ const coreMessenger = getMessenger(); -const remoteFeatureFlagsProvider = new RemoteFeatureFlagsProvider({ - messenger: coreMessenger as RemoteFeatureFlagsProviderMessenger, -}); const assetsProvider = new AssetsProvider({ messenger: coreMessenger as AssetsProviderMessenger, }); @@ -192,7 +182,6 @@ const assetsService = new AssetsService({ logger, configProvider, snapAssetsAdapter, - coreMessenger, accountsService, tokenApiClient, tokenPricesService, @@ -329,7 +318,6 @@ const snapContext: SnapExecutionContext = { accountsSynchronizer, tokenHelper, coreMessenger, - remoteFeatureFlagsProvider, assetsProvider, }; @@ -349,7 +337,6 @@ export { nameResolutionService, nftService, priceApiClient, - remoteFeatureFlagsProvider, sendSolBuilder, sendSplTokenBuilder, signer, diff --git a/packages/solana-wallet-snap/src/types/core-messenger.ts b/packages/solana-wallet-snap/src/types/core-messenger.ts index 3982bfb1..16dcdcf5 100644 --- a/packages/solana-wallet-snap/src/types/core-messenger.ts +++ b/packages/solana-wallet-snap/src/types/core-messenger.ts @@ -4,7 +4,6 @@ import type { AssetsControllerGetAccountAssetsByScopeAction, } from '@metamask/assets-controller'; import type { Messenger } from '@metamask/messenger'; -import type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller'; import type { AsyncMessenger } from '@metamask/snaps-sdk'; /** @@ -14,7 +13,6 @@ export const SOLANA_WALLET_SNAP_MESSENGER_NAMESPACE = 'SolanaWalletSnap' as const; export type CoreMessengerActions = - | RemoteFeatureFlagControllerGetStateAction | AssetsControllerGetAccountAssetByIDAction | AssetsControllerGetAccountAssetsByIDsAction | AssetsControllerGetAccountAssetsByScopeAction; diff --git a/yarn.lock b/yarn.lock index d9e3043e..803527da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3683,7 +3683,6 @@ __metadata: "@metamask/keyring-api": "npm:^23.7.0" "@metamask/keyring-snap-sdk": "npm:^9.2.1" "@metamask/messenger": "npm:^2.0.0" - "@metamask/remote-feature-flag-controller": "npm:^5.0.0" "@metamask/snap-networks-utils": "workspace:^" "@metamask/snaps-cli": "npm:^8.4.1" "@metamask/snaps-jest": "npm:^10.2.0" From 1ede6a09a8751652f325d8bc20f749d9ee1d7714 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 5 Aug 2026 14:03:25 +0000 Subject: [PATCH 2/2] chore: update snap manifest shasum after build Co-authored-by: Ulisses Ferreira --- packages/solana-wallet-snap/snap.manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/solana-wallet-snap/snap.manifest.json b/packages/solana-wallet-snap/snap.manifest.json index d7c3f283..c7c83247 100644 --- a/packages/solana-wallet-snap/snap.manifest.json +++ b/packages/solana-wallet-snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/internal-snaps.git" }, "source": { - "shasum": "5UbMR/XOp/xr10ccku+kJxErgd1zptASqMx4tavLCL8=", + "shasum": "8pC/CUT1b2BVxIsPy6IWJQ8SMrFeW13nBhWeWKriNUs=", "location": { "npm": { "filePath": "dist/bundle.js",