diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index eb4f41b3..c0f792ef 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -45,4 +45,4 @@ jobs: run: npm run test - name: e2e test - run: npm run test:e2e + run: npm run pretest:e2e && npm run test:e2e diff --git a/package.json b/package.json index a1a72f6e..fae6d093 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,11 @@ "serve:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\"", - "test": "dotenv -e .env.test jest", + "test": "dotenv -e .env.test -- jest", "test:watch": "npm run test -- --watch", "test:cov": "npm run test -- --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "npm run test -- --runInBand --config ./test/e2e/jest-e2e.json", + "test:e2e": "npm run test -- --runInBand --config ./test/e2e/jest-e2e.json --forceExit", "db": "docker compose up", "db:deploy": "docker compose up -d", "db:remove": "docker compose down -v", @@ -30,7 +30,7 @@ "db:push-schema": "npx prisma db push", "db:migrate": "npx prisma migrate deploy", "postdb:deploy": "sleep 1.5; npm run db:migrate", - "pretest:e2e": "dotenv -e .env.test npm run db:push-schema", + "pretest:e2e": "dotenv -e .env.test npx prisma migrate dev", "prisma:test-studio": "dotenv -e .env.test npx prisma studio", "prisma:studio": "npx prisma studio" }, diff --git a/src/api/tokens/tokens.service.ts b/src/api/tokens/tokens.service.ts index 99adc4a4..65930c25 100644 --- a/src/api/tokens/tokens.service.ts +++ b/src/api/tokens/tokens.service.ts @@ -1,3 +1,4 @@ +import { Cache } from '@nestjs/cache-manager'; import { Injectable } from '@nestjs/common'; import { Pair, PairLiquidityInfo, Token } from '@prisma/client'; @@ -8,7 +9,10 @@ import { presentInvalidTokens } from '@/lib/utils'; @Injectable() export class TokensService { - constructor(private readonly tokenDbService: TokenDbService) {} + constructor( + private readonly tokenDbService: TokenDbService, + private cacheManager: Cache, + ) {} async getCount(onlyListed?: boolean) { return this.tokenDbService.count(presentInvalidTokens, onlyListed); } @@ -23,11 +27,15 @@ export class TokensService { } async listToken(address: ContractAddress) { - return this.tokenDbService.updateListedValue(address, true); + const res = this.tokenDbService.updateListedValue(address, true); + await this.cacheManager.reset(); + return res; } async unlistToken(address: ContractAddress) { - return this.tokenDbService.updateListedValue(address, false); + const res = this.tokenDbService.updateListedValue(address, false); + await this.cacheManager.reset(); + return res; } async getToken(address: ContractAddress): Promise { diff --git a/test/e2e/__snapshots__/pair-liquidity-info-history-db.service.e2e-spec.ts.snap b/test/e2e/__snapshots__/pair-liquidity-info-history-db.service.e2e-spec.ts.snap index e914ac41..8791530a 100644 --- a/test/e2e/__snapshots__/pair-liquidity-info-history-db.service.e2e-spec.ts.snap +++ b/test/e2e/__snapshots__/pair-liquidity-info-history-db.service.e2e-spec.ts.snap @@ -14,6 +14,9 @@ exports[`PairLiquidityInfoHistoryDbService upsert should correctly upsert an exi "pairId": 1, "reserve0": "500", "reserve1": "500", + "senderAccount": "abc", + "token0AePrice": "0.060559", + "token1AePrice": "0.060559", "transactionHash": "th_entry2", "transactionIndex": 200002n, } diff --git a/test/e2e/__snapshots__/pairs.e2e-spec.ts.snap b/test/e2e/__snapshots__/pairs.e2e-spec.ts.snap new file mode 100644 index 00000000..de15a091 --- /dev/null +++ b/test/e2e/__snapshots__/pairs.e2e-spec.ts.snap @@ -0,0 +1,224 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PairsController GET /pairs should return all pairs 1`] = ` +[ + { + "address": "ct_pair1", + "synchronized": true, + "token0": "ct_token1", + "token1": "ct_token2", + "transactions": 2, + "tvlUsd": "0", + "volumeUsdAll": "0", + "volumeUsdDay": "0", + "volumeUsdMonth": "0", + "volumeUsdWeek": "0", + "volumeUsdYear": "0", + }, + { + "address": "ct_pair2", + "synchronized": true, + "token0": "ct_token2", + "token1": "ct_token3", + "transactions": 1, + "tvlUsd": "0", + "volumeUsdAll": "0", + "volumeUsdDay": "0", + "volumeUsdMonth": "0", + "volumeUsdWeek": "0", + "volumeUsdYear": "0", + }, + { + "address": "ct_pair3", + "synchronized": true, + "token0": "ct_token2", + "token1": "ct_token5", + "transactions": 0, + "tvlUsd": "0", + "volumeUsdAll": null, + "volumeUsdDay": null, + "volumeUsdMonth": null, + "volumeUsdWeek": null, + "volumeUsdYear": null, + }, + { + "address": "ct_pair4", + "synchronized": true, + "token0": "ct_token1", + "token1": "ct_token5", + "transactions": 0, + "tvlUsd": "0", + "volumeUsdAll": null, + "volumeUsdDay": null, + "volumeUsdMonth": null, + "volumeUsdWeek": null, + "volumeUsdYear": null, + }, +] +`; + +exports[`PairsController GET /pairs should return only listed pairs with only-listed=true 1`] = ` +[ + { + "address": "ct_pair1", + "synchronized": true, + "token0": "ct_token1", + "token1": "ct_token2", + "transactions": 2, + "tvlUsd": "0", + "volumeUsdAll": "0", + "volumeUsdDay": "0", + "volumeUsdMonth": "0", + "volumeUsdWeek": "0", + "volumeUsdYear": "0", + }, +] +`; + +exports[`PairsController GET /pairs/by-address/{pair_address} should return pair if it exists 1`] = ` +{ + "address": "ct_pair1", + "synchronized": true, + "token0": { + "address": "ct_token1", + "decimals": 18, + "listed": false, + "malformed": false, + "name": "1", + "noContract": false, + "symbol": "1", + }, + "token1": { + "address": "ct_token2", + "decimals": 18, + "listed": false, + "malformed": false, + "name": "2", + "noContract": false, + "symbol": "2", + }, +} +`; + +exports[`PairsController GET /pairs/swap-routes/{from}/{to} should return a direct path 1`] = ` +[ + [ + { + "address": "ct_pair2", + "liquidityInfo": { + "height": 1, + "reserve0": "1", + "reserve1": "1", + "totalSupply": "1", + }, + "synchronized": true, + "token0": "ct_token2", + "token1": "ct_token3", + }, + ], +] +`; + +exports[`PairsController GET /pairs/swap-routes/{from}/{to} should return an indirect path 1`] = ` +[ + [ + { + "address": "ct_pair1", + "synchronized": true, + "token0": "ct_token1", + "token1": "ct_token2", + }, + { + "address": "ct_pair2", + "liquidityInfo": { + "height": 1, + "reserve0": "1", + "reserve1": "1", + "totalSupply": "1", + }, + "synchronized": true, + "token0": "ct_token2", + "token1": "ct_token3", + }, + ], +] +`; + +exports[`PairsController GET /pairs/swap-routes/{from}/{to} should return one direct path and one indirect path 1`] = ` +[ + [ + { + "address": "ct_pair4", + "synchronized": true, + "token0": "ct_token1", + "token1": "ct_token5", + }, + ], + [ + { + "address": "ct_pair1", + "synchronized": true, + "token0": "ct_token1", + "token1": "ct_token2", + }, + { + "address": "ct_pair3", + "liquidityInfo": { + "height": 2, + "reserve0": "2", + "reserve1": "2", + "totalSupply": "2", + }, + "synchronized": true, + "token0": "ct_token2", + "token1": "ct_token5", + }, + ], +] +`; + +exports[`PairsController GET /pairs/swap-routes/{from}/{to} should return paths oven on reverse order of tokens 1`] = ` +[ + [ + { + "address": "ct_pair4", + "synchronized": true, + "token0": "ct_token1", + "token1": "ct_token5", + }, + ], + [ + { + "address": "ct_pair1", + "synchronized": true, + "token0": "ct_token1", + "token1": "ct_token2", + }, + { + "address": "ct_pair3", + "liquidityInfo": { + "height": 2, + "reserve0": "2", + "reserve1": "2", + "totalSupply": "2", + }, + "synchronized": true, + "token0": "ct_token2", + "token1": "ct_token5", + }, + ], +] +`; + +exports[`PairsController GET /pairs/swap-routes/{from}/{to} should suppress some paths with only-listed=true 1`] = ` +[ + [ + { + "address": "ct_pair4", + "synchronized": true, + "token0": "ct_token1", + "token1": "ct_token5", + }, + ], +] +`; diff --git a/test/e2e/__snapshots__/tokens.e2e-spec.ts.snap b/test/e2e/__snapshots__/tokens.e2e-spec.ts.snap new file mode 100644 index 00000000..5bd5661d --- /dev/null +++ b/test/e2e/__snapshots__/tokens.e2e-spec.ts.snap @@ -0,0 +1,464 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TokenController DELETE /tokens/listed/{token_address} should return 200 with valid auth key and valid token 1`] = ` +{ + "address": "ct_token3", + "decimals": 18, + "listed": true, + "malformed": false, + "name": "3", + "noContract": false, + "pairs": 1, + "priceAe": null, + "priceChangeDay": null, + "priceChangeMonth": null, + "priceChangeWeek": null, + "priceChangeYear": null, + "priceUsd": null, + "symbol": "3", + "totalReserve": null, + "tvlAe": "4.8e-17", + "tvlUsd": "0", + "volumeUsdAll": "0", + "volumeUsdDay": "0", + "volumeUsdMonth": "0", + "volumeUsdWeek": "0", + "volumeUsdYear": "0", +} +`; + +exports[`TokenController DELETE /tokens/listed/{token_address} should return 200 with valid auth key and valid token 2`] = ` +{ + "address": "ct_token3", + "decimals": 18, + "listed": false, + "malformed": false, + "name": "3", + "noContract": false, + "symbol": "3", +} +`; + +exports[`TokenController DELETE /tokens/listed/{token_address} should return 200 with valid auth key and valid token 3`] = ` +{ + "address": "ct_token3", + "decimals": 18, + "listed": false, + "malformed": false, + "name": "3", + "noContract": false, + "pairs": 1, + "priceAe": null, + "priceChangeDay": null, + "priceChangeMonth": null, + "priceChangeWeek": null, + "priceChangeYear": null, + "priceUsd": null, + "symbol": "3", + "totalReserve": null, + "tvlAe": "4.8e-17", + "tvlUsd": "0", + "volumeUsdAll": "0", + "volumeUsdDay": "0", + "volumeUsdMonth": "0", + "volumeUsdWeek": "0", + "volumeUsdYear": "0", +} +`; + +exports[`TokenController GET /tokens should return all tokens even if some are listed 1`] = ` +[ + { + "address": "ct_token1", + "decimals": 18, + "listed": true, + "malformed": false, + "name": "1", + "noContract": false, + "pairs": 1, + "priceAe": "0.05308695", + "priceChangeDay": "0", + "priceChangeMonth": "0", + "priceChangeWeek": "0", + "priceChangeYear": "0", + "priceUsd": "0.0027", + "symbol": "1", + "totalReserve": "1e-15", + "tvlAe": "5.3e-17", + "tvlUsd": "0", + "volumeUsdAll": "0", + "volumeUsdDay": "0", + "volumeUsdMonth": "0", + "volumeUsdWeek": "0", + "volumeUsdYear": "0", + }, + { + "address": "ct_token2", + "decimals": 18, + "listed": false, + "malformed": false, + "name": "2", + "noContract": false, + "pairs": 3, + "priceAe": "0.101118", + "priceChangeDay": "0", + "priceChangeMonth": "0", + "priceChangeWeek": "0", + "priceChangeYear": "0", + "priceUsd": "0.0051", + "symbol": "2", + "totalReserve": "1e-15", + "tvlAe": "1.01e-16", + "tvlUsd": "0", + "volumeUsdAll": "0", + "volumeUsdDay": "0", + "volumeUsdMonth": "0", + "volumeUsdWeek": "0", + "volumeUsdYear": "0", + }, + { + "address": "ct_token3", + "decimals": 18, + "listed": true, + "malformed": false, + "name": "3", + "noContract": false, + "pairs": 1, + "priceAe": null, + "priceChangeDay": null, + "priceChangeMonth": null, + "priceChangeWeek": null, + "priceChangeYear": null, + "priceUsd": null, + "symbol": "3", + "totalReserve": null, + "tvlAe": "4.8e-17", + "tvlUsd": "0", + "volumeUsdAll": "0", + "volumeUsdDay": "0", + "volumeUsdMonth": "0", + "volumeUsdWeek": "0", + "volumeUsdYear": "0", + }, + { + "address": "ct_token5", + "decimals": 18, + "listed": false, + "malformed": false, + "name": "5", + "noContract": false, + "pairs": 1, + "priceAe": null, + "priceChangeDay": null, + "priceChangeMonth": null, + "priceChangeWeek": null, + "priceChangeYear": null, + "priceUsd": null, + "symbol": "5", + "totalReserve": null, + "tvlAe": null, + "tvlUsd": null, + "volumeUsdAll": null, + "volumeUsdDay": null, + "volumeUsdMonth": null, + "volumeUsdWeek": null, + "volumeUsdYear": null, + }, +] +`; + +exports[`TokenController GET /tokens should return all tokens when none are listed 1`] = ` +[ + { + "address": "ct_token1", + "decimals": 18, + "listed": false, + "malformed": false, + "name": "1", + "noContract": false, + "pairs": 1, + "priceAe": "0.05308695", + "priceChangeDay": "0", + "priceChangeMonth": "0", + "priceChangeWeek": "0", + "priceChangeYear": "0", + "priceUsd": "0.0027", + "symbol": "1", + "totalReserve": "1e-15", + "tvlAe": "5.3e-17", + "tvlUsd": "0", + "volumeUsdAll": "0", + "volumeUsdDay": "0", + "volumeUsdMonth": "0", + "volumeUsdWeek": "0", + "volumeUsdYear": "0", + }, + { + "address": "ct_token2", + "decimals": 18, + "listed": false, + "malformed": false, + "name": "2", + "noContract": false, + "pairs": 3, + "priceAe": "0.101118", + "priceChangeDay": "0", + "priceChangeMonth": "0", + "priceChangeWeek": "0", + "priceChangeYear": "0", + "priceUsd": "0.0051", + "symbol": "2", + "totalReserve": "1e-15", + "tvlAe": "1.01e-16", + "tvlUsd": "0", + "volumeUsdAll": "0", + "volumeUsdDay": "0", + "volumeUsdMonth": "0", + "volumeUsdWeek": "0", + "volumeUsdYear": "0", + }, + { + "address": "ct_token3", + "decimals": 18, + "listed": false, + "malformed": false, + "name": "3", + "noContract": false, + "pairs": 1, + "priceAe": null, + "priceChangeDay": null, + "priceChangeMonth": null, + "priceChangeWeek": null, + "priceChangeYear": null, + "priceUsd": null, + "symbol": "3", + "totalReserve": null, + "tvlAe": "4.8e-17", + "tvlUsd": "0", + "volumeUsdAll": "0", + "volumeUsdDay": "0", + "volumeUsdMonth": "0", + "volumeUsdWeek": "0", + "volumeUsdYear": "0", + }, + { + "address": "ct_token5", + "decimals": 18, + "listed": false, + "malformed": false, + "name": "5", + "noContract": false, + "pairs": 1, + "priceAe": null, + "priceChangeDay": null, + "priceChangeMonth": null, + "priceChangeWeek": null, + "priceChangeYear": null, + "priceUsd": null, + "symbol": "5", + "totalReserve": null, + "tvlAe": null, + "tvlUsd": null, + "volumeUsdAll": null, + "volumeUsdDay": null, + "volumeUsdMonth": null, + "volumeUsdWeek": null, + "volumeUsdYear": null, + }, +] +`; + +exports[`TokenController GET /tokens/{token_address} should return a single token if it exists 1`] = ` +{ + "address": "ct_token1", + "decimals": 18, + "listed": false, + "malformed": false, + "name": "1", + "noContract": false, + "pairs": 1, + "priceAe": "0.05308695", + "priceChangeDay": "0", + "priceChangeMonth": "0", + "priceChangeWeek": "0", + "priceChangeYear": "0", + "priceUsd": "0.0027", + "symbol": "1", + "totalReserve": "1e-15", + "tvlAe": "5.3e-17", + "tvlUsd": "0", + "volumeUsdAll": "0", + "volumeUsdDay": "0", + "volumeUsdMonth": "0", + "volumeUsdWeek": "0", + "volumeUsdYear": "0", +} +`; + +exports[`TokenController GET /tokens/{token_address}/pairs should return pairs with liquidity info 1`] = ` +{ + "pairs0": [ + { + "address": "ct_pair2", + "liquidityInfo": { + "height": 1, + "reserve0": "1", + "reserve1": "1", + "totalSupply": "1", + }, + "oppositeToken": { + "address": "ct_token3", + "decimals": 18, + "listed": false, + "malformed": false, + "name": "3", + "noContract": false, + "symbol": "3", + }, + "synchronized": true, + }, + { + "address": "ct_pair3", + "liquidityInfo": { + "height": 2, + "reserve0": "2", + "reserve1": "2", + "totalSupply": "2", + }, + "oppositeToken": { + "address": "ct_token5", + "decimals": 18, + "listed": false, + "malformed": false, + "name": "5", + "noContract": false, + "symbol": "5", + }, + "synchronized": true, + }, + ], + "pairs1": [ + { + "address": "ct_pair1", + "oppositeToken": { + "address": "ct_token1", + "decimals": 18, + "listed": false, + "malformed": false, + "name": "1", + "noContract": false, + "symbol": "1", + }, + "synchronized": true, + }, + ], +} +`; + +exports[`TokenController GET /tokens/{token_address}/pairs should return pairs with no liquidity info 1`] = ` +{ + "pairs0": [ + { + "address": "ct_pair1", + "oppositeToken": { + "address": "ct_token2", + "decimals": 18, + "listed": false, + "malformed": false, + "name": "2", + "noContract": false, + "symbol": "2", + }, + "synchronized": true, + }, + ], + "pairs1": [], +} +`; + +exports[`TokenController GET /tokens/listed should return listed tokens if some are listed 1`] = ` +[ + { + "address": "ct_token1", + "decimals": 18, + "malformed": false, + "name": "1", + "noContract": false, + "symbol": "1", + }, + { + "address": "ct_token3", + "decimals": 18, + "malformed": false, + "name": "3", + "noContract": false, + "symbol": "3", + }, +] +`; + +exports[`TokenController POST /tokens/listed/{token_address} should return 201 with valid auth key and valid token and mark the token as listed 1`] = ` +{ + "address": "ct_token1", + "decimals": 18, + "listed": false, + "malformed": false, + "name": "1", + "noContract": false, + "pairs": 1, + "priceAe": "0.05308695", + "priceChangeDay": "0", + "priceChangeMonth": "0", + "priceChangeWeek": "0", + "priceChangeYear": "0", + "priceUsd": "0.0027", + "symbol": "1", + "totalReserve": "1e-15", + "tvlAe": "5.3e-17", + "tvlUsd": "0", + "volumeUsdAll": "0", + "volumeUsdDay": "0", + "volumeUsdMonth": "0", + "volumeUsdWeek": "0", + "volumeUsdYear": "0", +} +`; + +exports[`TokenController POST /tokens/listed/{token_address} should return 201 with valid auth key and valid token and mark the token as listed 2`] = ` +{ + "address": "ct_token1", + "decimals": 18, + "listed": true, + "malformed": false, + "name": "1", + "noContract": false, + "symbol": "1", +} +`; + +exports[`TokenController POST /tokens/listed/{token_address} should return 201 with valid auth key and valid token and mark the token as listed 3`] = ` +{ + "address": "ct_token1", + "decimals": 18, + "listed": true, + "malformed": false, + "name": "1", + "noContract": false, + "pairs": 1, + "priceAe": "0.05308695", + "priceChangeDay": "0", + "priceChangeMonth": "0", + "priceChangeWeek": "0", + "priceChangeYear": "0", + "priceUsd": "0.0027", + "symbol": "1", + "totalReserve": "1e-15", + "tvlAe": "5.3e-17", + "tvlUsd": "0", + "volumeUsdAll": "0", + "volumeUsdDay": "0", + "volumeUsdMonth": "0", + "volumeUsdWeek": "0", + "volumeUsdYear": "0", +} +`; diff --git a/test/e2e/pair-liquidity-info-history-db.service.e2e-spec.ts b/test/e2e/pair-liquidity-info-history-db.service.e2e-spec.ts index 83247aa8..922bf1c3 100644 --- a/test/e2e/pair-liquidity-info-history-db.service.e2e-spec.ts +++ b/test/e2e/pair-liquidity-info-history-db.service.e2e-spec.ts @@ -18,6 +18,7 @@ import { token1, token2, token3, + token5, } from '@/test/mock-data/pair-liquidity-info-history-mock-data'; describe('PairLiquidityInfoHistoryDbService', () => { @@ -36,7 +37,9 @@ describe('PairLiquidityInfoHistoryDbService', () => { }); beforeEach(async () => { - await prismaService.token.createMany({ data: [token1, token2, token3] }); + await prismaService.token.createMany({ + data: [token1, token2, token3, token5], + }); await prismaService.pair.createMany({ data: [pair1, pair2, pair3] }); await prismaService.pairLiquidityInfoHistory.createMany({ data: [historyEntry4, historyEntry2, historyEntry3, historyEntry1], @@ -62,11 +65,14 @@ describe('PairLiquidityInfoHistoryDbService', () => { reserve1: new Decimal(500), deltaReserve0: new Decimal(-500), deltaReserve1: new Decimal(-500), + token0AePrice: new Decimal(0.060559), + token1AePrice: new Decimal(0.060559), aeUsdPrice: new Decimal(0.060559), height: 200002, microBlockHash: historyEntry2.microBlockHash, microBlockTime: 2000000000002n, transactionHash: historyEntry2.transactionHash, + senderAccount: 'abc', transactionIndex: 200002n, logIndex: historyEntry2.logIndex, }; @@ -85,11 +91,14 @@ describe('PairLiquidityInfoHistoryDbService', () => { reserve1: new Decimal(500), deltaReserve0: new Decimal(-500), deltaReserve1: new Decimal(-500), + token0AePrice: new Decimal(0), + token1AePrice: new Decimal(0), aeUsdPrice: new Decimal(0), height: 500005, microBlockHash: 'mh_entry5', microBlockTime: 5000000000005n, transactionHash: 'th_entry5', + senderAccount: '', transactionIndex: 500005n, logIndex: 1, }; @@ -132,7 +141,16 @@ describe('PairLiquidityInfoHistoryDbService', () => { describe('getAll', () => { it('should return all entries', async () => { - const result = await service.getAll(100, 0, OrderQueryEnum.asc); + const result = await service.getAll({ + limit: 100, + offset: 0, + order: OrderQueryEnum.asc, + pairAddress: undefined, + tokenAddress: undefined, + height: undefined, + fromBlockTime: undefined, + toBlockTime: undefined, + }); expect(result.map((e) => e.id)).toEqual([ historyEntry1.id, historyEntry2.id, @@ -142,7 +160,16 @@ describe('PairLiquidityInfoHistoryDbService', () => { }); it('should return return entries with limit, offset and order', async () => { - const result = await service.getAll(2, 1, OrderQueryEnum.desc); + const result = await service.getAll({ + limit: 2, + offset: 1, + order: OrderQueryEnum.desc, + pairAddress: undefined, + tokenAddress: undefined, + height: undefined, + fromBlockTime: undefined, + toBlockTime: undefined, + }); expect(result.map((e) => e.id)).toEqual([ historyEntry3.id, historyEntry2.id, @@ -150,15 +177,16 @@ describe('PairLiquidityInfoHistoryDbService', () => { }); it('should correctly filter by pair address', async () => { - const result = await service.getAll( - 100, - 0, - OrderQueryEnum.asc, - pair1.address as ContractAddress, - undefined, - undefined, - undefined, - ); + const result = await service.getAll({ + limit: 100, + offset: 0, + order: OrderQueryEnum.asc, + pairAddress: pair1.address as ContractAddress, + tokenAddress: undefined, + height: undefined, + fromBlockTime: undefined, + toBlockTime: undefined, + }); expect(result.map((e) => e.id)).toEqual([ historyEntry1.id, historyEntry2.id, @@ -166,15 +194,16 @@ describe('PairLiquidityInfoHistoryDbService', () => { }); it('should correctly filter by height', async () => { - const result = await service.getAll( - 100, - 0, - OrderQueryEnum.asc, - undefined, - 300003, - undefined, - undefined, - ); + const result = await service.getAll({ + limit: 100, + offset: 0, + order: OrderQueryEnum.asc, + pairAddress: undefined, + tokenAddress: undefined, + height: 300003, + fromBlockTime: undefined, + toBlockTime: undefined, + }); expect(result.map((e) => e.id)).toEqual([ historyEntry3.id, historyEntry4.id, @@ -182,15 +211,16 @@ describe('PairLiquidityInfoHistoryDbService', () => { }); it('should correctly return entries newer or equal fromBlockTime', async () => { - const result = await service.getAll( - 100, - 0, - OrderQueryEnum.asc, - undefined, - undefined, - 2000000000002n, - undefined, - ); + const result = await service.getAll({ + limit: 100, + offset: 0, + order: OrderQueryEnum.asc, + pairAddress: undefined, + tokenAddress: undefined, + height: undefined, + fromBlockTime: 2000000000002n, + toBlockTime: undefined, + }); expect(result.map((e) => e.id)).toEqual([ historyEntry2.id, historyEntry3.id, @@ -199,15 +229,16 @@ describe('PairLiquidityInfoHistoryDbService', () => { }); it('should correctly return entries older or equal toBlockTime', async () => { - const result = await service.getAll( - 100, - 0, - OrderQueryEnum.desc, - undefined, - undefined, - undefined, - 3000000000003n, - ); + const result = await service.getAll({ + limit: 100, + offset: 0, + order: OrderQueryEnum.desc, + pairAddress: undefined, + tokenAddress: undefined, + height: undefined, + fromBlockTime: undefined, + toBlockTime: 3000000000003n, + }); expect(result.map((e) => e.id)).toEqual([ historyEntry4.id, historyEntry3.id, diff --git a/test/e2e/pair-sync.service.e2e-spec.ts b/test/e2e/pair-sync.service.e2e-spec.ts index a1c5cddd..59dc8a46 100644 --- a/test/e2e/pair-sync.service.e2e-spec.ts +++ b/test/e2e/pair-sync.service.e2e-spec.ts @@ -1,11 +1,19 @@ +import { CacheModule } from '@nestjs/cache-manager'; import { Test, TestingModule } from '@nestjs/testing'; import prisma from '@prisma/client'; +import { CoinmarketcapClientService } from '@/clients/coinmarketcap-client.service'; +import { HttpService } from '@/clients/http.service'; +import { MdwHttpClientService } from '@/clients/mdw-http-client.service'; import { MdwWsClientService } from '@/clients/mdw-ws-client.service'; import { SdkClientService } from '@/clients/sdk-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 { TokenDbService } from '@/database/token/token-db.service'; +import { PairLiquidityInfoHistoryImporterService } from '@/tasks/pair-liquidity-info-history-importer/pair-liquidity-info-history-importer.service'; +import { PairPathCalculatorService } from '@/tasks/pair-path-calculator/pair-path-calculator.service'; import { PairSyncService } from '@/tasks/pair-sync/pair-sync.service'; import * as data from '@/test/mock-data/context-mock-data'; import { mockContext } from '@/test/utils/context-mock'; @@ -22,13 +30,21 @@ describe('PairSyncService', () => { beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ + imports: [CacheModule.register({})], providers: [ PairSyncService, PrismaService, TokenDbService, PairDbService, + HttpService, + CoinmarketcapClientService, + MdwHttpClientService, MdwWsClientService, SdkClientService, + PairLiquidityInfoHistoryImporterService, + PairLiquidityInfoHistoryDbService, + PairLiquidityInfoHistoryErrorDbService, + PairPathCalculatorService, ], }).compile(); diff --git a/test/e2e/pairs.e2e-spec.ts b/test/e2e/pairs.e2e-spec.ts index cb596c9b..361d7a41 100644 --- a/test/e2e/pairs.e2e-spec.ts +++ b/test/e2e/pairs.e2e-spec.ts @@ -1,46 +1,58 @@ +import { CacheModule } from '@nestjs/cache-manager'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import * as request from 'supertest'; import { PairsController } from '@/api/pairs/pairs.controller'; import { PairsService } from '@/api/pairs/pairs.service'; -import { MdwWsClientService } from '@/clients/mdw-ws-client.service'; -import { SdkClientService } from '@/clients/sdk-client.service'; +import { TokensController } from '@/api/tokens/tokens.controller'; +import { TokensService } from '@/api/tokens/tokens.service'; import { PairDbService } from '@/database/pair/pair-db.service'; import { PrismaService } from '@/database/prisma.service'; import { TokenDbService } from '@/database/token/token-db.service'; -import { PairSyncService } from '@/tasks/pair-sync/pair-sync.service'; -import * as data from '@/test/mock-data/context-mock-data'; -import { mockContext } from '@/test/utils/context-mock'; -import { cleanDb, listToken } from '@/test/utils/db-helper'; -import { sortByAddress } from '@/test/utils/utils'; +import { nonNullable } from '@/lib/utils'; +import { + historyEntry1, + historyEntry2, + historyEntry3, + historyEntry4, + liquidityInfo1, + liquidityInfo2, + pair1, + pair2, + pair3, + pair4, + token1, + token2, + token3, + token4, + token5, +} from '@/test/mock-data/pair-liquidity-info-history-mock-data'; // Testing method // before all // - initiate nest app // before each // - clean db -// - create a common context -// - refreshPairs +// - insert test data // after all // - close nest app // - disconnect from prisma describe('PairsController', () => { let app: INestApplication; let prismaService: PrismaService; - let pairSyncService: PairSyncService; + + const authToken = nonNullable(process.env.AUTH_TOKEN); beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ - controllers: [PairsController], + controllers: [PairsController, TokensController], + imports: [CacheModule.register({})], providers: [ - MdwWsClientService, - SdkClientService, - PairDbService, - PairDbService, PairsService, - PairSyncService, + PairDbService, PrismaService, + TokensService, TokenDbService, ], }).compile(); @@ -49,246 +61,79 @@ describe('PairsController', () => { await app.init(); prismaService = module.get(PrismaService); - pairSyncService = module.get(PairSyncService); }); - afterAll(async () => { - await prismaService.$disconnect(); - await app.close(); + beforeAll(async () => { + await prismaService.pairLiquidityInfoHistory.deleteMany(); + await prismaService.pairLiquidityInfo.deleteMany(); + await prismaService.pair.deleteMany(); + await prismaService.token.deleteMany(); }); beforeEach(async () => { - await cleanDb(prismaService); - pairSyncService.ctx = mockContext(data.context2); - await pairSyncService['refreshPairs'](); - }); - - 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, - }, - ]); + await prismaService.token.createMany({ + data: [token1, token2, token3, token4, token5], }); + await prismaService.pair.createMany({ data: [pair1, pair2, pair3, pair4] }); + await prismaService.pairLiquidityInfo.createMany({ + data: [liquidityInfo1, liquidityInfo2], + }); + await prismaService.pairLiquidityInfoHistory.createMany({ + data: [historyEntry1, historyEntry2, historyEntry3, historyEntry4], + }); + }); - it('/pairs (GET) 200 synchronized', async () => { - await pairSyncService['refreshPairsLiquidity'](); - const response = await request(app.getHttpServer()) - .get('/pairs') - .expect(200); + afterEach(async () => { + await prismaService.pairLiquidityInfoHistory.deleteMany(); + await prismaService.pairLiquidityInfo.deleteMany(); + await prismaService.pair.deleteMany(); + await prismaService.token.deleteMany(); + }); - 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, - }, - ]); - }); + afterAll(async () => { + await prismaService.$disconnect(); + await app.close(); + }); - 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()) + describe('GET /pairs', () => { + it('should return all pairs', async () => { + return 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', - }, - ]); + .expect(200) + .then((res) => expect(JSON.parse(res.text)).toMatchSnapshot()); }); - it('/pairs (GET) 200 only-listed=true', async () => { - await pairSyncService['refreshPairsLiquidity'](); - pairSyncService.ctx = mockContext(data.context21); - await pairSyncService['refreshPairs'](); - - let response = await request(app.getHttpServer()) + it('should return only listed pairs with only-listed=true', async () => { + await request(app.getHttpServer()) .get('/pairs?only-listed=true') - .expect(200); + .expect(200) + .expect([]); - expect(sortByAddress(JSON.parse(response.text))).toEqual([]); - await listToken(prismaService, 'ct_t0'); - await listToken(prismaService, 'ct_t3'); - await listToken(prismaService, 'ct_t4'); + await request(app.getHttpServer()) + .post('/tokens/listed/ct_token1') + .set('Authorization', authToken); + await request(app.getHttpServer()) + .post('/tokens/listed/ct_token2') + .set('Authorization', authToken); - response = await request(app.getHttpServer()) + 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_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 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', - }, - }); + .then((res) => expect(JSON.parse(res.text)).toMatchSnapshot()); }); + }); - it('/pairs/by-address/ct_p1 (GET) 200 unsynchronized with liquidity', async () => { - await pairSyncService['refreshPairsLiquidity'](); - await pairSyncService['unsyncAllPairs'](); + describe('GET /pairs/by-address/{pair_address}', () => { + it('should return pair if it exists', async () => { return request(app.getHttpServer()) - .get('/pairs/by-address/ct_p1') + .get('/pairs/by-address/ct_pair1') .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', - }, - }); + .then((res) => expect(JSON.parse(res.text)).toMatchSnapshot()); }); - it('/pairs/by-address/ct_0000 (GET) 404 not founded pair', async () => { + it('should return 404 if pair does not exist', async () => { return request(app.getHttpServer()) - .get('/pairs/by-address/ct_0000') + .get('/pairs/by-address/ct_xxxx') .expect(404) .expect({ statusCode: 404, @@ -298,237 +143,61 @@ describe('PairsController', () => { }); }); - describe('/pairs/swap-routes', () => { - it('/pairs/swap-routes/ct_t0/ct_t5 (GET) 200 no path for unexisting token ', async () => { + describe('GET /pairs/swap-routes/{from}/{to}', () => { + it('should return 200 with [] if no path for unexisting token ', async () => { return request(app.getHttpServer()) - .get('/pairs/swap-routes/ct_t0/ct_t5') + .get('/pairs/swap-routes/ct_token1/ct_xxxx') .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, - }, - }), - }); - await pairSyncService['refreshPairs'](); - + it('should return 200 with [] for existing token if no pair or path exists', async () => { return request(app.getHttpServer()) - .get('/pairs/swap-routes/ct_t0/ct_t5') + .get('/pairs/swap-routes/ct_token1/ct_token4') .expect(200) .expect([]); }); - it('/pairs/swap-routes/ct_t0/ct_t4 (GET) 200 direct path', async () => { - pairSyncService.ctx = mockContext(data.context21); - await pairSyncService['refreshPairs'](); - + it('should return a direct path', async () => { return request(app.getHttpServer()) - .get('/pairs/swap-routes/ct_t0/ct_t4') + .get('/pairs/swap-routes/ct_token2/ct_token3') .expect(200) - .expect([ - [ - { - address: 'ct_p4', - token0: 'ct_t0', - token1: 'ct_t4', - synchronized: false, - }, - ], - ]); + .then((res) => expect(JSON.parse(res.text)).toMatchSnapshot()); }); - it('/pairs/swap-routes/ct_t0/ct_t3 (GET) 200 synchronized', async () => { - pairSyncService.ctx = mockContext(data.context21); - await pairSyncService['refreshPairsLiquidity'](); - + it('should return an indirect path', async () => { return request(app.getHttpServer()) - .get('/pairs/swap-routes/ct_t0/ct_t1') + .get('/pairs/swap-routes/ct_token1/ct_token3') .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', - }, - }, - ], - ]); + .then((res) => expect(JSON.parse(res.text)).toMatchSnapshot()); }); - it('/pairs/swap-routes/ct_t0/ct_t1 (GET) 200 one direct path and one indirect path', async () => { + it('should return one direct path and one indirect path', async () => { return request(app.getHttpServer()) - .get('/pairs/swap-routes/ct_t0/ct_t1') + .get('/pairs/swap-routes/ct_token1/ct_token5') .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, - }, - ], - ]); + .then((res) => expect(JSON.parse(res.text)).toMatchSnapshot()); }); - 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('should suppress some paths with only-listed=true ', async () => { + await request(app.getHttpServer()) + .post('/tokens/listed/ct_token1') + .set('Authorization', authToken); + await request(app.getHttpServer()) + .post('/tokens/listed/ct_token5') + .set('Authorization', authToken); - 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') + .get('/pairs/swap-routes/ct_token1/ct_token5?only-listed=true') .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, - }, - ], - ]); + .then((res) => expect(JSON.parse(res.text)).toMatchSnapshot()); }); - 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'](); - + it('should return paths oven on reverse order of tokens', async () => { return request(app.getHttpServer()) - .get('/pairs/swap-routes/ct_t0/ct_t1') + .get('/pairs/swap-routes/ct_token1/ct_token5') .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, - }, - ], - ]); + .then((res) => expect(JSON.parse(res.text)).toMatchSnapshot()); }); }); }); diff --git a/test/e2e/tokens.e2e-spec.ts b/test/e2e/tokens.e2e-spec.ts index 7e566de9..4ae8cd7c 100644 --- a/test/e2e/tokens.e2e-spec.ts +++ b/test/e2e/tokens.e2e-spec.ts @@ -1,62 +1,84 @@ +import * as process from 'node:process'; + +import { CacheModule } from '@nestjs/cache-manager'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import * as request from 'supertest'; -import * as dto from '@/api/api.model'; import { TokensController } from '@/api/tokens/tokens.controller'; import { TokensService } from '@/api/tokens/tokens.service'; -import { MdwWsClientService } from '@/clients/mdw-ws-client.service'; -import { SdkClientService } from '@/clients/sdk-client.service'; -import { PairDbService } from '@/database/pair/pair-db.service'; import { PrismaService } from '@/database/prisma.service'; import { TokenDbService } from '@/database/token/token-db.service'; import { nonNullable } from '@/lib/utils'; -import { PairSyncService } from '@/tasks/pair-sync/pair-sync.service'; -import * as data from '@/test/mock-data/context-mock-data'; -import { mockContext } from '@/test/utils/context-mock'; -import { cleanDb, listToken } from '@/test/utils/db-helper'; -import { sortByAddress } from '@/test/utils/utils'; +import { + historyEntry1, + historyEntry2, + historyEntry3, + historyEntry4, + liquidityInfo1, + liquidityInfo2, + pair1, + pair2, + pair3, + token1, + token2, + token3, + token5, +} from '@/test/mock-data/pair-liquidity-info-history-mock-data'; // Testing method // before all // - initiate nest app // before each // - clean db -// - create a common context -// - refreshPairs +// - insert test data // after all // - close nest app // - disconnect from prisma describe('TokenController', () => { let app: INestApplication; let prismaService: PrismaService; - let pairSyncService: PairSyncService; + + const authToken = nonNullable(process.env.AUTH_TOKEN); beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [TokensController], - providers: [ - MdwWsClientService, - SdkClientService, - PairDbService, - PairSyncService, - PrismaService, - TokenDbService, - TokensService, - ], + imports: [CacheModule.register({})], + providers: [TokenDbService, TokensService, PrismaService], }).compile(); app = module.createNestApplication(); await app.init(); prismaService = module.get(PrismaService); - pairSyncService = module.get(PairSyncService); + }); + + beforeAll(async () => { + await prismaService.pairLiquidityInfoHistory.deleteMany(); + await prismaService.pairLiquidityInfo.deleteMany(); + await prismaService.pair.deleteMany(); + await prismaService.token.deleteMany(); }); beforeEach(async () => { - await cleanDb(prismaService); - pairSyncService.ctx = mockContext(data.context2); - await pairSyncService['refreshPairs'](); + await prismaService.token.createMany({ + data: [token1, token2, token3, token5], + }); + await prismaService.pair.createMany({ data: [pair1, pair2, pair3] }); + await prismaService.pairLiquidityInfo.createMany({ + data: [liquidityInfo1, liquidityInfo2], + }); + await prismaService.pairLiquidityInfoHistory.createMany({ + data: [historyEntry1, historyEntry2, historyEntry3, historyEntry4], + }); + }); + + afterEach(async () => { + await prismaService.pairLiquidityInfoHistory.deleteMany(); + await prismaService.pairLiquidityInfo.deleteMany(); + await prismaService.pair.deleteMany(); + await prismaService.token.deleteMany(); }); afterAll(async () => { @@ -64,141 +86,39 @@ describe('TokenController', () => { await app.close(); }); - describe('/tokens', () => { - it('/tokens (GET)', async () => { - const response = await request(app.getHttpServer()) + describe('GET /tokens', () => { + it('should return all tokens when none are listed', async () => { + await request(app.getHttpServer()) .get('/tokens') - .expect(200); - const value: dto.TokenWithListed[] = JSON.parse(response.text); - - expect(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 listToken(prismaService, 'ct_t0'); - await listToken(prismaService, 'ct_t3'); - const response = await request(app.getHttpServer()) - .get('/tokens/listed') - .expect(200); - const value: dto.Token[] = JSON.parse(response.text); - - expect(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, - }, - ]); + .then((res) => expect(JSON.parse(res.text)).toMatchSnapshot()); }); - it('/tokens (GET) 200 with some listed', async () => { - await listToken(prismaService, 'ct_t0'); - await listToken(prismaService, 'ct_t3'); - const response = await request(app.getHttpServer()) + it('should return all tokens even if some are listed', async () => { + await request(app.getHttpServer()) + .post('/tokens/listed/ct_token1') + .set({ Authorization: authToken }); + await request(app.getHttpServer()) + .post('/tokens/listed/ct_token3') + .set({ Authorization: authToken }); + 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, - }, - ]); + .expect(200) + .then((res) => expect(JSON.parse(res.text)).toMatchSnapshot()); }); + }); - it('/tokens/by-address/ct_t0 (GET) 200', () => { - return request(app.getHttpServer()) - .get('/tokens/by-address/ct_t0') + describe('GET /tokens/{token_address}', () => { + it('should return a single token if it exists', async () => { + await request(app.getHttpServer()) + .get('/tokens/ct_token1') .expect(200) - .expect({ - address: 'ct_t0', - symbol: 'A', - name: 'A Token', - decimals: 18, - listed: false, - malformed: false, - noContract: false, - pairs: ['ct_p1', 'ct_p3'], - }); + .then((res) => expect(JSON.parse(res.text)).toMatchSnapshot()); }); - it('/tokens/by-address/ct_tXXX (GET) 404', () => { + it('should return 404 if the token does not exist', () => { return request(app.getHttpServer()) - .get('/tokens/by-address/ct_tXXX') + .get('/tokens/ct_xxxx') .expect(404) .expect({ statusCode: 404, @@ -206,291 +126,26 @@ describe('TokenController', () => { 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(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, - listed: false, - malformed: false, - noContract: false, - }, - }, - ]); - }); - - it('/tokens/by-address/ct_t0/pairs (GET) 200 with pairs only on pairs0', async () => { - pairSyncService.ctx = mockContext(data.context21); - await pairSyncService['refreshPairs'](); - await pairSyncService['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(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/by-address/ct_t3/pairs (GET) 200 with pairs only on pairs1', async () => { - pairSyncService.ctx = mockContext(data.context21); - await pairSyncService['refreshPairs'](); - await pairSyncService['refreshPairsLiquidity'](); - await listToken(prismaService, 'ct_t0'); - await 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(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', - }, - }, - ]); + describe('GET /tokens/{token_address}/pairs', () => { + it('should return pairs with no liquidity info', async () => { + await request(app.getHttpServer()) + .get('/tokens/ct_token1/pairs') + .expect(200) + .then((res) => expect(JSON.parse(res.text)).toMatchSnapshot()); }); - it('/tokens/by-address/ct_t3/pairs (GET) 200 with pairs on pairs0 and pairs1', async () => { - pairSyncService.ctx = 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 listToken(prismaService, 'ct_t0'); - await 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(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(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('should return pairs with liquidity info', async () => { + await request(app.getHttpServer()) + .get('/tokens/ct_token2/pairs') + .expect(200) + .then((res) => expect(JSON.parse(res.text)).toMatchSnapshot()); }); - it('/tokens/by-address/ct_tXXX/pairs (GET) 404', () => { + it('should return 404 if the token does not exist', () => { return request(app.getHttpServer()) - .get('/tokens/by-address/ct_tXXX') + .get('/tokens/ct_xxxx') .expect(404) .expect({ statusCode: 404, @@ -500,172 +155,143 @@ describe('TokenController', () => { }); }); - describe('/tokens/listed', () => { + describe('GET /tokens/listed', () => { + it('should return no tokens if none are listed', () => { + return request(app.getHttpServer()) + .get('/tokens/listed') + .expect(200) + .expect([]); + }); + + it('should return listed tokens if some are listed', async () => { + await request(app.getHttpServer()) + .post('/tokens/listed/ct_token1') + .set('Authorization', authToken); + await request(app.getHttpServer()) + .post('/tokens/listed/ct_token3') + .set('Authorization', authToken); + await request(app.getHttpServer()) + .get('/tokens/listed') + .expect(200) + .then((res) => expect(JSON.parse(res.text)).toMatchSnapshot()); + }); + }); + + describe('POST /tokens/listed/{token_address}', () => { + it('should return 401 with no auth key and invalid token address', async () => { + await request(app.getHttpServer()) + .post('/tokens/listed/ct_xxxx') + .expect(401); + }); + + it('should return 401 with no auth key and valid token address', async () => { + await request(app.getHttpServer()) + .post('/tokens/listed/ct_token1') + .expect(401); + }); + + it('should return 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('should return 401 with invalid auth key and valid token', async () => { + await request(app.getHttpServer()) + .post('/tokens/listed/ct_token1') + .set('Authorization', 'wrong-key') + .expect(401); + }); + + it('should return 404 with valid auth key but with invalid token', async () => { + await request(app.getHttpServer()) + .post('/tokens/listed/ct_xxxx') + .set('Authorization', authToken) + .expect(404); + }); + + it('should return 201 with valid auth key and valid token and mark the token as listed', async () => { + //verify before listing ct_t1 + await request(app.getHttpServer()) + .get('/tokens/ct_token1') + .expect(200) + .then((res) => expect(JSON.parse(res.text)).toMatchSnapshot()); + + //listing it + await request(app.getHttpServer()) + .post('/tokens/listed/ct_token1') + .set('Authorization', authToken) + .expect(201) + .then((res) => expect(JSON.parse(res.text)).toMatchSnapshot()); + + //re-verify ct_t1 to be sure it was persisted also + await request(app.getHttpServer()) + .get('/tokens/ct_token1') + .expect(200) + .then((res) => expect(JSON.parse(res.text)).toMatchSnapshot()); + }); + }); + + describe('DELETE /tokens/listed/{token_address}', () => { beforeEach(async () => { - await listToken(prismaService, 'ct_t0'); - await listToken(prismaService, 'ct_t3'); + await request(app.getHttpServer()) + .post('/tokens/listed/ct_token3') + .set('Authorization', authToken); }); - 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('should return 401 with no auth key and invalid token', async () => { + await request(app.getHttpServer()) + .delete('/tokens/listed/ct_xxxx') + .expect(401); }); - 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'], - }); - }); + it('should return 401 with no auth key and valid token address', async () => { + await request(app.getHttpServer()) + .delete('/tokens/listed/ct_token3') + .expect(401); + }); + + it('should return 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('should return 401 with invalid auth key and valid token', async () => { + await request(app.getHttpServer()) + .delete('/tokens/listed/ct_token3') + .set('Authorization', 'wrong-key') + .expect(401); + }); + + it('should return 404 with valid auth key but with invalid token', async () => { + await request(app.getHttpServer()) + .delete('/tokens/listed/ct_xxxx') + .set('Authorization', authToken) + .expect(404); + }); + + it('should return 200 with valid auth key and valid token', async () => { + //verify before unlisting ct_t3 + await request(app.getHttpServer()) + .get('/tokens/ct_token3') + .expect(200) + .then((res) => expect(JSON.parse(res.text)).toMatchSnapshot()); + + //unlisting it + await request(app.getHttpServer()) + .delete('/tokens/listed/ct_token3') + .set('Authorization', nonNullable(process.env.AUTH_TOKEN)) + .expect(200) + .then((res) => expect(JSON.parse(res.text)).toMatchSnapshot()); + + //re-verify ct_t3 to be sure the unlisting was persisted too + await request(app.getHttpServer()) + .get('/tokens/ct_token3') + .expect(200) + .then((res) => expect(JSON.parse(res.text)).toMatchSnapshot()); }); }); }); diff --git a/test/mock-data/pair-liquidity-info-history-mock-data.ts b/test/mock-data/pair-liquidity-info-history-mock-data.ts index 8ff12abf..6f184ce5 100644 --- a/test/mock-data/pair-liquidity-info-history-mock-data.ts +++ b/test/mock-data/pair-liquidity-info-history-mock-data.ts @@ -1,4 +1,9 @@ -import { Pair, PairLiquidityInfoHistory, Token } from '@prisma/client'; +import { + Pair, + PairLiquidityInfo, + PairLiquidityInfoHistory, + Token, +} from '@prisma/client'; import { Decimal } from '@prisma/client/runtime/library'; import { Contract } from '@/clients/mdw-http-client.model'; @@ -38,6 +43,28 @@ export const token3: Token = { listed: false, }; +export const token4: Token = { + id: 4, + address: 'ct_token4', + symbol: '4', + name: '4', + decimals: 18, + malformed: false, + noContract: false, + listed: false, +}; + +export const token5: Token = { + id: 5, + address: 'ct_token5', + symbol: '5', + name: '5', + decimals: 18, + malformed: false, + noContract: false, + listed: false, +}; + export const pair1: Pair = { id: 1, address: 'ct_pair1', @@ -56,12 +83,36 @@ export const pair2: Pair = { export const pair3: Pair = { id: 3, - address: 'ct_pair4', + address: 'ct_pair3', t0: 2, - t1: 3, + t1: 5, + synchronized: true, +}; + +export const pair4: Pair = { + id: 4, + address: 'ct_pair4', + t0: 1, + t1: 5, synchronized: true, }; +export const liquidityInfo1: PairLiquidityInfo = { + id: 2, + height: 1, + totalSupply: '1', + reserve0: '1', + reserve1: '1', +}; + +export const liquidityInfo2: PairLiquidityInfo = { + id: 3, + height: 2, + totalSupply: '2', + reserve0: '2', + reserve1: '2', +}; + export const historyEntry1: PairLiquidityInfoHistory = { senderAccount: '', token0AePrice: new Decimal(0.050559),