Skip to content

Commit 8d9e877

Browse files
committed
rebase
1 parent 057530c commit 8d9e877

17 files changed

+715
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
## [2.1.0]
11+
12+
### Added
13+
14+
- Export `TokenSearchDiscoveryControllerMessenger` type ([#5296](https://github.com/MetaMask/core/pull/5296))
15+
16+
### Changed
17+
18+
- Bump `@metamask/base-controller` from `^7.1.1` to `^8.0.0` ([#5305](https://github.com/MetaMask/core/pull/5305))
19+
20+
## [2.0.0]
21+
22+
### Added
23+
24+
- Introduce the `logoUrl` property to the `TokenSearchApiService` response ([#5195](https://github.com/MetaMask/core/pull/5195))
25+
- Specifically in the `TokenSearchResponseItem` type
26+
- Introduce `TokenDiscoveryApiService` to keep discovery and search responsibilities separate ([#5214](https://github.com/MetaMask/core/pull/5214))
27+
- This service is responsible for fetching discover related data
28+
- Add `getTrendingTokens` method to fetch trending tokens by chain
29+
- Add `TokenTrendingResponseItem` type for trending token responses
30+
- Export `TokenSearchResponseItem` type from the package index ([#5214](https://github.com/MetaMask/core/pull/5214))
31+
32+
### Changed
33+
34+
- Bump @metamask/utils to v11.1.0 ([#5223](https://github.com/MetaMask/core/pull/5223))
35+
- Update the `TokenSearchApiService` to use the updated URL for `searchTokens` ([#5195](https://github.com/MetaMask/core/pull/5195))
36+
- The URL is now `/tokens-search` instead of `/tokens-search/name`
37+
- **BREAKING:** The `searchTokens` method now takes a `query` parameter instead of `name` ([#5195](https://github.com/MetaMask/core/pull/5195))
38+
39+
## [1.0.0]
40+
41+
### Added
42+
43+
- Introduce the TokenSearchDiscoveryController ([#5142](https://github.com/MetaMask/core/pull/5142/))
44+
- This controller manages token search and discovery through the Portfolio API
45+
- Introduce the TokenSearchApiService ([#5142](https://github.com/MetaMask/core/pull/5142/))
46+
- This service is responsible for making search related requests to the Portfolio API
47+
- Specifically, it handles the `tokens-search` endpoint which returns a list of tokens based on the provided query parameters
48+
49+
[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/[email protected]
50+
[2.1.0]: https://github.com/MetaMask/core/compare/@metamask/[email protected]...@metamask/[email protected]
51+
[2.0.0]: https://github.com/MetaMask/core/compare/@metamask/[email protected]...@metamask/[email protected]
52+
[1.0.0]: https://github.com/MetaMask/core/releases/tag/@metamask/[email protected]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
MIT License
2+
3+
Copyright (c) 2025 MetaMask
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# `@metamask/token-search-discovery-controller`
2+
3+
Manages token search and discovery through the Portfolio API.
4+
5+
## Installation
6+
7+
`yarn add @metamask/token-search-discovery-controller`
8+
9+
or
10+
11+
`npm install @metamask/token-search-discovery-controller`
12+
13+
## Contributing
14+
15+
This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* For a detailed explanation regarding each configuration property and type check, visit:
3+
* https://jestjs.io/docs/configuration
4+
*/
5+
6+
const merge = require('deepmerge');
7+
const path = require('path');
8+
9+
const baseConfig = require('../../jest.config.packages');
10+
11+
const displayName = path.basename(__dirname);
12+
13+
module.exports = merge(baseConfig, {
14+
// The display name when running multiple projects
15+
displayName,
16+
17+
// An object that configures minimum threshold enforcement for coverage results
18+
coverageThreshold: {
19+
global: {
20+
branches: 100,
21+
functions: 100,
22+
lines: 100,
23+
statements: 100,
24+
},
25+
},
26+
});

packages/token-search-discovery-controller/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
"@metamask/utils": "^11.1.0"
5252
},
5353
"devDependencies": {
54-
"@metamask/assets-controllers": "workspace:^",
5554
"@metamask/auto-changelog": "^3.4.4",
5655
"@types/jest": "^27.4.1",
5756
"deepmerge": "^4.2.2",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export { TokenSearchDiscoveryController } from './token-search-discovery-controller';
2+
export type {
3+
TokenSearchDiscoveryControllerMessenger,
4+
TokenSearchDiscoveryControllerState,
5+
} from './token-search-discovery-controller';
6+
export type {
7+
TokenSearchResponseItem,
8+
TokenTrendingResponseItem,
9+
TokenSearchParams,
10+
TrendingTokensParams,
11+
} from './types';
12+
13+
export { AbstractTokenSearchApiService } from './token-search-api-service/abstract-token-search-api-service';
14+
export { TokenSearchApiService } from './token-search-api-service/token-search-api-service';
15+
16+
export { AbstractTokenDiscoveryApiService } from './token-discovery-api-service/abstract-token-discovery-api-service';
17+
export { TokenDiscoveryApiService } from './token-discovery-api-service/token-discovery-api-service';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const TEST_API_URLS = {
2+
BASE_URL: 'https://mock-api.test',
3+
PORTFOLIO_API: 'https://mock-portfolio-api.test',
4+
} as const;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { TokenTrendingResponseItem } from '../types';
2+
3+
/**
4+
* Abstract class for fetching token discovery results.
5+
*/
6+
export abstract class AbstractTokenDiscoveryApiService {
7+
/**
8+
* Fetches trending tokens by chains from the portfolio API.
9+
*
10+
* @param params - Optional parameters including chains and limit
11+
* @returns A promise resolving to an array of {@link TokenTrendingResponseItem}
12+
*/
13+
abstract getTrendingTokensByChains(params: {
14+
chains?: string[];
15+
limit?: string;
16+
}): Promise<TokenTrendingResponseItem[]>;
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import nock, { cleanAll } from 'nock';
2+
3+
import { TokenDiscoveryApiService } from './token-discovery-api-service';
4+
import { TEST_API_URLS } from '../test/constants';
5+
import type { TokenTrendingResponseItem } from '../types';
6+
7+
describe('TokenDiscoveryApiService', () => {
8+
let service: TokenDiscoveryApiService;
9+
const mockTrendingResponse: TokenTrendingResponseItem[] = [
10+
{
11+
chain_id: '1',
12+
token_address: '0x123',
13+
token_logo: 'https://example.com/logo.png',
14+
token_name: 'Test Token',
15+
token_symbol: 'TEST',
16+
price_usd: 100,
17+
token_age_in_days: 365,
18+
on_chain_strength_index: 85,
19+
security_score: 90,
20+
market_cap: 1000000,
21+
fully_diluted_valuation: 2000000,
22+
twitter_followers: 50000,
23+
holders_change: {
24+
'1h': 10,
25+
'1d': 100,
26+
'1w': 1000,
27+
'1M': 10000,
28+
},
29+
liquidity_change_usd: {
30+
'1h': 1000,
31+
'1d': 10000,
32+
'1w': 100000,
33+
'1M': 1000000,
34+
},
35+
experienced_net_buyers_change: {
36+
'1h': 5,
37+
'1d': 50,
38+
'1w': 500,
39+
'1M': 5000,
40+
},
41+
volume_change_usd: {
42+
'1h': 10000,
43+
'1d': 100000,
44+
'1w': 1000000,
45+
'1M': 10000000,
46+
},
47+
net_volume_change_usd: {
48+
'1h': 5000,
49+
'1d': 50000,
50+
'1w': 500000,
51+
'1M': 5000000,
52+
},
53+
price_percent_change_usd: {
54+
'1h': 1,
55+
'1d': 10,
56+
'1w': 20,
57+
'1M': 30,
58+
},
59+
},
60+
];
61+
62+
beforeEach(() => {
63+
service = new TokenDiscoveryApiService(TEST_API_URLS.PORTFOLIO_API);
64+
});
65+
66+
afterEach(() => {
67+
cleanAll();
68+
});
69+
70+
describe('constructor', () => {
71+
it('should throw if baseUrl is empty', () => {
72+
expect(() => new TokenDiscoveryApiService('')).toThrow(
73+
'Portfolio API URL is not set',
74+
);
75+
});
76+
});
77+
78+
describe('getTrendingTokensByChains', () => {
79+
it.each([
80+
{
81+
params: { chains: ['1'], limit: '5' },
82+
expectedPath: '/tokens-search/trending-by-chains?chains=1&limit=5',
83+
},
84+
{
85+
params: { chains: ['1', '137'] },
86+
expectedPath: '/tokens-search/trending-by-chains?chains=1,137',
87+
},
88+
{
89+
params: { limit: '10' },
90+
expectedPath: '/tokens-search/trending-by-chains?limit=10',
91+
},
92+
{
93+
params: {},
94+
expectedPath: '/tokens-search/trending-by-chains',
95+
},
96+
])(
97+
'should construct correct URL for params: $params',
98+
async ({ params, expectedPath }) => {
99+
nock(TEST_API_URLS.PORTFOLIO_API)
100+
.get(expectedPath)
101+
.reply(200, mockTrendingResponse);
102+
103+
const result = await service.getTrendingTokensByChains(params);
104+
expect(result).toStrictEqual(mockTrendingResponse);
105+
},
106+
);
107+
108+
it('should handle API errors', async () => {
109+
nock(TEST_API_URLS.PORTFOLIO_API)
110+
.get('/tokens-search/trending-by-chains')
111+
.reply(500, 'Server Error');
112+
113+
await expect(service.getTrendingTokensByChains({})).rejects.toThrow(
114+
'Portfolio API request failed with status: 500',
115+
);
116+
});
117+
118+
it('should return trending results', async () => {
119+
nock(TEST_API_URLS.PORTFOLIO_API)
120+
.get('/tokens-search/trending-by-chains')
121+
.reply(200, mockTrendingResponse);
122+
123+
const results = await service.getTrendingTokensByChains({});
124+
expect(results).toStrictEqual(mockTrendingResponse);
125+
});
126+
});
127+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { AbstractTokenDiscoveryApiService } from './abstract-token-discovery-api-service';
2+
import type { TokenTrendingResponseItem, TrendingTokensParams } from '../types';
3+
4+
export class TokenDiscoveryApiService extends AbstractTokenDiscoveryApiService {
5+
readonly #baseUrl: string;
6+
7+
constructor(baseUrl: string) {
8+
super();
9+
if (!baseUrl) {
10+
throw new Error('Portfolio API URL is not set');
11+
}
12+
this.#baseUrl = baseUrl;
13+
}
14+
15+
async getTrendingTokensByChains(
16+
trendingTokensParams: TrendingTokensParams,
17+
): Promise<TokenTrendingResponseItem[]> {
18+
const url = new URL('/tokens-search/trending-by-chains', this.#baseUrl);
19+
20+
if (trendingTokensParams.chains && trendingTokensParams.chains.length > 0) {
21+
url.searchParams.append('chains', trendingTokensParams.chains.join());
22+
}
23+
if (trendingTokensParams.limit) {
24+
url.searchParams.append('limit', trendingTokensParams.limit);
25+
}
26+
27+
const response = await fetch(url, {
28+
method: 'GET',
29+
headers: {
30+
'Content-Type': 'application/json',
31+
},
32+
});
33+
34+
if (!response.ok) {
35+
throw new Error(
36+
`Portfolio API request failed with status: ${response.status}`,
37+
);
38+
}
39+
40+
return response.json();
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { TokenSearchParams, TokenSearchResponseItem } from '../types';
2+
3+
/**
4+
* Abstract class for fetching token search results.
5+
*/
6+
export abstract class AbstractTokenSearchApiService {
7+
/**
8+
* Fetches token search results from the portfolio API.
9+
*
10+
* @param tokenSearchParams - Optional search parameters including chains, name, and limit {@link TokenSearchParams}
11+
* @returns A promise resolving to an array of {@link TokenSearchResponseItem}
12+
*/
13+
abstract searchTokens(
14+
tokenSearchParams?: TokenSearchParams,
15+
): Promise<TokenSearchResponseItem[]>;
16+
}

0 commit comments

Comments
 (0)