From 88c2687779ccfa150affbaf889203d5b00927385 Mon Sep 17 00:00:00 2001 From: Bigshmow Date: Wed, 29 Jan 2025 17:37:37 -0700 Subject: [PATCH 1/9] feat: add token discovery service to controller for discover methods --- .../abstract-token-discovery-api-service.ts | 17 +++++++ .../token-discovery-api-service.ts | 43 ++++++++++++++++ .../src/token-search-discovery-controller.ts | 21 +++++++- .../src/types.ts | 51 +++++++++++++++++++ 4 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 packages/token-search-discovery-controller/src/token-discovery-api-service/abstract-token-discovery-api-service.ts create mode 100644 packages/token-search-discovery-controller/src/token-discovery-api-service/token-discovery-api-service.ts diff --git a/packages/token-search-discovery-controller/src/token-discovery-api-service/abstract-token-discovery-api-service.ts b/packages/token-search-discovery-controller/src/token-discovery-api-service/abstract-token-discovery-api-service.ts new file mode 100644 index 00000000000..c676b91dd1d --- /dev/null +++ b/packages/token-search-discovery-controller/src/token-discovery-api-service/abstract-token-discovery-api-service.ts @@ -0,0 +1,17 @@ +import type { TokenTrendingResponseItem } from '../types'; + +/** + * Abstract class for fetching token discovery results. + */ +export abstract class AbstractTokenDiscoveryApiService { + /** + * Fetches trending tokens by chains from the portfolio API. + * + * @param params - Optional parameters including chains and limit + * @returns A promise resolving to an array of {@link TokenTrendingResponseItem} + */ + abstract getTrendingTokensByChains(params: { + chains?: string[]; + limit?: string; + }): Promise; +} diff --git a/packages/token-search-discovery-controller/src/token-discovery-api-service/token-discovery-api-service.ts b/packages/token-search-discovery-controller/src/token-discovery-api-service/token-discovery-api-service.ts new file mode 100644 index 00000000000..f322c42c153 --- /dev/null +++ b/packages/token-search-discovery-controller/src/token-discovery-api-service/token-discovery-api-service.ts @@ -0,0 +1,43 @@ +import { AbstractTokenDiscoveryApiService } from './abstract-token-discovery-api-service'; +import type { TokenTrendingResponseItem } from '../types'; + +export class TokenDiscoveryApiService extends AbstractTokenDiscoveryApiService { + readonly #baseUrl: string; + + constructor(baseUrl: string) { + super(); + if (!baseUrl) { + throw new Error('Portfolio API URL is not set'); + } + this.#baseUrl = baseUrl; + } + + async getTrendingTokensByChains(params: { + chains?: string[]; + limit?: string; + }): Promise { + const url = new URL('/tokens-search/trending-by-chains', this.#baseUrl); + + if (params.chains && params.chains.length > 0) { + url.searchParams.append('chains', params.chains.join()); + } + if (params.limit) { + url.searchParams.append('limit', params.limit); + } + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error( + `Portfolio API request failed with status: ${response.status}`, + ); + } + + return response.json(); + } +} diff --git a/packages/token-search-discovery-controller/src/token-search-discovery-controller.ts b/packages/token-search-discovery-controller/src/token-search-discovery-controller.ts index 0f14962ffab..2ea72c5c5c2 100644 --- a/packages/token-search-discovery-controller/src/token-search-discovery-controller.ts +++ b/packages/token-search-discovery-controller/src/token-search-discovery-controller.ts @@ -5,8 +5,13 @@ import type { } from '@metamask/base-controller'; import { BaseController } from '@metamask/base-controller'; +import type { AbstractTokenDiscoveryApiService } from './token-discovery-api-service/abstract-token-discovery-api-service'; import type { AbstractTokenSearchApiService } from './token-search-api-service/abstract-token-search-api-service'; -import type { TokenSearchParams, TokenSearchResponseItem } from './types'; +import type { + TokenSearchParams, + TokenSearchResponseItem, + TokenTrendingResponseItem, +} from './types'; // === GENERAL === @@ -100,7 +105,7 @@ export function getDefaultTokenSearchDiscoveryControllerState(): TokenSearchDisc /** * The TokenSearchDiscoveryController manages the retrieval of token search results and token discovery. - * It fetches token search results from the portfolio API. + * It fetches token search results and discovery data from the Portfolio API. */ export class TokenSearchDiscoveryController extends BaseController< typeof controllerName, @@ -109,12 +114,16 @@ export class TokenSearchDiscoveryController extends BaseController< > { readonly #tokenSearchService: AbstractTokenSearchApiService; + readonly #tokenDiscoveryService: AbstractTokenDiscoveryApiService; + constructor({ tokenSearchService, + tokenDiscoveryService, state = {}, messenger, }: { tokenSearchService: AbstractTokenSearchApiService; + tokenDiscoveryService: AbstractTokenDiscoveryApiService; state?: Partial; messenger: TokenSearchDiscoveryControllerMessenger; }) { @@ -126,6 +135,7 @@ export class TokenSearchDiscoveryController extends BaseController< }); this.#tokenSearchService = tokenSearchService; + this.#tokenDiscoveryService = tokenDiscoveryService; } async searchTokens( @@ -141,4 +151,11 @@ export class TokenSearchDiscoveryController extends BaseController< return results; } + + async getTrendingTokens(params: { + chains?: string[]; + limit?: string; + }): Promise { + return this.#tokenDiscoveryService.getTrendingTokensByChains(params); + } } diff --git a/packages/token-search-discovery-controller/src/types.ts b/packages/token-search-discovery-controller/src/types.ts index 4d9cf23b1b6..01f4a4d99aa 100644 --- a/packages/token-search-discovery-controller/src/types.ts +++ b/packages/token-search-discovery-controller/src/types.ts @@ -15,3 +15,54 @@ export type TokenSearchResponseItem = { }; logoUrl?: string; }; + +export type TokenTrendingResponseItem = { + chain_id: string; + token_address: string; + token_logo: string; + token_name: string; + token_symbol: string; + price_usd: number; + token_age_in_days: number; + on_chain_strength_index: number; + security_score: number; + market_cap: number; + fully_diluted_valuation: number; + twitter_followers: number; + holders_change: { + '1h': number | null; + '1d': number | null; + '1w': number | null; + '1M': number | null; + }; + liquidity_change_usd: { + '1h': number | null; + '1d': number | null; + '1w': number | null; + '1M': number | null; + }; + experienced_net_buyers_change: { + '1h': number | null; + '1d': number | null; + '1w': number | null; + '1M': number | null; + }; + volume_change_usd: { + '1h': number | null; + '1d': number | null; + '1w': number | null; + '1M': number | null; + }; + net_volume_change_usd: { + '1h': number | null; + '1d': number | null; + '1w': number | null; + '1M': number | null; + }; + price_percent_change_usd: { + '1h': number | null; + '1d': number | null; + '1w': number | null; + '1M': number | null; + }; +}; From 85286568fe9c3cf86deab74595fbbe3e1b72c4ef Mon Sep 17 00:00:00 2001 From: Bigshmow Date: Wed, 29 Jan 2025 17:38:47 -0700 Subject: [PATCH 2/9] chore: test new discovery service add nock in deps --- .../package.json | 1 + .../src/test/constants.ts | 4 + .../token-discovery-api-service.test.ts | 127 ++++++++++++++ .../token-search-api-service.test.ts | 163 +++++------------- .../token-search-discovery-controller.test.ts | 142 ++++++++++++--- yarn.lock | 1 + 6 files changed, 287 insertions(+), 151 deletions(-) create mode 100644 packages/token-search-discovery-controller/src/test/constants.ts create mode 100644 packages/token-search-discovery-controller/src/token-discovery-api-service/token-discovery-api-service.test.ts diff --git a/packages/token-search-discovery-controller/package.json b/packages/token-search-discovery-controller/package.json index 80233d33c76..1438cf65caf 100644 --- a/packages/token-search-discovery-controller/package.json +++ b/packages/token-search-discovery-controller/package.json @@ -55,6 +55,7 @@ "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", + "nock": "^13.3.1", "ts-jest": "^27.1.4", "typedoc": "^0.24.8", "typedoc-plugin-missing-exports": "^2.0.0", diff --git a/packages/token-search-discovery-controller/src/test/constants.ts b/packages/token-search-discovery-controller/src/test/constants.ts new file mode 100644 index 00000000000..20009c29e34 --- /dev/null +++ b/packages/token-search-discovery-controller/src/test/constants.ts @@ -0,0 +1,4 @@ +export const TEST_API_URLS = { + BASE_URL: 'https://mock-api.test', + PORTFOLIO_API: 'https://mock-portfolio-api.test', +} as const; diff --git a/packages/token-search-discovery-controller/src/token-discovery-api-service/token-discovery-api-service.test.ts b/packages/token-search-discovery-controller/src/token-discovery-api-service/token-discovery-api-service.test.ts new file mode 100644 index 00000000000..874f67463e1 --- /dev/null +++ b/packages/token-search-discovery-controller/src/token-discovery-api-service/token-discovery-api-service.test.ts @@ -0,0 +1,127 @@ +import nock, { cleanAll } from 'nock'; + +import { TokenDiscoveryApiService } from './token-discovery-api-service'; +import { TEST_API_URLS } from '../test/constants'; +import type { TokenTrendingResponseItem } from '../types'; + +describe('TokenDiscoveryApiService', () => { + let service: TokenDiscoveryApiService; + const mockTrendingResponse: TokenTrendingResponseItem[] = [ + { + chain_id: '1', + token_address: '0x123', + token_logo: 'https://example.com/logo.png', + token_name: 'Test Token', + token_symbol: 'TEST', + price_usd: 100, + token_age_in_days: 365, + on_chain_strength_index: 85, + security_score: 90, + market_cap: 1000000, + fully_diluted_valuation: 2000000, + twitter_followers: 50000, + holders_change: { + '1h': 10, + '1d': 100, + '1w': 1000, + '1M': 10000, + }, + liquidity_change_usd: { + '1h': 1000, + '1d': 10000, + '1w': 100000, + '1M': 1000000, + }, + experienced_net_buyers_change: { + '1h': 5, + '1d': 50, + '1w': 500, + '1M': 5000, + }, + volume_change_usd: { + '1h': 10000, + '1d': 100000, + '1w': 1000000, + '1M': 10000000, + }, + net_volume_change_usd: { + '1h': 5000, + '1d': 50000, + '1w': 500000, + '1M': 5000000, + }, + price_percent_change_usd: { + '1h': 1, + '1d': 10, + '1w': 20, + '1M': 30, + }, + }, + ]; + + beforeEach(() => { + service = new TokenDiscoveryApiService(TEST_API_URLS.PORTFOLIO_API); + }); + + afterEach(() => { + cleanAll(); + }); + + describe('constructor', () => { + it('should throw if baseUrl is empty', () => { + expect(() => new TokenDiscoveryApiService('')).toThrow( + 'Portfolio API URL is not set', + ); + }); + }); + + describe('getTrendingTokensByChains', () => { + it.each([ + { + params: { chains: ['1'], limit: '5' }, + expectedPath: '/tokens-search/trending-by-chains?chains=1&limit=5', + }, + { + params: { chains: ['1', '137'] }, + expectedPath: '/tokens-search/trending-by-chains?chains=1,137', + }, + { + params: { limit: '10' }, + expectedPath: '/tokens-search/trending-by-chains?limit=10', + }, + { + params: {}, + expectedPath: '/tokens-search/trending-by-chains', + }, + ])( + 'should construct correct URL for params: $params', + async ({ params, expectedPath }) => { + nock(TEST_API_URLS.PORTFOLIO_API) + .get(expectedPath) + .reply(200, mockTrendingResponse); + + const result = await service.getTrendingTokensByChains(params); + expect(result).toStrictEqual(mockTrendingResponse); + }, + ); + + it('should handle API errors', async () => { + nock(TEST_API_URLS.PORTFOLIO_API) + .get('/tokens-search/trending-by-chains') + .reply(500, 'Server Error'); + + await expect(service.getTrendingTokensByChains({})).rejects.toThrow( + 'Portfolio API request failed with status: 500', + ); + }); + + it('should return trending results', async () => { + nock(TEST_API_URLS.PORTFOLIO_API) + .get('/tokens-search/trending-by-chains') + .reply(200, mockTrendingResponse); + + const results = await service.getTrendingTokensByChains({}); + expect(results).toStrictEqual(mockTrendingResponse); + }); + }); +}); diff --git a/packages/token-search-discovery-controller/src/token-search-api-service/token-search-api-service.test.ts b/packages/token-search-discovery-controller/src/token-search-api-service/token-search-api-service.test.ts index 7fbf9a881ce..2350e9bbed1 100644 --- a/packages/token-search-discovery-controller/src/token-search-api-service/token-search-api-service.test.ts +++ b/packages/token-search-discovery-controller/src/token-search-api-service/token-search-api-service.test.ts @@ -1,62 +1,19 @@ +import nock, { cleanAll } from 'nock'; + import { TokenSearchApiService } from './token-search-api-service'; +import { TEST_API_URLS } from '../test/constants'; +import type { TokenSearchResponseItem } from '../types'; describe('TokenSearchApiService', () => { - const baseUrl = 'https://test-api'; let service: TokenSearchApiService; - let mockFetch: jest.SpyInstance; - - const mockResponses = { - allParams: [ - { - name: 'Token1', - symbol: 'TK1', - chainId: '1', - tokenAddress: '0x1', - usdPrice: 100, - usdPricePercentChange: { oneDay: 10 }, - logoUrl: 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x1.png', - }, - { - name: 'Token2', - symbol: 'TK2', - chainId: '1', - tokenAddress: '0x2', - usdPrice: 200, - usdPricePercentChange: { oneDay: 20 }, - }, - ], - onlyChain: [ - { - name: 'ChainToken', - symbol: 'CTK', - chainId: '1', - tokenAddress: '0x3', - usdPrice: 300, - usdPricePercentChange: { oneDay: 30 }, - logoUrl: 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x3.png', - }, - ], - onlyName: [ - { - name: 'NameMatch', - symbol: 'NM', - chainId: '1', - tokenAddress: '0x4', - usdPrice: 400, - usdPricePercentChange: { oneDay: 40 }, - }, - ], - }; + const mockSearchResults: TokenSearchResponseItem[] = []; beforeEach(() => { - service = new TokenSearchApiService(baseUrl); - mockFetch = jest - .spyOn(global, 'fetch') - .mockResolvedValue(new Response(JSON.stringify([]), { status: 200 })); + service = new TokenSearchApiService(TEST_API_URLS.BASE_URL); }); afterEach(() => { - mockFetch.mockRestore(); + cleanAll(); }); describe('constructor', () => { @@ -68,86 +25,44 @@ describe('TokenSearchApiService', () => { }); describe('searchTokens', () => { - it.each([ - { - params: { chains: ['1'], query: 'Test', limit: '10' }, - expectedUrl: new URL( - `${baseUrl}/tokens-search?chains=1&query=Test&limit=10`, - ), - }, - { - params: { chains: ['1', '137'], query: 'Test' }, - expectedUrl: new URL( - `${baseUrl}/tokens-search?chains=1%2C137&query=Test`, - ), - }, - { - params: { query: 'Test' }, - expectedUrl: new URL(`${baseUrl}/tokens-search?query=Test`), - }, - { - params: { chains: ['1'] }, - expectedUrl: new URL(`${baseUrl}/tokens-search?chains=1`), - }, - { - params: { limit: '20' }, - expectedUrl: new URL(`${baseUrl}/tokens-search?limit=20`), - }, - { - params: {}, - expectedUrl: new URL(`${baseUrl}/tokens-search`), - }, - ])( - 'should construct correct URL for params: $params', - async ({ params, expectedUrl }) => { - await service.searchTokens(params); - expect(mockFetch.mock.calls[0][0].toString()).toBe( - expectedUrl.toString(), - ); - }, - ); + it('should return search results', async () => { + nock(TEST_API_URLS.BASE_URL) + .get('/tokens-search') + .query({ query: 'ETH' }) + .reply(200, mockSearchResults); + + const results = await service.searchTokens({ query: 'ETH' }); + expect(results).toStrictEqual(mockSearchResults); + }); + + it('should handle chains parameter', async () => { + nock(TEST_API_URLS.BASE_URL) + .get('/tokens-search') + .query({ chains: '1,137' }) + .reply(200, mockSearchResults); + + const results = await service.searchTokens({ chains: ['1', '137'] }); + expect(results).toStrictEqual(mockSearchResults); + }); + + it('should handle limit parameter', async () => { + nock(TEST_API_URLS.BASE_URL) + .get('/tokens-search') + .query({ limit: '10' }) + .reply(200, mockSearchResults); + + const results = await service.searchTokens({ limit: '10' }); + expect(results).toStrictEqual(mockSearchResults); + }); it('should handle API errors', async () => { - mockFetch.mockResolvedValueOnce( - new Response('Server Error', { status: 500 }), - ); + nock(TEST_API_URLS.BASE_URL) + .get('/tokens-search') + .reply(500, 'Server Error'); await expect(service.searchTokens({})).rejects.toThrow( 'Portfolio API request failed with status: 500', ); }); }); - - describe('searchTokens response handling', () => { - it.each([ - { - params: { chains: ['1'], query: 'Test', limit: '2' }, - mockResponse: mockResponses.allParams, - description: 'all parameters', - }, - { - params: { chains: ['1'] }, - mockResponse: mockResponses.onlyChain, - description: 'only chain parameter', - }, - { - params: { query: 'Name' }, - mockResponse: mockResponses.onlyName, - description: 'only name parameter', - }, - ])( - 'should handle response correctly regardless of params', - async ({ params, mockResponse }) => { - mockFetch = jest - .spyOn(global, 'fetch') - .mockResolvedValue( - new Response(JSON.stringify(mockResponse), { status: 200 }), - ); - - const response = await service.searchTokens(params); - - expect(response).toStrictEqual(mockResponse); - }, - ); - }); }); diff --git a/packages/token-search-discovery-controller/src/token-search-discovery-controller.test.ts b/packages/token-search-discovery-controller/src/token-search-discovery-controller.test.ts index 1e5f35e969e..32f8840310d 100644 --- a/packages/token-search-discovery-controller/src/token-search-discovery-controller.test.ts +++ b/packages/token-search-discovery-controller/src/token-search-discovery-controller.test.ts @@ -1,12 +1,16 @@ import { ControllerMessenger } from '@metamask/base-controller'; +import { AbstractTokenDiscoveryApiService } from './token-discovery-api-service/abstract-token-discovery-api-service'; import { AbstractTokenSearchApiService } from './token-search-api-service/abstract-token-search-api-service'; import { getDefaultTokenSearchDiscoveryControllerState, TokenSearchDiscoveryController, } from './token-search-discovery-controller'; import type { TokenSearchDiscoveryControllerMessenger } from './token-search-discovery-controller'; -import type { TokenSearchResponseItem } from './types'; +import type { + TokenSearchResponseItem, + TokenTrendingResponseItem, +} from './types'; const controllerName = 'TokenSearchDiscoveryController'; @@ -35,7 +39,59 @@ describe('TokenSearchDiscoveryController', () => { usdPricePercentChange: { oneDay: 10, }, - // no logoUrl to test optional case + }, + ]; + + const mockTrendingResults: TokenTrendingResponseItem[] = [ + { + chain_id: '1', + token_address: '0x123', + token_logo: 'https://example.com/logo.png', + token_name: 'Test Token', + token_symbol: 'TEST', + price_usd: 100, + token_age_in_days: 365, + on_chain_strength_index: 85, + security_score: 90, + market_cap: 1000000, + fully_diluted_valuation: 2000000, + twitter_followers: 50000, + holders_change: { + '1h': 10, + '1d': 100, + '1w': 1000, + '1M': 10000, + }, + liquidity_change_usd: { + '1h': 1000, + '1d': 10000, + '1w': 100000, + '1M': 1000000, + }, + experienced_net_buyers_change: { + '1h': 5, + '1d': 50, + '1w': 500, + '1M': 5000, + }, + volume_change_usd: { + '1h': 10000, + '1d': 100000, + '1w': 1000000, + '1M': 10000000, + }, + net_volume_change_usd: { + '1h': 5000, + '1d': 50000, + '1w': 500000, + '1M': 5000000, + }, + price_percent_change_usd: { + '1h': 1, + '1d': 10, + '1w': 20, + '1M': 30, + }, }, ]; @@ -45,10 +101,27 @@ describe('TokenSearchDiscoveryController', () => { } } + class MockTokenDiscoveryService extends AbstractTokenDiscoveryApiService { + async getTrendingTokensByChains(): Promise { + return mockTrendingResults; + } + } + + let mainController: TokenSearchDiscoveryController; + + beforeEach(() => { + mainController = new TokenSearchDiscoveryController({ + tokenSearchService: new MockTokenSearchService(), + tokenDiscoveryService: new MockTokenDiscoveryService(), + messenger: getRestrictedMessenger(), + }); + }); + describe('constructor', () => { it('should initialize with default state', () => { const controller = new TokenSearchDiscoveryController({ tokenSearchService: new MockTokenSearchService(), + tokenDiscoveryService: new MockTokenDiscoveryService(), messenger: getRestrictedMessenger(), }); @@ -65,47 +138,62 @@ describe('TokenSearchDiscoveryController', () => { const controller = new TokenSearchDiscoveryController({ tokenSearchService: new MockTokenSearchService(), + tokenDiscoveryService: new MockTokenDiscoveryService(), state: initialState, messenger: getRestrictedMessenger(), }); expect(controller.state).toStrictEqual(initialState); }); + }); - it('should merge to complete state', () => { - const partialState = { - recentSearches: mockSearchResults, - }; - - const controller = new TokenSearchDiscoveryController({ - tokenSearchService: new MockTokenSearchService(), - state: partialState, - messenger: getRestrictedMessenger(), - }); + describe('searchTokens', () => { + it('should return search results', async () => { + const results = await mainController.searchTokens({}); + expect(results).toStrictEqual(mockSearchResults); + }); + }); - expect(controller.state).toStrictEqual({ - ...getDefaultTokenSearchDiscoveryControllerState(), - ...partialState, - }); + describe('getTrendingTokens', () => { + it('should return trending results', async () => { + const results = await mainController.getTrendingTokens({}); + expect(results).toStrictEqual(mockTrendingResults); }); }); - describe('searchTokens', () => { - it('should update state with search results', async () => { - const mockService = new MockTokenSearchService(); - const controller = new TokenSearchDiscoveryController({ - tokenSearchService: mockService, + describe('error handling', () => { + class ErrorTokenSearchService extends AbstractTokenSearchApiService { + async searchTokens(): Promise { + return []; + } + } + + class ErrorTokenDiscoveryService extends AbstractTokenDiscoveryApiService { + async getTrendingTokensByChains(): Promise { + return []; + } + } + + it('should handle search service errors', async () => { + const errorController = new TokenSearchDiscoveryController({ + tokenSearchService: new ErrorTokenSearchService(), + tokenDiscoveryService: new MockTokenDiscoveryService(), messenger: getRestrictedMessenger(), }); - const response = await controller.searchTokens({ - chains: ['1'], - query: 'Test', + const results = await errorController.searchTokens({}); + expect(results).toStrictEqual([]); + }); + + it('should handle discovery service errors', async () => { + const errorController = new TokenSearchDiscoveryController({ + tokenSearchService: new MockTokenSearchService(), + tokenDiscoveryService: new ErrorTokenDiscoveryService(), + messenger: getRestrictedMessenger(), }); - expect(response).toStrictEqual(mockSearchResults); - expect(controller.state.recentSearches).toStrictEqual(mockSearchResults); - expect(controller.state.lastSearchTimestamp).toBeDefined(); + const results = await errorController.getTrendingTokens({}); + expect(results).toStrictEqual([]); }); }); }); diff --git a/yarn.lock b/yarn.lock index 40d8c3058be..69d7a49a125 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4030,6 +4030,7 @@ __metadata: "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" + nock: "npm:^13.3.1" ts-jest: "npm:^27.1.4" typedoc: "npm:^0.24.8" typedoc-plugin-missing-exports: "npm:^2.0.0" From 77b5b4aaabbae8e723b2a84d269fb3156f6f5dce Mon Sep 17 00:00:00 2001 From: Bigshmow Date: Wed, 29 Jan 2025 17:40:13 -0700 Subject: [PATCH 3/9] chore: update exports --- packages/token-search-discovery-controller/src/index.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/token-search-discovery-controller/src/index.ts b/packages/token-search-discovery-controller/src/index.ts index ce5c5022ef1..a5eb817ef11 100644 --- a/packages/token-search-discovery-controller/src/index.ts +++ b/packages/token-search-discovery-controller/src/index.ts @@ -1,6 +1,13 @@ export { TokenSearchDiscoveryController } from './token-search-discovery-controller'; export type { TokenSearchDiscoveryControllerState } from './token-search-discovery-controller'; -export type { TokenSearchResponseItem } from './types'; +export type { + TokenSearchResponseItem, + TokenTrendingResponseItem, +} from './types'; +export type { TokenSearchParams } from './types'; export { AbstractTokenSearchApiService } from './token-search-api-service/abstract-token-search-api-service'; export { TokenSearchApiService } from './token-search-api-service/token-search-api-service'; + +export { AbstractTokenDiscoveryApiService } from './token-discovery-api-service/abstract-token-discovery-api-service'; +export { TokenDiscoveryApiService } from './token-discovery-api-service/token-discovery-api-service'; From 85cac2bbc2f8f84dabb304afd0d16fa2b81af32b Mon Sep 17 00:00:00 2001 From: Bigshmow Date: Wed, 29 Jan 2025 17:53:39 -0700 Subject: [PATCH 4/9] chore: update changelog --- packages/token-search-discovery-controller/CHANGELOG.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/token-search-discovery-controller/CHANGELOG.md b/packages/token-search-discovery-controller/CHANGELOG.md index 6146505035d..6bb742b9775 100644 --- a/packages/token-search-discovery-controller/CHANGELOG.md +++ b/packages/token-search-discovery-controller/CHANGELOG.md @@ -9,8 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Introduce the logoUrl property to the TokenSearchApiService response +- Introduce the `logoUrl` property to the `TokenSearchApiService` response - Specifically in the `TokenSearchResponseItem` type +- Introduce `TokenDiscoveryApiService` to keep discovery and search responsibilities separate + - This service is responsible for fetching discover related data + - Add `getTrendingTokens` method to fetch trending tokens by chain + - Add `TokenTrendingResponseItem` type for trending token responses +- Export `TokenSearchResponseItem` type from the package index ### Changed @@ -26,7 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Introduce the TokenSearchDiscoveryController ([#5142](https://github.com/MetaMask/core/pull/5142/)) - This controller manages token search and discovery through the Portfolio API - Introduce the TokenSearchApiService ([#5142](https://github.com/MetaMask/core/pull/5142/)) - - This service is responsible for making requests to the Portfolio API + - This service is responsible for making search related requests to the Portfolio API - Specifically, it handles the `tokens-search` endpoint which returns a list of tokens based on the provided query parameters [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/token-search-discovery-controller@1.0.0...HEAD From 43885b878019c6bea8f7d108e8a558fdd1ff8e5d Mon Sep 17 00:00:00 2001 From: Bigshmow Date: Wed, 29 Jan 2025 18:11:24 -0700 Subject: [PATCH 5/9] chore: more realist service testing --- .../token-search-api-service.test.ts | 84 +++++++++++++++---- 1 file changed, 66 insertions(+), 18 deletions(-) diff --git a/packages/token-search-discovery-controller/src/token-search-api-service/token-search-api-service.test.ts b/packages/token-search-discovery-controller/src/token-search-api-service/token-search-api-service.test.ts index 2350e9bbed1..6bcf7d54c45 100644 --- a/packages/token-search-discovery-controller/src/token-search-api-service/token-search-api-service.test.ts +++ b/packages/token-search-discovery-controller/src/token-search-api-service/token-search-api-service.test.ts @@ -6,7 +6,30 @@ import type { TokenSearchResponseItem } from '../types'; describe('TokenSearchApiService', () => { let service: TokenSearchApiService; - const mockSearchResults: TokenSearchResponseItem[] = []; + const mockSearchResults: TokenSearchResponseItem[] = [ + { + name: 'Test Token', + symbol: 'TEST', + chainId: '1', + tokenAddress: '0x123', + usdPrice: 100, + usdPricePercentChange: { + oneDay: 10, + }, + logoUrl: 'https://example.com/logo.png', + }, + { + name: 'Another Token', + symbol: 'ANOT', + chainId: '137', + tokenAddress: '0x456', + usdPrice: 50, + usdPricePercentChange: { + oneDay: -5, + }, + // logoUrl intentionally omitted to match API behavior + }, + ]; beforeEach(() => { service = new TokenSearchApiService(TEST_API_URLS.BASE_URL); @@ -25,34 +48,36 @@ describe('TokenSearchApiService', () => { }); describe('searchTokens', () => { - it('should return search results', async () => { + it('should return search results with all parameters', async () => { nock(TEST_API_URLS.BASE_URL) .get('/tokens-search') - .query({ query: 'ETH' }) + .query({ + query: 'TEST', + chains: '1,137', + limit: '10', + }) .reply(200, mockSearchResults); - const results = await service.searchTokens({ query: 'ETH' }); + const results = await service.searchTokens({ + query: 'TEST', + chains: ['1', '137'], + limit: '10', + }); expect(results).toStrictEqual(mockSearchResults); }); - it('should handle chains parameter', async () => { - nock(TEST_API_URLS.BASE_URL) - .get('/tokens-search') - .query({ chains: '1,137' }) - .reply(200, mockSearchResults); - - const results = await service.searchTokens({ chains: ['1', '137'] }); - expect(results).toStrictEqual(mockSearchResults); - }); + it('should filter results by chain when only chains parameter is provided', async () => { + const chainSpecificResults = mockSearchResults.filter( + (token) => token.chainId === '137', + ); - it('should handle limit parameter', async () => { nock(TEST_API_URLS.BASE_URL) .get('/tokens-search') - .query({ limit: '10' }) - .reply(200, mockSearchResults); + .query({ chains: '137' }) + .reply(200, chainSpecificResults); - const results = await service.searchTokens({ limit: '10' }); - expect(results).toStrictEqual(mockSearchResults); + const results = await service.searchTokens({ chains: ['137'] }); + expect(results).toStrictEqual(chainSpecificResults); }); it('should handle API errors', async () => { @@ -64,5 +89,28 @@ describe('TokenSearchApiService', () => { 'Portfolio API request failed with status: 500', ); }); + + it('should handle tokens with missing logoUrl', async () => { + const tokenWithoutLogo = { + name: 'No Logo Token', + symbol: 'NOLOG', + chainId: '1', + tokenAddress: '0x789', + usdPrice: 75, + usdPricePercentChange: { + oneDay: 2, + }, + // logoUrl intentionally omitted to match API behavior + }; + + nock(TEST_API_URLS.BASE_URL) + .get('/tokens-search') + .query({ query: 'NOLOG' }) + .reply(200, [tokenWithoutLogo]); + + const results = await service.searchTokens({ query: 'NOLOG' }); + expect(results).toStrictEqual([tokenWithoutLogo]); + expect(results[0].logoUrl).toBeUndefined(); + }); }); }); From 2566ff3ae85b62a7307c7eb8006c175f160ebf33 Mon Sep 17 00:00:00 2001 From: Bigshmow Date: Thu, 30 Jan 2025 09:35:03 -0700 Subject: [PATCH 6/9] chore: add type for trending tokens params --- .../src/index.ts | 2 +- .../token-discovery-api-service.ts | 17 ++++++++--------- .../src/types.ts | 5 +++++ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/token-search-discovery-controller/src/index.ts b/packages/token-search-discovery-controller/src/index.ts index a5eb817ef11..fa2bcb101ad 100644 --- a/packages/token-search-discovery-controller/src/index.ts +++ b/packages/token-search-discovery-controller/src/index.ts @@ -4,7 +4,7 @@ export type { TokenSearchResponseItem, TokenTrendingResponseItem, } from './types'; -export type { TokenSearchParams } from './types'; +export type { TokenSearchParams, TrendingTokensParams } from './types'; export { AbstractTokenSearchApiService } from './token-search-api-service/abstract-token-search-api-service'; export { TokenSearchApiService } from './token-search-api-service/token-search-api-service'; diff --git a/packages/token-search-discovery-controller/src/token-discovery-api-service/token-discovery-api-service.ts b/packages/token-search-discovery-controller/src/token-discovery-api-service/token-discovery-api-service.ts index f322c42c153..c493dd6d80f 100644 --- a/packages/token-search-discovery-controller/src/token-discovery-api-service/token-discovery-api-service.ts +++ b/packages/token-search-discovery-controller/src/token-discovery-api-service/token-discovery-api-service.ts @@ -1,5 +1,5 @@ import { AbstractTokenDiscoveryApiService } from './abstract-token-discovery-api-service'; -import type { TokenTrendingResponseItem } from '../types'; +import type { TokenTrendingResponseItem, TrendingTokensParams } from '../types'; export class TokenDiscoveryApiService extends AbstractTokenDiscoveryApiService { readonly #baseUrl: string; @@ -12,17 +12,16 @@ export class TokenDiscoveryApiService extends AbstractTokenDiscoveryApiService { this.#baseUrl = baseUrl; } - async getTrendingTokensByChains(params: { - chains?: string[]; - limit?: string; - }): Promise { + async getTrendingTokensByChains( + trendingTokensParams: TrendingTokensParams, + ): Promise { const url = new URL('/tokens-search/trending-by-chains', this.#baseUrl); - if (params.chains && params.chains.length > 0) { - url.searchParams.append('chains', params.chains.join()); + if (trendingTokensParams.chains && trendingTokensParams.chains.length > 0) { + url.searchParams.append('chains', trendingTokensParams.chains.join()); } - if (params.limit) { - url.searchParams.append('limit', params.limit); + if (trendingTokensParams.limit) { + url.searchParams.append('limit', trendingTokensParams.limit); } const response = await fetch(url, { diff --git a/packages/token-search-discovery-controller/src/types.ts b/packages/token-search-discovery-controller/src/types.ts index 01f4a4d99aa..5660d4ba566 100644 --- a/packages/token-search-discovery-controller/src/types.ts +++ b/packages/token-search-discovery-controller/src/types.ts @@ -66,3 +66,8 @@ export type TokenTrendingResponseItem = { '1M': number | null; }; }; + +export interface TrendingTokensParams { + chains?: string[]; + limit?: string; +} From 597a3a79620b964bdc3bd0251d9da32b680d7190 Mon Sep 17 00:00:00 2001 From: Bigshmow Date: Thu, 30 Jan 2025 09:47:48 -0700 Subject: [PATCH 7/9] chore: linter prefers type to interface --- packages/token-search-discovery-controller/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/token-search-discovery-controller/src/types.ts b/packages/token-search-discovery-controller/src/types.ts index 5660d4ba566..9933d085f8f 100644 --- a/packages/token-search-discovery-controller/src/types.ts +++ b/packages/token-search-discovery-controller/src/types.ts @@ -67,7 +67,7 @@ export type TokenTrendingResponseItem = { }; }; -export interface TrendingTokensParams { +export type TrendingTokensParams { chains?: string[]; limit?: string; } From 40c37e2e91579952a31aa20be65ee8234d8ea79a Mon Sep 17 00:00:00 2001 From: Bigshmow Date: Thu, 30 Jan 2025 09:48:40 -0700 Subject: [PATCH 8/9] chore: bad --- packages/token-search-discovery-controller/src/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/token-search-discovery-controller/src/types.ts b/packages/token-search-discovery-controller/src/types.ts index 9933d085f8f..ff8757951ff 100644 --- a/packages/token-search-discovery-controller/src/types.ts +++ b/packages/token-search-discovery-controller/src/types.ts @@ -67,7 +67,7 @@ export type TokenTrendingResponseItem = { }; }; -export type TrendingTokensParams { +export type TrendingTokensParams = { chains?: string[]; limit?: string; -} +}; From 116cd75812cfdba1cf81ee0e7386aed74a416e9e Mon Sep 17 00:00:00 2001 From: Bigshmow Date: Thu, 30 Jan 2025 12:57:47 -0700 Subject: [PATCH 9/9] chore: use declared type and export types in one block --- packages/token-search-discovery-controller/src/index.ts | 3 ++- .../src/token-search-discovery-controller.ts | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/token-search-discovery-controller/src/index.ts b/packages/token-search-discovery-controller/src/index.ts index fa2bcb101ad..682a8f06231 100644 --- a/packages/token-search-discovery-controller/src/index.ts +++ b/packages/token-search-discovery-controller/src/index.ts @@ -3,8 +3,9 @@ export type { TokenSearchDiscoveryControllerState } from './token-search-discove export type { TokenSearchResponseItem, TokenTrendingResponseItem, + TokenSearchParams, + TrendingTokensParams, } from './types'; -export type { TokenSearchParams, TrendingTokensParams } from './types'; export { AbstractTokenSearchApiService } from './token-search-api-service/abstract-token-search-api-service'; export { TokenSearchApiService } from './token-search-api-service/token-search-api-service'; diff --git a/packages/token-search-discovery-controller/src/token-search-discovery-controller.ts b/packages/token-search-discovery-controller/src/token-search-discovery-controller.ts index 2ea72c5c5c2..725607b12f9 100644 --- a/packages/token-search-discovery-controller/src/token-search-discovery-controller.ts +++ b/packages/token-search-discovery-controller/src/token-search-discovery-controller.ts @@ -11,6 +11,7 @@ import type { TokenSearchParams, TokenSearchResponseItem, TokenTrendingResponseItem, + TrendingTokensParams, } from './types'; // === GENERAL === @@ -152,10 +153,9 @@ export class TokenSearchDiscoveryController extends BaseController< return results; } - async getTrendingTokens(params: { - chains?: string[]; - limit?: string; - }): Promise { + async getTrendingTokens( + params: TrendingTokensParams, + ): Promise { return this.#tokenDiscoveryService.getTrendingTokensByChains(params); } }