diff --git a/eslint-suppressions.json b/eslint-suppressions.json index acd48b2b..22de45f9 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -144,19 +144,6 @@ "count": 2 } }, - "packages/solana-wallet-snap/src/core/caching/useCache.test.ts": { - "@typescript-eslint/explicit-function-return-type": { - "count": 4 - } - }, - "packages/solana-wallet-snap/src/core/caching/useCache.ts": { - "@typescript-eslint/explicit-function-return-type": { - "count": 1 - }, - "@typescript-eslint/no-explicit-any": { - "count": 3 - } - }, "packages/solana-wallet-snap/src/core/clients/nft-api/NftApiClient.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 5 @@ -339,10 +326,7 @@ }, "packages/solana-wallet-snap/src/core/services/connection/SolanaConnection.ts": { "@typescript-eslint/explicit-function-return-type": { - "count": 2 - }, - "@typescript-eslint/no-explicit-any": { - "count": 2 + "count": 1 } }, "packages/solana-wallet-snap/src/core/services/connection/transport/createToggleInfuraBigtableLookupsTransports.ts": { diff --git a/packages/solana-wallet-snap/src/core/caching/ICache.ts b/packages/solana-wallet-snap/src/core/caching/ICache.ts deleted file mode 100644 index 8a7a71f4..00000000 --- a/packages/solana-wallet-snap/src/core/caching/ICache.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Interface for a generic cache implementation. - * - * @template TValue - The type of values stored in the cache - */ -export type ICache = { - /** - * Retrieves a value from the cache by key. - * - * @param key - The key to retrieve - * @returns The value if found, undefined if not found - */ - get(key: string): Promise; - - /** - * Stores a value in the cache with an optional TTL. - * - If a value is undefined, it will not be stored in the cache. - * - If a value is null, it will be stored in the cache. - * - * @param key - The key to store the value under - * @param value - The value to store - * @param ttlMilliseconds - Optional time-to-live in milliseconds. If not provided, the value will not expire. - * @throws Error if any entry's ttlMilliseconds is not a number, is negative, or is greater than 2^53 - 1 - */ - set(key: string, value: TValue, ttlMilliseconds?: number): Promise; - - /** - * Removes a value from the cache. - * - * @param key - The key to remove - * @returns true if the key was found and removed, false otherwise - */ - delete(key: string): Promise; - - /** - * Removes all values from the cache. - */ - clear(): Promise; - - /** - * Checks if a key exists in the cache. - * - * @param key - The key to check - * @returns true if the key exists, false otherwise - */ - has(key: string): Promise; - - /** - * Returns all keys currently in the cache. - * - * @returns Array of keys - */ - keys(): Promise; - - /** - * Returns the number of items in the cache. - * - * @returns The number of items - */ - size(): Promise; - - /** - * Retrieves a value from the cache without affecting its TTL or last accessed time. - * - * @param key - The key to peek at - * @returns The value if found, undefined if not found - */ - peek(key: string): Promise; - - /** - * Retrieves multiple values from the cache in a single operation. - * - * @param keys - Array of keys to retrieve - * @returns Object mapping keys to their values (or undefined if not found) - */ - mget(keys: string[]): Promise>; - - /** - * Stores multiple values in the cache in a single operation. - * - If a value is undefined, it will not be stored in the cache. - * - If a value is null, it will be stored in the cache. - * - * @param entries - Array of entries to store, each with key, value, and optional TTL (if not provided, the value will not expire) - * @throws Error if any entry's ttlMilliseconds is not a number, is negative, or is greater than 2^53 - 1 - */ - mset( - entries: { key: string; value: TValue; ttlMilliseconds?: number }[], - ): Promise; - - /** - * Removes multiple values from the cache. - * - * @param keys - Array of keys to remove - * @returns An object mapping each key to a boolean indicating whether it was found and removed - */ - mdelete(keys: string[]): Promise>; -}; diff --git a/packages/solana-wallet-snap/src/core/caching/InMemoryCache.ts b/packages/solana-wallet-snap/src/core/caching/InMemoryCache.ts deleted file mode 100644 index 90750af2..00000000 --- a/packages/solana-wallet-snap/src/core/caching/InMemoryCache.ts +++ /dev/null @@ -1,181 +0,0 @@ -import type { Logger, Serializable } from '@metamask/snap-networks-utils'; -import { assert } from '@metamask/utils'; - -import type { ICache } from './ICache'; -import type { CacheEntry } from './types'; - -/** - * A simple in-memory cache implementation supporting TTL (Time To Live) functionality. - * - * WARNINGS: - * - This cache is not persistent and will be lost when the process is restarted. - */ -export class InMemoryCache implements ICache { - readonly #cache: Map = new Map(); - - public readonly logger: Logger; - - constructor(logger: Logger) { - this.logger = logger.withPrefix('[💾 InMemoryCache]'); - } - - #validateTtlOrThrow(ttlMilliseconds?: number): void { - if (ttlMilliseconds === undefined) { - return; - } - - if (typeof ttlMilliseconds !== 'number') { - throw new Error('TTL must be a number'); - } - - if (ttlMilliseconds < 0) { - throw new Error('TTL must be positive'); - } - - if (ttlMilliseconds > Number.MAX_SAFE_INTEGER) { - throw new Error('TTL must be less than 2^53 - 1'); - } - } - - #isExpired(cacheEntry: CacheEntry): boolean { - return cacheEntry.expiresAt < Date.now(); - } - - async #cleanupExpiredEntries(): Promise { - const expiredKeys: string[] = []; - for (const [key, entry] of this.#cache.entries()) { - if (this.#isExpired(entry)) { - expiredKeys.push(key); - } - } - await this.mdelete(expiredKeys); - } - - async get(key: string): Promise { - const result = await this.mget([key]); - return result[key]; - } - - async set( - key: string, - value: Serializable, - ttlMilliseconds = Number.MAX_SAFE_INTEGER, - ): Promise { - this.#validateTtlOrThrow(ttlMilliseconds); - - this.#cache.set(key, { - value, - expiresAt: Math.min( - Date.now() + (ttlMilliseconds ?? Number.MAX_SAFE_INTEGER), - Number.MAX_SAFE_INTEGER, - ), - }); - } - - async delete(key: string): Promise { - const result = await this.mdelete([key]); - return result[key] ?? false; - } - - async clear(): Promise { - this.#cache.clear(); - } - - async has(key: string): Promise { - const cacheEntry = this.#cache.get(key); - if (!cacheEntry) { - return false; - } - - if (this.#isExpired(cacheEntry)) { - this.#cache.delete(key); - return false; - } - - return true; - } - - async keys(): Promise { - await this.#cleanupExpiredEntries(); - return Array.from(this.#cache.keys()); - } - - async size(): Promise { - await this.#cleanupExpiredEntries(); - return this.#cache.size; - } - - async peek(key: string): Promise { - const cacheEntry = this.#cache.get(key); - if (!cacheEntry) { - return undefined; - } - - if (this.#isExpired(cacheEntry)) { - this.#cache.delete(key); - return undefined; - } - - return cacheEntry.value; - } - - async mget( - keys: string[], - ): Promise> { - await this.#cleanupExpiredEntries(); - - const result: Record = {}; - - for (const key of keys) { - const cacheEntry = this.#cache.get(key); - if (!cacheEntry) { - this.logger.info(`❌ Cache miss for key "${key}"`); - result[key] = undefined; - continue; - } - - this.logger.info(`🎉 Cache hit for key "${key}"`); - result[key] = cacheEntry.value; - } - - return result; - } - - async mset( - entries: { key: string; value: Serializable; ttlMilliseconds?: number }[], - ): Promise { - if (entries.length === 0) { - return; - } - - if (entries.length === 1) { - assert(entries[0]); // Enforce type narrowing as TS cannot infer that entries[0] is defined - const { key, value, ttlMilliseconds } = entries[0]; - await this.set(key, value, ttlMilliseconds); - return; - } - - entries.forEach(({ ttlMilliseconds }) => { - this.#validateTtlOrThrow(ttlMilliseconds); - }); - - entries.forEach(({ key, value, ttlMilliseconds }) => { - if (value === undefined) { - return; - } - this.#cache.set(key, { - value, - expiresAt: Math.min( - Date.now() + (ttlMilliseconds ?? Number.MAX_SAFE_INTEGER), - Number.MAX_SAFE_INTEGER, - ), - }); - }); - } - - async mdelete(keys: string[]): Promise> { - return Object.fromEntries( - keys.map((key) => [key, this.#cache.delete(key)]), - ); - } -} diff --git a/packages/solana-wallet-snap/src/core/caching/types.ts b/packages/solana-wallet-snap/src/core/caching/types.ts deleted file mode 100644 index e18316b3..00000000 --- a/packages/solana-wallet-snap/src/core/caching/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Serializable } from '@metamask/snap-networks-utils'; - -export type TimestampMilliseconds = number; - -/** - * A single cache entry. - */ -export type CacheEntry = { - value: Serializable; - expiresAt: TimestampMilliseconds; -}; diff --git a/packages/solana-wallet-snap/src/core/caching/useCache.test.ts b/packages/solana-wallet-snap/src/core/caching/useCache.test.ts deleted file mode 100644 index 0dd46965..00000000 --- a/packages/solana-wallet-snap/src/core/caching/useCache.test.ts +++ /dev/null @@ -1,247 +0,0 @@ -import type { Serializable } from '@metamask/snap-networks-utils'; - -import type { ICache } from './ICache'; -import { useCache } from './useCache'; -import type { CacheOptions } from './useCache'; - -describe('useCache', () => { - // Spy to check if the original function was executed or not - let actualExecutionSpy: jest.Mock; - - // Mock cache - let cache: ICache; - - // Common cache options - let cacheOptions: CacheOptions; - - // Original test functions - let testFunction: () => Promise; - let testFunctionWithArgs: (arg1: string, arg2: number) => Promise; - let testFunctionWithComplexArgs: (obj: { - name: string; - age: number; - }) => Promise; - - // Cached versions - let cachedTestFunction: () => Promise; - let cachedTestFunctionWithArgs: ( - arg1: string, - arg2: number, - ) => Promise; - let cachedTestFunctionWithComplexArgs: (obj: { - name: string; - age: number; - }) => Promise; - - beforeEach(() => { - // Reset mocks for each test - actualExecutionSpy = jest.fn().mockResolvedValue('test'); - - // Create a mock cache - cache = { - get: jest.fn().mockResolvedValue(undefined), - set: jest.fn().mockResolvedValue(undefined), - } as unknown as ICache; - - // Define common cache options - cacheOptions = { - ttlMilliseconds: 1000, - functionName: 'testFunction', - }; - - // Define original functions - testFunction = async () => actualExecutionSpy(); - testFunctionWithArgs = async (arg1: string, arg2: number) => - actualExecutionSpy(arg1, arg2); - testFunctionWithComplexArgs = async (obj: { name: string; age: number }) => - actualExecutionSpy(obj); - - // Create cached versions - cachedTestFunction = useCache(testFunction, cache, { - ...cacheOptions, - functionName: 'testFunction', - }); - - cachedTestFunctionWithArgs = useCache(testFunctionWithArgs, cache, { - ...cacheOptions, - functionName: 'testFunctionWithArgs', - }); - - cachedTestFunctionWithComplexArgs = useCache( - testFunctionWithComplexArgs, - cache, - { - ...cacheOptions, - functionName: 'testFunctionWithComplexArgs', - }, - ); - }); - - describe('when the data is not cached', () => { - it('should cache the result of a function', async () => { - // No cached data - jest.spyOn(cache, 'get').mockResolvedValue(undefined); - - const result = await cachedTestFunction(); - - expect(result).toBe('test'); - expect(cache.get).toHaveBeenCalledTimes(1); - expect(actualExecutionSpy).toHaveBeenCalledTimes(1); - expect(cache.set).toHaveBeenCalledWith('testFunction:', 'test', 1000); - }); - }); - - describe('when the data is cached', () => { - it('should return the cached result', async () => { - // Init the cache with some data - jest.spyOn(cache, 'get').mockResolvedValue('test'); - - const result = await cachedTestFunction(); - - expect(result).toBe('test'); - expect(cache.get).toHaveBeenCalledTimes(1); - expect(actualExecutionSpy).not.toHaveBeenCalled(); - expect(cache.set).not.toHaveBeenCalled(); - }); - }); - - describe('error handling', () => { - it('should propagate errors from the original function', async () => { - const error = new Error('Test error'); - actualExecutionSpy.mockRejectedValueOnce(error); - - await expect(cachedTestFunction()).rejects.toThrow('Test error'); - expect(cache.set).not.toHaveBeenCalled(); - }); - - it('should handle cache get errors gracefully', async () => { - jest.spyOn(cache, 'get').mockRejectedValueOnce(new Error('Cache error')); - actualExecutionSpy.mockResolvedValueOnce('test'); - - const result = await cachedTestFunction(); - - expect(result).toBe('test'); - expect(actualExecutionSpy).toHaveBeenCalledTimes(1); - expect(cache.set).toHaveBeenCalledWith('testFunction:', 'test', 1000); - }); - - it('should handle cache set errors gracefully', async () => { - jest.spyOn(cache, 'get').mockResolvedValue(undefined); - jest - .spyOn(cache, 'set') - .mockRejectedValueOnce(new Error('Cache set error')); - actualExecutionSpy.mockResolvedValueOnce('test'); - - const result = await cachedTestFunction(); - - expect(result).toBe('test'); - expect(actualExecutionSpy).toHaveBeenCalledTimes(1); - }); - }); - - describe('different argument types', () => { - it('should handle primitive arguments correctly', async () => { - jest.spyOn(cache, 'get').mockResolvedValue(undefined); - jest.spyOn(cache, 'set').mockResolvedValueOnce(undefined); - actualExecutionSpy.mockResolvedValueOnce('test with args'); - - const result = await cachedTestFunctionWithArgs('hello', 42); - - expect(result).toBe('test with args'); - expect(cache.get).toHaveBeenCalledWith('testFunctionWithArgs:"hello":42'); - expect(actualExecutionSpy).toHaveBeenCalledWith('hello', 42); - }); - - it('should handle complex object arguments correctly', async () => { - jest.spyOn(cache, 'get').mockResolvedValue(undefined); - jest.spyOn(cache, 'set').mockResolvedValueOnce(undefined); - const testObj = { name: 'John', age: 30 }; - actualExecutionSpy.mockResolvedValueOnce('test with complex args'); - - const result = await cachedTestFunctionWithComplexArgs(testObj); - - expect(result).toBe('test with complex args'); - expect(cache.get).toHaveBeenCalledWith( - 'testFunctionWithComplexArgs:{"name":"John","age":30}', - ); - expect(actualExecutionSpy).toHaveBeenCalledWith(testObj); - }); - }); - - describe('custom generateCacheKey', () => { - it('should use a custom key generator if provided', async () => { - const customKeyGenerator = jest.fn().mockReturnValue('custom-key'); - - const customCachedFunction = useCache(testFunction, cache, { - ...cacheOptions, - generateCacheKey: customKeyGenerator, - }); - - await customCachedFunction(); - - expect(customKeyGenerator).toHaveBeenCalledTimes(1); - expect(cache.get).toHaveBeenCalledWith('custom-key'); - }); - }); - - describe('anonymous functions', () => { - it('should handle anonymous functions with a default name', async () => { - // Anonymous function with no name - const anonymousFunction = async () => actualExecutionSpy(); - Object.defineProperty(anonymousFunction, 'name', { value: null }); - - const cachedAnonymousFunction = useCache(anonymousFunction, cache, { - ttlMilliseconds: 1000, - }); - - await cachedAnonymousFunction(); - - expect(cache.get).toHaveBeenCalledWith('anonymousFunction:'); - }); - }); - - describe('function name override', () => { - it('should use the provided function name if given', async () => { - const cachedWithCustomName = useCache(testFunction, cache, { - ttlMilliseconds: 1000, - functionName: 'customFunctionName', - }); - - await cachedWithCustomName(); - - expect(cache.get).toHaveBeenCalledWith('customFunctionName:'); - }); - }); - - describe('falsy but valid cache values', () => { - it('should handle falsy but valid cache values (false, 0, empty string)', async () => { - // Test with false - jest.spyOn(cache, 'get').mockResolvedValue(false); - let result = await cachedTestFunction(); - expect(result).toBe(false); - expect(actualExecutionSpy).not.toHaveBeenCalled(); - - // Test with 0 - jest.spyOn(cache, 'get').mockResolvedValue(0); - result = await cachedTestFunction(); - expect(result).toBe(0); - expect(actualExecutionSpy).not.toHaveBeenCalled(); - - // Test with empty string - jest.spyOn(cache, 'get').mockResolvedValue(''); - result = await cachedTestFunction(); - expect(result).toBe(''); - expect(actualExecutionSpy).not.toHaveBeenCalled(); - }); - - it('should execute the function when cache returns undefined', async () => { - jest.spyOn(cache, 'get').mockResolvedValue(undefined); - actualExecutionSpy.mockResolvedValueOnce('test'); - - const result = await cachedTestFunction(); - - expect(result).toBe('test'); - expect(actualExecutionSpy).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/packages/solana-wallet-snap/src/core/caching/useCache.ts b/packages/solana-wallet-snap/src/core/caching/useCache.ts deleted file mode 100644 index 028f2584..00000000 --- a/packages/solana-wallet-snap/src/core/caching/useCache.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* eslint-disable no-void */ - -import type { Serializable } from '@metamask/snap-networks-utils'; - -import logger from '../utils/logger'; -import type { ICache } from './ICache'; - -/** - * Options for configuring the caching behavior of a function. - */ -export type CacheOptions = { - /** - * The time to live for the cache in milliseconds. - */ - ttlMilliseconds: number; - /** - * Set this if you want to use a custom function name for the cache key. - */ - functionName?: string; - /** - * Optional function to generate the cache key for the function call. - * Defaults to a function that generates the key based on function name and JSON stringified args separated by colons. - */ - generateCacheKey?: (functionName: string, args: any[]) => string; -}; - -/** - * Default function to generate the cache key for a function call. - * - * @param functionName - The name of the function. - * @param args - The arguments of the function call. - * @returns The cache key. - */ -const defaultGenerateCacheKey = (functionName: string, args: any[]) => - `${functionName}:${args.map((arg) => JSON.stringify(arg)).join(':')}`; - -/** - * Wraps an asynchronous function with caching behavior. - * - * @template TArgs - Tuple type representing the arguments of the function. - * @template TResult - The return type of the function, must be Serializable. - * @param fn - The asynchronous function to wrap. Must return a Promise. - * @param cache - The cache instance to use. - * @param options - The caching options. - * @param options.ttlMilliseconds - The time to live for the cache in milliseconds. - * @param options.functionName - The name of the function. - * @param options.generateCacheKey - Optional function to generate the cache key. - * @returns A new asynchronous function with caching behavior. - */ -export const useCache = ( - fn: (...args: TArgs) => Promise, - cache: ICache, - { ttlMilliseconds, functionName, generateCacheKey }: CacheOptions, -): ((...args: TArgs) => Promise) => { - // Use provided key generator or default, adapting the default to use the function's name - const _generateCacheKey = generateCacheKey ?? defaultGenerateCacheKey; - - // Get the function name for the default key generator, handle anonymous functions - const _functionName = functionName ?? fn.name ?? 'anonymousFunction'; - - return async (...args: TArgs): Promise => { - const cacheKey = _generateCacheKey(_functionName, args); - - // Check if the data is cached - try { - const cached = await cache.get(cacheKey); - // Check explicitly for undefined, as null or other falsy values might be valid cache results - if (cached !== undefined) { - // Type assertion because cache stores Serializable, but we expect TResult - return cached as TResult; - } - } catch (error) { - // Log cache get errors but proceed to execute the function - logger.error(`Cache get error for key "${cacheKey}":`, error); - } - - // Execute the original function - const result = await fn(...args); - - // Cache the result, handle potential errors silently - // We don't await this, allowing it to happen in the background - void cache.set(cacheKey, result, ttlMilliseconds).catch((error) => { - logger.error(`Cache set error for key "${cacheKey}":`, error); - }); - - return result; - }; -}; diff --git a/packages/solana-wallet-snap/src/core/clients/nft-api/NftApiClient.test.ts b/packages/solana-wallet-snap/src/core/clients/nft-api/NftApiClient.test.ts index 77e27281..ab9b338f 100644 --- a/packages/solana-wallet-snap/src/core/clients/nft-api/NftApiClient.test.ts +++ b/packages/solana-wallet-snap/src/core/clients/nft-api/NftApiClient.test.ts @@ -1,7 +1,6 @@ -import type { Serializable } from '@metamask/snap-networks-utils'; +import type { Serializable, ICache } from '@metamask/snap-networks-utils'; +import { InMemoryCache } from '@metamask/snap-networks-utils'; -import type { ICache } from '../../caching/ICache'; -import { InMemoryCache } from '../../caching/InMemoryCache'; import { mockLogger } from '../../services/__mocks__/logger'; import type { ConfigProvider } from '../../services/config'; import { trackError } from '../../utils/errors'; diff --git a/packages/solana-wallet-snap/src/core/clients/nft-api/NftApiClient.ts b/packages/solana-wallet-snap/src/core/clients/nft-api/NftApiClient.ts index 72d248ea..fd01f07a 100644 --- a/packages/solana-wallet-snap/src/core/clients/nft-api/NftApiClient.ts +++ b/packages/solana-wallet-snap/src/core/clients/nft-api/NftApiClient.ts @@ -1,11 +1,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { UrlStruct, buildUrl } from '@metamask/snap-networks-utils'; -import type { Serializable } from '@metamask/snap-networks-utils'; +import { UrlStruct, buildUrl, useCache } from '@metamask/snap-networks-utils'; +import type { ICache, Serializable } from '@metamask/snap-networks-utils'; import { assert } from '@metamask/superstruct'; -import type { ICache } from '../../caching/ICache'; -import { useCache } from '../../caching/useCache'; import type { ConfigProvider } from '../../services/config'; import { trackError } from '../../utils/errors'; import type { diff --git a/packages/solana-wallet-snap/src/core/clients/price-api/PriceApiClient.test.ts b/packages/solana-wallet-snap/src/core/clients/price-api/PriceApiClient.test.ts index b1c066a8..2d7c5a81 100644 --- a/packages/solana-wallet-snap/src/core/clients/price-api/PriceApiClient.test.ts +++ b/packages/solana-wallet-snap/src/core/clients/price-api/PriceApiClient.test.ts @@ -1,10 +1,9 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import type { CaipAssetType } from '@metamask/keyring-api'; -import type { Serializable } from '@metamask/snap-networks-utils'; +import type { Serializable, ICache } from '@metamask/snap-networks-utils'; +import { InMemoryCache } from '@metamask/snap-networks-utils'; import { cloneDeep } from 'lodash'; -import type { ICache } from '../../caching/ICache'; -import { InMemoryCache } from '../../caching/InMemoryCache'; import { KnownCaip19Id } from '../../constants/solana'; import { mockLogger } from '../../services/__mocks__/logger'; import type { ConfigProvider } from '../../services/config'; diff --git a/packages/solana-wallet-snap/src/core/clients/price-api/PriceApiClient.ts b/packages/solana-wallet-snap/src/core/clients/price-api/PriceApiClient.ts index f7f94189..589407c8 100644 --- a/packages/solana-wallet-snap/src/core/clients/price-api/PriceApiClient.ts +++ b/packages/solana-wallet-snap/src/core/clients/price-api/PriceApiClient.ts @@ -2,12 +2,15 @@ import type { CaipAssetType } from '@metamask/keyring-api'; import { UrlStruct, buildUrl } from '@metamask/snap-networks-utils'; -import type { Logger, Serializable } from '@metamask/snap-networks-utils'; +import type { + ICache, + Logger, + Serializable, +} from '@metamask/snap-networks-utils'; import { array, assert } from '@metamask/superstruct'; import { CaipAssetTypeStruct } from '@metamask/utils'; import { mapKeys } from 'lodash'; -import type { ICache } from '../../caching/ICache'; import type { ConfigProvider } from '../../services/config'; import logger from '../../utils/logger'; import type { SpotPrices, VsCurrencyParam } from './types'; 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 fa21c8a2..c662757c 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,10 +1,9 @@ import { KeyringEvent } from '@metamask/keyring-api'; import { emitSnapKeyringEvent } from '@metamask/keyring-snap-sdk'; -import type { Serializable } from '@metamask/snap-networks-utils'; +import type { Serializable, ICache } from '@metamask/snap-networks-utils'; +import { InMemoryCache } from '@metamask/snap-networks-utils'; import { cloneDeep } from 'lodash'; -import type { ICache } from '../../caching/ICache'; -import { InMemoryCache } from '../../caching/InMemoryCache'; import { MOCK_NFTS_LIST_RESPONSE_MAPPED } from '../../clients/nft-api/mocks/mockNftsListResponseMapped'; import type { NftApiClient } from '../../clients/nft-api/NftApiClient'; import type { TokenApiClient } from '../../clients/token-api-client/TokenApiClient'; diff --git a/packages/solana-wallet-snap/src/core/services/assets/adapters/SnapAssetsAdapter.test.ts b/packages/solana-wallet-snap/src/core/services/assets/adapters/SnapAssetsAdapter.test.ts index 90c47162..74a2802d 100644 --- a/packages/solana-wallet-snap/src/core/services/assets/adapters/SnapAssetsAdapter.test.ts +++ b/packages/solana-wallet-snap/src/core/services/assets/adapters/SnapAssetsAdapter.test.ts @@ -1,8 +1,7 @@ -import type { Serializable } from '@metamask/snap-networks-utils'; +import type { Serializable, ICache } from '@metamask/snap-networks-utils'; +import { InMemoryCache } from '@metamask/snap-networks-utils'; import { cloneDeep } from 'lodash'; -import type { ICache } from '../../../caching/ICache'; -import { InMemoryCache } from '../../../caching/InMemoryCache'; import { MOCK_NFTS_LIST_RESPONSE_MAPPED } from '../../../clients/nft-api/mocks/mockNftsListResponseMapped'; import type { NftApiClient } from '../../../clients/nft-api/NftApiClient'; import type { TokenApiClient } from '../../../clients/token-api-client/TokenApiClient'; diff --git a/packages/solana-wallet-snap/src/core/services/assets/adapters/SnapAssetsAdapter.ts b/packages/solana-wallet-snap/src/core/services/assets/adapters/SnapAssetsAdapter.ts index a1672d6e..db599acd 100644 --- a/packages/solana-wallet-snap/src/core/services/assets/adapters/SnapAssetsAdapter.ts +++ b/packages/solana-wallet-snap/src/core/services/assets/adapters/SnapAssetsAdapter.ts @@ -6,7 +6,12 @@ import type { Balance, } from '@metamask/keyring-api'; import { emitSnapKeyringEvent } from '@metamask/keyring-snap-sdk'; -import type { Logger, Serializable } from '@metamask/snap-networks-utils'; +import type { + Logger, + Serializable, + ICache, +} from '@metamask/snap-networks-utils'; +import { useCache } from '@metamask/snap-networks-utils'; import type { FungibleAssetMetadata } from '@metamask/snaps-sdk'; import type { CaipAssetType, CaipChainId } from '@metamask/utils'; import { Duration, parseCaipAssetType } from '@metamask/utils'; @@ -25,8 +30,6 @@ import type { SolanaKeyringAccount, TokenAsset, } from '../../../../entities'; -import type { ICache } from '../../../caching/ICache'; -import { useCache } from '../../../caching/useCache'; import type { NftApiClient } from '../../../clients/nft-api/NftApiClient'; import type { TokenApiClient } from '../../../clients/token-api-client/TokenApiClient'; import { Network, SolanaCaip19Tokens } from '../../../constants/solana'; @@ -268,7 +271,11 @@ export class SnapAssetsAdapter { ttlMilliseconds: SnapAssetsAdapter.cacheTtlsMilliseconds.tokenAccountsByOwner, generateCacheKey: (functionName, args) => { - const [account, programId, scope] = args; + const [account, programId, scope] = args as [ + SolanaKeyringAccount, + Address, + Network, + ]; return `${functionName}:${account.id}:${programId}:${scope}`; }, }); diff --git a/packages/solana-wallet-snap/src/core/services/connection/SolanaConnection.test.ts b/packages/solana-wallet-snap/src/core/services/connection/SolanaConnection.test.ts index 721867c6..921456b0 100644 --- a/packages/solana-wallet-snap/src/core/services/connection/SolanaConnection.test.ts +++ b/packages/solana-wallet-snap/src/core/services/connection/SolanaConnection.test.ts @@ -1,10 +1,9 @@ /* eslint-disable @typescript-eslint/no-require-imports */ -import type { Serializable } from '@metamask/snap-networks-utils'; +import type { Serializable, ICache } from '@metamask/snap-networks-utils'; +import { InMemoryCache } from '@metamask/snap-networks-utils'; import { fetchJsonParsedAccount } from '@solana/kit'; -import type { ICache } from '../../caching/ICache'; -import { InMemoryCache } from '../../caching/InMemoryCache'; import { KnownCaip19Id, Network } from '../../constants/solana'; import { mockLogger } from '../__mocks__/logger'; import { diff --git a/packages/solana-wallet-snap/src/core/services/connection/SolanaConnection.ts b/packages/solana-wallet-snap/src/core/services/connection/SolanaConnection.ts index 4f889e71..4615087b 100644 --- a/packages/solana-wallet-snap/src/core/services/connection/SolanaConnection.ts +++ b/packages/solana-wallet-snap/src/core/services/connection/SolanaConnection.ts @@ -1,4 +1,5 @@ -import type { Serializable } from '@metamask/snap-networks-utils'; +import type { Serializable, ICache } from '@metamask/snap-networks-utils'; +import { useCache } from '@metamask/snap-networks-utils'; import { assert } from '@metamask/superstruct'; import { Duration } from '@metamask/utils'; import { fetchMint } from '@solana-program/token-2022'; @@ -12,19 +13,29 @@ import type { Account, Address, Blockhash, + Commitment, FetchAccountConfig, MaybeAccount, MaybeEncodedAccount, + Slot, } from '@solana/kit'; import type { Rpc, SolanaRpcApi } from '@solana/kit'; -import type { ICache } from '../../caching/ICache'; -import { useCache } from '../../caching/useCache'; import type { Network } from '../../constants/solana'; import { NetworkStruct } from '../../validation/structs'; import type { ConfigProvider } from '../config/ConfigProvider'; import { createMainTransport } from './transport'; +/** + * The result of {@link fetchJsonParsedAccount}. + * + * The SDK's account types are not statically `Serializable`, so the result is + * asserted to be cache-safe via an intersection with `Serializable`. + */ +type JsonParsedAccountResult = + | (MaybeAccount & Serializable) + | (MaybeEncodedAccount
& Serializable); + /** * The SolanaConnection class is responsible for managing the connections to the Solana networks. */ @@ -113,17 +124,33 @@ export class SolanaConnection { >; } - // Create a cached version of the function + // Create a cached version of the function. + // The cache key is built from the result-affecting, serializable parts of + // the config (`commitment` and `minContextSlot`). `abortSignal` only + // affects cancellation, so it is neither part of the key nor forwarded to + // the cached fetch. const cached = useCache< - [string, Network, FetchAccountConfig | undefined], - | (MaybeAccount & Serializable) - | (MaybeEncodedAccount
& Serializable) - >(internal as any, this.#cache, { - ttlMilliseconds: this.#cacheTtlsMilliseconds.fetchJsonParsedAccount, - functionName: 'SolanaConnection::fetchJsonParsedAccount', - }); - - return cached(address, caip2Id, config); + [string, Network, Commitment | undefined, Slot | undefined], + JsonParsedAccountResult + >( + async ( + _address, + _caip2Id, + _commitment, + _minContextSlot, + ): Promise> => + fetchJsonParsedAccount(this.getRpc(_caip2Id), asAddress(_address), { + commitment: _commitment, + minContextSlot: _minContextSlot, + }) as Promise>, + this.#cache, + { + ttlMilliseconds: this.#cacheTtlsMilliseconds.fetchJsonParsedAccount, + functionName: 'SolanaConnection::fetchJsonParsedAccount', + }, + ); + + return cached(address, caip2Id, config?.commitment, config?.minContextSlot); } /** @@ -146,28 +173,40 @@ export class SolanaConnection { * * This wrapper is used instead of directly caching the SDK's fetchMint function * to ensure that only simple arguments are used, as these arguments form the cache key. + * + * As with {@link SolanaConnection.fetchJsonParsedAccount}, the cache key is + * built from the result-affecting, serializable parts of the config + * (`commitment` and `minContextSlot`); `abortSignal` is not forwarded to + * the cached fetch. */ - - const fetchMintInternal = async ( - _address: Address, - _caip2Id: Network, - _config?: FetchAccountConfig, - ) => { - const rpc = this.getRpc(caip2Id); - return fetchMint(rpc, asAddress(address), config); - }; - - // Create a cached version of the function const fetchMintCached = useCache< - [Address, Network, FetchAccountConfig | undefined], + [Address, Network, Commitment | undefined, Slot | undefined], Account & Serializable - >(fetchMintInternal as any, this.#cache, { - ttlMilliseconds: this.#cacheTtlsMilliseconds.fetchMint, - functionName: 'SolanaConnection::fetchMint', - }); + >( + async ( + _address, + _caip2Id, + _commitment, + _minContextSlot, + ): Promise & Serializable> => + fetchMint(this.getRpc(_caip2Id), _address, { + commitment: _commitment, + minContextSlot: _minContextSlot, + }) as Promise & Serializable>, + this.#cache, + { + ttlMilliseconds: this.#cacheTtlsMilliseconds.fetchMint, + functionName: 'SolanaConnection::fetchMint', + }, + ); // Use the cached version of the function - return fetchMintCached(asAddress(address), caip2Id, config); + return fetchMintCached( + asAddress(address), + caip2Id, + config?.commitment, + config?.minContextSlot, + ); } /** diff --git a/packages/solana-wallet-snap/src/core/services/send/SendService.test.ts b/packages/solana-wallet-snap/src/core/services/send/SendService.test.ts index 511982d7..9f373c7a 100644 --- a/packages/solana-wallet-snap/src/core/services/send/SendService.test.ts +++ b/packages/solana-wallet-snap/src/core/services/send/SendService.test.ts @@ -1,10 +1,9 @@ import { SolMethod } from '@metamask/keyring-api'; -import type { Serializable } from '@metamask/snap-networks-utils'; +import type { Serializable, ICache } from '@metamask/snap-networks-utils'; +import { InMemoryCache } from '@metamask/snap-networks-utils'; import { lamports } from '@solana/kit'; import type { AssetEntity, SolanaKeyringAccount } from '../../../entities'; -import type { ICache } from '../../caching/ICache'; -import { InMemoryCache } from '../../caching/InMemoryCache'; import { KnownCaip19Id, METAMASK_ORIGIN, diff --git a/packages/solana-wallet-snap/src/core/services/send/SendService.ts b/packages/solana-wallet-snap/src/core/services/send/SendService.ts index 92cd1787..b072eab6 100644 --- a/packages/solana-wallet-snap/src/core/services/send/SendService.ts +++ b/packages/solana-wallet-snap/src/core/services/send/SendService.ts @@ -1,13 +1,16 @@ import type { KeyringRequest } from '@metamask/keyring-api'; import { SolMethod } from '@metamask/keyring-api'; -import type { Logger, Serializable } from '@metamask/snap-networks-utils'; +import type { + Logger, + Serializable, + ICache, +} from '@metamask/snap-networks-utils'; +import { useCache } from '@metamask/snap-networks-utils'; import type { Json } from '@metamask/snaps-sdk'; import { Duration, parseCaipAssetType } from '@metamask/utils'; import { address as asAddress, compileTransaction } from '@solana/kit'; import { BigNumber } from 'bignumber.js'; -import type { ICache } from '../../caching/ICache'; -import { useCache } from '../../caching/useCache'; import type { Network } from '../../constants/solana'; import { METAMASK_ORIGIN, Networks } from '../../constants/solana'; import type { SolanaKeyring } from '../../handlers/onKeyringRequest/Keyring'; @@ -83,7 +86,7 @@ export class SendService { ttlMilliseconds: this.#cacheTtlsMilliseconds.minimumBalanceForRentExemption, generateCacheKey: (functionName, args) => { - const [_scope] = args; + const [_scope] = args as [Network]; return `${functionName}:${_scope}`; }, }, diff --git a/packages/solana-wallet-snap/src/snapContext.ts b/packages/solana-wallet-snap/src/snapContext.ts index 0e808591..76c8948e 100644 --- a/packages/solana-wallet-snap/src/snapContext.ts +++ b/packages/solana-wallet-snap/src/snapContext.ts @@ -1,4 +1,5 @@ -import { InMemoryCache } from './core/caching/InMemoryCache'; +import { InMemoryCache } from '@metamask/snap-networks-utils'; + import { NftApiClient } from './core/clients/nft-api/NftApiClient'; import { PriceApiClient } from './core/clients/price-api/PriceApiClient'; import { SecurityAlertsApiClient } from './core/clients/security-alerts-api/SecurityAlertsApiClient';