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: add token info api #77

Merged
merged 8 commits into from
Nov 2, 2023
Merged
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
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
Loading