diff --git a/src/api/pair-liquidity-info-history/pair-liquidity-info-history.controller.ts b/src/api/pair-liquidity-info-history/pair-liquidity-info-history.controller.ts index 97d312d..437c5c1 100644 --- a/src/api/pair-liquidity-info-history/pair-liquidity-info-history.controller.ts +++ b/src/api/pair-liquidity-info-history/pair-liquidity-info-history.controller.ts @@ -9,7 +9,8 @@ import { PairLiquidityInfoHistoryService } from './pair-liquidity-info-history.s import { ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; import * as dto from '../../dto'; import { OrderQueryEnum } from '../../dto'; -import { ContractAddress } from '../../lib/utils'; + +import { ContractAddress } from '../../clients/sdk-client.model'; @Controller('history/liquidity') export class PairLiquidityInfoHistoryController { diff --git a/src/api/pair-liquidity-info-history/pair-liquidity-info-history.service.ts b/src/api/pair-liquidity-info-history/pair-liquidity-info-history.service.ts index fe56750..a868475 100644 --- a/src/api/pair-liquidity-info-history/pair-liquidity-info-history.service.ts +++ b/src/api/pair-liquidity-info-history/pair-liquidity-info-history.service.ts @@ -2,7 +2,8 @@ import { Injectable } from '@nestjs/common'; import { PairLiquidityInfoHistoryDbService } from '../../database/pair-liquidity-info-history/pair-liquidity-info-history-db.service'; import { Pair, PairLiquidityInfoHistory } from '@prisma/client'; import { OrderQueryEnum } from '../../dto'; -import { ContractAddress } from '../../lib/utils'; + +import { ContractAddress } from '../../clients/sdk-client.model'; @Injectable() export class PairLiquidityInfoHistoryService { diff --git a/src/api/tokens/tokens.controller.ts b/src/api/tokens/tokens.controller.ts index ffde28b..9ca1d32 100644 --- a/src/api/tokens/tokens.controller.ts +++ b/src/api/tokens/tokens.controller.ts @@ -16,8 +16,9 @@ import { ApiParam, ApiHeaders, } from '@nestjs/swagger'; -import { removeId, ContractAddress } from '../../lib/utils'; +import { removeId } from '../../lib/utils'; import * as prisma from '@prisma/client'; +import { ContractAddress } from '../../clients/sdk-client.model'; const withTokenAuthorization = async ( auth: string, diff --git a/src/api/tokens/tokens.service.ts b/src/api/tokens/tokens.service.ts index 5fd0151..bd35a79 100644 --- a/src/api/tokens/tokens.service.ts +++ b/src/api/tokens/tokens.service.ts @@ -1,7 +1,8 @@ import { Injectable } from '@nestjs/common'; import { Pair, PairLiquidityInfo, Token } from '@prisma/client'; -import { ContractAddress, presentInvalidTokens } from '../../lib/utils'; +import { presentInvalidTokens } from '../../lib/utils'; import { TokenDbService } from '../../database/token/token-db.service'; +import { ContractAddress } from '../../clients/sdk-client.model'; @Injectable() export class TokensService { diff --git a/src/app.module.ts b/src/app.module.ts index 73855d5..8d79f8c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,10 +5,12 @@ import { TokensService } from './api/tokens/tokens.service'; import { PairsService } from './api/pairs/pairs.service'; import { ClientsModule } from './clients/clients.module'; import { ApiModule } from './api/api.module'; +import { PairSyncService } from './tasks/pair-sync.service'; +import { MdwWsClientService } from './clients/mdw-ws-client.service'; @Module({ imports: [ApiModule, ClientsModule, DatabaseModule], controllers: [AppController], - providers: [TokensService, PairsService], + providers: [MdwWsClientService, PairsService, TokensService, PairSyncService], }) export class AppModule {} diff --git a/src/clients/clients.module.ts b/src/clients/clients.module.ts index 9aef069..c59af78 100644 --- a/src/clients/clients.module.ts +++ b/src/clients/clients.module.ts @@ -1,8 +1,9 @@ import { Module } from '@nestjs/common'; -import { MdwClientService } from './mdw-client.service'; +import { MdwHttpClientService } from './mdw-http-client.service'; +import { SdkClientService } from './sdk-client.service'; @Module({ - providers: [MdwClientService], - exports: [MdwClientService], + providers: [MdwHttpClientService, MdwHttpClientService, SdkClientService], + exports: [MdwHttpClientService, MdwHttpClientService, SdkClientService], }) export class ClientsModule {} diff --git a/src/clients/mdw-client.model.ts b/src/clients/mdw-http-client.model.ts similarity index 98% rename from src/clients/mdw-client.model.ts rename to src/clients/mdw-http-client.model.ts index 9046d71..46367c3 100644 --- a/src/clients/mdw-client.model.ts +++ b/src/clients/mdw-http-client.model.ts @@ -4,7 +4,7 @@ import { KeyBlockHash, MicroBlockHash, TxHash, -} from '../lib/utils'; +} from './sdk-client.model'; export type MdwPaginatedResponse = { next?: string; diff --git a/src/clients/mdw-client.service.ts b/src/clients/mdw-http-client.service.ts similarity index 96% rename from src/clients/mdw-client.service.ts rename to src/clients/mdw-http-client.service.ts index 0440e7f..2217f75 100644 --- a/src/clients/mdw-client.service.ts +++ b/src/clients/mdw-http-client.service.ts @@ -1,12 +1,6 @@ import { Injectable } from '@nestjs/common'; import NETWORKS from '../lib/networks'; -import { - AccountAddress, - ContractAddress, - KeyBlockHash, - MicroBlockHash, - nonNullable, -} from '../lib/utils'; +import { nonNullable } from '../lib/utils'; import { AccountBalance, ContractBalance, @@ -14,10 +8,16 @@ import { ContractLog, MdwMicroBlock, MdwPaginatedResponse, -} from './mdw-client.model'; +} from './mdw-http-client.model'; +import { + AccountAddress, + ContractAddress, + KeyBlockHash, + MicroBlockHash, +} from './sdk-client.model'; @Injectable() -export class MdwClientService { +export class MdwHttpClientService { private readonly LIMIT = 100; private readonly DIRECTION = 'forward'; private readonly INT_AS_STRING = true; diff --git a/src/clients/mdw-ws-client.model.ts b/src/clients/mdw-ws-client.model.ts new file mode 100644 index 0000000..6aa95ec --- /dev/null +++ b/src/clients/mdw-ws-client.model.ts @@ -0,0 +1,43 @@ +import { + CallData, + ContractAddress, + MicroBlockHash, + Payload, + Signature, + TxHash, + WalletAddress, +} from './sdk-client.model'; + +export type SubscriptionEvent = { + subscription: 'Object' | 'Transactions'; // add any other additional enum values if are used + source: string; + payload: { + tx: { + version: number; + nonce: number; + fee: number; + amount: number; + } & ( + | { + type: 'ContractCallTx'; // add any other additional enum values if are used + gas_price: number; + gas: number; + contract_id: ContractAddress; + caller_id: WalletAddress; + call_data: CallData; + abi_version: number; + } + | { + type: 'SpendTx'; + ttl: number; + sender_id: WalletAddress; + recipient_id: WalletAddress; + payload: Payload; + } + ); + signatures: Signature[]; + hash: TxHash; + block_height: number; + block_hash: MicroBlockHash; + }; +}; diff --git a/src/clients/mdw-ws-client.service.ts b/src/clients/mdw-ws-client.service.ts new file mode 100644 index 0000000..7ab9757 --- /dev/null +++ b/src/clients/mdw-ws-client.service.ts @@ -0,0 +1,156 @@ +import { Injectable, Logger } from '@nestjs/common'; +import * as WebSocket from 'ws'; +import NETWORKS from '../lib/networks'; +import { nonNullable, pluralize } from '../lib/utils'; +import { SubscriptionEvent } from './mdw-ws-client.model'; +import { ContractAddress } from './sdk-client.model'; + +export type Callbacks = { + onDisconnected?: (error?: Error) => any; + onEventReceived?: (event: SubscriptionEvent) => any; + onConnected?: () => any; +}; + +@Injectable() +export class MdwWsClientService { + readonly logger = new Logger(MdwWsClientService.name); + + createNewConnection = async (callbacks: Callbacks = {}) => { + //1. connect + const ws = this.createWebSocketConnection(); + + //2. crate ping time-out checker + const { setAlive, stopPing } = this.startPingMechanism(ws); + + // + // set up the subscription + // + + //3. on connect... + const openHandler = async () => { + setAlive(); + ws.on('pong', setAlive); + + const { ROUTER_ADDRESS, SUBSCRIBE_TO_ALL_TXS } = process.env; + if (SUBSCRIBE_TO_ALL_TXS && parseInt(SUBSCRIBE_TO_ALL_TXS)) { + this.subscribeToAllTxs(ws); + } else { + this.subscribeToContract( + ws, + nonNullable(ROUTER_ADDRESS) as ContractAddress, + ); + } + callbacks.onConnected && callbacks.onConnected(); + }; + + //4. when receive new messages + const messageHandler = this.createMessageHandler( + callbacks, + ws, + this.logger, + ); + + const errorHandler = (error?: Error) => { + callbacks.onDisconnected && callbacks.onDisconnected(error); + stopPing(); + ws.removeAllListeners(); + }; + const closeHandler = () => errorHandler(); + const onPing = (event: Buffer) => ws.pong(event); + + ws.on('error', errorHandler); + ws.on('message', messageHandler); + ws.on('open', openHandler); + ws.on('close', closeHandler); + ws.on('ping', onPing); + + return ws; + }; + + private createMessageHandler = + (callbacks: Callbacks, ws: WebSocket, logger: Logger) => + async (msg: WebSocket.RawData) => { + const stringMessage = msg.toString(); + const objMessage = JSON.parse(stringMessage); + const onUnknownMessage = () => { + ws.close(); + throw new Error(`Unknown message received: ${stringMessage}`); + }; + if (Array.isArray(objMessage)) { + if (objMessage.some((x) => x === 'Transactions')) { + logger.debug(`Subscribed to all transactions`); + } else { + logger.debug( + `Subscribed to ${pluralize(objMessage.length, 'contract')}`, + ); + } + return; + } + if (typeof objMessage === 'string') { + // if the message doesn't represent an already existing subscription + if (objMessage.indexOf('already subscribed to target')) { + onUnknownMessage(); + } + // there is nothing of interest here, let's exit + return; + } else if ( + !['Object', 'Transactions'].some((x) => objMessage.subscription === x) + ) { + onUnknownMessage(); + return; + } + const event: SubscriptionEvent = objMessage; + //if pair update subscribe to pair + const callback = callbacks.onEventReceived; + callback && (await callback(event)); + }; + + private createWebSocketConnection = () => + new WebSocket( + NETWORKS[nonNullable(process.env.NETWORK_NAME)].middlewareWebsocketUrl, + ); + + private subscribeToContract = (ws: WebSocket, address: ContractAddress) => + ws.send( + JSON.stringify({ + op: 'Subscribe', + payload: 'Object', + target: address, + }), + ); + + private subscribeToAllTxs = (ws: WebSocket) => + ws.send( + JSON.stringify({ + op: 'Subscribe', + payload: 'Transactions', + }), + ); + + private startPingMechanism = (ws: WebSocket) => { + let isAlive = false; + + const pingTimeOut = parseInt(process.env.MDW_PING_TIMEOUT_MS || '0'); + const interval = pingTimeOut + ? setInterval(() => { + if (!isAlive) { + this.logger.warn('Ws terminate because of ping-timeout'); + interval && clearInterval(interval); + ws.terminate(); + return; + } + + isAlive = false; + ws.ping(); + }, pingTimeOut) + : null; + return { + setAlive: () => { + isAlive = true; + }, + stopPing: () => { + interval && clearInterval(interval); + }, + }; + }; +} diff --git a/src/clients/sdk-client.model.ts b/src/clients/sdk-client.model.ts new file mode 100644 index 0000000..dfa944d --- /dev/null +++ b/src/clients/sdk-client.model.ts @@ -0,0 +1,15 @@ +import { Encoded } from '@aeternity/aepp-sdk'; + +export type AccountAddress = Encoded.AccountAddress; +export type ContractAddress = Encoded.ContractAddress; +export type WalletAddress = Encoded.AccountAddress; +export type CallData = Encoded.ContractBytearray; +export type Signature = Encoded.Signature; +export type TxHash = Encoded.TxHash; +export type MicroBlockHash = Encoded.MicroBlockHash; +export type KeyBlockHash = Encoded.KeyBlockHash; +export type Payload = Encoded.Bytearray; + +export const contractAddrToAccountAddr = ( + contractAddress: ContractAddress, +): AccountAddress => contractAddress.replace('ct_', 'ak_') as AccountAddress; diff --git a/src/clients/sdk-client.service.ts b/src/clients/sdk-client.service.ts new file mode 100644 index 0000000..00639ab --- /dev/null +++ b/src/clients/sdk-client.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { AeSdk, Node } from '@aeternity/aepp-sdk'; +import { nonNullable } from '../lib/utils'; +import NETWORKS from '../lib/networks'; + +@Injectable() +export class SdkClientService { + private client: AeSdk; + private node: Node; + + async getClient(): Promise<[AeSdk, Node]> { + const NETWORK_NAME = nonNullable(process.env.NETWORK_NAME); + if (!this.client) { + this.node = new Node(NETWORKS[NETWORK_NAME].nodeUrl, { + ignoreVersion: true, + }); + + this.client = new AeSdk({ + nodes: [{ name: NETWORK_NAME, instance: this.node }], + }); + } + return [this.client, this.node]; + } + + async getHeight(): Promise { + return await this.getClient().then(([client]) => client.getHeight()); + } +} diff --git a/src/dal/client.ts b/src/dal/client.ts deleted file mode 100644 index 8b96ca8..0000000 --- a/src/dal/client.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -export default new PrismaClient(); diff --git a/src/dal/index.ts b/src/dal/index.ts deleted file mode 100644 index 96cfe3e..0000000 --- a/src/dal/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * as pair from './pair'; -export * as token from './token'; diff --git a/src/dal/pair.ts b/src/dal/pair.ts deleted file mode 100644 index 05da332..0000000 --- a/src/dal/pair.ts +++ /dev/null @@ -1,145 +0,0 @@ -import prisma from './client'; -import { ContractAddress } from '../lib/utils'; -import { validTokenCondition } from './token'; - -const tokenCondition = (showInvalidTokens: boolean, onlyListed?: boolean) => ({ - is: { - ...(onlyListed ? { listed: true } : {}), - ...(showInvalidTokens ? {} : validTokenCondition), - }, -}); - -const tokensCondition = (showInvalidTokens: boolean, onlyListed?: boolean) => { - const condition = tokenCondition(showInvalidTokens, !!onlyListed); - return { - token0: condition, - token1: condition, - }; -}; - -export const getAllAddresses = async () => - ( - await prisma.pair.findMany({ - select: { - address: true, - }, - }) - ).map((x) => x.address as ContractAddress); - -export const getAll = (showInvalidTokens: boolean, onlyListed?: boolean) => - prisma.pair.findMany({ - where: tokensCondition(showInvalidTokens, onlyListed), - include: { - token0: true, - token1: true, - }, - }); - -export const getTopHeight = async () => - ( - await prisma.pairLiquidityInfo.aggregate({ - _max: { height: true }, - }) - )._max.height; - -export const getAllWithLiquidityInfo = ( - showInvalidTokens: boolean, - onlyListed?: boolean, -) => - prisma.pair.findMany({ - where: tokensCondition(showInvalidTokens, onlyListed), - include: { - token0: true, - token1: true, - liquidityInfo: true, - }, - }); - -export const getOne = (address: string) => - prisma.pair.findUnique({ - where: { address }, - include: { token0: true, token1: true, liquidityInfo: true }, - }); - -export const getOneLite = (address: string) => - prisma.pair.findUnique({ - where: { address }, - }); - -export type CountMode = 'all' | 'listed' | 'synchronized'; -export const count = (showInvalidTokens: boolean, mode?: CountMode) => - prisma.pair.count({ - where: { - ...tokensCondition(showInvalidTokens, mode === 'listed'), - ...(mode === 'synchronized' ? { synchronized: true } : {}), - }, - }); - -export const insert = (address: string, token0: number, token1: number) => - prisma.pair.create({ - data: { - address, - t0: token0, - t1: token1, - liquidityInfo: undefined, - synchronized: false, - }, - }); - -export const insertByTokenAddresses = ( - address: string, - token0: ContractAddress, - token1: ContractAddress, -) => - prisma.pair.create({ - select: { - id: true, - address: true, - token0: true, - token1: true, - liquidityInfo: false, - synchronized: false, - }, - data: { - address, - token0: { connect: { address: token0 } }, - token1: { connect: { address: token1 } }, - liquidityInfo: undefined, - synchronized: false, - }, - }); - -export const synchronise = async ( - pairId: number, - totalSupply: bigint, - reserve0: bigint, - reserve1: bigint, - height: number, -) => { - const update = { - totalSupply: totalSupply.toString(), - reserve0: reserve0.toString(), - reserve1: reserve1.toString(), - height, - }; - return prisma.pair.update({ - where: { id: pairId }, - select: { - id: true, - address: true, - token0: true, - token1: true, - liquidityInfo: true, - synchronized: true, - }, - data: { - liquidityInfo: { upsert: { update, create: update } }, - synchronized: true, - }, - }); -}; - -export const unsyncAllPairs = async () => - prisma.pair.updateMany({ - data: { synchronized: false }, - }); diff --git a/src/dal/token.ts b/src/dal/token.ts deleted file mode 100644 index ed54d18..0000000 --- a/src/dal/token.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { Token } from '@prisma/client'; -import prisma from './client'; -import { ContractAddress } from '../lib/utils'; - -export const validTokenCondition = { malformed: false, noContract: false }; - -export const getAll = (showInvalidTokens: boolean): Promise => - prisma.token.findMany({ - where: showInvalidTokens ? {} : validTokenCondition, - }); - -export const getListed = (): Promise => - //there is no reason to list invalid tokens - prisma.token.findMany({ where: { ...validTokenCondition, listed: true } }); - -export const getByAddress = (address: string) => - prisma.token.findFirst({ - where: { address }, - }); - -export const updateListedValue = async (address: string, listed: boolean) => { - //ensure the token is valid in order to be listed - if (listed) { - const exists = await prisma.token.findFirst({ - where: { address }, - select: { malformed: true, noContract: true }, - }); - if (exists?.malformed || exists?.noContract) { - throw new Error("An invalid token can't be listed"); - } - } - return prisma.token.update({ - //we don't want to list invalid tokens - where: { address }, - data: { listed }, - }); -}; - -export const getByAddressWithPairs = (address: string) => - prisma.token.findFirst({ - where: { address }, - include: { pairs0: true, pairs1: true }, - }); - -export const count = (showInvalidTokens: boolean, onlyListed?: boolean) => - prisma.token.count({ - where: { - ...(onlyListed ? { listed: true } : {}), - ...(() => (showInvalidTokens ? {} : validTokenCondition))(), - }, - }); - -export const getByAddressWithPairsAndLiquidity = (address: string) => - prisma.token.findFirst({ - where: { address }, - include: { - pairs0: { include: { token1: true, liquidityInfo: true } }, - pairs1: { include: { token0: true, liquidityInfo: true } }, - }, - }); - -export const getAllAddresses = async ( - showInvalidTokens: boolean, -): Promise => - ( - await prisma.token.findMany({ - where: showInvalidTokens ? {} : validTokenCondition, - select: { - address: true, - }, - }) - ).map((x) => x.address as ContractAddress); - -export const upsertToken = ( - address: string, - symbol: string, - name: string, - decimals: number, -): Promise => - commonUpsert(address, { - symbol, - name, - decimals, - noContract: false, - malformed: false, - }); - -export const upsertMalformedToken = (address: string): Promise => - commonUpsert(address, { malformed: true, noContract: false }); - -export const upsertNoContractForToken = (address: string): Promise => - commonUpsert(address, { malformed: false, noContract: true }); - -const commonUpsert = ( - address: string, - common: Partial, -): Promise => - prisma.token.upsert({ - where: { - address, - }, - update: common, - create: { address, ...common }, - }); diff --git a/src/database/pair-liquidity-info-history-error/pair-liquidity-info-history-error-db.service.e2e-spec.ts b/src/database/pair-liquidity-info-history-error/pair-liquidity-info-history-error-db.service.e2e-spec.ts index bcb71af..961d82a 100644 --- a/src/database/pair-liquidity-info-history-error/pair-liquidity-info-history-error-db.service.e2e-spec.ts +++ b/src/database/pair-liquidity-info-history-error/pair-liquidity-info-history-error-db.service.e2e-spec.ts @@ -59,7 +59,8 @@ const errorEntry2: PairLiquidityInfoHistoryError = { describe('PairLiquidityInfoHistoryErrorDbService', () => { let service: PairLiquidityInfoHistoryErrorDbService; let prismaService: PrismaService; - beforeEach(async () => { + + beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [PairLiquidityInfoHistoryErrorDbService, PrismaService], }).compile(); @@ -68,7 +69,9 @@ describe('PairLiquidityInfoHistoryErrorDbService', () => { PairLiquidityInfoHistoryErrorDbService, ); prismaService = module.get(PrismaService); + }); + beforeEach(async () => { await prismaService.token.createMany({ data: [token1, token2] }); await prismaService.pair.createMany({ data: [pair1, pair2] }); await prismaService.pairLiquidityInfoHistoryError.createMany({ @@ -84,6 +87,10 @@ describe('PairLiquidityInfoHistoryErrorDbService', () => { jest.useRealTimers(); }); + afterAll(async () => { + await prismaService.$disconnect(); + }); + describe('getErrorByPairIdAndMicroBlockHashWithinHours', () => { it('should correctly return an error within a recent given time window in hours by pairId', async () => { jest.useFakeTimers().setSystemTime(new Date('2024-01-01 17:59:00.000')); diff --git a/src/database/pair-liquidity-info-history/pair-liquidity-info-history-db.service.e2e-spec.ts b/src/database/pair-liquidity-info-history/pair-liquidity-info-history-db.service.e2e-spec.ts index 9c63ddd..05bc576 100644 --- a/src/database/pair-liquidity-info-history/pair-liquidity-info-history-db.service.e2e-spec.ts +++ b/src/database/pair-liquidity-info-history/pair-liquidity-info-history-db.service.e2e-spec.ts @@ -3,7 +3,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { PrismaService } from '../prisma.service'; import { Pair, PairLiquidityInfoHistory, Token } from '@prisma/client'; import { OrderQueryEnum } from '../../dto'; -import { ContractAddress } from '../../lib/utils'; + +import { ContractAddress } from '../../clients/sdk-client.model'; const token1: Token = { id: 1, @@ -104,7 +105,8 @@ const historyEntry4: PairLiquidityInfoHistory = { describe('PairLiquidityInfoHistoryDbService', () => { let service: PairLiquidityInfoHistoryDbService; let prismaService: PrismaService; - beforeEach(async () => { + + beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [PairLiquidityInfoHistoryDbService, PrismaService], }).compile(); @@ -113,7 +115,9 @@ describe('PairLiquidityInfoHistoryDbService', () => { PairLiquidityInfoHistoryDbService, ); prismaService = module.get(PrismaService); + }); + beforeEach(async () => { await prismaService.token.createMany({ data: [token1, token2, token3] }); await prismaService.pair.createMany({ data: [pair1, pair2, pair3] }); await prismaService.pairLiquidityInfoHistory.createMany({ @@ -127,6 +131,10 @@ describe('PairLiquidityInfoHistoryDbService', () => { await prismaService.token.deleteMany(); }); + afterAll(async () => { + await prismaService.$disconnect(); + }); + describe('getAll', () => { it('should return all entries', async () => { const result = await service.getAll(100, 0, OrderQueryEnum.asc); diff --git a/src/database/pair-liquidity-info-history/pair-liquidity-info-history-db.service.ts b/src/database/pair-liquidity-info-history/pair-liquidity-info-history-db.service.ts index 5741447..8761d56 100644 --- a/src/database/pair-liquidity-info-history/pair-liquidity-info-history-db.service.ts +++ b/src/database/pair-liquidity-info-history/pair-liquidity-info-history-db.service.ts @@ -2,7 +2,8 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '../prisma.service'; import { PairLiquidityInfoHistory } from '@prisma/client'; import { OrderQueryEnum } from '../../dto'; -import { ContractAddress } from '../../lib/utils'; + +import { ContractAddress } from '../../clients/sdk-client.model'; @Injectable() export class PairLiquidityInfoHistoryDbService { diff --git a/src/database/pair/pair-db.service.ts b/src/database/pair/pair-db.service.ts index b725f50..2a666c3 100644 --- a/src/database/pair/pair-db.service.ts +++ b/src/database/pair/pair-db.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { Pair, Token } from '@prisma/client'; import { PrismaService } from '../prisma.service'; -import { validTokenCondition } from '../../dal/token'; -import { ContractAddress } from '../../lib/utils'; +import { validTokenCondition } from '../token/token-db.service'; +import { ContractAddress } from '../../clients/sdk-client.model'; export type PairWithTokens = { token0: Token; token1: Token } & Pair; export type CountMode = 'all' | 'listed' | 'synchronized'; diff --git a/src/database/token/token-db.service.ts b/src/database/token/token-db.service.ts index 2acd3da..745714b 100644 --- a/src/database/token/token-db.service.ts +++ b/src/database/token/token-db.service.ts @@ -1,7 +1,10 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '../prisma.service'; import { Token } from '@prisma/client'; -import { ContractAddress } from '../../lib/utils'; + +import { ContractAddress } from '../../clients/sdk-client.model'; + +export const validTokenCondition = { malformed: false, noContract: false }; @Injectable() export class TokenDbService { @@ -9,14 +12,14 @@ export class TokenDbService { getAll(showInvalidTokens: boolean): Promise { return this.prisma.token.findMany({ - where: showInvalidTokens ? {} : this.validTokenCondition, + where: showInvalidTokens ? {} : validTokenCondition, }); } getListed(): Promise { //there is no reason to list invalid tokens return this.prisma.token.findMany({ - where: { ...this.validTokenCondition, listed: true }, + where: { ...validTokenCondition, listed: true }, }); } @@ -55,7 +58,7 @@ export class TokenDbService { return this.prisma.token.count({ where: { ...(onlyListed ? { listed: true } : {}), - ...(() => (showInvalidTokens ? {} : this.validTokenCondition))(), + ...(() => (showInvalidTokens ? {} : validTokenCondition))(), }, }); } @@ -75,7 +78,7 @@ export class TokenDbService { ): Promise { return ( await this.prisma.token.findMany({ - where: showInvalidTokens ? {} : this.validTokenCondition, + where: showInvalidTokens ? {} : validTokenCondition, select: { address: true, }, @@ -117,6 +120,4 @@ export class TokenDbService { create: { address, ...common }, }); } - - private validTokenCondition = { malformed: false, noContract: false }; } diff --git a/src/lib/contracts.ts b/src/lib/contracts.ts deleted file mode 100644 index e94ddd3..0000000 --- a/src/lib/contracts.ts +++ /dev/null @@ -1,136 +0,0 @@ -import NETWORKS from './networks'; -import { AeSdk, Node, ContractMethodsBase } from '@aeternity/aepp-sdk'; -import { ContractAddress, nonNullable } from './utils'; -import * as routerInterface from 'dex-contracts-v2/build/AedexV2Router.aci.json'; -import * as factoryInterface from 'dex-contracts-v2/build/AedexV2Factory.aci.json'; -import * as pairInterface from 'dex-contracts-v2/build/AedexV2Pair.aci.json'; -import ContractWithMethods from '@aeternity/aepp-sdk/es/contract/Contract'; - -let client: AeSdk; -let node: Node; - -export const getClient = async (): Promise<[AeSdk, Node]> => { - const NETWORK_NAME = nonNullable(process.env.NETWORK_NAME); - if (!client) { - node = new Node(NETWORKS[NETWORK_NAME].nodeUrl, { - ignoreVersion: true, - }); - - client = new AeSdk({ - nodes: [{ name: NETWORK_NAME, instance: node }], - }); - } - return [client, node]; -}; - -export type RouterMethods = { - factory: () => ContractAddress; -}; - -export type FactoryMethods = { - get_all_pairs: () => ContractAddress[]; -}; - -export type PairMethods = { - token0: () => ContractAddress; - token1: () => ContractAddress; - total_supply: () => bigint; - get_reserves: () => { - reserve0: bigint; - reserve1: bigint; - }; -}; - -export type MetaInfo = { - name: string; - symbol: string; - decimals: bigint; -}; - -export type Aex9Methods = { - meta_info: () => MetaInfo; -}; - -export type Context = { - router: ContractWithMethods; - factory: ContractWithMethods; - getPair: ( - address: ContractAddress, - ) => Promise>; - getToken: ( - address: ContractAddress, - ) => Promise>; - node: Node; -}; - -const createGetToken = - ( - tokens: { [key: string]: ContractWithMethods | undefined }, - getInstance: Awaited>, - ) => - async ( - tokenAddress: ContractAddress, - ): Promise> => { - const cached = tokens[tokenAddress]; - if (cached) { - return cached; - } - const token = await getInstance(pairInterface, tokenAddress); - tokens[tokenAddress] = token; - return token; - }; - -const createGetPair = - ( - pairs: { [key: string]: ContractWithMethods | undefined }, - getInstance: Awaited>, - ) => - async ( - pairAddress: ContractAddress, - ): Promise> => { - const cached = pairs[pairAddress]; - if (cached) { - return cached; - } - const pair = await getInstance(pairInterface, pairAddress); - pairs[pairAddress] = pair; - return pair; - }; - -const instanceFactory = async (client: AeSdk) => { - return ( - aci: any, - contractAddress: ContractAddress, - ) => client.initializeContract({ aci, address: contractAddress }); -}; - -export const getContext = async (): Promise => { - const routerAddress = process.env.ROUTER_ADDRESS; - if (!routerAddress) { - throw new Error('Router address is not set'); - } - const [client, node] = await getClient(); - const getInstance = await instanceFactory(client); - const router = await getInstance( - routerInterface, - nonNullable(routerAddress as ContractAddress), - ); - const factory = await getInstance( - factoryInterface, - nonNullable( - process.env.FACTORY_ADDRESS as ContractAddress, - ), - ); - const pairs: { [key: string]: ContractWithMethods | undefined } = - {}; - const tokens: { - [key: string]: ContractWithMethods | undefined; - } = {}; - return { - router, - factory, - getPair: createGetPair(pairs, getInstance), - getToken: createGetToken(tokens, getInstance), - node, - }; -}; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index b44434c..906277c 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,5 +1,3 @@ -import { Encoded } from '@aeternity/aepp-sdk'; - export const nonNullable = (t: T | null | undefined, label?: string): T => { if (t == null) { throw new Error( @@ -20,19 +18,5 @@ export const removeId = (t: T) => { export const pluralize = (count: number, noun: string, suffix = 's') => `${count} ${noun}${count !== 1 ? suffix : ''}`; -export type AccountAddress = Encoded.AccountAddress; -export type ContractAddress = Encoded.ContractAddress; -export type WalletAddress = Encoded.AccountAddress; -export type CallData = Encoded.ContractBytearray; -export type Signature = Encoded.Signature; -export type TxHash = Encoded.TxHash; -export type MicroBlockHash = Encoded.MicroBlockHash; -export type KeyBlockHash = Encoded.KeyBlockHash; -export type Payload = Encoded.Bytearray; - const parseEnv = (x) => x && JSON.parse(x); export const presentInvalidTokens = parseEnv(process.env.SHOW_INVALID_TOKENS); - -export const contractAddrToAccountAddr = ( - contractAddress: ContractAddress, -): AccountAddress => contractAddress.replace('ct_', 'ak_') as AccountAddress; diff --git a/src/main.ts b/src/main.ts index e33a181..052a8cd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,11 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; -import createWorker from './worker'; -import { getContext } from './lib/contracts'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { nonNullable } from './lib/utils'; +import { PairSyncService } from './tasks/pair-sync.service'; const version = nonNullable(process.env.npm_package_version); async function bootstrap() { - createWorker(await getContext()).startWorker(true, true); const app = await NestFactory.create(AppModule); app.enableCors(); const config = new DocumentBuilder() @@ -18,5 +16,6 @@ async function bootstrap() { const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('', app, document); await app.listen(3000); + app.get(PairSyncService).startSync(true, true); } bootstrap(); diff --git a/src/tasks/pair-liquidity-info-history-importer.service.spec.ts b/src/tasks/pair-liquidity-info-history-importer.service.spec.ts index fb70e16..9a11fee 100644 --- a/src/tasks/pair-liquidity-info-history-importer.service.spec.ts +++ b/src/tasks/pair-liquidity-info-history-importer.service.spec.ts @@ -1,11 +1,12 @@ import { PairLiquidityInfoHistoryImporterService } from './pair-liquidity-info-history-importer.service'; import { Test, TestingModule } from '@nestjs/testing'; -import { MdwClientService } from '../clients/mdw-client.service'; +import { MdwHttpClientService } from '../clients/mdw-http-client.service'; import { PairDbService } from '../database/pair/pair-db.service'; import { PairLiquidityInfoHistoryDbService } from '../database/pair-liquidity-info-history/pair-liquidity-info-history-db.service'; import { PairLiquidityInfoHistoryErrorDbService } from '../database/pair-liquidity-info-history-error/pair-liquidity-info-history-error-db.service'; -import { ContractAddress } from '../lib/utils'; -import { Contract } from '../clients/mdw-client.model'; +import { Contract } from '../clients/mdw-http-client.model'; +import { SdkClientService } from '../clients/sdk-client.service'; +import { ContractAddress } from '../clients/sdk-client.model'; const mockMdwClientService = { getContract: jest.fn(), @@ -35,7 +36,8 @@ describe('PairLiquidityInfoHistoryImporterService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ PairLiquidityInfoHistoryImporterService, - { provide: MdwClientService, useValue: mockMdwClientService }, + SdkClientService, + { provide: MdwHttpClientService, useValue: mockMdwClientService }, { provide: PairDbService, useValue: mockPairDb }, { provide: PairLiquidityInfoHistoryDbService, diff --git a/src/tasks/pair-liquidity-info-history-importer.service.ts b/src/tasks/pair-liquidity-info-history-importer.service.ts index a3c9042..92b6a41 100644 --- a/src/tasks/pair-liquidity-info-history-importer.service.ts +++ b/src/tasks/pair-liquidity-info-history-importer.service.ts @@ -1,19 +1,19 @@ import { Injectable, Logger } from '@nestjs/common'; -import { MdwClientService } from '../clients/mdw-client.service'; +import { MdwHttpClientService } from '../clients/mdw-http-client.service'; import { PairDbService, PairWithTokens, } from '../database/pair/pair-db.service'; import { isEqual, orderBy, uniqWith } from 'lodash'; import { PairLiquidityInfoHistoryDbService } from '../database/pair-liquidity-info-history/pair-liquidity-info-history-db.service'; +import { PairLiquidityInfoHistoryErrorDbService } from '../database/pair-liquidity-info-history-error/pair-liquidity-info-history-error-db.service'; +import { ContractLog } from '../clients/mdw-http-client.model'; +import { SdkClientService } from '../clients/sdk-client.service'; import { ContractAddress, contractAddrToAccountAddr, MicroBlockHash, -} from '../lib/utils'; -import { PairLiquidityInfoHistoryErrorDbService } from '../database/pair-liquidity-info-history-error/pair-liquidity-info-history-error-db.service'; -import { getClient } from '../lib/contracts'; -import { ContractLog } from '../clients/mdw-client.model'; +} from '../clients/sdk-client.model'; type MicroBlock = { hash: MicroBlockHash; @@ -24,10 +24,11 @@ type MicroBlock = { @Injectable() export class PairLiquidityInfoHistoryImporterService { constructor( - private mdwClientService: MdwClientService, + private mdwClient: MdwHttpClientService, private pairDb: PairDbService, private pairLiquidityInfoHistoryDb: PairLiquidityInfoHistoryDbService, private pairLiquidityInfoHistoryErrorDb: PairLiquidityInfoHistoryErrorDbService, + private sdkClient: SdkClientService, ) {} readonly logger = new Logger(PairLiquidityInfoHistoryImporterService.name); @@ -59,9 +60,7 @@ export class PairLiquidityInfoHistoryImporterService { } // Get current height - const currentHeight = await getClient().then(([client]) => - client.getHeight(), - ); + const currentHeight = await this.sdkClient.getHeight(); // Get lastly synced block const { @@ -95,7 +94,7 @@ export class PairLiquidityInfoHistoryImporterService { : parseInt(contractLog.height) < currentHeight - 10; const pairContractLogs = - await this.mdwClientService.getContractLogsUntilCondition( + await this.mdwClient.getContractLogsUntilCondition( fetchContractLogsFilter, pairWithTokens.address as ContractAddress, ); @@ -197,10 +196,10 @@ export class PairLiquidityInfoHistoryImporterService { } private async insertInitialLiquidity(pairWithTokens: PairWithTokens) { - const pairContract = await this.mdwClientService.getContract( + const pairContract = await this.mdwClient.getContract( pairWithTokens.address as ContractAddress, ); - const microBlock = await this.mdwClientService.getMicroBlock( + const microBlock = await this.mdwClient.getMicroBlock( pairContract.block_hash, ); await this.pairLiquidityInfoHistoryDb @@ -225,7 +224,7 @@ export class PairLiquidityInfoHistoryImporterService { ) { // Total supply is the sum of all amounts of the pair contract's balances const pairBalances = - await this.mdwClientService.getContractBalancesAtMicroBlockHash( + await this.mdwClient.getContractBalancesAtMicroBlockHash( pairWithTokens.address as ContractAddress, block.hash, ); @@ -235,7 +234,7 @@ export class PairLiquidityInfoHistoryImporterService { // reserve0 is the balance of the pair contract's account of token0 const reserve0 = ( - await this.mdwClientService.getAccountBalanceForContractAtMicroBlockHash( + await this.mdwClient.getAccountBalanceForContractAtMicroBlockHash( pairWithTokens.token0.address as ContractAddress, contractAddrToAccountAddr(pairWithTokens.address as ContractAddress), block.hash, @@ -244,7 +243,7 @@ export class PairLiquidityInfoHistoryImporterService { // reserve1 is the balance of the pair contract's account of token1 const reserve1 = ( - await this.mdwClientService.getAccountBalanceForContractAtMicroBlockHash( + await this.mdwClient.getAccountBalanceForContractAtMicroBlockHash( pairWithTokens.token1.address as ContractAddress, contractAddrToAccountAddr(pairWithTokens.address as ContractAddress), block.hash, diff --git a/src/tasks/pair-liquidity-info-history-validator.service.spec.ts b/src/tasks/pair-liquidity-info-history-validator.service.spec.ts index 0cbd0fb..f73ab87 100644 --- a/src/tasks/pair-liquidity-info-history-validator.service.spec.ts +++ b/src/tasks/pair-liquidity-info-history-validator.service.spec.ts @@ -1,7 +1,8 @@ import { PairLiquidityInfoHistoryValidatorService } from './pair-liquidity-info-history-validator.service'; import { Test, TestingModule } from '@nestjs/testing'; -import { MdwClientService } from '../clients/mdw-client.service'; +import { MdwHttpClientService } from '../clients/mdw-http-client.service'; import { PairLiquidityInfoHistoryDbService } from '../database/pair-liquidity-info-history/pair-liquidity-info-history-db.service'; +import { SdkClientService } from '../clients/sdk-client.service'; const mockMdwClientService = { getKeyBlockMicroBlocks: jest.fn(), @@ -19,7 +20,8 @@ describe('PairLiquidityInfoHistoryValidatorService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ PairLiquidityInfoHistoryValidatorService, - { provide: MdwClientService, useValue: mockMdwClientService }, + SdkClientService, + { provide: MdwHttpClientService, useValue: mockMdwClientService }, { provide: PairLiquidityInfoHistoryDbService, useValue: mockPairLiquidityInfoHistoryDb, diff --git a/src/tasks/pair-liquidity-info-history-validator.service.ts b/src/tasks/pair-liquidity-info-history-validator.service.ts index b254cf7..620b8de 100644 --- a/src/tasks/pair-liquidity-info-history-validator.service.ts +++ b/src/tasks/pair-liquidity-info-history-validator.service.ts @@ -1,15 +1,16 @@ -import { MdwClientService } from '../clients/mdw-client.service'; +import { MdwHttpClientService } from '../clients/mdw-http-client.service'; import { PairLiquidityInfoHistoryDbService } from '../database/pair-liquidity-info-history/pair-liquidity-info-history-db.service'; import { Injectable, Logger } from '@nestjs/common'; import { uniq } from 'lodash'; -import { getClient } from '../lib/contracts'; -import { MicroBlockHash } from '../lib/utils'; +import { SdkClientService } from '../clients/sdk-client.service'; +import { MicroBlockHash } from '../clients/sdk-client.model'; @Injectable() export class PairLiquidityInfoHistoryValidatorService { constructor( - private mdwClientService: MdwClientService, + private mdwClient: MdwHttpClientService, private pairLiquidityInfoHistoryDb: PairLiquidityInfoHistoryDbService, + private sdkClient: SdkClientService, ) {} readonly logger = new Logger(PairLiquidityInfoHistoryValidatorService.name); @@ -18,9 +19,7 @@ export class PairLiquidityInfoHistoryValidatorService { this.logger.log(`Started validating pair liquidity info history.`); // Get current height - const currentHeight = await getClient().then(([client]) => - client.getHeight(), - ); + const currentHeight = await this.sdkClient.getHeight(); // Get all liquidity entries greater or equal the current height minus 20 const liquidityEntriesWithinHeightSorted = @@ -36,9 +35,7 @@ export class PairLiquidityInfoHistoryValidatorService { // Fetch microBlocks for these unique keyBlocks from mdw const microBlockHashsOnMdw = ( await Promise.all( - uniqueHeights.map((h) => - this.mdwClientService.getKeyBlockMicroBlocks(h), - ), + uniqueHeights.map((h) => this.mdwClient.getKeyBlockMicroBlocks(h)), ) ) .flat() diff --git a/src/tasks/pair-sync.model.ts b/src/tasks/pair-sync.model.ts new file mode 100644 index 0000000..ef6b40d --- /dev/null +++ b/src/tasks/pair-sync.model.ts @@ -0,0 +1,43 @@ +import ContractWithMethods from '@aeternity/aepp-sdk/es/contract/Contract'; +import { Node } from '@aeternity/aepp-sdk'; +import { ContractAddress } from '../clients/sdk-client.model'; + +export type MetaInfo = { + name: string; + symbol: string; + decimals: bigint; +}; + +export type Aex9Methods = { + meta_info: () => MetaInfo; +}; + +export type RouterMethods = { + factory: () => ContractAddress; +}; + +export type FactoryMethods = { + get_all_pairs: () => ContractAddress[]; +}; + +export type PairMethods = { + token0: () => ContractAddress; + token1: () => ContractAddress; + total_supply: () => bigint; + get_reserves: () => { + reserve0: bigint; + reserve1: bigint; + }; +}; + +export type Context = { + router: ContractWithMethods; + factory: ContractWithMethods; + getPair: ( + address: ContractAddress, + ) => Promise>; + getToken: ( + address: ContractAddress, + ) => Promise>; + node: Node; +}; diff --git a/src/tasks/pair-sync.service.ts b/src/tasks/pair-sync.service.ts new file mode 100644 index 0000000..4359ede --- /dev/null +++ b/src/tasks/pair-sync.service.ts @@ -0,0 +1,406 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { TokenDbService } from '../database/token/token-db.service'; +import { PairDbService } from '../database/pair/pair-db.service'; +import ContractWithMethods from '@aeternity/aepp-sdk/es/contract/Contract'; +import { Pair } from '@prisma/client'; +import { MdwWsClientService } from '../clients/mdw-ws-client.service'; +import { SubscriptionEvent } from '../clients/mdw-ws-client.model'; +import * as routerInterface from 'dex-contracts-v2/build/AedexV2Router.aci.json'; +import * as factoryInterface from 'dex-contracts-v2/build/AedexV2Factory.aci.json'; +import * as pairInterface from 'dex-contracts-v2/build/AedexV2Pair.aci.json'; +import { AeSdk, ContractMethodsBase } from '@aeternity/aepp-sdk'; +import { nonNullable } from '../lib/utils'; +import { + Aex9Methods, + Context, + FactoryMethods, + PairMethods, + RouterMethods, +} from './pair-sync.model'; +import { SdkClientService } from '../clients/sdk-client.service'; +import { ContractAddress } from '../clients/sdk-client.model'; + +@Injectable() +export class PairSyncService implements OnModuleInit { + constructor( + private readonly tokenDb: TokenDbService, + private readonly pairDb: PairDbService, + private readonly mdwWsClient: MdwWsClientService, + private readonly sdkClient: SdkClientService, + ) {} + + readonly logger = new Logger(PairSyncService.name); + ctx: Context; + + async onModuleInit() { + this.ctx = await this.getContext(); + } + + async startSync( + autoStart?: boolean, + crashWhenClosed?: boolean, + ): Promise { + this.logger.log(`Starting ${process.env.NETWORK_NAME} worker...`); + await this.unsyncAllPairs(); + await this.mdwWsClient.createNewConnection({ + onConnected: async () => { + await this.refreshPairs(); + await this.refreshPairsLiquidity(); + }, + onDisconnected: async (error) => { + this.logger.warn(`Middleware disconnected: ${error}`); + await this.unsyncAllPairs(); + if (autoStart) { + setTimeout(() => this.startSync(true), 2000); + } else if (crashWhenClosed) { + throw new Error('Middleware connection closed'); + } + }, + onEventReceived: this.createOnEventReceived( + this.logger, + this.onFactoryEventReceived, + this.refreshPairLiquidityByAddress, + () => this.pairDb.getAllAddresses(), + ), + }); + } + + private getContext = async (): Promise => { + const createGetToken = + ( + tokens: { [key: string]: ContractWithMethods | undefined }, + getInstance: Awaited>, + ) => + async ( + tokenAddress: ContractAddress, + ): Promise> => { + const cached = tokens[tokenAddress]; + if (cached) { + return cached; + } + const token = await getInstance( + pairInterface, + tokenAddress, + ); + tokens[tokenAddress] = token; + return token; + }; + + const createGetPair = + ( + pairs: { [key: string]: ContractWithMethods | undefined }, + getInstance: Awaited>, + ) => + async ( + pairAddress: ContractAddress, + ): Promise> => { + const cached = pairs[pairAddress]; + if (cached) { + return cached; + } + const pair = await getInstance(pairInterface, pairAddress); + pairs[pairAddress] = pair; + return pair; + }; + + const instanceFactory = async (client: AeSdk) => { + return ( + aci: any, + contractAddress: ContractAddress, + ) => client.initializeContract({ aci, address: contractAddress }); + }; + + const routerAddress = process.env.ROUTER_ADDRESS; + if (!routerAddress) { + throw new Error('Router address is not set'); + } + const [client, node] = await this.sdkClient.getClient(); + const getInstance = await instanceFactory(client); + const router = await getInstance( + routerInterface, + nonNullable(routerAddress as ContractAddress), + ); + const factory = await getInstance( + factoryInterface, + nonNullable( + process.env.FACTORY_ADDRESS as ContractAddress, + ), + ); + const pairs: { + [key: string]: ContractWithMethods | undefined; + } = {}; + const tokens: { + [key: string]: ContractWithMethods | undefined; + } = {}; + return { + router, + factory, + getPair: createGetPair(pairs, getInstance), + getToken: createGetToken(tokens, getInstance), + node, + }; + }; + + private async unsyncAllPairs(): Promise { + const batch = await this.pairDb.unsyncAllPairs(); + this.logger.log(`${batch.count} pairs marked as unsync`); + } + + private async refreshPairs(): Promise { + this.logger.log(`Getting all pairs from Factory...`); + const { decodedResult: allFactoryPairs } = + await this.ctx.factory.get_all_pairs(); + this.logger.log(`${allFactoryPairs.length} pairs found on DEX`); + const allDbPairsLen = await this.pairDb.count(true); + //get new pairs, and reverse it , because allFactoryPairs is reversed by the factory contract + const newAddresses = allFactoryPairs + .slice(0, allFactoryPairs.length - allDbPairsLen) + .reverse(); + + this.logger.log(`${newAddresses.length} new pairs found`); + + if (!newAddresses.length) { + this.logger.log(`Pairs refresh completed`); + return newAddresses; + } + + //get pair tokens in parallel + const pairWithTokens = await Promise.all( + newAddresses.map( + async ( + pairAddress: ContractAddress, + ): Promise<[ContractAddress, [ContractAddress, ContractAddress]]> => [ + pairAddress, + await this.getPairTokens(pairAddress), + ], + ), + ); + + const tokenSet: Set = new Set( + pairWithTokens.reduce( + (acc: ContractAddress[], data) => acc.concat(data[1]), + [], + ), + ); + + //ensure all new tokens will be inserted + await this.insertOnlyNewTokens([...tokenSet]); + + //finally insert new pairs sequential to preserve the right order + for (const [ + pairAddress, + [token0Address, token1Address], + ] of pairWithTokens) { + await this.insertNewPair(pairAddress, token0Address, token1Address); + } + + // because there are new pairs let's go another round to ensure + // no other pair was created during this time + this.logger.debug('We go another round to see if any pair was created'); + const futurePairs = await this.refreshPairs(); + return newAddresses.concat(futurePairs); + } + + private async refreshPairsLiquidity(): Promise { + //get the all pairs + const dbPairs = await this.pairDb.getAllWithCondition(true); + this.logger.log(`Refreshing pairs liquidity...`); + await Promise.all( + dbPairs.map((dbPair) => this.refreshPairLiquidity(dbPair)), + ); + this.logger.log(`Pairs liquidity refresh completed`); + } + + private createOnEventReceived = + ( + logger: Logger, + onFactory: typeof this.onFactoryEventReceived, + refreshPairLiquidity: typeof this.refreshPairLiquidityByAddress, + getAllAddresses: () => Promise, + ) => + async (event: SubscriptionEvent) => { + const { + hash, + tx: { type }, + } = event.payload; + if (type !== 'ContractCallTx') { + logger.debug(`Ignoring transaction of type '${type}'`); + return; + } + //TODO: try to trow exception here to see if it reconnects + const txInfo = await this.ctx.node.getTransactionInfoByHash(hash); + if (!txInfo) { + throw new Error(`No tx info for hash '${hash}'`); + } + if (!txInfo.callInfo) { + throw new Error(`No tx.callInfo for hash '${hash}'`); + } + if (txInfo.callInfo.returnType !== 'ok') { + logger.debug(`Ignore reverted transaction: '${hash}'`); + return; + } + // make a list with all unique contracts + const contracts = [ + ...new Set( + txInfo.callInfo?.log.map((x) => x.address as ContractAddress), + ), + ]; + + // get all known addresses + const addresses: { [key: ContractAddress]: boolean | undefined } = ( + await getAllAddresses() + ).reduce((a, v) => ({ ...a, [v]: true }), {}); + + //parse events on be on + const allPromises = contracts.map((contract) => { + // factory state was modified + if (contract === process.env.FACTORY_ADDRESS) { + return onFactory(event.payload.block_height); + } + // if the pair is newly created within this transaction + // the pair will be ignored in this loop, but that's not a problem, because + // the factory event handler was also involved here and will take care of the + // newly created pair + else if (addresses[contract]) { + return refreshPairLiquidity(contract, event.payload.block_height); + } + return Promise.resolve(); + }); + return Promise.all(allPromises); + }; + + private async updateTokenMetadata( + address: ContractAddress, + tokenMethods: ContractWithMethods, + ) { + try { + const { + decodedResult: { name, symbol, decimals }, + } = await tokenMethods.meta_info(); + + const tokenFromDb = await this.tokenDb.upsertToken( + address, + symbol, + name, + Number(decimals), + ); + this.logger.debug(`Token ${symbol} [${address}] updated/inserted`); + return tokenFromDb.id; + } catch (error) { + const tokenFromDb = await this.tokenDb.upsertMalformedToken(address); + this.logger.warn(`Token ${address} is malformed`, error.message); + return tokenFromDb.id; + } + } + + private async upsertTokenInformation( + address: ContractAddress, + ): Promise { + const token = await this.tokenDb.getByAddress(address); + if (token) { + return token.id; + } + let tokenMethods: ContractWithMethods; + try { + tokenMethods = await this.ctx.getToken(address); + } catch (error) { + const noContract = `v3/contracts/${address} error: Contract not found`; + if (error.message && error.message.indexOf(noContract) > -1) { + const tokenFromDb = + await this.tokenDb.upsertNoContractForToken(address); + this.logger.warn(`No contract for Token ${address}`); + return tokenFromDb.id; + } + throw error; + } + + return await this.updateTokenMetadata(address, tokenMethods); + } + + private async insertNewPair( + address: ContractAddress, + token0Address: ContractAddress, + token1Address: ContractAddress, + ) { + const ret = await this.pairDb.insertByTokenAddresses( + address, + token0Address, + token1Address, + ); + this.logger.debug( + `Pair ${ret.token0.symbol}/${ret.token1.symbol} [${address}] inserted`, + ); + return ret; + } + + private async getPairTokens( + address: ContractAddress, + ): Promise<[ContractAddress, ContractAddress]> { + const instance = await this.ctx.getPair(address); + return [ + (await instance.token0()).decodedResult, + (await instance.token1()).decodedResult, + ]; + } + + private async insertOnlyNewTokens(tokenAddresses: ContractAddress[]) { + const allAddresses = new Set(await this.tokenDb.getAllAddresses(true)); + const newOnes = tokenAddresses.filter( + (tokenAddress) => !allAddresses.has(tokenAddress), + ); + return Promise.all( + newOnes.map((tokenAddress) => this.upsertTokenInformation(tokenAddress)), + ); + } + + private async refreshPairLiquidityByAddress( + address: ContractAddress, + height?: number, + ) { + const found = await this.pairDb.getOneLite(address); + if (!found) { + throw new Error(`Pair not found ${address}`); + } + await this.refreshPairLiquidity(found, height); + } + + private async refreshPairLiquidity(dbPair: Pair, height?: number) { + const pair = await this.ctx.getPair(dbPair.address as ContractAddress); + const { decodedResult: totalSupply } = await pair.total_supply(); + const { + decodedResult: { reserve0, reserve1 }, + result, + } = await pair.get_reserves(); + const syncHeight = height || result?.height; + if (!syncHeight) { + console.error('Could not get height'); + return; + } + const ret = await this.pairDb.synchronise( + dbPair.id, + totalSupply, + reserve0, + reserve1, + syncHeight, + ); + this.logger.debug( + `Pair ${ret.token0.symbol}/${ret.token1.symbol} [${ + dbPair.address + }] synchronized with ${JSON.stringify({ + totalSupply: totalSupply.toString(), + reserve0: reserve0.toString(), + reserve1: reserve1.toString(), + })}`, + ); + return ret; + } + + private async onFactoryEventReceived(height: number) { + const newAddresses = await this.refreshPairs(); + await Promise.all( + newAddresses.map((address) => + this.refreshPairLiquidityByAddress(address, height), + ), + ); + } +} diff --git a/src/tasks/tasks.module.ts b/src/tasks/tasks.module.ts index c6cfbfb..1d49e21 100644 --- a/src/tasks/tasks.module.ts +++ b/src/tasks/tasks.module.ts @@ -5,6 +5,7 @@ import { ClientsModule } from '../clients/clients.module'; import { ScheduleModule } from '@nestjs/schedule'; import { PairLiquidityInfoHistoryValidatorService } from './pair-liquidity-info-history-validator.service'; import { TasksService } from './tasks.service'; +import { PairSyncService } from './pair-sync.service'; @Module({ imports: [ClientsModule, DatabaseModule, ScheduleModule.forRoot()], @@ -12,6 +13,7 @@ import { TasksService } from './tasks.service'; PairLiquidityInfoHistoryImporterService, PairLiquidityInfoHistoryValidatorService, TasksService, + PairSyncService, ], }) export class TasksModule {} diff --git a/src/tasks/tasks.service.spec.ts b/src/tasks/tasks.service.spec.ts index 8d316fc..f46e476 100644 --- a/src/tasks/tasks.service.spec.ts +++ b/src/tasks/tasks.service.spec.ts @@ -2,11 +2,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import { TasksService } from './tasks.service'; import { PairLiquidityInfoHistoryImporterService } from './pair-liquidity-info-history-importer.service'; import { PairLiquidityInfoHistoryValidatorService } from './pair-liquidity-info-history-validator.service'; -import { MdwClientService } from '../clients/mdw-client.service'; +import { MdwHttpClientService } from '../clients/mdw-http-client.service'; import { PairDbService } from '../database/pair/pair-db.service'; import { PairLiquidityInfoHistoryDbService } from '../database/pair-liquidity-info-history/pair-liquidity-info-history-db.service'; import { PairLiquidityInfoHistoryErrorDbService } from '../database/pair-liquidity-info-history-error/pair-liquidity-info-history-error-db.service'; import { PrismaService } from '../database/prisma.service'; +import { SdkClientService } from '../clients/sdk-client.service'; describe('TasksService', () => { let tasksService: TasksService; @@ -19,7 +20,8 @@ describe('TasksService', () => { TasksService, PairLiquidityInfoHistoryImporterService, PairLiquidityInfoHistoryValidatorService, - MdwClientService, + MdwHttpClientService, + SdkClientService, PairDbService, PairLiquidityInfoHistoryDbService, PairLiquidityInfoHistoryErrorDbService, diff --git a/src/worker/index.ts b/src/worker/index.ts deleted file mode 100644 index 5d4cf8a..0000000 --- a/src/worker/index.ts +++ /dev/null @@ -1,312 +0,0 @@ -import { Context, Aex9Methods } from '../lib/contracts'; -import * as dal from '../dal'; -import * as db from '@prisma/client'; -import * as mdw from './middleware'; - -import { Logger } from '@nestjs/common'; -import { ContractAddress } from 'src/lib/utils'; -import ContractWithMethods from '@aeternity/aepp-sdk/es/contract/Contract'; -const logger = new Logger('Worker'); - -const updateTokenMetadata = async ( - address: ContractAddress, - tokenMethods: ContractWithMethods, -) => { - try { - const { - decodedResult: { name, symbol, decimals }, - } = await tokenMethods.meta_info(); - - const tokenFromDb = await dal.token.upsertToken( - address, - symbol, - name, - Number(decimals), - ); - logger.debug(`Token ${symbol} [${address}] updated/inserted`); - return tokenFromDb.id; - } catch (error) { - const tokenFromDb = await dal.token.upsertMalformedToken(address); - logger.warn(`Token ${address} is malformed`, error.message); - return tokenFromDb.id; - } -}; - -const upsertTokenInformation = async ( - ctx: Context, - address: ContractAddress, -): Promise => { - const token = await dal.token.getByAddress(address); - if (token) { - return token.id; - } - let tokenMethods: ContractWithMethods; - try { - tokenMethods = await ctx.getToken(address); - } catch (error) { - const noContract = `v3/contracts/${address} error: Contract not found`; - if (error.message && error.message.indexOf(noContract) > -1) { - const tokenFromDb = await dal.token.upsertNoContractForToken(address); - logger.warn(`No contract for Token ${address}`); - return tokenFromDb.id; - } - throw error; - } - - return await updateTokenMetadata(address, tokenMethods); -}; - -const insertNewPair = async ( - address: ContractAddress, - token0Address: ContractAddress, - token1Address: ContractAddress, -) => { - const ret = await dal.pair.insertByTokenAddresses( - address, - token0Address, - token1Address, - ); - logger.debug( - `Pair ${ret.token0.symbol}/${ret.token1.symbol} [${address}] inserted`, - ); - return ret; -}; - -const getPairTokens = async ( - ctx: Context, - address: ContractAddress, -): Promise<[ContractAddress, ContractAddress]> => { - const instance = await ctx.getPair(address); - return [ - (await instance.token0()).decodedResult, - (await instance.token1()).decodedResult, - ]; -}; - -const insertOnlyNewTokens = async ( - ctx: Context, - tokenAddresses: ContractAddress[], -) => { - const allAddresses = new Set(await dal.token.getAllAddresses(true)); - const newOnes = tokenAddresses.filter( - (tokenAddress) => !allAddresses.has(tokenAddress), - ); - return Promise.all( - newOnes.map((tokenAddress) => upsertTokenInformation(ctx, tokenAddress)), - ); -}; - -const refreshPairLiquidityByAddress = async ( - ctx: Context, - address: ContractAddress, - height?: number, -) => { - const found = await dal.pair.getOneLite(address); - if (!found) { - throw new Error(`Pair not found ${address}`); - } - await refreshPairLiquidity(ctx, found, height); -}; -const refreshPairLiquidity = async ( - ctx: Context, - dbPair: db.Pair, - height?: number, -) => { - const pair = await ctx.getPair(dbPair.address as ContractAddress); - const { decodedResult: totalSupply } = await pair.total_supply(); - const { - decodedResult: { reserve0, reserve1 }, - result, - } = await pair.get_reserves(); - const syncHeight = height || result?.height; - if (!syncHeight) { - console.error('Could not get height'); - return; - } - const ret = await dal.pair.synchronise( - dbPair.id, - totalSupply, - reserve0, - reserve1, - syncHeight, - ); - logger.debug( - `Pair ${ret.token0.symbol}/${ret.token1.symbol} [${ - dbPair.address - }] synchronized with ${JSON.stringify({ - totalSupply: totalSupply.toString(), - reserve0: reserve0.toString(), - reserve1: reserve1.toString(), - })}`, - ); - return ret; -}; - -const refreshPairs = async (ctx: Context): Promise => { - logger.log(`Getting all pairs from Factory...`); - const { decodedResult: allFactoryPairs } = await ctx.factory.get_all_pairs(); - logger.log(`${allFactoryPairs.length} pairs found on DEX`); - const allDbPairsLen = await dal.pair.count(true); - //get new pairs, and reverse it , because allFactoryPairs is reversed by the factory contract - const newAddresses = allFactoryPairs - .slice(0, allFactoryPairs.length - allDbPairsLen) - .reverse(); - - logger.log(`${newAddresses.length} new pairs found`); - - if (!newAddresses.length) { - logger.log(`Pairs refresh completed`); - return newAddresses; - } - - //get pair tokens in parallel - const pairWithTokens = await Promise.all( - newAddresses.map( - async ( - pairAddress: ContractAddress, - ): Promise<[ContractAddress, [ContractAddress, ContractAddress]]> => [ - pairAddress, - await getPairTokens(ctx, pairAddress), - ], - ), - ); - - const tokenSet: Set = new Set( - pairWithTokens.reduce( - (acc: ContractAddress[], data) => acc.concat(data[1]), - [], - ), - ); - - //ensure all new tokens will be inserted - await insertOnlyNewTokens(ctx, [...tokenSet]); - - //finally insert new pairs sequential to preserve the right order - for (const [pairAddress, [token0Address, token1Address]] of pairWithTokens) { - await insertNewPair(pairAddress, token0Address, token1Address); - } - - // because there are new pairs let's go another round to ensure - // no other pair was created during this time - logger.debug('We go another round to see if any pair was created'); - const futurePairs = await refreshPairs(ctx); - return newAddresses.concat(futurePairs); -}; - -const refreshPairsLiquidity = async (ctx: Context) => { - //get the all pairs - const dbPairs = await dal.pair.getAll(true); - logger.log(`Refreshing pairs liquidity...`); - await Promise.all(dbPairs.map((dbPair) => refreshPairLiquidity(ctx, dbPair))); - logger.log(`Pairs liquidity refresh completed`); -}; - -const onFactoryEventReceived = async (ctx: Context, height: number) => { - const newAddresses = await refreshPairs(ctx); - await Promise.all( - newAddresses.map((address) => - refreshPairLiquidityByAddress(ctx, address, height), - ), - ); -}; - -export const createOnEventReceived = - ( - ctx: Context, - logger: Logger, - onFactory: typeof onFactoryEventReceived, - refreshPairLiquidity: typeof refreshPairLiquidityByAddress, - getAllAddresses: () => Promise, - ) => - async (event: mdw.SubscriptionEvent) => { - const { - hash, - tx: { type }, - } = event.payload; - if (type !== 'ContractCallTx') { - logger.debug(`Ignoring transaction of type '${type}'`); - return; - } - //TODO: try to trow exception here to see if it reconnects - const txInfo = await ctx.node.getTransactionInfoByHash(hash); - if (!txInfo) { - throw new Error(`No tx info for hash '${hash}'`); - } - if (!txInfo.callInfo) { - throw new Error(`No tx.callInfo for hash '${hash}'`); - } - if (txInfo.callInfo.returnType !== 'ok') { - logger.debug(`Ignore reverted transaction: '${hash}'`); - return; - } - // make a list with all unique contracts - const contracts = [ - ...new Set(txInfo.callInfo?.log.map((x) => x.address as ContractAddress)), - ]; - - // get all known addresses - const addresses: { [key: ContractAddress]: boolean | undefined } = ( - await getAllAddresses() - ).reduce((a, v) => ({ ...a, [v]: true }), {}); - - //parse events on be on - const allPromises = contracts.map((contract) => { - //factory state was modified was modified - if (contract === process.env.FACTORY_ADDRESS) { - return onFactory(ctx, event.payload.block_height); - } - // if the pair is newly created withing this transaction - // the pair will be ignore in this loop, but that's not a problem, because - // factory event handler was also involved here and it will take care of - // newly created pair - else if (addresses[contract]) { - return refreshPairLiquidity(ctx, contract, event.payload.block_height); - } - return Promise.resolve(); - }); - return Promise.all(allPromises); - }; - -export default (ctx: Context) => { - const unsyncAllPairs = async () => { - const batch = await dal.pair.unsyncAllPairs(); - logger.log(`${batch.count} pairs marked as unsync`); - }; - const onEventReceived = createOnEventReceived( - ctx, - logger, - onFactoryEventReceived, - refreshPairLiquidityByAddress, - () => dal.pair.getAllAddresses(), - ); - - const startWorker = async ( - autoStart?: boolean, - crashWhenClosed?: boolean, - ) => { - logger.log(`Starting ${process.env.NETWORK_NAME} worker...`); - await unsyncAllPairs(); - await mdw.createNewConnection({ - onConnected: async () => { - await refreshPairs(ctx); - await refreshPairsLiquidity(ctx); - }, - onDisconnected: async (error) => { - logger.warn(`Middleware disconnected: ${error}`); - await unsyncAllPairs(); - if (autoStart) { - setTimeout(() => startWorker(true), 2000); - } else if (crashWhenClosed) { - throw new Error('Middleware connection closed'); - } - }, - onEventReceived, - }); - }; - return { - refreshPairsLiquidity: () => refreshPairsLiquidity(ctx), - refreshPairs: () => refreshPairs(ctx), - unsyncAllPairs, - startWorker, - }; -}; diff --git a/src/worker/middleware.ts b/src/worker/middleware.ts deleted file mode 100644 index ac647dd..0000000 --- a/src/worker/middleware.ts +++ /dev/null @@ -1,188 +0,0 @@ -import * as WebSocket from 'ws'; -import NETWORKS from '../lib/networks'; -import { - MicroBlockHash, - CallData, - ContractAddress, - TxHash, - nonNullable, - pluralize, - Signature, - WalletAddress, - Payload, -} from '../lib/utils'; - -import { Logger } from '@nestjs/common'; -const logger = new Logger('WebSocket'); - -const createWebSocketConnection = () => - new WebSocket( - NETWORKS[nonNullable(process.env.NETWORK_NAME)].middlewareWebsocketUrl, - ); - -export type SubscriptionEvent = { - subscription: 'Object' | 'Transactions'; // add any other additional enum values if are used - source: string; - payload: { - tx: { - version: number; - nonce: number; - fee: number; - amount: number; - } & ( - | { - type: 'ContractCallTx'; // add any other additional enum values if are used - gas_price: number; - gas: number; - contract_id: ContractAddress; - caller_id: WalletAddress; - call_data: CallData; - abi_version: number; - } - | { - type: 'SpendTx'; - ttl: number; - sender_id: WalletAddress; - recipient_id: WalletAddress; - payload: Payload; - } - ); - signatures: Signature[]; - hash: TxHash; - block_height: number; - block_hash: MicroBlockHash; - }; -}; - -const subscribeToContract = (ws: WebSocket, address: ContractAddress) => - ws.send( - JSON.stringify({ - op: 'Subscribe', - payload: 'Object', - target: address, - }), - ); - -const subscribeToAllTxs = (ws: WebSocket) => - ws.send( - JSON.stringify({ - op: 'Subscribe', - payload: 'Transactions', - }), - ); - -const startPingMechanism = (ws: WebSocket) => { - let isAlive = false; - - const pingTimeOut = parseInt(process.env.MDW_PING_TIMEOUT_MS || '0'); - const interval = pingTimeOut - ? setInterval(function ping() { - if (isAlive === false) { - logger.warn('Ws terminate because of ping-timeout'); - interval && clearInterval(interval); - ws.terminate(); - return; - } - - isAlive = false; - ws.ping(); - }, pingTimeOut) - : null; - return { - setAlive: () => { - isAlive = true; - }, - stopPing: () => { - interval && clearInterval(interval); - }, - }; -}; - -export type Callbacks = { - onDisconnected?: (error?: Error) => any; - onEventReceived?: (event: SubscriptionEvent) => any; - onConnected?: () => any; -}; - -export const createMessageHandler = - (callbacks: Callbacks, ws: WebSocket, logger: Logger) => - async (msg: WebSocket.RawData) => { - const stringMessage = msg.toString(); - const objMessage = JSON.parse(stringMessage); - const onUnknownMessage = () => { - ws.close(); - throw new Error(`Unknown message received: ${stringMessage}`); - }; - if (Array.isArray(objMessage)) { - if (objMessage.some((x) => x === 'Transactions')) { - logger.debug(`Subscribed to all transactions`); - } else { - logger.debug( - `Subscribed to ${pluralize(objMessage.length, 'contract')}`, - ); - } - return; - } - if (typeof objMessage === 'string') { - // if the message doesn't represent an already subscription - if (objMessage.indexOf('already subscribed to target')) { - onUnknownMessage(); - } - // there is nothing of interest here, let's exit - return; - } else if ( - !['Object', 'Transactions'].some((x) => objMessage.subscription === x) - ) { - onUnknownMessage(); - return; - } - const event: SubscriptionEvent = objMessage; - //if pair update subscribe to pair - const callback = callbacks.onEventReceived; - callback && (await callback(event)); - }; - -export const createNewConnection = async (callbacks: Callbacks = {}) => { - //1. connect - const ws = createWebSocketConnection(); - - //2. crate ping time-out checker - const { setAlive, stopPing } = startPingMechanism(ws); - - // - // setup the subscription - // - - //3. on connect... - const openHandler = async () => { - setAlive(); - ws.on('pong', setAlive); - - const { ROUTER_ADDRESS, SUBSCRIBE_TO_ALL_TXS } = process.env; - if (SUBSCRIBE_TO_ALL_TXS && parseInt(SUBSCRIBE_TO_ALL_TXS)) { - subscribeToAllTxs(ws); - } else { - subscribeToContract(ws, nonNullable(ROUTER_ADDRESS) as ContractAddress); - } - callbacks.onConnected && callbacks.onConnected(); - }; - - //4. when receive new messages - const messageHandler = createMessageHandler(callbacks, ws, logger); - - const errorHandler = (error?: Error) => { - callbacks.onDisconnected && callbacks.onDisconnected(error); - stopPing(); - ws.removeAllListeners(); - }; - const closeHandler = () => errorHandler(); - const onPing = (event: Buffer) => ws.pong(event); - - ws.on('error', errorHandler); - ws.on('message', messageHandler); - ws.on('open', openHandler); - ws.on('close', closeHandler); - ws.on('ping', onPing); - - return ws; -}; diff --git a/test/context-mockup.spec.ts b/test/context-mockup.spec.ts index abde826..807f685 100644 --- a/test/context-mockup.spec.ts +++ b/test/context-mockup.spec.ts @@ -1,8 +1,8 @@ -import { Context, PairMethods } from '../src/lib/contracts'; import { mockDeep } from 'jest-mock-extended'; import { mockContext, ContextData, mockupContractMethod } from './utils'; import * as data from './data/context-mockups'; import ContractWithMethods from '@aeternity/aepp-sdk/es/contract/Contract'; +import { Context, PairMethods } from '../src/tasks/pair-sync.model'; describe('Context', () => { it('sample mockup', async () => { diff --git a/test/contracts.spec.ts b/test/contracts.spec.ts deleted file mode 100644 index eb073a2..0000000 --- a/test/contracts.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { getContext, Context } from '../src/lib/contracts'; -import { mockupEnvVars, TEST_NET_VARS } from './utils/env.mockups'; - -describe('with real Context on testnet', () => { - let context: Context | null = null; - mockupEnvVars(TEST_NET_VARS); - - beforeAll(async () => { - context = await getContext(); - }); - - const ctx = (): Context => { - if (!context) { - throw 'initiate context first'; - } - return context; - }; - - it('matches the .env factory', async () => { - const { decodedResult: factoryAddress } = await ctx().router.factory(); - expect(factoryAddress).toBe(process.env.FACTORY_ADDRESS); - }); - it('should have at least 16 pairs', async () => { - expect( - (await ctx().factory.get_all_pairs()).decodedResult.length, - ).toBeGreaterThanOrEqual(16); - }); - it('pair should return right token addresses', async () => { - const { decodedResult: allPairs } = await ctx().factory.get_all_pairs(); - const pairAddress = allPairs[allPairs.length - 1]; - expect(pairAddress).toBe( - 'ct_efYtiwDg4YZxDWE3iLPzvrjb92CJPvzGwriv4ZRuvuTDMNMb9', - ); - const pairMethods = await ctx().getPair(pairAddress); - if (!pairMethods) { - fail('pairMethods is null'); - } - expect((await pairMethods.token0()).decodedResult).toBe( - 'ct_7tTzPfvv3Vx8pCEcuk1kmgtn4sFsYCQDzLi1LvFs8T5PJqgsC', - ); - expect((await pairMethods.token1()).decodedResult).toBe( - 'ct_b7FZHQzBcAW4r43ECWpV3qQJMQJp5BxkZUGNKrqqLyjVRN3SC', - ); - }); - it('regular aex9 token should have right metaInfo', async () => { - const tokenMethods = await ctx().getToken( - 'ct_7tTzPfvv3Vx8pCEcuk1kmgtn4sFsYCQDzLi1LvFs8T5PJqgsC', - ); - - const { decodedResult: metaInfo } = await tokenMethods.meta_info(); - expect(metaInfo).toEqual({ - decimals: 18n, - name: 'TestAEX9-B', - symbol: 'TAEX9-B', - }); - }); - it('WAE token should have right metaInfo', async () => { - const tokenMethods = await ctx().getToken( - 'ct_JDp175ruWd7mQggeHewSLS1PFXt9AzThCDaFedxon8mF8xTRF', - ); - const { decodedResult: metaInfo } = await tokenMethods.meta_info(); - expect(metaInfo).toEqual({ - decimals: 18n, - name: 'Wrapped Aeternity', - symbol: 'WAE', - }); - }); -}); diff --git a/test/data/context-mockups.ts b/test/data/context-mockups.ts index 26caff7..f42c70c 100644 --- a/test/data/context-mockups.ts +++ b/test/data/context-mockups.ts @@ -1,5 +1,5 @@ -import { ContractAddress } from '../../src/lib/utils'; -import { ContextData } from '../utils/context.mockup'; +import { ContextData } from '../utils'; +import { ContractAddress } from '../../src/clients/sdk-client.model'; const tokens = [ { diff --git a/test/data/subscription-events.ts b/test/data/subscription-events.ts index 2bc5784..08d21a6 100644 --- a/test/data/subscription-events.ts +++ b/test/data/subscription-events.ts @@ -1,4 +1,4 @@ -import { SubscriptionEvent } from '../../src/worker/middleware'; +import { SubscriptionEvent } from '../../src/clients/mdw-ws-client.model'; export const objSubEv: SubscriptionEvent = { subscription: 'Object', diff --git a/test/mdw-ws-client.service.spec.ts b/test/mdw-ws-client.service.spec.ts new file mode 100644 index 0000000..080e613 --- /dev/null +++ b/test/mdw-ws-client.service.spec.ts @@ -0,0 +1,111 @@ +import * as WebSocket from 'ws'; +import { Logger } from '@nestjs/common'; +import { mock } from 'jest-mock-extended'; +import { objSubEv, txSubEv } from './data/subscription-events'; + +import { Test, TestingModule } from '@nestjs/testing'; +import { + Callbacks, + MdwWsClientService, +} from '../src/clients/mdw-ws-client.service'; + +describe('MdwWsClientService', () => { + let service: MdwWsClientService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [MdwWsClientService], + }).compile(); + service = module.get(MdwWsClientService); + }); + + it('just connects', async () => { + let future: any; + const promise = new Promise((resolve, reject) => { + future = { resolve, reject }; + }); + const ws = await service.createNewConnection({ + onDisconnected: (error) => { + if (error) { + console.error(error); + future?.reject(error); + } else { + future?.resolve(); + } + }, + onConnected: () => { + ws.close(); + }, + }); + + await promise; + }); + + describe('createMessageHandler', () => { + const initTestContext = () => { + const callbacks = mock(); + const ws = mock(); + const logger = mock(); + const msgHandler = service['createMessageHandler'](callbacks, ws, logger); + return { callbacks, ws, logger, msgHandler }; + }; + + type T = ReturnType; + let callbacks: T['callbacks'] = null as any; + let ws: T['ws'] = null as any; + let logger: T['logger'] = null as any; + let msgHandler: T['msgHandler'] = null as any; + + beforeEach(() => { + ({ callbacks, ws, logger, msgHandler } = initTestContext()); + }); + + it('random string will cause error', async () => { + expect(() => msgHandler('"some string"' as any)).rejects.toThrow( + 'Unknown message received: "some string"', + ); + expect(ws.close).toHaveBeenCalledWith(); + }); + + it('"already subscribed to target" not throwing error', async () => { + //not throwing an error is enough + await msgHandler('"already subscribed to target"' as any); + }); + + it('subscribed to all transactions', async () => { + await msgHandler('[1,"sentence","Transactions"]' as any); + expect(logger.debug).toHaveBeenCalledWith( + 'Subscribed to all transactions', + ); + }); + + it('subscribed to one contract', async () => { + await msgHandler('["the-contract"]' as any); + expect(logger.debug).toHaveBeenCalledWith('Subscribed to 1 contract'); + }); + + it('subscribed to multiple contracts', async () => { + await msgHandler('["ct_1",2,5]' as any); + expect(logger.debug).toHaveBeenCalledWith('Subscribed to 3 contracts'); + }); + + it('unrecognized object will cause error', async () => { + await expect(() => + msgHandler('{"prop": "oject without proper shape"}' as any), + ).rejects.toThrow( + 'Unknown message received: {"prop": "oject without proper shape"}', + ); + expect(ws.close).toHaveBeenCalledWith(); + }); + + it('Oject subscription event succeeds', async () => { + await msgHandler(JSON.stringify(objSubEv) as any); + expect(callbacks.onEventReceived).toHaveBeenCalledWith(objSubEv); + }); + + it('Transactions subscription event succeeds', async () => { + await msgHandler(JSON.stringify(txSubEv) as any); + expect(callbacks.onEventReceived).toHaveBeenCalledWith(txSubEv); + }); + }); +}); diff --git a/test/middleware.spec.ts b/test/middleware.spec.ts deleted file mode 100644 index e3cafcf..0000000 --- a/test/middleware.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -import * as mdw from '../src/worker/middleware'; -import * as WebSocket from 'ws'; -import { Logger } from '@nestjs/common'; -import { mock } from 'jest-mock-extended'; -import { objSubEv, txSubEv } from './data/subscription-events'; - -import { createMessageHandler, Callbacks } from '../src/worker/middleware'; - -it('just connects', async () => { - let future: any; - const promise = new Promise((resolve, reject) => { - future = { resolve, reject }; - }); - const ws = await mdw.createNewConnection({ - onDisconnected: (error) => { - if (error) { - console.error(error); - future?.reject(error); - } else { - future?.resolve(); - } - }, - onConnected: () => { - ws.close(); - }, - }); - - await promise; -}); - -describe('createMessageHandler', () => { - const initTestContext = () => { - const callbacks = mock(); - const ws = mock(); - const logger = mock(); - const msgHandler = createMessageHandler(callbacks, ws, logger); - return { callbacks, ws, logger, msgHandler }; - }; - - type T = ReturnType; - let callbacks: T['callbacks'] = null as any; - let ws: T['ws'] = null as any; - let logger: T['logger'] = null as any; - let msgHandler: T['msgHandler'] = null as any; - beforeEach(() => { - ({ callbacks, ws, logger, msgHandler } = initTestContext()); - }); - it('random string will cause error', async () => { - expect(() => msgHandler('"some string"' as any)).rejects.toThrow( - 'Unknown message received: "some string"', - ); - expect(ws.close).toHaveBeenCalledWith(); - }); - it('"already subscribed to target" not throwing error', async () => { - //not throwing an error is enough - await msgHandler('"already subscribed to target"' as any); - }); - - it('subscribed to all transactions', async () => { - await msgHandler('[1,"sentence","Transactions"]' as any); - expect(logger.debug).toHaveBeenCalledWith('Subscribed to all transactions'); - }); - it('subscribed to one contract', async () => { - await msgHandler('["the-contract"]' as any); - expect(logger.debug).toHaveBeenCalledWith('Subscribed to 1 contract'); - }); - it('subscribed to multiple contracts', async () => { - await msgHandler('["ct_1",2,5]' as any); - expect(logger.debug).toHaveBeenCalledWith('Subscribed to 3 contracts'); - }); - it('unrecognized object will cause error', async () => { - expect(() => - msgHandler('{"prop": "oject without proper shape"}' as any), - ).rejects.toThrow( - 'Unknown message received: {"prop": "oject without proper shape"}', - ); - expect(ws.close).toHaveBeenCalledWith(); - }); - it('Oject subscription event succeeds', async () => { - await msgHandler(JSON.stringify(objSubEv) as any); - expect(callbacks.onEventReceived).toHaveBeenCalledWith(objSubEv); - }); - it('Transactions subscription event succeeds', async () => { - await msgHandler(JSON.stringify(txSubEv) as any); - expect(callbacks.onEventReceived).toHaveBeenCalledWith(txSubEv); - }); -}); diff --git a/test/pair-sync.service.e2e-spec.ts b/test/pair-sync.service.e2e-spec.ts new file mode 100644 index 0000000..5aed138 --- /dev/null +++ b/test/pair-sync.service.e2e-spec.ts @@ -0,0 +1,113 @@ +import { mockContext } from './utils'; +import prisma from '@prisma/client'; +import * as data from './data/context-mockups'; +import { PairSyncService } from '../src/tasks/pair-sync.service'; +import { PrismaService } from '../src/database/prisma.service'; +import { Test, TestingModule } from '@nestjs/testing'; +import { TokenDbService } from '../src/database/token/token-db.service'; +import { PairDbService } from '../src/database/pair/pair-db.service'; +import { MdwWsClientService } from '../src/clients/mdw-ws-client.service'; +import { clean as cleanDb } from './utils/db'; +import { SdkClientService } from '../src/clients/sdk-client.service'; + +// Testing method +// 1. before all create a common context +// 2. before all reset mockups (Context and Prisma Client) +// 3. run worker methods and test the impact on db + +describe('PairSyncService', () => { + let service: PairSyncService; + let prismaService: PrismaService; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PairSyncService, + PrismaService, + TokenDbService, + PairDbService, + MdwWsClientService, + SdkClientService, + ], + }).compile(); + + service = module.get(PairSyncService); + prismaService = module.get(PrismaService); + await module.init(); + }); + + beforeEach(async () => { + await cleanDb(prismaService); + service.ctx = mockContext(data.context2); + await service['refreshPairs'](); + }); + + afterAll(async () => { + await prismaService.$disconnect(); + }); + + it('inserts new pairs', async () => { + const pairs: prisma.Pair[] = await prismaService.pair.findMany(); + expect(await prismaService.pair.count()).toBe(3); + for (let i = 0; i < 3; i++) { + expect(pairs[i]).toMatchObject({ + address: data.context2.pairs[i].address, + synchronized: false, + }); + } + }); + + it('refresh pairs liquidity', async () => { + await service['refreshPairsLiquidity'](); + + const pairs = await prismaService.pair.findMany({ + include: { liquidityInfo: true }, + orderBy: { address: 'asc' }, + }); + for (let i = 0; i < 3; i++) { + const expected = data.context2.pairs[i]; + expect(pairs[i].liquidityInfo).toMatchObject({ + reserve0: expected.reserve0.toString(), + reserve1: expected.reserve1.toString(), + totalSupply: expected.totalSupply.toString(), + }); + } + }); + + it('refresh new added pairs', async () => { + service.ctx = mockContext(data.context21); + await service['refreshPairs'](); + const pairs: prisma.Pair[] = await prismaService.pair.findMany({ + orderBy: { id: 'asc' }, + }); + expect(await prismaService.pair.count()).toBe(4); + expect(pairs[3]).toMatchObject({ + address: data.context21.pairs[3].address, + synchronized: false, + }); + }); + + it('refresh liquidity for new added pairs', async () => { + service.ctx = mockContext(data.context21); + await service['refreshPairs'](); + await service['refreshPairsLiquidity'](); + const pairs = await prismaService.pair.findMany({ + include: { liquidityInfo: true }, + orderBy: { address: 'asc' }, + }); + const expected = data.context21.pairs[3]; + expect(pairs[3].liquidityInfo).toMatchObject({ + reserve0: expected.reserve0.toString(), + reserve1: expected.reserve1.toString(), + totalSupply: expected.totalSupply.toString(), + }); + }); + + it('unsync all pairs', async () => { + await service['unsyncAllPairs'](); + const pairs = await prismaService.pair.findMany({ + where: { synchronized: true }, + }); + expect(pairs.length).toBe(0); + }); +}); diff --git a/test/pair-sync.service.spec.ts b/test/pair-sync.service.spec.ts new file mode 100644 index 0000000..abb93cf --- /dev/null +++ b/test/pair-sync.service.spec.ts @@ -0,0 +1,259 @@ +import { Logger } from '@nestjs/common'; +import { mock } from 'jest-mock-extended'; + +import * as data from './data/context-mockups'; +import * as utils from './utils'; +import { mockContext, mockupEnvVars, TEST_NET_VARS } from './utils'; +import { objSubEv, swapEvent, swapTxInfo } from './data/subscription-events'; +import { PairSyncService } from '../src/tasks/pair-sync.service'; +import { Test, TestingModule } from '@nestjs/testing'; +import { PairDbService } from '../src/database/pair/pair-db.service'; +import { TokenDbService } from '../src/database/token/token-db.service'; +import { MdwWsClientService } from '../src/clients/mdw-ws-client.service'; +import { Context } from '../src/tasks/pair-sync.model'; +import { SdkClientService } from '../src/clients/sdk-client.service'; +import { ContractAddress } from '../src/clients/sdk-client.model'; + +describe('PairSyncService', () => { + let service: PairSyncService; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PairSyncService, + SdkClientService, + { provide: MdwWsClientService, useValue: {} }, + { provide: PairDbService, useValue: {} }, + { + provide: TokenDbService, + useValue: {}, + }, + ], + }).compile(); + service = module.get(PairSyncService); + await module.init(); + }); + + describe('createOnEventReceived', () => { + let ctx: ReturnType = null as any; + + const initTestContext = (service: PairSyncService) => { + type Ev = { + onFactory: () => Promise; + refreshPairsLiquidity: (contract: ContractAddress) => Promise; + getAllAddresses: () => Promise; + }; + const { onFactory, refreshPairsLiquidity, getAllAddresses } = mock(); + const logger = mock(); + const eventHandler = service['createOnEventReceived']( + logger, + onFactory, + refreshPairsLiquidity, + getAllAddresses, + ); + return { + getAllAddresses, + logger, + eventHandler, + onFactory, + refreshPairsLiquidity, + }; + }; + type T = ReturnType; + let logger: T['logger'] = null as any; + let eventHandler: T['eventHandler'] = null as any; + let onFactory: T['onFactory'] = null as any; + let refreshPairsLiquidity: T['refreshPairsLiquidity'] = null as any; + let getAllAddresses: T['getAllAddresses'] = null as any; + + beforeEach(async () => { + ctx = mockContext(data.context2); + service.ctx = ctx; + ({ + logger, + eventHandler, + onFactory, + refreshPairsLiquidity, + getAllAddresses, + } = initTestContext(service)); + }); + it('ignores unknown types', async () => { + await eventHandler({ + ...objSubEv, + payload: { + ...objSubEv.payload, + tx: { + ...objSubEv.payload.tx, + type: 'Some-Random-Type' as any, + }, + }, + }); + expect(logger.debug).toHaveBeenCalledWith( + `Ignoring transaction of type 'Some-Random-Type'`, + ); + }); + it('throws error if no txInfo', async () => { + expect( + async () => + await eventHandler({ + ...objSubEv, + }), + ).rejects.toThrow(`No tx info for hash 'th_1'`); + }); + + it('ignores inverted transaction', async () => { + ctx.node.getTransactionInfoByHash + .calledWith(swapEvent.payload.hash) + .mockReturnValue( + Promise.resolve({ + ...swapTxInfo, + callInfo: { + ...swapTxInfo.callInfo, + returnType: 'revert', + }, + }), + ); + await eventHandler({ + ...swapEvent, + }); + expect(logger.debug).toHaveBeenCalledWith( + `Ignore reverted transaction: '${swapEvent.payload.hash}'`, + ); + }); + + it('refresh pairs in factory', async () => { + ctx.node.getTransactionInfoByHash + .calledWith(objSubEv.payload.hash) + .mockReturnValue( + Promise.resolve({ + ...swapTxInfo, + callInfo: { + ...swapTxInfo.callInfo, + log: [ + { + address: process.env.FACTORY_ADDRESS as any, + data: 'cb_1=', + topics: [], + }, + ], + }, + }), + ); + getAllAddresses.calledWith().mockReturnValue(Promise.resolve([])); + await eventHandler({ + ...objSubEv, + }); + expect(onFactory).toHaveBeenCalledWith(1); + }); + + it('refresh pair liquidity', async () => { + ctx.node.getTransactionInfoByHash + .calledWith(objSubEv.payload.hash) + .mockReturnValue( + Promise.resolve({ + ...swapTxInfo, + callInfo: { + ...swapTxInfo.callInfo, + log: [ + { + address: 'ct_p1', + data: 'cb_1=', + topics: [], + }, + { + address: 'ct_p2', + data: 'cb_1=', + topics: [], + }, + //this will not be called + { + address: 'ct_p3', + data: 'cb_1=', + topics: [], + }, + ], + }, + }), + ); + getAllAddresses + .calledWith() + .mockReturnValue(Promise.resolve(['ct_p1', 'ct_p2'])); + + await eventHandler({ + ...objSubEv, + }); + expect(refreshPairsLiquidity).toHaveBeenCalledWith('ct_p1', 1); + expect(refreshPairsLiquidity).toHaveBeenCalledWith('ct_p2', 1); + }); + }); + + describe('getContext() on testnet', () => { + let context: Context | null = null; + mockupEnvVars(TEST_NET_VARS); + + beforeAll(async () => { + context = await service['getContext'](); + }); + + const ctx = (): Context => { + if (!context) { + throw 'initiate context first'; + } + return context; + }; + + it('matches the .env factory', async () => { + const { decodedResult: factoryAddress } = await ctx().router.factory(); + expect(factoryAddress).toBe(process.env.FACTORY_ADDRESS); + }); + + it('should have at least 16 pairs', async () => { + expect( + (await ctx().factory.get_all_pairs()).decodedResult.length, + ).toBeGreaterThanOrEqual(16); + }); + + it('pair should return right token addresses', async () => { + const { decodedResult: allPairs } = await ctx().factory.get_all_pairs(); + const pairAddress = allPairs[allPairs.length - 1]; + expect(pairAddress).toBe( + 'ct_efYtiwDg4YZxDWE3iLPzvrjb92CJPvzGwriv4ZRuvuTDMNMb9', + ); + const pairMethods = await ctx().getPair(pairAddress); + if (!pairMethods) { + fail('pairMethods is null'); + } + expect((await pairMethods.token0()).decodedResult).toBe( + 'ct_7tTzPfvv3Vx8pCEcuk1kmgtn4sFsYCQDzLi1LvFs8T5PJqgsC', + ); + expect((await pairMethods.token1()).decodedResult).toBe( + 'ct_b7FZHQzBcAW4r43ECWpV3qQJMQJp5BxkZUGNKrqqLyjVRN3SC', + ); + }); + + it('regular aex9 token should have right metaInfo', async () => { + const tokenMethods = await ctx().getToken( + 'ct_7tTzPfvv3Vx8pCEcuk1kmgtn4sFsYCQDzLi1LvFs8T5PJqgsC', + ); + + const { decodedResult: metaInfo } = await tokenMethods.meta_info(); + expect(metaInfo).toEqual({ + decimals: 18n, + name: 'TestAEX9-B', + symbol: 'TAEX9-B', + }); + }); + + it('WAE token should have right metaInfo', async () => { + const tokenMethods = await ctx().getToken( + 'ct_JDp175ruWd7mQggeHewSLS1PFXt9AzThCDaFedxon8mF8xTRF', + ); + const { decodedResult: metaInfo } = await tokenMethods.meta_info(); + expect(metaInfo).toEqual({ + decimals: 18n, + name: 'Wrapped Aeternity', + symbol: 'WAE', + }); + }); + }); +}); diff --git a/test/pairs.e2e-spec.ts b/test/pairs.e2e-spec.ts index 2cef85b..739a5c6 100644 --- a/test/pairs.e2e-spec.ts +++ b/test/pairs.e2e-spec.ts @@ -2,269 +2,531 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; import { listToken, mockContext, sortByAddress } from './utils'; -import createWorkerMethods from '../src/worker'; import * as data from './data/context-mockups'; -import * as db from './utils/db'; -import { ApiModule } from '../src/api/api.module'; - -type WorkerMethods = ReturnType; -let activeWorker: WorkerMethods; +import { clean as cleanDb } from './utils/db'; +import { PrismaService } from '../src/database/prisma.service'; +import { PairSyncService } from '../src/tasks/pair-sync.service'; +import { PairsController } from '../src/api/pairs/pairs.controller'; +import { PairsService } from '../src/api/pairs/pairs.service'; +import { PairDbService } from '../src/database/pair/pair-db.service'; +import { TokenDbService } from '../src/database/token/token-db.service'; +import { MdwWsClientService } from '../src/clients/mdw-ws-client.service'; +import { SdkClientService } from '../src/clients/sdk-client.service'; // Testing method -// 1. before each -// - create a common context -// - reset mockups (Context and Prisma Client) -// - refreshPairs -// 2. before each -// - initiate nest app - -describe('pairs fetching (e2e)', () => { +// before all +// - initiate nest app +// before each +// - clean db +// - create a common context +// - refreshPairs +// after all +// - close nest app +// - disconnect from prisma +describe('PairsController', () => { let app: INestApplication; - beforeEach(async () => { - await db.clean(); - const ctx = mockContext(data.context2); - activeWorker = createWorkerMethods(ctx); - await activeWorker.refreshPairs(); - }); + let prismaService: PrismaService; + let pairSyncService: PairSyncService; - beforeEach(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [ApiModule], + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [PairsController], + providers: [ + MdwWsClientService, + SdkClientService, + PairDbService, + PairDbService, + PairsService, + PairSyncService, + PrismaService, + TokenDbService, + ], }).compile(); - app = moduleFixture.createNestApplication(); + app = module.createNestApplication(); await app.init(); + + prismaService = module.get(PrismaService); + pairSyncService = module.get(PairSyncService); + }); + + afterAll(async () => { + await prismaService.$disconnect(); + await app.close(); + }); + + beforeEach(async () => { + await cleanDb(prismaService); + pairSyncService.ctx = mockContext(data.context2); + await pairSyncService['refreshPairs'](); }); - it('/pairs (GET) 200 unsynced', () => { - return request(app.getHttpServer()) - .get('/pairs') - .expect(200) - .expect([ + describe('/pairs', () => { + it('/pairs (GET) 200 unsynced', () => { + return request(app.getHttpServer()) + .get('/pairs') + .expect(200) + .expect([ + { + address: 'ct_p1', + token0: 'ct_t0', + token1: 'ct_t1', + synchronized: false, + }, + { + address: 'ct_p2', + token0: 'ct_t1', + token1: 'ct_t3', + synchronized: false, + }, + { + address: 'ct_p3', + token0: 'ct_t0', + token1: 'ct_t3', + synchronized: false, + }, + ]); + }); + + it('/pairs (GET) 200 synchronized', async () => { + await pairSyncService['refreshPairsLiquidity'](); + const response = await request(app.getHttpServer()) + .get('/pairs') + .expect(200); + + expect(sortByAddress(JSON.parse(response.text))).toEqual([ { address: 'ct_p1', token0: 'ct_t0', token1: 'ct_t1', - synchronized: false, + synchronized: true, }, { address: 'ct_p2', token0: 'ct_t1', token1: 'ct_t3', - synchronized: false, + synchronized: true, }, { address: 'ct_p3', token0: 'ct_t0', token1: 'ct_t3', + synchronized: true, + }, + ]); + }); + + it('/pairs (GET) 200 with new pair', async () => { + await pairSyncService['refreshPairsLiquidity'](); + pairSyncService.ctx = mockContext(data.context21); + await pairSyncService['refreshPairs'](); + const response = await request(app.getHttpServer()) + .get('/pairs') + .expect(200); + + expect(sortByAddress(JSON.parse(response.text))).toEqual([ + { + address: 'ct_p1', + token0: 'ct_t0', + token1: 'ct_t1', + synchronized: true, + }, + { + address: 'ct_p2', + token0: 'ct_t1', + token1: 'ct_t3', + synchronized: true, + }, + { + address: 'ct_p3', + token0: 'ct_t0', + token1: 'ct_t3', + synchronized: true, + }, + { + address: 'ct_p4', synchronized: false, + token0: 'ct_t0', + token1: 'ct_t4', }, ]); - }); + }); - it('/pairs (GET) 200 synchronized', async () => { - await activeWorker.refreshPairsLiquidity(); - const response = await request(app.getHttpServer()) - .get('/pairs') - .expect(200); + it('/pairs (GET) 200 only-listed=true', async () => { + await pairSyncService['refreshPairsLiquidity'](); + pairSyncService.ctx = mockContext(data.context21); + await pairSyncService['refreshPairs'](); - expect(sortByAddress(JSON.parse(response.text))).toEqual([ - { - address: 'ct_p1', - token0: 'ct_t0', - token1: 'ct_t1', - synchronized: true, - }, - { - address: 'ct_p2', - token0: 'ct_t1', - token1: 'ct_t3', - synchronized: true, - }, - { - address: 'ct_p3', - token0: 'ct_t0', - token1: 'ct_t3', - synchronized: true, - }, - ]); - }); + let response = await request(app.getHttpServer()) + .get('/pairs?only-listed=true') + .expect(200); - it('/pairs (GET) 200 with new pair', async () => { - await activeWorker.refreshPairsLiquidity(); - const ctx = mockContext(data.context21); - activeWorker = createWorkerMethods(ctx); - await activeWorker.refreshPairs(); - const response = await request(app.getHttpServer()) - .get('/pairs') - .expect(200); + expect(sortByAddress(JSON.parse(response.text))).toEqual([]); + await listToken(prismaService, 'ct_t0'); + await listToken(prismaService, 'ct_t3'); + await listToken(prismaService, 'ct_t4'); - expect(sortByAddress(JSON.parse(response.text))).toEqual([ - { - address: 'ct_p1', - token0: 'ct_t0', - token1: 'ct_t1', - synchronized: true, - }, - { - address: 'ct_p2', - token0: 'ct_t1', - token1: 'ct_t3', - synchronized: true, - }, - { - address: 'ct_p3', - token0: 'ct_t0', - token1: 'ct_t3', - synchronized: true, - }, - { - address: 'ct_p4', - synchronized: false, - token0: 'ct_t0', - token1: 'ct_t4', - }, - ]); - }); + response = await request(app.getHttpServer()) + .get('/pairs?only-listed=true') + .expect(200); + expect(sortByAddress(JSON.parse(response.text))).toEqual([ + { + address: 'ct_p3', + token0: 'ct_t0', + token1: 'ct_t3', + synchronized: true, + }, + { + address: 'ct_p4', + token0: 'ct_t0', + token1: 'ct_t4', + synchronized: false, + }, + ]); + }); - it('/pairs (GET) 200 only-listed=true', async () => { - await activeWorker.refreshPairsLiquidity(); - const ctx = mockContext(data.context21); - activeWorker = createWorkerMethods(ctx); - await activeWorker.refreshPairs(); + it('/pairs/by-address/ct_p1 (GET) 200 no liquidity', async () => { + return request(app.getHttpServer()) + .get('/pairs/by-address/ct_p1') + .expect(200) + .expect({ + address: 'ct_p1', + token0: { + address: 'ct_t0', + symbol: 'A', + name: 'A Token', + decimals: 18, + listed: false, + malformed: false, + noContract: false, + }, + token1: { + address: 'ct_t1', + symbol: 'B', + name: 'B Token', + decimals: 6, + listed: false, + malformed: false, + noContract: false, + }, + synchronized: false, + }); + }); - let response = await request(app.getHttpServer()) - .get('/pairs?only-listed=true') - .expect(200); + it('/pairs/by-address/ct_p1 (GET) 200 synchronized', async () => { + await pairSyncService['refreshPairsLiquidity'](); + return request(app.getHttpServer()) + .get('/pairs/by-address/ct_p1') + .expect(200) + .expect({ + address: 'ct_p1', + token0: { + address: 'ct_t0', + symbol: 'A', + name: 'A Token', + decimals: 18, + listed: false, + malformed: false, + noContract: false, + }, + token1: { + address: 'ct_t1', + symbol: 'B', + name: 'B Token', + decimals: 6, + listed: false, + malformed: false, + noContract: false, + }, + synchronized: true, + liquidityInfo: { + height: 1, + totalSupply: '2', + reserve0: '1', + reserve1: '2', + }, + }); + }); - expect(sortByAddress(JSON.parse(response.text))).toEqual([]); - await listToken('ct_t0'); - await listToken('ct_t3'); - await listToken('ct_t4'); + it('/pairs/by-address/ct_p1 (GET) 200 unsynchronized with liquidity', async () => { + await pairSyncService['refreshPairsLiquidity'](); + await pairSyncService['unsyncAllPairs'](); + return request(app.getHttpServer()) + .get('/pairs/by-address/ct_p1') + .expect(200) + .expect({ + address: 'ct_p1', + token0: { + address: 'ct_t0', + symbol: 'A', + name: 'A Token', + decimals: 18, + listed: false, + malformed: false, + noContract: false, + }, + token1: { + address: 'ct_t1', + symbol: 'B', + name: 'B Token', + decimals: 6, + listed: false, + malformed: false, + noContract: false, + }, + synchronized: false, + liquidityInfo: { + height: 1, + totalSupply: '2', + reserve0: '1', + reserve1: '2', + }, + }); + }); - response = await request(app.getHttpServer()) - .get('/pairs?only-listed=true') - .expect(200); - expect(sortByAddress(JSON.parse(response.text))).toEqual([ - { - address: 'ct_p3', - token0: 'ct_t0', - token1: 'ct_t3', - synchronized: true, - }, - { - address: 'ct_p4', - token0: 'ct_t0', - token1: 'ct_t4', - synchronized: false, - }, - ]); + it('/pairs/by-address/ct_0000 (GET) 404 not founded pair', async () => { + return request(app.getHttpServer()) + .get('/pairs/by-address/ct_0000') + .expect(404) + .expect({ + statusCode: 404, + message: 'pair not found', + error: 'Not Found', + }); + }); }); - it('/pairs/by-address/ct_p1 (GET) 200 no liquidity', async () => { - return request(app.getHttpServer()) - .get('/pairs/by-address/ct_p1') - .expect(200) - .expect({ - address: 'ct_p1', - token0: { - address: 'ct_t0', - symbol: 'A', - name: 'A Token', - decimals: 18, - listed: false, - malformed: false, - noContract: false, - }, - token1: { - address: 'ct_t1', - symbol: 'B', - name: 'B Token', - decimals: 6, - listed: false, - malformed: false, - noContract: false, - }, - synchronized: false, - }); - }); - it('/pairs/by-address/ct_p1 (GET) 200 synchronized', async () => { - await activeWorker.refreshPairsLiquidity(); - return request(app.getHttpServer()) - .get('/pairs/by-address/ct_p1') - .expect(200) - .expect({ - address: 'ct_p1', - token0: { - address: 'ct_t0', - symbol: 'A', - name: 'A Token', - decimals: 18, - listed: false, - malformed: false, - noContract: false, - }, - token1: { - address: 'ct_t1', - symbol: 'B', - name: 'B Token', - decimals: 6, - listed: false, - malformed: false, - noContract: false, - }, - synchronized: true, - liquidityInfo: { - height: 1, - totalSupply: '2', - reserve0: '1', - reserve1: '2', - }, - }); - }); - it('/pairs/by-address/ct_p1 (GET) 200 unsynchronized with liquidity', async () => { - await activeWorker.refreshPairsLiquidity(); - await activeWorker.unsyncAllPairs(); - return request(app.getHttpServer()) - .get('/pairs/by-address/ct_p1') - .expect(200) - .expect({ - address: 'ct_p1', - token0: { - address: 'ct_t0', - symbol: 'A', - name: 'A Token', - decimals: 18, - listed: false, - malformed: false, - noContract: false, - }, - token1: { - address: 'ct_t1', - symbol: 'B', - name: 'B Token', - decimals: 6, - listed: false, - malformed: false, - noContract: false, - }, - synchronized: false, - liquidityInfo: { - height: 1, - totalSupply: '2', - reserve0: '1', - reserve1: '2', - }, + describe('/pairs/swap-routes', () => { + it('/pairs/swap-routes/ct_t0/ct_t5 (GET) 200 no path for unexisting token ', async () => { + return request(app.getHttpServer()) + .get('/pairs/swap-routes/ct_t0/ct_t5') + .expect(200) + .expect([]); + }); + + it('/pairs/swap-routes/ct_t0/ct_t5 (GET) 200 no path for unexisting pair', async () => { + pairSyncService.ctx = mockContext({ + ...data.context2, + tokens: data.context2.tokens.concat({ + address: 'ct_t4', + metaInfo: { + name: 'D Token', + symbol: 'D', + decimals: 18n, + }, + }), }); - }); - it('/pairs/by-address/ct_0000 (GET) 404 not founded pair', async () => { - return request(app.getHttpServer()) - .get('/pairs/by-address/ct_0000') - .expect(404) - .expect({ - statusCode: 404, - message: 'pair not found', - error: 'Not Found', + await pairSyncService['refreshPairs'](); + + return request(app.getHttpServer()) + .get('/pairs/swap-routes/ct_t0/ct_t5') + .expect(200) + .expect([]); + }); + + it('/pairs/swap-routes/ct_t0/ct_t4 (GET) 200 direct path', async () => { + pairSyncService.ctx = mockContext(data.context21); + await pairSyncService['refreshPairs'](); + + return request(app.getHttpServer()) + .get('/pairs/swap-routes/ct_t0/ct_t4') + .expect(200) + .expect([ + [ + { + address: 'ct_p4', + token0: 'ct_t0', + token1: 'ct_t4', + synchronized: false, + }, + ], + ]); + }); + + it('/pairs/swap-routes/ct_t0/ct_t3 (GET) 200 synchronized', async () => { + pairSyncService.ctx = mockContext(data.context21); + await pairSyncService['refreshPairsLiquidity'](); + + return request(app.getHttpServer()) + .get('/pairs/swap-routes/ct_t0/ct_t1') + .expect(200) + .expect([ + [ + { + address: 'ct_p1', + token0: 'ct_t0', + token1: 'ct_t1', + synchronized: true, + liquidityInfo: { + height: 1, + totalSupply: '2', + reserve0: '1', + reserve1: '2', + }, + }, + ], + [ + { + address: 'ct_p3', + token0: 'ct_t0', + token1: 'ct_t3', + synchronized: true, + liquidityInfo: { + height: 1, + totalSupply: '3', + reserve0: '1', + reserve1: '3', + }, + }, + { + address: 'ct_p2', + token0: 'ct_t1', + token1: 'ct_t3', + synchronized: true, + liquidityInfo: { + height: 1, + totalSupply: '200000', + reserve0: '10', + reserve1: '20000', + }, + }, + ], + ]); + }); + + it('/pairs/swap-routes/ct_t0/ct_t1 (GET) 200 one direct path and one indirect path', async () => { + return request(app.getHttpServer()) + .get('/pairs/swap-routes/ct_t0/ct_t1') + .expect(200) + .expect([ + [ + { + address: 'ct_p1', + token0: 'ct_t0', + token1: 'ct_t1', + synchronized: false, + }, + ], + [ + { + address: 'ct_p3', + token0: 'ct_t0', + token1: 'ct_t3', + synchronized: false, + }, + { + address: 'ct_p2', + token0: 'ct_t1', + token1: 'ct_t3', + synchronized: false, + }, + ], + ]); + }); + + it('/pairs/swap-routes/ct_t0/ct_t1?only-listed=true (GET) 200 suppress some paths', async () => { + await listToken(prismaService, 'ct_t0'); + await listToken(prismaService, 'ct_t1'); + return request(app.getHttpServer()) + .get('/pairs/swap-routes/ct_t0/ct_t1?only-listed=true') + .expect(200) + .expect([ + [ + { + address: 'ct_p1', + token0: 'ct_t0', + token1: 'ct_t1', + synchronized: false, + }, + ], + ]); + }); + + it('/pairs/swap-routes/ct_t1/ct_t2 (GET) 200 testing reverse order of tokens', async () => { + return request(app.getHttpServer()) + .get('/pairs/swap-routes/ct_t1/ct_t0') + .expect(200) + .expect([ + [ + { + address: 'ct_p1', + token0: 'ct_t0', + token1: 'ct_t1', + synchronized: false, + }, + ], + [ + { + address: 'ct_p2', + token0: 'ct_t1', + token1: 'ct_t3', + synchronized: false, + }, + { + address: 'ct_p3', + token0: 'ct_t0', + token1: 'ct_t3', + synchronized: false, + }, + ], + ]); + }); + + it('/pairs/swap-routes/ct_t0/ct_t1 (GET) 200 one direct path and multiple indirect path', async () => { + pairSyncService.ctx = mockContext({ + ...data.context21, + pairs: data.context21.pairs.concat({ + address: 'ct_p5', + reserve0: 1n, + reserve1: 4n, + totalSupply: 1n * 4n, + t0: 1, + t1: 3, + }), }); + await pairSyncService['refreshPairs'](); + + return request(app.getHttpServer()) + .get('/pairs/swap-routes/ct_t0/ct_t1') + .expect(200) + .expect([ + [ + { + address: 'ct_p1', + token0: 'ct_t0', + token1: 'ct_t1', + synchronized: false, + }, + ], + [ + { + address: 'ct_p3', + token0: 'ct_t0', + token1: 'ct_t3', + synchronized: false, + }, + { + address: 'ct_p2', + token0: 'ct_t1', + token1: 'ct_t3', + synchronized: false, + }, + ], + [ + { + address: 'ct_p4', + token0: 'ct_t0', + token1: 'ct_t4', + synchronized: false, + }, + { + address: 'ct_p5', + token0: 'ct_t1', + token1: 'ct_t4', + synchronized: false, + }, + ], + ]); + }); }); }); diff --git a/test/paths.spec.ts b/test/paths.spec.ts index 6792966..7a9f754 100644 --- a/test/paths.spec.ts +++ b/test/paths.spec.ts @@ -17,6 +17,7 @@ it('a path with one in the middle', () => { expect(getPaths('a', 'c', edges)).toStrictEqual([['a_b', 'b_c']]); }); + it('a path with two in the middle and a direct one', () => { const edges: Edge[] = [ { data: 'a_b', t0: 'a', t1: 'b' }, diff --git a/test/swap-routes.e2e-spec.ts b/test/swap-routes.e2e-spec.ts deleted file mode 100644 index 0c6fb4a..0000000 --- a/test/swap-routes.e2e-spec.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; - -import { listToken, mockContext } from './utils'; -import worker from '../src/worker'; -import * as db from './utils/db'; -import * as data from './data/context-mockups'; -import { ApiModule } from '../src/api/api.module'; - -type WorkerMethods = ReturnType; -let activeWorker: WorkerMethods; - -// Testing method: -// for every test we initiate context with different data as: -// - create a common context -// - reset mockups (Context and Prisma Client) -// - refreshPairs -// - initiate nest app - -let app: INestApplication; -const initWorker = async (dataCtx: any) => { - await db.clean(); - const ctx = mockContext(dataCtx); - activeWorker = worker(ctx); - await activeWorker.refreshPairs(); -}; - -const initApp = async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [ApiModule], - }).compile(); - - app = moduleFixture.createNestApplication(); - await app.init(); -}; - -const init = async (dataCtx: any) => { - await initWorker(dataCtx); - await initApp(); -}; - -describe('swap-routes fetching (e2e)', () => { - it('/pairs/swap-routes/ct_t0/ct_t5 (GET) 200 no path for unexisting token ', async () => { - await init(data.context2); - - return request(app.getHttpServer()) - .get('/pairs/swap-routes/ct_t0/ct_t5') - .expect(200) - .expect([]); - }); - it('/pairs/swap-routes/ct_t0/ct_t5 (GET) 200 no path for unexisting pair', async () => { - await init({ - ...data.context2, - tokens: data.context2.tokens.concat({ - address: 'ct_t4', - metaInfo: { - name: 'D Token', - symbol: 'D', - decimals: 18n, - }, - }), - }); - - return request(app.getHttpServer()) - .get('/pairs/swap-routes/ct_t0/ct_t5') - .expect(200) - .expect([]); - }); - it('/pairs/swap-routes/ct_t0/ct_t4 (GET) 200 direct path', async () => { - await init(data.context21); - - return request(app.getHttpServer()) - .get('/pairs/swap-routes/ct_t0/ct_t4') - .expect(200) - .expect([ - [ - { - address: 'ct_p4', - token0: 'ct_t0', - token1: 'ct_t4', - synchronized: false, - }, - ], - ]); - }); - it('/pairs/swap-routes/ct_t0/ct_t3 (GET) 200 synchronized', async () => { - await init(data.context2); - await activeWorker.refreshPairsLiquidity(); - - return request(app.getHttpServer()) - .get('/pairs/swap-routes/ct_t0/ct_t1') - .expect(200) - .expect([ - [ - { - address: 'ct_p1', - token0: 'ct_t0', - token1: 'ct_t1', - synchronized: true, - liquidityInfo: { - height: 1, - totalSupply: '2', - reserve0: '1', - reserve1: '2', - }, - }, - ], - [ - { - address: 'ct_p3', - token0: 'ct_t0', - token1: 'ct_t3', - synchronized: true, - liquidityInfo: { - height: 1, - totalSupply: '3', - reserve0: '1', - reserve1: '3', - }, - }, - { - address: 'ct_p2', - token0: 'ct_t1', - token1: 'ct_t3', - synchronized: true, - liquidityInfo: { - height: 1, - totalSupply: '200000', - reserve0: '10', - reserve1: '20000', - }, - }, - ], - ]); - }); - it('/pairs/swap-routes/ct_t0/ct_t1 (GET) 200 one direct path and one indirect path', async () => { - await init(data.context2); - - return request(app.getHttpServer()) - .get('/pairs/swap-routes/ct_t0/ct_t1') - .expect(200) - .expect([ - [ - { - address: 'ct_p1', - token0: 'ct_t0', - token1: 'ct_t1', - synchronized: false, - }, - ], - [ - { - address: 'ct_p3', - token0: 'ct_t0', - token1: 'ct_t3', - synchronized: false, - }, - { - address: 'ct_p2', - token0: 'ct_t1', - token1: 'ct_t3', - synchronized: false, - }, - ], - ]); - }); - it('/pairs/swap-routes/ct_t0/ct_t1?only-listed=true (GET) 200 suppress some paths', async () => { - await init(data.context2); - - await listToken('ct_t0'); - await listToken('ct_t1'); - return request(app.getHttpServer()) - .get('/pairs/swap-routes/ct_t0/ct_t1?only-listed=true') - .expect(200) - .expect([ - [ - { - address: 'ct_p1', - token0: 'ct_t0', - token1: 'ct_t1', - synchronized: false, - }, - ], - ]); - }); - it('/pairs/swap-routes/ct_t1/ct_t2 (GET) 200 testing reverse order of tokens', async () => { - await init(data.context2); - - return request(app.getHttpServer()) - .get('/pairs/swap-routes/ct_t1/ct_t0') - .expect(200) - .expect([ - [ - { - address: 'ct_p1', - token0: 'ct_t0', - token1: 'ct_t1', - synchronized: false, - }, - ], - [ - { - address: 'ct_p2', - token0: 'ct_t1', - token1: 'ct_t3', - synchronized: false, - }, - { - address: 'ct_p3', - token0: 'ct_t0', - token1: 'ct_t3', - synchronized: false, - }, - ], - ]); - }); - it('/pairs/swap-routes/ct_t0/ct_t1 (GET) 200 one direct path and multiple indirect path', async () => { - await init({ - ...data.context21, - pairs: data.context21.pairs.concat({ - address: 'ct_p5', - reserve0: 1n, - reserve1: 4n, - totalSupply: 1n * 4n, - t0: 1, - t1: 3, - }), - }); - - return request(app.getHttpServer()) - .get('/pairs/swap-routes/ct_t0/ct_t1') - .expect(200) - .expect([ - [ - { - address: 'ct_p1', - token0: 'ct_t0', - token1: 'ct_t1', - synchronized: false, - }, - ], - [ - { - address: 'ct_p3', - token0: 'ct_t0', - token1: 'ct_t3', - synchronized: false, - }, - { - address: 'ct_p2', - token0: 'ct_t1', - token1: 'ct_t3', - synchronized: false, - }, - ], - [ - { - address: 'ct_p4', - token0: 'ct_t0', - token1: 'ct_t4', - synchronized: false, - }, - { - address: 'ct_p5', - token0: 'ct_t1', - token1: 'ct_t4', - synchronized: false, - }, - ], - ]); - }); -}); diff --git a/test/tokens.e2e-spec.ts b/test/tokens.e2e-spec.ts index 8af3189..f953b7b 100644 --- a/test/tokens.e2e-spec.ts +++ b/test/tokens.e2e-spec.ts @@ -2,239 +2,86 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; -import worker from '../src/worker'; import { clean as cleanDb } from './utils/db'; import * as data from './data/context-mockups'; import * as dto from '../src/dto'; import * as utils from './utils'; import { nonNullable } from '../src/lib/utils'; -import { ApiModule } from '../src/api/api.module'; - -type WorkerMethods = ReturnType; -let activeWorker: WorkerMethods; +import { PrismaService } from '../src/database/prisma.service'; +import { PairSyncService } from '../src/tasks/pair-sync.service'; +import { PairDbService } from '../src/database/pair/pair-db.service'; +import { TokenDbService } from '../src/database/token/token-db.service'; +import { MdwWsClientService } from '../src/clients/mdw-ws-client.service'; +import { TokensController } from '../src/api/tokens/tokens.controller'; +import { TokensService } from '../src/api/tokens/tokens.service'; +import { mockContext } from './utils'; +import { SdkClientService } from '../src/clients/sdk-client.service'; // Testing method -// 1. before each -// - create a common context -// - reset mockups (Context and Prisma Client) -// - refreshPairs -// 2. before each -// - initiate nest app - -describe('tokens fetching (e2e)', () => { +// before all +// - initiate nest app +// before each +// - clean db +// - create a common context +// - refreshPairs +// after all +// - close nest app +// - disconnect from prisma +describe('TokenController', () => { let app: INestApplication; - beforeEach(async () => { - await cleanDb(); - const ctx = utils.mockContext(data.context2); - activeWorker = worker(ctx); - await activeWorker.refreshPairs(); - }); + let prismaService: PrismaService; + let pairSyncService: PairSyncService; - beforeEach(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [ApiModule], + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [TokensController], + providers: [ + MdwWsClientService, + SdkClientService, + PairDbService, + PairSyncService, + PrismaService, + TokenDbService, + TokensService, + ], }).compile(); - app = moduleFixture.createNestApplication(); + app = module.createNestApplication(); await app.init(); - }); - - it('/tokens (GET)', async () => { - const response = await request(app.getHttpServer()) - .get('/tokens') - .expect(200); - const value: dto.TokenWithListed[] = JSON.parse(response.text); - - expect(utils.sortByAddress(value)).toEqual([ - { - address: 'ct_t0', - symbol: 'A', - name: 'A Token', - decimals: 18, - listed: false, - malformed: false, - noContract: false, - }, - { - address: 'ct_t1', - symbol: 'B', - name: 'B Token', - decimals: 6, - listed: false, - malformed: false, - noContract: false, - }, - { - address: 'ct_t3', - symbol: 'C', - name: 'C Token', - decimals: 10, - listed: false, - malformed: false, - noContract: false, - }, - ]); - }); - - it('/tokens/listed (GET) 200 empty', () => { - return request(app.getHttpServer()) - .get('/tokens/listed') - .expect(200) - .expect([]); - }); - - it('/tokens/listed (GET) 200 non-empty', async () => { - await utils.listToken('ct_t0'); - await utils.listToken('ct_t3'); - const response = await request(app.getHttpServer()) - .get('/tokens/listed') - .expect(200); - const value: dto.Token[] = JSON.parse(response.text); - - expect(utils.sortByAddress(value)).toEqual([ - { - address: 'ct_t0', - symbol: 'A', - name: 'A Token', - decimals: 18, - malformed: false, - noContract: false, - }, - { - address: 'ct_t3', - symbol: 'C', - name: 'C Token', - decimals: 10, - malformed: false, - noContract: false, - }, - ]); - }); - it('/tokens (GET) 200 with some listed', async () => { - await utils.listToken('ct_t0'); - await utils.listToken('ct_t3'); - const response = await request(app.getHttpServer()) - .get('/tokens') - .expect(200); - const value: dto.TokenWithListed[] = JSON.parse(response.text); - - expect( - value.sort((a: dto.TokenWithListed, b: dto.TokenWithListed) => - a.address.localeCompare(b.address), - ), - ).toEqual([ - { - address: 'ct_t0', - symbol: 'A', - name: 'A Token', - decimals: 18, - listed: true, - malformed: false, - noContract: false, - }, - { - address: 'ct_t1', - symbol: 'B', - name: 'B Token', - decimals: 6, - listed: false, - malformed: false, - noContract: false, - }, - { - address: 'ct_t3', - symbol: 'C', - name: 'C Token', - decimals: 10, - listed: true, - malformed: false, - noContract: false, - }, - ]); + prismaService = module.get(PrismaService); + pairSyncService = module.get(PairSyncService); }); - it('/tokens/by-address/ct_t0 (GET) 200', () => { - return request(app.getHttpServer()) - .get('/tokens/by-address/ct_t0') - .expect(200) - .expect({ - address: 'ct_t0', - symbol: 'A', - name: 'A Token', - decimals: 18, - listed: false, - malformed: false, - noContract: false, - pairs: ['ct_p1', 'ct_p3'], - }); + beforeEach(async () => { + await cleanDb(prismaService); + pairSyncService.ctx = mockContext(data.context2); + await pairSyncService['refreshPairs'](); }); - it('/tokens/by-address/ct_tXXX (GET) 404', () => { - return request(app.getHttpServer()) - .get('/tokens/by-address/ct_tXXX') - .expect(404) - .expect({ - statusCode: 404, - message: 'token not found', - error: 'Not Found', - }); + afterAll(async () => { + await prismaService.$disconnect(); + await app.close(); }); - it('/tokens/by-address/ct_t0/pairs (GET) 200 with no liquidityInfo', async () => { - const response = await request(app.getHttpServer()) - .get('/tokens/by-address/ct_t0/pairs') - .expect(200); + describe('/tokens', () => { + it('/tokens (GET)', async () => { + const response = await request(app.getHttpServer()) + .get('/tokens') + .expect(200); + const value: dto.TokenWithListed[] = JSON.parse(response.text); - const value: dto.TokenPairs = JSON.parse(response.text); - - expect(utils.sortByAddress(value.pairs0)).toEqual([ - { - address: 'ct_p1', - synchronized: false, - oppositeToken: { - address: 'ct_t1', - symbol: 'B', - name: 'B Token', - decimals: 6, - listed: false, - malformed: false, - noContract: false, - }, - }, - { - address: 'ct_p3', - synchronized: false, - oppositeToken: { - address: 'ct_t3', - symbol: 'C', - name: 'C Token', - decimals: 10, + expect(utils.sortByAddress(value)).toEqual([ + { + address: 'ct_t0', + symbol: 'A', + name: 'A Token', + decimals: 18, listed: false, malformed: false, noContract: false, }, - }, - ]); - }); - it('/tokens/by-address/ct_t0/pairs (GET) 200 with pairs only on pairs0', async () => { - const ctx = utils.mockContext(data.context21); - activeWorker = worker(ctx); - await activeWorker.refreshPairs(); - await activeWorker.refreshPairsLiquidity(); - - const response = await request(app.getHttpServer()) - .get('/tokens/by-address/ct_t0/pairs') - .expect(200); - - const value: dto.TokenPairs = JSON.parse(response.text); - - expect(value.pairs1).toEqual([]); - expect(utils.sortByAddress(value.pairs0)).toEqual([ - { - address: 'ct_p1', - synchronized: true, - oppositeToken: { + { address: 'ct_t1', symbol: 'B', name: 'B Token', @@ -243,17 +90,7 @@ describe('tokens fetching (e2e)', () => { malformed: false, noContract: false, }, - liquidityInfo: { - height: 1, - totalSupply: '2', - reserve0: '1', - reserve1: '2', - }, - }, - { - address: 'ct_p3', - synchronized: true, - oppositeToken: { + { address: 'ct_t3', symbol: 'C', name: 'C Token', @@ -262,73 +99,58 @@ describe('tokens fetching (e2e)', () => { malformed: false, noContract: false, }, - liquidityInfo: { - height: 1, - totalSupply: '3', - reserve0: '1', - reserve1: '3', - }, - }, - { - address: 'ct_p4', - synchronized: true, - oppositeToken: { - address: 'ct_t4', - symbol: 'D', - name: 'D Token', - decimals: 10, - listed: false, + ]); + }); + + it('/tokens/listed (GET) 200 empty', () => { + return request(app.getHttpServer()) + .get('/tokens/listed') + .expect(200) + .expect([]); + }); + + it('/tokens/listed (GET) 200 non-empty', async () => { + await utils.listToken(prismaService, 'ct_t0'); + await utils.listToken(prismaService, 'ct_t3'); + const response = await request(app.getHttpServer()) + .get('/tokens/listed') + .expect(200); + const value: dto.Token[] = JSON.parse(response.text); + + expect(utils.sortByAddress(value)).toEqual([ + { + address: 'ct_t0', + symbol: 'A', + name: 'A Token', + decimals: 18, malformed: false, noContract: false, }, - liquidityInfo: { - height: 1, - totalSupply: '3', - reserve0: '1', - reserve1: '3', - }, - }, - ]); - }); - it('/tokens/by-address/ct_t3/pairs (GET) 200 with pairs only on pairs1', async () => { - const ctx = utils.mockContext(data.context21); - activeWorker = worker(ctx); - await activeWorker.refreshPairs(); - await activeWorker.refreshPairsLiquidity(); - await utils.listToken('ct_t0'); - await utils.listToken('ct_t3'); - - const response = await request(app.getHttpServer()) - .get('/tokens/by-address/ct_t3/pairs') - .expect(200); - - const value: dto.TokenPairs = JSON.parse(response.text); - - expect(value.pairs0).toEqual([]); - expect(utils.sortByAddress(value.pairs1)).toEqual([ - { - address: 'ct_p2', - synchronized: true, - oppositeToken: { - address: 'ct_t1', - symbol: 'B', - name: 'B Token', - decimals: 6, - listed: false, + { + address: 'ct_t3', + symbol: 'C', + name: 'C Token', + decimals: 10, malformed: false, noContract: false, }, - liquidityInfo: { - totalSupply: '200000', - reserve0: '10', - reserve1: '20000', - height: 1, - }, - }, - { - address: 'ct_p3', - synchronized: true, - oppositeToken: { + ]); + }); + + it('/tokens (GET) 200 with some listed', async () => { + await utils.listToken(prismaService, 'ct_t0'); + await utils.listToken(prismaService, 'ct_t3'); + const response = await request(app.getHttpServer()) + .get('/tokens') + .expect(200); + const value: dto.TokenWithListed[] = JSON.parse(response.text); + + expect( + value.sort((a: dto.TokenWithListed, b: dto.TokenWithListed) => + a.address.localeCompare(b.address), + ), + ).toEqual([ + { address: 'ct_t0', symbol: 'A', name: 'A Token', @@ -337,73 +159,7 @@ describe('tokens fetching (e2e)', () => { malformed: false, noContract: false, }, - liquidityInfo: { - height: 1, - totalSupply: '3', - reserve0: '1', - reserve1: '3', - }, - }, - ]); - }); - it('/tokens/by-address/ct_t3/pairs (GET) 200 with pairs on pairs0 and pairs1', async () => { - const ctx = utils.mockContext({ - ...data.context21, - pairs: data.context21.pairs.concat([ { - address: 'ct_p5', - reserve0: 1n, - reserve1: 3n, - totalSupply: 1n * 3n, - t0: 2, - t1: 3, - }, - { - address: 'ct_p6', - reserve0: 4n, - reserve1: 10n, - totalSupply: 4n * 10n, - t0: 2, - t1: 1, - }, - ]), - }); - activeWorker = worker(ctx); - await activeWorker.refreshPairs(); - await activeWorker.refreshPairsLiquidity(); - await utils.listToken('ct_t0'); - await utils.listToken('ct_t3'); - - const response = await request(app.getHttpServer()) - .get('/tokens/by-address/ct_t3/pairs') - .expect(200); - - const value: dto.TokenPairs = JSON.parse(response.text); - - expect(utils.sortByAddress(value.pairs0)).toEqual([ - { - address: 'ct_p5', - synchronized: true, - oppositeToken: { - address: 'ct_t4', - symbol: 'D', - name: 'D Token', - decimals: 10, - listed: false, - malformed: false, - noContract: false, - }, - liquidityInfo: { - height: 1, - totalSupply: '3', - reserve0: '1', - reserve1: '3', - }, - }, - { - address: 'ct_p6', - synchronized: true, - oppositeToken: { address: 'ct_t1', symbol: 'B', name: 'B Token', @@ -412,234 +168,504 @@ describe('tokens fetching (e2e)', () => { malformed: false, noContract: false, }, - liquidityInfo: { - height: 1, - totalSupply: '40', - reserve0: '4', - reserve1: '10', - }, - }, - ]); - expect(utils.sortByAddress(value.pairs1)).toEqual([ - { - address: 'ct_p2', - synchronized: true, - oppositeToken: { - address: 'ct_t1', - symbol: 'B', - name: 'B Token', - decimals: 6, - listed: false, + { + address: 'ct_t3', + symbol: 'C', + name: 'C Token', + decimals: 10, + listed: true, malformed: false, noContract: false, }, - liquidityInfo: { - height: 1, - totalSupply: '200000', - reserve0: '10', - reserve1: '20000', - }, - }, - { - address: 'ct_p3', - synchronized: true, - oppositeToken: { + ]); + }); + + it('/tokens/by-address/ct_t0 (GET) 200', () => { + return request(app.getHttpServer()) + .get('/tokens/by-address/ct_t0') + .expect(200) + .expect({ address: 'ct_t0', symbol: 'A', name: 'A Token', decimals: 18, - listed: true, + listed: false, malformed: false, noContract: false, + pairs: ['ct_p1', 'ct_p3'], + }); + }); + + it('/tokens/by-address/ct_tXXX (GET) 404', () => { + return request(app.getHttpServer()) + .get('/tokens/by-address/ct_tXXX') + .expect(404) + .expect({ + statusCode: 404, + message: 'token not found', + error: 'Not Found', + }); + }); + + it('/tokens/by-address/ct_t0/pairs (GET) 200 with no liquidityInfo', async () => { + const response = await request(app.getHttpServer()) + .get('/tokens/by-address/ct_t0/pairs') + .expect(200); + + const value: dto.TokenPairs = JSON.parse(response.text); + + expect(utils.sortByAddress(value.pairs0)).toEqual([ + { + address: 'ct_p1', + synchronized: false, + oppositeToken: { + address: 'ct_t1', + symbol: 'B', + name: 'B Token', + decimals: 6, + listed: false, + malformed: false, + noContract: false, + }, }, - liquidityInfo: { - height: 1, - totalSupply: '3', - reserve0: '1', - reserve1: '3', + { + address: 'ct_p3', + synchronized: false, + oppositeToken: { + address: 'ct_t3', + symbol: 'C', + name: 'C Token', + decimals: 10, + listed: false, + malformed: false, + noContract: false, + }, }, - }, - ]); - }); - it('/tokens/by-address/ct_tXXX/pairs (GET) 404', () => { - return request(app.getHttpServer()) - .get('/tokens/by-address/ct_tXXX') - .expect(404) - .expect({ - statusCode: 404, - message: 'token not found', - error: 'Not Found', - }); - }); -}); -describe('listed tokens management (e2e)', () => { - let app: INestApplication; - beforeEach(async () => { - await cleanDb(); - const ctx = utils.mockContext(data.context2); - activeWorker = worker(ctx); - await activeWorker.refreshPairs(); - //add some listed tokens - await utils.listToken('ct_t0'); - await utils.listToken('ct_t3'); - }); + ]); + }); - beforeEach(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [ApiModule], - }).compile(); + it('/tokens/by-address/ct_t0/pairs (GET) 200 with pairs only on pairs0', async () => { + pairSyncService.ctx = utils.mockContext(data.context21); + await pairSyncService['refreshPairs'](); + await pairSyncService['refreshPairsLiquidity'](); - app = moduleFixture.createNestApplication(); - await app.init(); - }); - describe('add to token list', () => { - it('/tokens/listed/ct_xxxx(POST) 401 with no auth key and with invalid token', async () => { - await request(app.getHttpServer()) - .post('/tokens/listed/ct_xxxx') - .expect(401); - }); - it('/tokens/listed/ct_t0 (POST) 401 with no auth key provided and valid token address', async () => { - await request(app.getHttpServer()) - .post('/tokens/listed/ct_t0') - .expect(401); - }); - it('/tokens/listed/ct_xxxx (POST) 401 with invalid auth key and invalid token', async () => { - await request(app.getHttpServer()) - .post('/tokens/listed/ct_xxxx') - .set('Authorization', 'wrong-key') - .expect(401); + const response = await request(app.getHttpServer()) + .get('/tokens/by-address/ct_t0/pairs') + .expect(200); + + const value: dto.TokenPairs = JSON.parse(response.text); + + expect(value.pairs1).toEqual([]); + expect(utils.sortByAddress(value.pairs0)).toEqual([ + { + address: 'ct_p1', + synchronized: true, + oppositeToken: { + address: 'ct_t1', + symbol: 'B', + name: 'B Token', + decimals: 6, + listed: false, + malformed: false, + noContract: false, + }, + liquidityInfo: { + height: 1, + totalSupply: '2', + reserve0: '1', + reserve1: '2', + }, + }, + { + address: 'ct_p3', + synchronized: true, + oppositeToken: { + address: 'ct_t3', + symbol: 'C', + name: 'C Token', + decimals: 10, + listed: false, + malformed: false, + noContract: false, + }, + liquidityInfo: { + height: 1, + totalSupply: '3', + reserve0: '1', + reserve1: '3', + }, + }, + { + address: 'ct_p4', + synchronized: true, + oppositeToken: { + address: 'ct_t4', + symbol: 'D', + name: 'D Token', + decimals: 10, + listed: false, + malformed: false, + noContract: false, + }, + liquidityInfo: { + height: 1, + totalSupply: '3', + reserve0: '1', + reserve1: '3', + }, + }, + ]); }); - it('/tokens/listed/ct_xxxx (POST) 401 with invalid auth key and valid token', async () => { - await request(app.getHttpServer()) - .post('/tokens/listed/ct_t0') - .set('Authorization', 'wrong-key') - .expect(401); + + it('/tokens/by-address/ct_t3/pairs (GET) 200 with pairs only on pairs1', async () => { + pairSyncService.ctx = utils.mockContext(data.context21); + await pairSyncService['refreshPairs'](); + await pairSyncService['refreshPairsLiquidity'](); + await utils.listToken(prismaService, 'ct_t0'); + await utils.listToken(prismaService, 'ct_t3'); + + const response = await request(app.getHttpServer()) + .get('/tokens/by-address/ct_t3/pairs') + .expect(200); + + const value: dto.TokenPairs = JSON.parse(response.text); + + expect(value.pairs0).toEqual([]); + expect(utils.sortByAddress(value.pairs1)).toEqual([ + { + address: 'ct_p2', + synchronized: true, + oppositeToken: { + address: 'ct_t1', + symbol: 'B', + name: 'B Token', + decimals: 6, + listed: false, + malformed: false, + noContract: false, + }, + liquidityInfo: { + totalSupply: '200000', + reserve0: '10', + reserve1: '20000', + height: 1, + }, + }, + { + address: 'ct_p3', + synchronized: true, + oppositeToken: { + address: 'ct_t0', + symbol: 'A', + name: 'A Token', + decimals: 18, + listed: true, + malformed: false, + noContract: false, + }, + liquidityInfo: { + height: 1, + totalSupply: '3', + reserve0: '1', + reserve1: '3', + }, + }, + ]); }); - it('/tokens/listed/ct_xxxx (POST) 404 with valid auth key but with invalid token', async () => { - await request(app.getHttpServer()) - .post('/tokens/listed/ct_xxxx') - .set('Authorization', nonNullable(process.env.AUTH_TOKEN)) - .expect(404); + + it('/tokens/by-address/ct_t3/pairs (GET) 200 with pairs on pairs0 and pairs1', async () => { + pairSyncService.ctx = utils.mockContext({ + ...data.context21, + pairs: data.context21.pairs.concat([ + { + address: 'ct_p5', + reserve0: 1n, + reserve1: 3n, + totalSupply: 1n * 3n, + t0: 2, + t1: 3, + }, + { + address: 'ct_p6', + reserve0: 4n, + reserve1: 10n, + totalSupply: 4n * 10n, + t0: 2, + t1: 1, + }, + ]), + }); + await pairSyncService['refreshPairs'](); + await pairSyncService['refreshPairsLiquidity'](); + await utils.listToken(prismaService, 'ct_t0'); + await utils.listToken(prismaService, 'ct_t3'); + + const response = await request(app.getHttpServer()) + .get('/tokens/by-address/ct_t3/pairs') + .expect(200); + + const value: dto.TokenPairs = JSON.parse(response.text); + + expect(utils.sortByAddress(value.pairs0)).toEqual([ + { + address: 'ct_p5', + synchronized: true, + oppositeToken: { + address: 'ct_t4', + symbol: 'D', + name: 'D Token', + decimals: 10, + listed: false, + malformed: false, + noContract: false, + }, + liquidityInfo: { + height: 1, + totalSupply: '3', + reserve0: '1', + reserve1: '3', + }, + }, + { + address: 'ct_p6', + synchronized: true, + oppositeToken: { + address: 'ct_t1', + symbol: 'B', + name: 'B Token', + decimals: 6, + listed: false, + malformed: false, + noContract: false, + }, + liquidityInfo: { + height: 1, + totalSupply: '40', + reserve0: '4', + reserve1: '10', + }, + }, + ]); + expect(utils.sortByAddress(value.pairs1)).toEqual([ + { + address: 'ct_p2', + synchronized: true, + oppositeToken: { + address: 'ct_t1', + symbol: 'B', + name: 'B Token', + decimals: 6, + listed: false, + malformed: false, + noContract: false, + }, + liquidityInfo: { + height: 1, + totalSupply: '200000', + reserve0: '10', + reserve1: '20000', + }, + }, + { + address: 'ct_p3', + synchronized: true, + oppositeToken: { + address: 'ct_t0', + symbol: 'A', + name: 'A Token', + decimals: 18, + listed: true, + malformed: false, + noContract: false, + }, + liquidityInfo: { + height: 1, + totalSupply: '3', + reserve0: '1', + reserve1: '3', + }, + }, + ]); }); - it('/tokens/listed/ct_t1 (POST) 201 with valid auth key and with valid token', async () => { - //verify before listing ct_t1 - await request(app.getHttpServer()) - .get('/tokens/by-address/ct_t1') - .expect(200) - .expect({ - address: 'ct_t1', - symbol: 'B', - name: 'B Token', - decimals: 6, - listed: false, - malformed: false, - noContract: false, - pairs: ['ct_p2', 'ct_p1'], - }); - //listing it - await request(app.getHttpServer()) - .post('/tokens/listed/ct_t1') - .set('Authorization', nonNullable(process.env.AUTH_TOKEN)) - .expect(201) + it('/tokens/by-address/ct_tXXX/pairs (GET) 404', () => { + return request(app.getHttpServer()) + .get('/tokens/by-address/ct_tXXX') + .expect(404) .expect({ - address: 'ct_t1', - symbol: 'B', - name: 'B Token', - decimals: 6, - listed: true, - malformed: false, - noContract: false, - }); - //re-verify ct_t1 to be sure it was persisted also - await request(app.getHttpServer()) - .get('/tokens/by-address/ct_t1') - .expect(200) - .expect({ - address: 'ct_t1', - symbol: 'B', - name: 'B Token', - decimals: 6, - listed: true, - malformed: false, - noContract: false, - pairs: ['ct_p2', 'ct_p1'], + statusCode: 404, + message: 'token not found', + error: 'Not Found', }); }); }); - describe('remove from token list', () => { - it('/tokens/listed/ct_xxxx (DELETE) 401 with no auth key and with invalid token', async () => { - await request(app.getHttpServer()) - .delete('/tokens/listed/ct_xxxx') - .expect(401); - }); - it('/tokens/listed/ct_t0 (DELETE) 401 with no auth key provided and valid token address', async () => { - await request(app.getHttpServer()) - .delete('/tokens/listed/ct_t0') - .expect(401); - }); - it('/tokens/listed/ct_xxxx (DELETE) 401 with invalid auth key and invalid token', async () => { - await request(app.getHttpServer()) - .delete('/tokens/listed/ct_xxxx') - .set('Authorization', 'wrong-key') - .expect(401); - }); - it('/tokens/listed/ct_xxxx (DELETE) 401 with invalid auth key and valid token', async () => { - await request(app.getHttpServer()) - .delete('/tokens/listed/ct_t0') - .set('Authorization', 'wrong-key') - .expect(401); + + describe('/tokens/listed', () => { + beforeEach(async () => { + await utils.listToken(prismaService, 'ct_t0'); + await utils.listToken(prismaService, 'ct_t3'); }); - it('/tokens/listed/ct_xxxx (DELETE) 404 with valid auth key but with invalid token', async () => { - await request(app.getHttpServer()) - .delete('/tokens/listed/ct_xxxx') - .set('Authorization', nonNullable(process.env.AUTH_TOKEN)) - .expect(404); + + describe('add to token list', () => { + it('/tokens/listed/ct_xxxx(POST) 401 with no auth key and with invalid token', async () => { + await request(app.getHttpServer()) + .post('/tokens/listed/ct_xxxx') + .expect(401); + }); + + it('/tokens/listed/ct_t0 (POST) 401 with no auth key provided and valid token address', async () => { + await request(app.getHttpServer()) + .post('/tokens/listed/ct_t0') + .expect(401); + }); + + it('/tokens/listed/ct_xxxx (POST) 401 with invalid auth key and invalid token', async () => { + await request(app.getHttpServer()) + .post('/tokens/listed/ct_xxxx') + .set('Authorization', 'wrong-key') + .expect(401); + }); + + it('/tokens/listed/ct_xxxx (POST) 401 with invalid auth key and valid token', async () => { + await request(app.getHttpServer()) + .post('/tokens/listed/ct_t0') + .set('Authorization', 'wrong-key') + .expect(401); + }); + + it('/tokens/listed/ct_xxxx (POST) 404 with valid auth key but with invalid token', async () => { + await request(app.getHttpServer()) + .post('/tokens/listed/ct_xxxx') + .set('Authorization', nonNullable(process.env.AUTH_TOKEN)) + .expect(404); + }); + + it('/tokens/listed/ct_t1 (POST) 201 with valid auth key and with valid token', async () => { + //verify before listing ct_t1 + await request(app.getHttpServer()) + .get('/tokens/by-address/ct_t1') + .expect(200) + .expect({ + address: 'ct_t1', + symbol: 'B', + name: 'B Token', + decimals: 6, + listed: false, + malformed: false, + noContract: false, + pairs: ['ct_p2', 'ct_p1'], + }); + + //listing it + await request(app.getHttpServer()) + .post('/tokens/listed/ct_t1') + .set('Authorization', nonNullable(process.env.AUTH_TOKEN)) + .expect(201) + .expect({ + address: 'ct_t1', + symbol: 'B', + name: 'B Token', + decimals: 6, + listed: true, + malformed: false, + noContract: false, + }); + //re-verify ct_t1 to be sure it was persisted also + await request(app.getHttpServer()) + .get('/tokens/by-address/ct_t1') + .expect(200) + .expect({ + address: 'ct_t1', + symbol: 'B', + name: 'B Token', + decimals: 6, + listed: true, + malformed: false, + noContract: false, + pairs: ['ct_p2', 'ct_p1'], + }); + }); }); - it('/tokens/listed/ct_t3 (DELETE) 200 with valid auth key and with valid token', async () => { - //verify before unlisting ct_t3 - await request(app.getHttpServer()) - .get('/tokens/by-address/ct_t3') - .expect(200) - .expect({ - address: 'ct_t3', - symbol: 'C', - name: 'C Token', - decimals: 10, - listed: true, - malformed: false, - noContract: false, - pairs: ['ct_p2', 'ct_p3'], - }); - //unlisting it - await request(app.getHttpServer()) - .delete('/tokens/listed/ct_t3') - .set('Authorization', nonNullable(process.env.AUTH_TOKEN)) - .expect(200) - .expect({ - address: 'ct_t3', - symbol: 'C', - name: 'C Token', - decimals: 10, - listed: false, - malformed: false, - noContract: false, - }); - //re-verify ct_t3 to be sure the unlisting was persisted too - await request(app.getHttpServer()) - .get('/tokens/by-address/ct_t3') - .expect(200) - .expect({ - address: 'ct_t3', - symbol: 'C', - name: 'C Token', - decimals: 10, - listed: false, - malformed: false, - noContract: false, - pairs: ['ct_p2', 'ct_p3'], - }); + describe('remove from token list', () => { + it('/tokens/listed/ct_xxxx (DELETE) 401 with no auth key and with invalid token', async () => { + await request(app.getHttpServer()) + .delete('/tokens/listed/ct_xxxx') + .expect(401); + }); + + it('/tokens/listed/ct_t0 (DELETE) 401 with no auth key provided and valid token address', async () => { + await request(app.getHttpServer()) + .delete('/tokens/listed/ct_t0') + .expect(401); + }); + + it('/tokens/listed/ct_xxxx (DELETE) 401 with invalid auth key and invalid token', async () => { + await request(app.getHttpServer()) + .delete('/tokens/listed/ct_xxxx') + .set('Authorization', 'wrong-key') + .expect(401); + }); + + it('/tokens/listed/ct_xxxx (DELETE) 401 with invalid auth key and valid token', async () => { + await request(app.getHttpServer()) + .delete('/tokens/listed/ct_t0') + .set('Authorization', 'wrong-key') + .expect(401); + }); + + it('/tokens/listed/ct_xxxx (DELETE) 404 with valid auth key but with invalid token', async () => { + await request(app.getHttpServer()) + .delete('/tokens/listed/ct_xxxx') + .set('Authorization', nonNullable(process.env.AUTH_TOKEN)) + .expect(404); + }); + + it('/tokens/listed/ct_t3 (DELETE) 200 with valid auth key and with valid token', async () => { + //verify before unlisting ct_t3 + await request(app.getHttpServer()) + .get('/tokens/by-address/ct_t3') + .expect(200) + .expect({ + address: 'ct_t3', + symbol: 'C', + name: 'C Token', + decimals: 10, + listed: true, + malformed: false, + noContract: false, + pairs: ['ct_p2', 'ct_p3'], + }); + + //unlisting it + await request(app.getHttpServer()) + .delete('/tokens/listed/ct_t3') + .set('Authorization', nonNullable(process.env.AUTH_TOKEN)) + .expect(200) + .expect({ + address: 'ct_t3', + symbol: 'C', + name: 'C Token', + decimals: 10, + listed: false, + malformed: false, + noContract: false, + }); + //re-verify ct_t3 to be sure the unlisting was persisted too + await request(app.getHttpServer()) + .get('/tokens/by-address/ct_t3') + .expect(200) + .expect({ + address: 'ct_t3', + symbol: 'C', + name: 'C Token', + decimals: 10, + listed: false, + malformed: false, + noContract: false, + pairs: ['ct_p2', 'ct_p3'], + }); + }); }); }); }); diff --git a/test/utils/context.mockup.ts b/test/utils/context.mockup.ts index 583a491..97cbe26 100644 --- a/test/utils/context.mockup.ts +++ b/test/utils/context.mockup.ts @@ -1,12 +1,13 @@ +import { nonNullable } from '../../src/lib/utils'; +import { mockDeep } from 'jest-mock-extended'; +import ContractWithMethods from '@aeternity/aepp-sdk/es/contract/Contract'; import { + Aex9Methods, Context, - PairMethods, MetaInfo, - Aex9Methods, -} from '../../src/lib/contracts'; -import { ContractAddress, CallData, nonNullable } from '../../src/lib/utils'; -import { mockDeep } from 'jest-mock-extended'; -import ContractWithMethods from '@aeternity/aepp-sdk/es/contract/Contract'; + PairMethods, +} from '../../src/tasks/pair-sync.model'; +import { CallData, ContractAddress } from '../../src/clients/sdk-client.model'; const mockupResult = () => mockDeep<{ callerId: string; diff --git a/test/utils/db.ts b/test/utils/db.ts index 446a4b5..b30ae01 100644 --- a/test/utils/db.ts +++ b/test/utils/db.ts @@ -1,6 +1,7 @@ -import db from '../../src/dal/client'; -export const clean = async () => { - await db.pairLiquidityInfo.deleteMany(); - await db.pair.deleteMany(); - await db.token.deleteMany(); +import { PrismaService } from '../../src/database/prisma.service'; + +export const clean = async (prismaService: PrismaService) => { + await prismaService.pairLiquidityInfo.deleteMany(); + await prismaService.pair.deleteMany(); + await prismaService.token.deleteMany(); }; diff --git a/test/utils/index.ts b/test/utils/index.ts index bd455bb..8b46ae0 100644 --- a/test/utils/index.ts +++ b/test/utils/index.ts @@ -1,13 +1,14 @@ +import { PrismaService } from '../../src/database/prisma.service'; + export * from './context.mockup'; export * from './env.mockups'; export * from './db'; -import db from '../../src/dal/client'; export const sortByAddress = (xs: { address: string }[]) => [...xs].sort((a, b) => a.address.localeCompare(b.address)); -export const listToken = (address: string) => - db.token.update({ +export const listToken = (prismaService: PrismaService, address: string) => + prismaService.token.update({ where: { address, }, diff --git a/test/worker-advanced.spec.ts b/test/worker-advanced.spec.ts deleted file mode 100644 index b859f27..0000000 --- a/test/worker-advanced.spec.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { mock } from 'jest-mock-extended'; - -import { createOnEventReceived } from '../src/worker'; -import { Context } from '../src/lib/contracts'; -import { ContractAddress } from '../src/lib/utils'; -import * as data from './data/context-mockups'; -import * as utils from './utils'; -import { objSubEv, swapEvent, swapTxInfo } from './data/subscription-events'; - -let ctx: ReturnType = null as any; - -const initTestContext = () => { - type Ev = { - onFactory: (ctx: Context) => Promise; - refreshPairsLiquidity: ( - ctx: Context, - contract: ContractAddress, - ) => Promise; - getAllAddresses: () => Promise; - }; - const { onFactory, refreshPairsLiquidity, getAllAddresses } = mock(); - const logger = mock(); - const eventHandler = createOnEventReceived( - ctx, - logger, - onFactory, - refreshPairsLiquidity, - getAllAddresses, - ); - return { - getAllAddresses, - logger, - eventHandler, - onFactory, - refreshPairsLiquidity, - }; -}; -type T = ReturnType; -let logger: T['logger'] = null as any; -let eventHandler: T['eventHandler'] = null as any; -let onFactory: T['onFactory'] = null as any; -let refreshPairsLiquidity: T['refreshPairsLiquidity'] = null as any; -let getAllAddresses: T['getAllAddresses'] = null as any; - -beforeEach(() => { - ctx = utils.mockContext(data.context2); - ({ logger, eventHandler, onFactory, refreshPairsLiquidity, getAllAddresses } = - initTestContext()); -}); - -describe('createOnEventReceived', () => { - it('ignores unknown types', async () => { - await eventHandler({ - ...objSubEv, - payload: { - ...objSubEv.payload, - tx: { - ...objSubEv.payload.tx, - type: 'Some-Random-Type' as any, - }, - }, - }); - expect(logger.debug).toHaveBeenCalledWith( - `Ignoring transaction of type 'Some-Random-Type'`, - ); - }); - it('throws error if no txInfo', async () => { - expect( - async () => - await eventHandler({ - ...objSubEv, - }), - ).rejects.toThrow(`No tx info for hash 'th_1'`); - }); - it('ignores inverted transaction', async () => { - ctx.node.getTransactionInfoByHash - .calledWith(swapEvent.payload.hash) - .mockReturnValue( - Promise.resolve({ - ...swapTxInfo, - callInfo: { - ...swapTxInfo.callInfo, - returnType: 'revert', - }, - }), - ); - await eventHandler({ - ...swapEvent, - }); - expect(logger.debug).toHaveBeenCalledWith( - `Ignore reverted transaction: '${swapEvent.payload.hash}'`, - ); - }); - it('refresh pairs in factory', async () => { - ctx.node.getTransactionInfoByHash - .calledWith(objSubEv.payload.hash) - .mockReturnValue( - Promise.resolve({ - ...swapTxInfo, - callInfo: { - ...swapTxInfo.callInfo, - log: [ - { - address: process.env.FACTORY_ADDRESS as any, - data: 'cb_1=', - topics: [], - }, - ], - }, - }), - ); - getAllAddresses.calledWith().mockReturnValue(Promise.resolve([])); - await eventHandler({ - ...objSubEv, - }); - expect(onFactory).toHaveBeenCalledWith(ctx, 1); - }); - it('refresh pair liquidity', async () => { - ctx.node.getTransactionInfoByHash - .calledWith(objSubEv.payload.hash) - .mockReturnValue( - Promise.resolve({ - ...swapTxInfo, - callInfo: { - ...swapTxInfo.callInfo, - log: [ - { - address: 'ct_p1', - data: 'cb_1=', - topics: [], - }, - { - address: 'ct_p2', - data: 'cb_1=', - topics: [], - }, - //this will not be called - { - address: 'ct_p3', - data: 'cb_1=', - topics: [], - }, - ], - }, - }), - ); - getAllAddresses - .calledWith() - .mockReturnValue(Promise.resolve(['ct_p1', 'ct_p2'])); - - await eventHandler({ - ...objSubEv, - }); - expect(refreshPairsLiquidity).toHaveBeenCalledWith(ctx, 'ct_p1', 1); - expect(refreshPairsLiquidity).toHaveBeenCalledWith(ctx, 'ct_p2', 1); - }); -}); diff --git a/test/worker-basics.e2e-spec.ts b/test/worker-basics.e2e-spec.ts deleted file mode 100644 index 02b92e4..0000000 --- a/test/worker-basics.e2e-spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { mockContext } from './utils'; -import createWorkerMethods from '../src/worker'; -import db from '../src/dal/client'; -import { clean as cleanDb } from './utils/db'; -import prisma from '@prisma/client'; -import * as data from './data/context-mockups'; - -type WorkerMethods = ReturnType; -let activeWorker: WorkerMethods; - -// Testing method -// 1. before all create a common context -// 2. before all reset mockups (Context and Prisma Client) -// 3. run worker methods and and test the impact on db - -// -beforeEach(async () => { - await cleanDb(); - const ctx = mockContext(data.context2); - activeWorker = createWorkerMethods(ctx); - await activeWorker.refreshPairs(); -}); -it('inserts new pairs', async () => { - const pairs: prisma.Pair[] = await db.pair.findMany(); - expect(await db.pair.count()).toBe(3); - for (let i = 0; i < 3; i++) { - expect(pairs[i]).toMatchObject({ - address: data.context2.pairs[i].address, - synchronized: false, - }); - } -}); - -it('refresh pairs liquidity', async () => { - await activeWorker.refreshPairsLiquidity(); - - const pairs = await db.pair.findMany({ - include: { liquidityInfo: true }, - orderBy: { address: 'asc' }, - }); - for (let i = 0; i < 3; i++) { - const expected = data.context2.pairs[i]; - expect(pairs[i].liquidityInfo).toMatchObject({ - reserve0: expected.reserve0.toString(), - reserve1: expected.reserve1.toString(), - totalSupply: expected.totalSupply.toString(), - }); - } -}); - -it('refresh new added pairs', async () => { - const ctx = mockContext(data.context21); - activeWorker = createWorkerMethods(ctx); - await activeWorker.refreshPairs(); - const pairs: prisma.Pair[] = await db.pair.findMany({ - orderBy: { id: 'asc' }, - }); - expect(await db.pair.count()).toBe(4); - expect(pairs[3]).toMatchObject({ - address: data.context21.pairs[3].address, - synchronized: false, - }); -}); - -it('refresh liquidity for new added pairs', async () => { - const ctx = mockContext(data.context21); - activeWorker = createWorkerMethods(ctx); - await activeWorker.refreshPairs(); - await activeWorker.refreshPairsLiquidity(); - const pairs = await db.pair.findMany({ - include: { liquidityInfo: true }, - orderBy: { address: 'asc' }, - }); - const expected = data.context21.pairs[3]; - expect(pairs[3].liquidityInfo).toMatchObject({ - reserve0: expected.reserve0.toString(), - reserve1: expected.reserve1.toString(), - totalSupply: expected.totalSupply.toString(), - }); -}); - -it('unsync all pairs', async () => { - await activeWorker.unsyncAllPairs(); - const pairs = await db.pair.findMany({ where: { synchronized: true } }); - expect(pairs.length).toBe(0); -});