Skip to content

Commit

Permalink
Merge branch 'main' into integration-tests-ether-migration
Browse files Browse the repository at this point in the history
  • Loading branch information
pcheremu authored Aug 23, 2024
2 parents 0c71d0d + f58cb8f commit a49b121
Show file tree
Hide file tree
Showing 83 changed files with 800 additions and 312 deletions.
16 changes: 16 additions & 0 deletions packages/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,19 @@ DISABLE_EXTERNAL_API=false
DATABASE_STATEMENT_TIMEOUT_MS=90000
CONTRACT_VERIFICATION_API_URL=http://127.0.0.1:3070
NETWORK_NAME=testnet-sepolia

BASE_TOKEN_SYMBOL=ETH
BASE_TOKEN_DECIMALS=18
BASE_TOKEN_L1_ADDRESS=0x0000000000000000000000000000000000000000
BASE_TOKEN_ICON_URL=https://assets.coingecko.com/coins/images/279/large/ethereum.png?1698873266
BASE_TOKEN_NAME=Ether
BASE_TOKEN_LIQUIDITY=220000000000
BASE_TOKEN_USDPRICE=1800

ETH_TOKEN_SYMBOL=ETH
ETH_TOKEN_DECIMALS=18
ETH_TOKEN_L2_ADDRESS=0x000000000000000000000000000000000000800A
ETH_TOKEN_ICON_URL=https://assets.coingecko.com/coins/images/279/large/ethereum.png?1698873266
ETH_TOKEN_NAME=Ether
ETH_TOKEN_LIQUIDITY=220000000000
ETH_TOKEN_USDPRICE=1800
8 changes: 8 additions & 0 deletions packages/api/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,11 @@ PORT=3007
LIMITED_PAGINATION_MAX_ITEMS=15
API_LIMITED_PAGINATION_MAX_ITEMS=15
CONTRACT_VERIFICATION_API_URL=http://verification.api
BASE_TOKEN_SYMBOL=ETH
BASE_TOKEN_DECIMALS=18
BASE_TOKEN_L1_ADDRESS=0x0000000000000000000000000000000000000000
BASE_TOKEN_ICON_URL=https://assets.coingecko.com/coins/images/279/large/ethereum.png?1698873266
BASE_TOKEN_NAME=Ether
BASE_TOKEN_LIQUIDITY=220000000000
BASE_TOKEN_USDPRICE=1800
ETH_TOKEN_L2_ADDRESS=0x000000000000000000000000000000000000800A
55 changes: 55 additions & 0 deletions packages/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,61 @@ You need to have a running Worker database, for instructions on how to run the w
- `DATABASE_CONNECTION_POOL_SIZE`
- Set `CONTRACT_VERIFICATION_API_URL` to your verification API URL. For zkSync Era testnet use `https://zksync2-testnet-explorer.zksync.dev`. For zkSync Era mainnet - `https://zksync2-mainnet-explorer.zksync.io`.

## Custom base token configuration
For networks with a custom base token, there are a number of environment variables used to configure custom base and ETH tokens:
- `BASE_TOKEN_L1_ADDRESS` - required, example: `0xB44A106F271944fEc1c27cd60b8D6C8792df86d8`. Base token L1 address can be fetched using the RPC call:
```
curl http://localhost:3050 \
-X POST \
-H "Content-Type: application/json" \
--data '{"method":"zks_getBaseTokenL1Address","params":[],"id":1,"jsonrpc":"2.0"}'
```
or SDK:
```
import { Provider } from "zksync-ethers";
async function main() {
const l2provider = new Provider("http://localhost:3050");
const baseTokenAddress = await l2provider.getBaseTokenContractAddress();
console.log('baseTokenAddress', baseTokenAddress);
}
main()
.then()
.catch((error) => {
console.error(error);
process.exitCode = 1;
});
```
- `BASE_TOKEN_SYMBOL` - required, example: `ZK`
- `BASE_TOKEN_NAME` - required, example: `ZK`
- `BASE_TOKEN_DECIMALS` - required, example: `18`
- `BASE_TOKEN_LIQUIDITY` - optional, example: `20000`
- `BASE_TOKEN_ICON_URL` - optional, example: `https://assets.coingecko.com/coins/images/279/large/ethereum.png?1698873266`
- `BASE_TOKEN_USDPRICE` - optional, example: `3300.30`.

- `ETH_TOKEN_L2_ADDRESS` - required, example: `0x642C0689b87dEa060B9f0E2e715DaB8564840861`. Eth L2 address can be calculated using SDK:
```
import { utils, Provider } from "zksync-ethers";
async function main() {
const l2provider = new Provider("http://localhost:3050");
const ethL2Address = await l2provider.l2TokenAddress(utils.ETH_ADDRESS);
console.log('ethL2Address', ethL2Address);
}
main()
.then()
.catch((error) => {
console.error(error);
process.exitCode = 1;
});
```
- `ETH_TOKEN_NAME` - optional, default is `Ether`
- `ETH_TOKEN_SYMBOL` - optional, default is `ETH`
- `ETH_TOKEN_DECIMALS` - optional, default is `18`
- `ETH_TOKEN_LIQUIDITY` - optional, example: `20000`
- `ETH_TOKEN_ICON_URL` - optional, default (ETH icon) is: `https://assets.coingecko.com/coins/images/279/large/ethereum.png?1698873266`
- `ETH_TOKEN_USDPRICE` - optional, example: `3300.30`.

## Running the app

```bash
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/api/account/account.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Test } from "@nestjs/testing";
import { mock } from "jest-mock-extended";
import { BadRequestException, Logger } from "@nestjs/common";
import { L2_ETH_TOKEN_ADDRESS } from "../../common/constants";
import { BASE_TOKEN_L2_ADDRESS } from "../../common/constants";
import { BlockService } from "../../block/block.service";
import { BlockDetails } from "../../block/blockDetails.entity";
import { TransactionService } from "../../transaction/transaction.service";
Expand Down Expand Up @@ -557,7 +557,7 @@ describe("AccountController", () => {
describe("getAccountEtherBalance", () => {
it("calls balanceService.getBalance and returns account ether balance", async () => {
const response = await controller.getAccountEtherBalance(address);
expect(balanceServiceMock.getBalance).toBeCalledWith(address, L2_ETH_TOKEN_ADDRESS);
expect(balanceServiceMock.getBalance).toBeCalledWith(address, BASE_TOKEN_L2_ADDRESS);
expect(response).toEqual({
status: ResponseStatus.OK,
message: ResponseMessage.OK,
Expand Down Expand Up @@ -588,7 +588,7 @@ describe("AccountController", () => {

it("calls balanceService.getBalancesByAddresses and returns accounts ether balances", async () => {
const response = await controller.getAccountsEtherBalances([address, "address2"]);
expect(balanceServiceMock.getBalancesByAddresses).toBeCalledWith([address, "address2"], L2_ETH_TOKEN_ADDRESS);
expect(balanceServiceMock.getBalancesByAddresses).toBeCalledWith([address, "address2"], BASE_TOKEN_L2_ADDRESS);
expect(response).toEqual({
status: ResponseStatus.OK,
message: ResponseMessage.OK,
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/api/account/account.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Controller, Get, Query, Logger, UseFilters, ParseArrayPipe, BadRequestException } from "@nestjs/common";
import { ApiTags, ApiExcludeController } from "@nestjs/swagger";
import { L2_ETH_TOKEN_ADDRESS } from "../../common/constants";
import { BASE_TOKEN_L2_ADDRESS } from "../../common/constants";
import { TokenType } from "../../token/token.entity";
import { dateToTimestamp } from "../../common/utils";
import { BlockService } from "../../block/block.service";
Expand Down Expand Up @@ -176,7 +176,7 @@ export class AccountController {
public async getAccountEtherBalance(
@Query("address", new ParseAddressPipe()) address: string
): Promise<AccountEtherBalanceResponseDto> {
const balance = await this.balanceService.getBalance(address, L2_ETH_TOKEN_ADDRESS);
const balance = await this.balanceService.getBalance(address, BASE_TOKEN_L2_ADDRESS);
return {
status: ResponseStatus.OK,
message: ResponseMessage.OK,
Expand All @@ -201,7 +201,7 @@ export class AccountController {
if (uniqueAddresses.length > 20) {
throw new BadRequestException("Maximum 20 addresses per request");
}
const balances = await this.balanceService.getBalancesByAddresses(addresses, L2_ETH_TOKEN_ADDRESS);
const balances = await this.balanceService.getBalancesByAddresses(addresses, BASE_TOKEN_L2_ADDRESS);
const result = addresses.map((address) => ({
account: address,
balance: balances.find((balance) => balance.address.toLowerCase() === address.toLowerCase())?.balance || "0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Transfer } from "../../transfer/transfer.entity";
import { TransactionStatus } from "../../transaction/entities/transaction.entity";
import { L2_ETH_TOKEN_ADDRESS } from "../../common/constants";
import { BASE_TOKEN_L2_ADDRESS } from "../../common/constants";
import { mapInternalTransactionListItem } from "./internalTransactionMapper";

describe("internalTransactionMapper", () => {
Expand All @@ -11,7 +11,7 @@ describe("internalTransactionMapper", () => {
from: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35C",
to: "0xc7e0220d02d549c4846A6EC31D89C3B670Ebe35D",
amount: "1000000",
tokenAddress: L2_ETH_TOKEN_ADDRESS,
tokenAddress: BASE_TOKEN_L2_ADDRESS,
transaction: {
blockNumber: 20,
receivedAt: new Date("2023-01-01"),
Expand Down
18 changes: 15 additions & 3 deletions packages/api/src/api/stats/stats.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import { Test } from "@nestjs/testing";
import { mock } from "jest-mock-extended";
import { ConfigService } from "@nestjs/config";
import { Logger } from "@nestjs/common";
import { TokenService } from "../../token/token.service";
import { Token, ETH_TOKEN } from "../../token/token.entity";
import { Token } from "../../token/token.entity";
import { StatsController } from "./stats.controller";
import { BASE_TOKEN_L2_ADDRESS } from "../../common/constants";

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

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

const module = await Test.createTestingModule({
controllers: [StatsController],
Expand All @@ -21,6 +29,10 @@ describe("StatsController", () => {
provide: TokenService,
useValue: tokenServiceMock,
},
{
provide: ConfigService,
useValue: configServiceMock,
},
],
}).compile();
module.useLogger(mock<Logger>());
Expand All @@ -31,7 +43,7 @@ describe("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,
usdPrice: 1000,
offChainDataUpdatedAt: new Date("2023-03-03"),
} as Token);

Expand All @@ -40,7 +52,7 @@ describe("StatsController", () => {
status: "1",
message: "OK",
result: {
ethusd: ETH_TOKEN.usdPrice.toString(),
ethusd: "1000",
ethusd_timestamp: Math.floor(new Date("2023-03-03").getTime() / 1000).toString(),
},
});
Expand Down
14 changes: 11 additions & 3 deletions packages/api/src/api/stats/stats.controller.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Controller, Get, UseFilters } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
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";
import { type BaseToken } from "../../config";

const entityName = "stats";

Expand All @@ -14,11 +15,18 @@ const entityName = "stats";
@Controller(`api/${entityName}`)
@UseFilters(ApiExceptionFilter)
export class StatsController {
constructor(private readonly tokenService: TokenService) {}
private readonly ethTokenAddress: string;

constructor(private readonly tokenService: TokenService, private readonly configService: ConfigService) {
this.ethTokenAddress = this.configService.get<BaseToken>("ethToken").l2Address;
}

@Get("/ethprice")
public async ethPrice(): Promise<EthPriceResponseDto> {
const token = await this.tokenService.findOne(ETH_TOKEN.l2Address, { usdPrice: true, offChainDataUpdatedAt: true });
const token = await this.tokenService.findOne(this.ethTokenAddress, {
usdPrice: true,
offChainDataUpdatedAt: true,
});
return {
status: token ? ResponseStatus.OK : ResponseStatus.NOTOK,
message: token ? ResponseMessage.OK : ResponseMessage.NO_DATA_FOUND,
Expand Down
31 changes: 20 additions & 11 deletions packages/api/src/api/token/token.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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 { Token } from "../../token/token.entity";
import { TokenController } from "./token.controller";

describe("TokenController", () => {
Expand Down Expand Up @@ -32,22 +32,31 @@ describe("TokenController", () => {

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

const baseToken = {
l2Address: "l2Address",
l1Address: "l1Address",
symbol: "ETH",
name: "Ether",
decimals: 18,
liquidity: 10,
iconURL: "iconURL",
usdPrice: 20,
} as Token;
jest.spyOn(tokenServiceMock, "findOne").mockResolvedValueOnce(baseToken);
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(),
contractAddress: baseToken.l2Address,
iconURL: baseToken.iconURL,
l1Address: baseToken.l1Address,
liquidity: baseToken.liquidity.toString(),
symbol: baseToken.symbol,
tokenDecimal: baseToken.decimals.toString(),
tokenName: baseToken.name,
tokenPriceUSD: baseToken.usdPrice.toString(),
},
],
});
Expand Down
1 change: 0 additions & 1 deletion packages/api/src/api/token/token.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { ResponseStatus, ResponseMessage } from "../dtos/common/responseBase.dto
import { ApiExceptionFilter } from "../exceptionFilter";
import { TokenInfoResponseDto } from "../dtos/token/tokenInfo.dto";
import { TokenService } from "../../token/token.service";

const entityName = "token";

@ApiExcludeController()
Expand Down
15 changes: 11 additions & 4 deletions packages/api/src/balance/balance.entity.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Entity, Column, PrimaryColumn, Index, ManyToOne, JoinColumn, AfterLoad } from "typeorm";
import { BaseEntity } from "../common/entities/base.entity";
import { Token, ETH_TOKEN } from "../token/token.entity";
import { Token } from "../token/token.entity";
import { normalizeAddressTransformer } from "../common/transformers/normalizeAddress.transformer";
import { bigIntNumberTransformer } from "../common/transformers/bigIntNumber.transformer";
import { baseToken, ethToken } from "../config";

@Entity({ name: "balances" })
export class Balance extends BaseEntity {
Expand All @@ -24,9 +25,15 @@ export class Balance extends BaseEntity {
public readonly balance: string;

@AfterLoad()
populateEthToken() {
if (this.tokenAddress === ETH_TOKEN.l2Address && !this.token) {
this.token = ETH_TOKEN;
populateBaseToken() {
// tokenAddress might be empty when not all entity fields are requested from the DB
if (this.tokenAddress && !this.token) {
const tokenAddress = this.tokenAddress.toLowerCase();
if (tokenAddress === baseToken.l2Address.toLowerCase()) {
this.token = baseToken as Token;
} else if (tokenAddress === ethToken.l2Address.toLowerCase()) {
this.token = ethToken as Token;
}
}
}
}
3 changes: 2 additions & 1 deletion packages/api/src/common/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const L2_ETH_TOKEN_ADDRESS = "0x000000000000000000000000000000000000800a";
export const BASE_TOKEN_L2_ADDRESS = "0x000000000000000000000000000000000000800A";
export const BASE_TOKEN_L1_ADDRESS = "0x0000000000000000000000000000000000000000";
Loading

0 comments on commit a49b121

Please sign in to comment.