Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: MMPD-1522 add fetch trending token data #5214

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions packages/token-search-discovery-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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/[email protected]
Expand Down
1 change: 1 addition & 0 deletions packages/token-search-discovery-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 9 additions & 1 deletion packages/token-search-discovery-controller/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
export { TokenSearchDiscoveryController } from './token-search-discovery-controller';
export type { TokenSearchDiscoveryControllerState } from './token-search-discovery-controller';
export type { TokenSearchResponseItem } from './types';
export type {
TokenSearchResponseItem,
TokenTrendingResponseItem,
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';

export { AbstractTokenDiscoveryApiService } from './token-discovery-api-service/abstract-token-discovery-api-service';
export { TokenDiscoveryApiService } from './token-discovery-api-service/token-discovery-api-service';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const TEST_API_URLS = {
BASE_URL: 'https://mock-api.test',
PORTFOLIO_API: 'https://mock-portfolio-api.test',
} as const;
Original file line number Diff line number Diff line change
@@ -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<TokenTrendingResponseItem[]>;
}
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { AbstractTokenDiscoveryApiService } from './abstract-token-discovery-api-service';
import type { TokenTrendingResponseItem, TrendingTokensParams } 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(
trendingTokensParams: TrendingTokensParams,
): Promise<TokenTrendingResponseItem[]> {
const url = new URL('/tokens-search/trending-by-chains', this.#baseUrl);

if (trendingTokensParams.chains && trendingTokensParams.chains.length > 0) {
url.searchParams.append('chains', trendingTokensParams.chains.join());
}
if (trendingTokensParams.limit) {
url.searchParams.append('limit', trendingTokensParams.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();
}
}
Loading
Loading