Skip to content

Commit

Permalink
feat: add token info api (#77)
Browse files Browse the repository at this point in the history
# What ❔

Add APIs to get token info.

## Why ❔

To be able to get token info including token usd price.
  • Loading branch information
Romsters authored Nov 2, 2023
1 parent daea529 commit 09ccc29
Show file tree
Hide file tree
Showing 16 changed files with 690 additions and 9 deletions.
14 changes: 14 additions & 0 deletions packages/api/src/api/api.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,18 @@ describe("ApiController", () => {
expect(result).toBe(null);
});
});

describe("tokenInfo", () => {
it("returns null as it is defined only to appear in docs and cannot be called", async () => {
const result = await controller.tokenInfo();
expect(result).toBe(null);
});
});

describe("ethPrice", () => {
it("returns null as it is defined only to appear in docs and cannot be called", async () => {
const result = await controller.ethPrice();
expect(result).toBe(null);
});
});
});
32 changes: 32 additions & 0 deletions packages/api/src/api/api.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import { ParseModulePipe } from "./pipes/parseModule.pipe";
import { ParseActionPipe } from "./pipes/parseAction.pipe";
import { ApiExceptionFilter } from "./exceptionFilter";
import { LogsResponseDto, LogApiDto } from "./dtos/log/logs.dto";
import { TokenInfoResponseDto, TokenInfoDto } from "./dtos/token/tokenInfo.dto";
import { EthPriceResponseDto, EthPriceDto } from "./dtos/stats/ethPrice.dto";
import { constants } from "../config/docs";

@Controller("")
Expand Down Expand Up @@ -604,4 +606,34 @@ export class ApiController {
): Promise<LogsResponseDto> {
return null;
}

@ApiTags("Token API")
@Get("api?module=token&action=tokeninfo")
@ApiOperation({ summary: "Returns token information" })
@ApiQuery({
name: "contractaddress",
description: "The contract address of the ERC-20/ERC-721 token to retrieve token info",
example: constants.tokenAddress,
required: true,
})
@ApiExtraModels(TokenInfoDto)
@ApiOkResponse({
description: "Token information",
type: TokenInfoResponseDto,
})
public async tokenInfo(): Promise<TokenInfoResponseDto> {
return null;
}

@ApiTags("Stats API")
@Get("api?module=stats&action=ethprice")
@ApiOperation({ summary: "Returns price of 1 ETH" })
@ApiExtraModels(EthPriceDto)
@ApiOkResponse({
description: "ETH price",
type: EthPriceResponseDto,
})
public async ethPrice(): Promise<EthPriceResponseDto> {
return null;
}
}
26 changes: 26 additions & 0 deletions packages/api/src/api/dtos/stats/ethPrice.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ApiProperty } from "@nestjs/swagger";
import { ResponseBaseDto } from "../common/responseBase.dto";

export class EthPriceDto {
@ApiProperty({
type: String,
description: "ETH price in USD",
example: "1823.567",
})
public readonly ethusd: string;

@ApiProperty({
type: String,
description: "ETH price timestamp",
example: "1624961308",
})
public readonly ethusd_timestamp: string;
}

export class EthPriceResponseDto extends ResponseBaseDto {
@ApiProperty({
description: "ETH price",
type: EthPriceDto,
})
public readonly result: EthPriceDto;
}
69 changes: 69 additions & 0 deletions packages/api/src/api/dtos/token/tokenInfo.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { ApiProperty } from "@nestjs/swagger";
import { ResponseBaseDto } from "../common/responseBase.dto";

export class TokenInfoDto {
@ApiProperty({
type: String,
description: "Token contract address",
example: "0x000000000000000000000000000000000000800A",
})
public readonly contractAddress: string;

@ApiProperty({
type: String,
description: "Token name",
example: "Ether",
})
public readonly tokenName: string;

@ApiProperty({
type: String,
description: "Token symbol",
example: "ETH",
})
public readonly symbol: string;

@ApiProperty({
type: String,
description: "Token decimals",
example: "18",
})
public readonly tokenDecimal: string;

@ApiProperty({
type: String,
description: "Token price in USD",
example: "1823.567",
})
public readonly tokenPriceUSD: string;

@ApiProperty({
type: String,
description: "Token liquidity in USD",
example: "220000000000",
})
public readonly liquidity: string;

@ApiProperty({
type: String,
description: "Token L1 address",
example: "0x0000000000000000000000000000000000000000",
})
public readonly l1Address: string;

@ApiProperty({
type: String,
description: "Token icon URL",
example: "https://assets.coingecko.com/coins/images/279/large/ethereum.png?1698873266",
})
public readonly iconURL: string;
}

export class TokenInfoResponseDto extends ResponseBaseDto {
@ApiProperty({
description: "Token info",
type: TokenInfoDto,
isArray: true,
})
public readonly result: TokenInfoDto[];
}
72 changes: 72 additions & 0 deletions packages/api/src/api/stats/stats.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Test } from "@nestjs/testing";
import { mock } from "jest-mock-extended";
import { Logger } from "@nestjs/common";
import { TokenService } from "../../token/token.service";
import { Token, ETH_TOKEN } from "../../token/token.entity";
import { StatsController } from "./stats.controller";

describe("StatsController", () => {
let controller: StatsController;
let tokenServiceMock: TokenService;

beforeEach(async () => {
tokenServiceMock = mock<TokenService>({
findOne: jest.fn().mockResolvedValue(null),
});

const module = await Test.createTestingModule({
controllers: [StatsController],
providers: [
{
provide: TokenService,
useValue: tokenServiceMock,
},
],
}).compile();
module.useLogger(mock<Logger>());

controller = module.get<StatsController>(StatsController);
});

describe("ethPrice", () => {
it("returns ok response and ETH price when ETH token is found", async () => {
jest.spyOn(tokenServiceMock, "findOne").mockResolvedValueOnce({
usdPrice: ETH_TOKEN.usdPrice,
offChainDataUpdatedAt: new Date("2023-03-03"),
} as Token);

const response = await controller.ethPrice();
expect(response).toEqual({
status: "1",
message: "OK",
result: {
ethusd: ETH_TOKEN.usdPrice.toString(),
ethusd_timestamp: Math.floor(new Date("2023-03-03").getTime() / 1000).toString(),
},
});
});

it("returns ok response and ETH price with default values when ETH token doesn't have price details", async () => {
jest.spyOn(tokenServiceMock, "findOne").mockResolvedValueOnce({} as Token);

const response = await controller.ethPrice();
expect(response).toEqual({
status: "1",
message: "OK",
result: {
ethusd: "",
ethusd_timestamp: "",
},
});
});

it("returns not ok response and no ETH price info when ETH token is not found", async () => {
const response = await controller.ethPrice();
expect(response).toEqual({
status: "0",
message: "No data found",
result: null,
});
});
});
});
35 changes: 35 additions & 0 deletions packages/api/src/api/stats/stats.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Controller, Get, UseFilters } from "@nestjs/common";
import { ApiTags, ApiExcludeController } from "@nestjs/swagger";
import { ResponseStatus, ResponseMessage } from "../dtos/common/responseBase.dto";
import { ApiExceptionFilter } from "../exceptionFilter";
import { EthPriceResponseDto } from "../dtos/stats/ethPrice.dto";
import { TokenService } from "../../token/token.service";
import { ETH_TOKEN } from "../../token/token.entity";
import { dateToTimestamp } from "../../common/utils";

const entityName = "stats";

@ApiExcludeController()
@ApiTags(entityName)
@Controller(`api/${entityName}`)
@UseFilters(ApiExceptionFilter)
export class StatsController {
constructor(private readonly tokenService: TokenService) {}

@Get("/ethprice")
public async ethPrice(): Promise<EthPriceResponseDto> {
const token = await this.tokenService.findOne(ETH_TOKEN.l2Address, { usdPrice: true, offChainDataUpdatedAt: true });
return {
status: token ? ResponseStatus.OK : ResponseStatus.NOTOK,
message: token ? ResponseMessage.OK : ResponseMessage.NO_DATA_FOUND,
result: token
? {
ethusd: token.usdPrice?.toString() || "",
ethusd_timestamp: token.offChainDataUpdatedAt
? dateToTimestamp(token.offChainDataUpdatedAt).toString()
: "",
}
: null,
};
}
}
9 changes: 9 additions & 0 deletions packages/api/src/api/stats/stats.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from "@nestjs/common";
import { StatsController } from "./stats.controller";
import { TokenModule } from "../../token/token.module";

@Module({
imports: [TokenModule],
controllers: [StatsController],
})
export class ApiStatsModule {}
91 changes: 91 additions & 0 deletions packages/api/src/api/token/token.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Test } from "@nestjs/testing";
import { mock } from "jest-mock-extended";
import { Logger } from "@nestjs/common";
import { TokenService } from "../../token/token.service";
import { Token, ETH_TOKEN } from "../../token/token.entity";
import { TokenController } from "./token.controller";

describe("TokenController", () => {
let controller: TokenController;
let tokenServiceMock: TokenService;

const contractAddress = "address";

beforeEach(async () => {
tokenServiceMock = mock<TokenService>({
findOne: jest.fn().mockResolvedValue(null),
});

const module = await Test.createTestingModule({
controllers: [TokenController],
providers: [
{
provide: TokenService,
useValue: tokenServiceMock,
},
],
}).compile();
module.useLogger(mock<Logger>());

controller = module.get<TokenController>(TokenController);
});

describe("tokenInfo", () => {
it("returns ok response and token info when token is found", async () => {
jest.spyOn(tokenServiceMock, "findOne").mockResolvedValueOnce(ETH_TOKEN);

const response = await controller.tokenInfo(contractAddress);
expect(response).toEqual({
status: "1",
message: "OK",
result: [
{
contractAddress: ETH_TOKEN.l2Address,
iconURL: ETH_TOKEN.iconURL,
l1Address: ETH_TOKEN.l1Address,
liquidity: ETH_TOKEN.liquidity.toString(),
symbol: ETH_TOKEN.symbol,
tokenDecimal: ETH_TOKEN.decimals.toString(),
tokenName: ETH_TOKEN.name,
tokenPriceUSD: ETH_TOKEN.usdPrice.toString(),
},
],
});
});

it("returns ok response and token info with default values when token doesn't have all details", async () => {
jest.spyOn(tokenServiceMock, "findOne").mockResolvedValueOnce({
l2Address: "0x000000000000000000000000000000000000800A",
symbol: "",
decimals: 6,
} as Token);

const response = await controller.tokenInfo(contractAddress);
expect(response).toEqual({
status: "1",
message: "OK",
result: [
{
contractAddress: "0x000000000000000000000000000000000000800A",
iconURL: "",
l1Address: "",
liquidity: "",
symbol: "",
tokenDecimal: "6",
tokenName: "",
tokenPriceUSD: "",
},
],
});
});

it("returns not ok response and no token info when token is not found", async () => {
const response = await controller.tokenInfo(contractAddress);
expect(response).toEqual({
status: "0",
message: "No data found",
result: [],
});
});
});
});
Loading

0 comments on commit 09ccc29

Please sign in to comment.