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 interface and refactor to have multiple service providers #16

Merged
merged 12 commits into from
Sep 27, 2024
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
NETWORK='mainnet'

PUBLIC_API_PORT=3000
PUBLIC_API_PREFIX='dex-screener-adapter'

PRIVATE_API_PORT=4000
OFFLINE_JOBS_PORT=4001

API_URL='https://api.multiversx.com'
REDIS_URL='127.0.0.1'
ELASTIC_URL='https://index.multiversx.com'
98 changes: 14 additions & 84 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,116 +1,46 @@
REST API facade template for microservices that interacts with the MultiversX blockchain.
# MultiversX DEX Screener Adapter Service

## Quick start

1. Run `npm install` in the project directory
2. Optionally make edits to `config.yaml` or create `config.custom.yaml` for each microservice
2. Create `.env.mainnet` or `.env.custom` based on the `.env.example` file

## Dependencies

1. Redis Server is required to be installed [docs](https://redis.io/).
2. MySQL Server is required to be installed [docs](https://dev.mysql.com/doc/refman/8.0/en/installing.html).
3. MongoDB Server is required to be installed [docs](https://docs.mongodb.com/).
- Redis Server is required to be installed [docs](https://redis.io/).

You can run `docker-compose up` in a separate terminal to use a local Docker container for all these dependencies.

After running the sample, you can stop the Docker container with `docker-compose down`

## Available Features

These features can be enabled/disabled in config file

### `Public API`

Endpoints that can be used by anyone (public endpoints).

### `Private API`

Endpoints that are not exposed on the internet
For example: We do not want to expose our metrics and cache interactions to anyone (/metrics /cache)

### `Cache Warmer`

This is used to keep the application cache in sync with new updates.

### `Transaction Processor`

This is used for scanning the transactions from MultiversX Blockchain.

### `Queue Worker`

This is used for concurrently processing heavy jobs.

## Available Scripts

This is a MultiversX project built on Nest.js framework.

### `npm run start:mainnet`
### `npm run start:api:mainnet`

Runs the app in the production mode.
Make requests to [http://localhost:3001](http://localhost:3001).
Make requests to [http://localhost:3000/dex-screener-adapter](http://localhost:3000/dex-screener-adapter).

Redis Server is required to be installed.

## Running the api

```bash
# development watch mode on devnet
$ npm run start:devnet:watch

# development debug mode on devnet
$ npm run start:devnet:debug

# development mode on devnet
$ npm run start:devnet

# production mode
$ npm run start:mainnet
```

## Running the transactions-processor

```bash
# development watch mode on devnet
$ npm run start:transactions-processor:devnet:watch
# development debug mode on mainnet
$ npm run start:api:mainnet

# development debug mode on devnet
$ npm run start:transactions-processor:devnet:debug

# development mode on devnet
$ npm run start:transactions-processor:devnet

# production mode
$ npm run start:transactions-processor:mainnet
# development debug mode on a custom network
$ npm run start:api:custom
```

## Running the queue-worker
## Running the offline-jobs

```bash
# development watch mode on devnet
$ npm run start:queue-worker:devnet:watch

# development debug mode on devnet
$ npm run start:queue-worker:devnet:debug

# development mode on devnet
$ npm run start:queue-worker:devnet

# production mode
$ npm run start:queue-worker:mainnet
```

Requests can be made to http://localhost:3001 for the api. The app will reload when you'll make edits (if opened in watch mode). You will also see any lint errors in the console.​

### `npm run test`

```bash
# unit tests
$ npm run test

# e2e tests
$ npm run test:e2e
# development debug mode on mainnet
$ start:offline-jobs:mainnet

# test coverage
$ npm run test:cov
# development debug mode on a custom network
$ start:offline-jobs:custom
```
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BadRequestException, Controller, Get, Query } from "@nestjs/common";
import { AssetResponse, EventsResponse, LatestBlockResponse, PairResponse } from "./entities";
import { AssetResponse, EventsResponse, LatestBlockResponse, PairResponse } from "@mvx-monorepo/common";
import { ApiResponse } from "@nestjs/swagger";
import { DataIntegrationService } from "./data-integration.service";
import { ParseIntPipe } from "@multiversx/sdk-nestjs-common";
Expand Down
97 changes: 61 additions & 36 deletions apps/api/src/endpoints/data-integration/data-integration.service.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { AssetResponse, EventsResponse, LatestBlockResponse, PairResponse } from "./entities";
import { IndexerService, MultiversXApiService, XExchangeAddLiquidityEvent, XExchangeRemoveLiquidityEvent, XExchangeService, XExchangeSwapEvent } from "@mvx-monorepo/common";
import { Asset, Block, JoinExitEvent, Pair, SwapEvent } from "../../entitites";
import { ApiConfigService } from "@mvx-monorepo/common";
import {
ApiConfigService, Asset, AssetResponse, Block, ElasticRound, EventsResponse,
IndexerService, JoinExitEvent, LatestBlockResponse,
MultiversXApiService, PairResponse, SwapEvent, XExchangeService,
} from "@mvx-monorepo/common";
import { OriginLogger } from "@multiversx/sdk-nestjs-common";
import { IProviderService } from "@mvx-monorepo/common/providers/interface";
import { GeneralEvent } from "@mvx-monorepo/common/providers/entities/general.event";

@Injectable()
export class DataIntegrationService {
private readonly logger = new OriginLogger(DataIntegrationService.name);

private providers: IProviderService[] = [];
constructor(
private readonly apiConfigService: ApiConfigService,
private readonly indexerService: IndexerService,
private readonly multiversXApiService: MultiversXApiService,
private readonly xExchangeService: XExchangeService,
) { }
xExchangeService: XExchangeService,
) {
this.providers = [
xExchangeService,
];
}

public async getLatestBlock(): Promise<LatestBlockResponse> {
// we are using rounds instead of blocks because the MultiversX blockchain is multi-sharded,
Expand Down Expand Up @@ -44,22 +51,16 @@ export class DataIntegrationService {
};
}

public async getPair(address: string): Promise<PairResponse> {
const xExchangePairs = await this.xExchangeService.getPairs();
const xExchangePair = xExchangePairs.find((p) => p.address === address);
if (!xExchangePair) {
throw new NotFoundException(`Pair with address ${address} not found`);
public async getPair(identifier: string): Promise<PairResponse> {
for (const provider of this.providers) {
const pairResponse = await provider.getPair(identifier);
if (pairResponse) {
return pairResponse;
}
}

const pairFeePercent = await this.xExchangeService.getPairFeePercent(address);

const { deployTxHash, deployedAt } = await this.multiversXApiService.getContractDeployInfo(address);
const round = deployedAt ? await this.indexerService.getRound(deployedAt) : undefined;

const pair = Pair.fromXExchangePair(xExchangePair, pairFeePercent, { deployTxHash, deployedAt, deployRound: round?.round });
return {
pair,
};
this.logger.error(`Pair with identifier ${identifier} not found`);
throw new NotFoundException(`Pair with identifier ${identifier} not found`);
}

public async getEvents(fromBlockNonce: number, toBlockNonce: number): Promise<EventsResponse> {
Expand All @@ -69,43 +70,67 @@ export class DataIntegrationService {
events: [],
};
}

const after = rounds[0].timestamp;
const before = rounds[rounds.length - 1].timestamp;

const xExchangeEvents = await this.xExchangeService.getEvents(before, after);
const allEvents: ({ block: Block } & (SwapEvent | JoinExitEvent))[] = [];
for (const provider of this.providers) {
const generalEvents = await provider.getEvents(before, after);
const event = this.processEvents(generalEvents, provider, rounds);
allEvents.push(...event);
}

const sortedEvents = allEvents.sort((a, b) => {
if (a.block.blockTimestamp !== b.block.blockTimestamp) {
return a.block.blockTimestamp - b.block.blockTimestamp;
}
return a.txnId.localeCompare(b.txnId);
});

let txnIndex = 0;
let lastBlockTimestamp = 0;
for (const event of sortedEvents) {
if (event.block.blockTimestamp !== lastBlockTimestamp) {
txnIndex = 0;
} else {
txnIndex++;
}
event.txnIndex = txnIndex;
lastBlockTimestamp = event.block.blockTimestamp;
}

return {
events: sortedEvents,
};
}

private processEvents(generalEvents: GeneralEvent[], provider: IProviderService, rounds: ElasticRound[]): ({ block: Block } & (SwapEvent | JoinExitEvent))[] {
const events: ({ block: Block } & (SwapEvent | JoinExitEvent))[] = [];
for (const xExchangeEvent of xExchangeEvents) {
for (const generalEvent of generalEvents) {
let event: SwapEvent | JoinExitEvent;
switch (xExchangeEvent.type) {
switch (generalEvent.type) {
case "swap":
event = SwapEvent.fromXExchangeSwapEvent(xExchangeEvent as XExchangeSwapEvent);
event = provider.fromSwapEvent(generalEvent);
break;
case "addLiquidity":
event = JoinExitEvent.fromXExchangeEvent(xExchangeEvent as XExchangeAddLiquidityEvent);
break;
case "removeLiquidity":
event = JoinExitEvent.fromXExchangeEvent(xExchangeEvent as XExchangeRemoveLiquidityEvent);
event = provider.fromAddRemoveLiquidityEvent(generalEvent);
break;
default:
this.logger.error(`Unknown event type: ${xExchangeEvent.type} for event: ${JSON.stringify(xExchangeEvent)}`);
this.logger.error(`Unknown event type: ${generalEvent.type} for event: ${JSON.stringify(generalEvent)}`);
continue;
}

const round = rounds.find((round) => round.timestamp === xExchangeEvent.timestamp);
const round = rounds.find((round: { timestamp: number }) => round.timestamp === generalEvent.timestamp);
if (!round) {
this.logger.error(`Round not found for event: ${JSON.stringify(xExchangeEvent)}`);
this.logger.error(`Round not found for event: ${JSON.stringify(generalEvent)}`);
continue;
}

const block = Block.fromElasticRound(round, { onlyRequiredFields: true });
events.push({
block,
...event,
});
}

return { events };
return events;
}
}
4 changes: 0 additions & 4 deletions apps/api/src/endpoints/data-integration/entities/index.ts

This file was deleted.

5 changes: 0 additions & 5 deletions apps/api/src/entitites/index.ts

This file was deleted.

64 changes: 0 additions & 64 deletions apps/api/src/entitites/join.exit.event.ts

This file was deleted.

Loading
Loading