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

Add top gainers losers to token discovery service #5309

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
11 changes: 11 additions & 0 deletions packages/token-search-discovery-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- All param types (e.g. `TokenSearchParams`, `TrendingTokensParams`, etc.) inherit from `ParamsBase`
- Add eponymous methods to the `TokenDiscoveryApiService`
- Add `getTopGainersByChains`
- Add `getTopLosersByChains`

### Changed

- **BREAKING:** Renamed `TokenTrendingResponseItem` name to `MoralisTokenResponseItem`
Bigshmow marked this conversation as resolved.
Show resolved Hide resolved

## [2.1.0]

### Added
Expand Down
4 changes: 3 additions & 1 deletion packages/token-search-discovery-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ export type {
} from './token-search-discovery-controller';
export type {
TokenSearchResponseItem,
TokenTrendingResponseItem,
MoralisTokenResponseItem,
TokenSearchParams,
TrendingTokensParams,
TopGainersParams,
TopLosersParams,
} from './types';

export { AbstractTokenSearchApiService } from './token-search-api-service/abstract-token-search-api-service';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { TokenTrendingResponseItem } from '../types';
import type {
MoralisTokenResponseItem,
TrendingTokensParams,
TopLosersParams,
TopGainersParams,
} from '../types';

/**
* Abstract class for fetching token discovery results.
Expand All @@ -10,8 +15,15 @@ export abstract class AbstractTokenDiscoveryApiService {
* @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[]>;
abstract getTrendingTokensByChains(
params?: TrendingTokensParams,
): Promise<MoralisTokenResponseItem[]>;

abstract getTopLosersByChains(
params?: TopLosersParams,
): Promise<MoralisTokenResponseItem[]>;

abstract getTopGainersByChains(
params?: TopGainersParams,
): Promise<MoralisTokenResponseItem[]>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import nock, { cleanAll } from 'nock';

import { TokenDiscoveryApiService } from './token-discovery-api-service';
import { TEST_API_URLS } from '../test/constants';
import type { TokenTrendingResponseItem } from '../types';
import type { MoralisTokenResponseItem } from '../types';

describe('TokenDiscoveryApiService', () => {
let service: TokenDiscoveryApiService;
const mockTrendingResponse: TokenTrendingResponseItem[] = [
const mockTrendingResponse: MoralisTokenResponseItem[] = [
{
chain_id: '1',
token_address: '0x123',
Expand Down Expand Up @@ -124,4 +124,26 @@ describe('TokenDiscoveryApiService', () => {
expect(results).toStrictEqual(mockTrendingResponse);
});
});

describe('getTopGainersByChains', () => {
it('should return top gainers results', async () => {
nock(TEST_API_URLS.PORTFOLIO_API)
.get('/tokens-search/top-gainers-by-chains')
.reply(200, mockTrendingResponse);

const results = await service.getTopGainersByChains({});
Bigshmow marked this conversation as resolved.
Show resolved Hide resolved
expect(results).toStrictEqual(mockTrendingResponse);
});
});

describe('getTopLosersByChains', () => {
it('should return top losers results', async () => {
nock(TEST_API_URLS.PORTFOLIO_API)
.get('/tokens-search/top-losers-by-chains')
.reply(200, mockTrendingResponse);

const results = await service.getTopLosersByChains({});
expect(results).toStrictEqual(mockTrendingResponse);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { AbstractTokenDiscoveryApiService } from './abstract-token-discovery-api-service';
import type { TokenTrendingResponseItem, TrendingTokensParams } from '../types';
import type {
MoralisTokenResponseItem,
TopGainersParams,
TopLosersParams,
TrendingTokensParams,
} from '../types';

export class TokenDiscoveryApiService extends AbstractTokenDiscoveryApiService {
readonly #baseUrl: string;
Expand All @@ -13,14 +18,17 @@ export class TokenDiscoveryApiService extends AbstractTokenDiscoveryApiService {
}

async getTrendingTokensByChains(
trendingTokensParams: TrendingTokensParams,
): Promise<TokenTrendingResponseItem[]> {
trendingTokensParams?: TrendingTokensParams,
): Promise<MoralisTokenResponseItem[]> {
const url = new URL('/tokens-search/trending-by-chains', this.#baseUrl);

if (trendingTokensParams.chains && trendingTokensParams.chains.length > 0) {
if (
trendingTokensParams?.chains &&
trendingTokensParams.chains.length > 0
) {
url.searchParams.append('chains', trendingTokensParams.chains.join());
}
if (trendingTokensParams.limit) {
if (trendingTokensParams?.limit) {
url.searchParams.append('limit', trendingTokensParams.limit);
}

Expand All @@ -39,4 +47,60 @@ export class TokenDiscoveryApiService extends AbstractTokenDiscoveryApiService {

return response.json();
}

async getTopLosersByChains(
topLosersParams?: TopLosersParams,
): Promise<MoralisTokenResponseItem[]> {
const url = new URL('/tokens-search/top-losers-by-chains', this.#baseUrl);

if (topLosersParams?.chains && topLosersParams.chains.length > 0) {
url.searchParams.append('chains', topLosersParams.chains.join());
}
if (topLosersParams?.limit) {
url.searchParams.append('limit', topLosersParams.limit);
}

const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
Bigshmow marked this conversation as resolved.
Show resolved Hide resolved

if (!response.ok) {
throw new Error(
`Portfolio API request failed with status: ${response.status}`,
Bigshmow marked this conversation as resolved.
Show resolved Hide resolved
);
}

return response.json();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to validate the response in any way or not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the validation but how do you feel about revisiting this in portfolio api first since it's closest to the server, and bad shapes would never even reach this point?
(this gives more credence to refactoring the error handling in the core repo you mention elsewhere)

}

async getTopGainersByChains(
topGainersParams?: TopGainersParams,
): Promise<MoralisTokenResponseItem[]> {
const url = new URL('/tokens-search/top-gainers-by-chains', this.#baseUrl);

if (topGainersParams?.chains && topGainersParams.chains.length > 0) {
url.searchParams.append('chains', topGainersParams.chains.join());
}
if (topGainersParams?.limit) {
url.searchParams.append('limit', topGainersParams.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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import type { TokenSearchDiscoveryControllerMessenger } from './token-search-discovery-controller';
import type {
TokenSearchResponseItem,
TokenTrendingResponseItem,
MoralisTokenResponseItem,
} from './types';

const controllerName = 'TokenSearchDiscoveryController';
Expand Down Expand Up @@ -42,7 +42,7 @@ describe('TokenSearchDiscoveryController', () => {
},
];

const mockTrendingResults: TokenTrendingResponseItem[] = [
const mockTrendingResults: MoralisTokenResponseItem[] = [
{
chain_id: '1',
token_address: '0x123',
Expand Down Expand Up @@ -102,7 +102,15 @@ describe('TokenSearchDiscoveryController', () => {
}

class MockTokenDiscoveryService extends AbstractTokenDiscoveryApiService {
async getTrendingTokensByChains(): Promise<TokenTrendingResponseItem[]> {
async getTrendingTokensByChains(): Promise<MoralisTokenResponseItem[]> {
return mockTrendingResults;
}

async getTopGainersByChains(): Promise<MoralisTokenResponseItem[]> {
return mockTrendingResults;
}

async getTopLosersByChains(): Promise<MoralisTokenResponseItem[]> {
return mockTrendingResults;
}
}
Expand Down Expand Up @@ -161,6 +169,20 @@ describe('TokenSearchDiscoveryController', () => {
});
});

describe('getTopGainers', () => {
it('should return top gainers results', async () => {
const results = await mainController.getTopGainers({});
expect(results).toStrictEqual(mockTrendingResults);
});
});

describe('getTopLosers', () => {
it('should return top losers results', async () => {
const results = await mainController.getTopLosers({});
expect(results).toStrictEqual(mockTrendingResults);
});
});

describe('error handling', () => {
class ErrorTokenSearchService extends AbstractTokenSearchApiService {
async searchTokens(): Promise<TokenSearchResponseItem[]> {
Expand All @@ -169,7 +191,15 @@ describe('TokenSearchDiscoveryController', () => {
}

class ErrorTokenDiscoveryService extends AbstractTokenDiscoveryApiService {
async getTrendingTokensByChains(): Promise<TokenTrendingResponseItem[]> {
async getTrendingTokensByChains(): Promise<MoralisTokenResponseItem[]> {
return [];
}

async getTopGainersByChains(): Promise<MoralisTokenResponseItem[]> {
return [];
}

async getTopLosersByChains(): Promise<MoralisTokenResponseItem[]> {
return [];
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import type { AbstractTokenSearchApiService } from './token-search-api-service/a
import type {
TokenSearchParams,
TokenSearchResponseItem,
TokenTrendingResponseItem,
MoralisTokenResponseItem,
TrendingTokensParams,
TopGainersParams,
TopLosersParams,
} from './types';

// === GENERAL ===
Expand Down Expand Up @@ -154,7 +156,19 @@ export class TokenSearchDiscoveryController extends BaseController<

async getTrendingTokens(
params: TrendingTokensParams,
): Promise<TokenTrendingResponseItem[]> {
): Promise<MoralisTokenResponseItem[]> {
return this.#tokenDiscoveryService.getTrendingTokensByChains(params);
}

async getTopGainers(
params: TopGainersParams,
): Promise<MoralisTokenResponseItem[]> {
return this.#tokenDiscoveryService.getTopGainersByChains(params);
}

async getTopLosers(
params: TopLosersParams,
): Promise<MoralisTokenResponseItem[]> {
return this.#tokenDiscoveryService.getTopLosersByChains(params);
}
}
24 changes: 16 additions & 8 deletions packages/token-search-discovery-controller/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
export type TokenSearchParams = {
// Function params

type ParamsBase = {
chains?: string[];
query?: string;
limit?: string;
};

export type TokenSearchParams = ParamsBase & {
query?: string;
};

export type TrendingTokensParams = ParamsBase;

export type TopLosersParams = ParamsBase;

export type TopGainersParams = ParamsBase;

// API response types

export type TokenSearchResponseItem = {
tokenAddress: string;
chainId: string;
Expand All @@ -16,7 +29,7 @@ export type TokenSearchResponseItem = {
logoUrl?: string;
};

export type TokenTrendingResponseItem = {
export type MoralisTokenResponseItem = {
chain_id: string;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: this seems unrelated to a vanilla Response object and looks to be only the tokenItem. Adding Response into the name confuses me a bit and could lead people to think this includes status and other properties of an HTTP Response

https://developer.mozilla.org/en-US/docs/Web/API/Response

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommend dropping the ResponseItem portion all together to simply be MoralisToken? I like that too.

token_address: string;
token_logo: string;
Expand Down Expand Up @@ -66,8 +79,3 @@ export type TokenTrendingResponseItem = {
'1M': number | null;
};
};

export type TrendingTokensParams = {
chains?: string[];
limit?: string;
};
Loading