Skip to content

Commit

Permalink
feat(yield): return liquidity pools info (#99)
Browse files Browse the repository at this point in the history
  • Loading branch information
shoom3301 authored Dec 13, 2024
1 parent bb2bdb9 commit 6e1e26a
Show file tree
Hide file tree
Showing 18 changed files with 356 additions and 4 deletions.
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@
#ORDERBOOK_DATABASE_USERNAME=
#ORDERBOOK_DATABASE_PASSWORD=

# Analytics database (used in Yield aka Vampire attack)
#COW_ANALYTICS_DATABASE_NAME=
#COW_ANALYTICS_DATABASE_HOST=
#COW_ANALYTICS_DATABASE_PORT=
#COW_ANALYTICS_DATABASE_USERNAME=
#COW_ANALYTICS_DATABASE_PASSWORD=

# CMS
#CMS_API_KEY=
#CMS_BASE_URL=https://cms.cow.fi
Expand Down
32 changes: 32 additions & 0 deletions apps/api/src/app/data/poolInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
Column,
Entity,
PrimaryColumn
} from 'typeorm';
import { bufferToString, stringToBuffer } from '@cowprotocol/shared';

@Entity({ name: 'cow_amm_competitor_info', schema: 'public' })
export class PoolInfo {
@PrimaryColumn('bytea', {
transformer: { from: bufferToString, to: stringToBuffer },
})
contract_address: string;

@Column('int')
chain_id: number;

@Column('varchar')
project: string;

@Column('double precision')
apr: number;

@Column('double precision')
fee: number;

@Column('double precision')
tvl: number;

@Column('double precision')
volume: number;
}
17 changes: 17 additions & 0 deletions apps/api/src/app/plugins/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@ const schema = {
MORALIS_API_KEY: {
type: 'string',
},

// CoW Analytics DB
COW_ANALYTICS_DATABASE_NAME: {
type: 'string',
},
COW_ANALYTICS_DATABASE_HOST: {
type: 'string',
},
COW_ANALYTICS_DATABASE_PORT: {
type: 'number',
},
COW_ANALYTICS_DATABASE_USERNAME: {
type: 'string',
},
COW_ANALYTICS_DATABASE_PASSWORD: {
type: 'string',
},
},
};

Expand Down
42 changes: 42 additions & 0 deletions apps/api/src/app/plugins/orm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import 'reflect-metadata';
import { FastifyInstance } from 'fastify';
import typeORMPlugin from 'typeorm-fastify-plugin';
import fp from 'fastify-plugin';
import { PoolInfo } from '../data/poolInfo';

export default fp(async function (fastify: FastifyInstance) {
const dbParams = {
host: fastify.config.COW_ANALYTICS_DATABASE_HOST,
port: Number(fastify.config.COW_ANALYTICS_DATABASE_PORT),
database: fastify.config.COW_ANALYTICS_DATABASE_NAME,
username: fastify.config.COW_ANALYTICS_DATABASE_USERNAME,
password: fastify.config.COW_ANALYTICS_DATABASE_PASSWORD,
}

const dbParamsAreInvalid = Object.values(dbParams).some((v) => Number.isNaN(v) || v === undefined);

if (dbParamsAreInvalid) {
console.error('Invalid CoW Analytics database parameters, please check COW_ANALYTICS_* env vars');
return
}

fastify.register(typeORMPlugin, {
...dbParams,
type: 'postgres',
entities: [PoolInfo],
ssl: true,
extra: {
ssl: {
rejectUnauthorized: false
}
}
});

fastify.ready((err) => {
if (err) {
throw err;
}

fastify.orm.runMigrations({ transaction: 'all' });
});
});
4 changes: 4 additions & 0 deletions apps/api/src/app/routes/__chainId/yield/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import ms from 'ms';

export const POOLS_RESULT_LIMIT = 500
export const POOLS_QUERY_CACHE = ms('12h')
65 changes: 65 additions & 0 deletions apps/api/src/app/routes/__chainId/yield/getPoolsAverageApr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { FastifyPluginAsync } from 'fastify';
import { FromSchema } from 'json-schema-to-ts';
import { PoolInfo } from '../../../data/poolInfo';
import {
errorSchema,
paramsSchema,
poolsAverageAprBodySchema
} from './schemas';
import { trimDoubleQuotes } from './utils';
import { CACHE_CONTROL_HEADER, getCacheControlHeaderValue } from '../../../../utils/cache';

type RouteSchema = FromSchema<typeof paramsSchema>;
type SuccessSchema = FromSchema<typeof poolsAverageAprBodySchema>;
type ErrorSchema = FromSchema<typeof errorSchema>;

interface PoolInfoResult {
project: string;
average_apr: number;
}

const CACHE_SECONDS = 21600; // 6 hours

const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get<{
Params: RouteSchema;
Reply: SuccessSchema | ErrorSchema;
}>(
'/pools-average-apr',
{
schema: {
params: paramsSchema,
response: {
'2XX': poolsAverageAprBodySchema,
'400': errorSchema,
},
},
},
async function (request, reply) {
const { chainId } = request.params;

const poolInfoRepository = fastify.orm.getRepository(PoolInfo);

const result = await poolInfoRepository.query(`
SELECT project,
AVG(apr) AS average_apr
FROM cow_amm_competitor_info
WHERE chain_id = ${chainId}
GROUP BY project;
`)

const averageApr = result.reduce((acc: Record<string, number>, val: PoolInfoResult) => {
const projectName = trimDoubleQuotes(val.project)

acc[projectName] = +val.average_apr.toFixed(6)

return acc
}, {})

reply.header(CACHE_CONTROL_HEADER, getCacheControlHeaderValue(CACHE_SECONDS));
reply.status(200).send(averageApr);
}
);
};

export default root;
58 changes: 58 additions & 0 deletions apps/api/src/app/routes/__chainId/yield/getPoolsInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { FastifyPluginAsync } from 'fastify';
import { FromSchema } from 'json-schema-to-ts';
import { PoolInfo } from '../../../data/poolInfo';
import { In } from 'typeorm';
import { poolsInfoBodySchema, errorSchema, paramsSchema, poolsInfoSuccessSchema } from './schemas';
import { POOLS_QUERY_CACHE, POOLS_RESULT_LIMIT } from './const';
import { trimDoubleQuotes } from './utils';

type RouteSchema = FromSchema<typeof paramsSchema>;
type SuccessSchema = FromSchema<typeof poolsInfoSuccessSchema>;
type ErrorSchema = FromSchema<typeof errorSchema>;
type BodySchema = FromSchema<typeof poolsInfoBodySchema>;

const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<{
Params: RouteSchema;
Reply: SuccessSchema | ErrorSchema;
Body: BodySchema;
}>(
'/pools',
{
schema: {
params: paramsSchema,
response: {
'2XX': poolsInfoSuccessSchema,
'400': errorSchema,
},
body: poolsInfoBodySchema
},
},
async function (request, reply) {
const { chainId } = request.params;
const poolsAddresses = request.body;

const poolInfoRepository = fastify.orm.getRepository(PoolInfo);

const results = await poolInfoRepository.find({
take: POOLS_RESULT_LIMIT,
where: {
...(poolsAddresses.length > 0 ? { contract_address: In(poolsAddresses) } : null),
chain_id: chainId
},
cache: POOLS_QUERY_CACHE
})

const mappedResults = results.map(res => {
return {
...res,
project: trimDoubleQuotes(res.project)
}
})

reply.status(200).send(mappedResults);
}
);
};

export default root;
92 changes: 92 additions & 0 deletions apps/api/src/app/routes/__chainId/yield/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { AddressSchema, ChainIdSchema } from '../../../schemas';
import { JSONSchema } from 'json-schema-to-ts';
import { POOLS_RESULT_LIMIT } from './const';

export const paramsSchema = {
type: 'object',
required: ['chainId'],
additionalProperties: false,
properties: {
chainId: ChainIdSchema,
},
} as const satisfies JSONSchema;

export const poolsInfoSuccessSchema = {
type: 'array',
items: {
type: 'object',
required: [
'contract_address',
'chain_id',
'project',
'apr',
'fee',
'tvl',
'volume'
],
additionalProperties: false,
properties: {
contract_address: {
title: 'Pool address',
type: 'string',
pattern: AddressSchema.pattern
},
chain_id: ChainIdSchema,
project: {
title: 'Liquidity provider',
type: 'string',
},
apr: {
title: 'APR',
description: 'Annual Percentage Rate',
type: 'number',
},
fee: {
title: 'Fee tier',
description: 'Pool fee percent',
type: 'number',
},
tvl: {
title: 'TVL',
description: 'Total value locked (in USD)',
type: 'number',
},
volume: {
title: 'Volume 24h',
description: 'Trading volume in the last 24 hours (in USD)',
type: 'number',
},
},
},
} as const satisfies JSONSchema;

export const poolsInfoBodySchema = {
type: 'array',
items: {
title: 'Pool address',
description: 'Blockchain address of the pool',
type: 'string',
pattern: AddressSchema.pattern,
},
maxItems: POOLS_RESULT_LIMIT
} as const satisfies JSONSchema;

export const poolsAverageAprBodySchema = {
type: 'object',
title: 'Liquidity provider - apr',
additionalProperties: true
} as const satisfies JSONSchema;


export const errorSchema = {
type: 'object',
required: ['message'],
additionalProperties: false,
properties: {
message: {
title: 'Message',
description: 'Message describing the error.',
type: 'string',
},
},
} as const satisfies JSONSchema;
6 changes: 6 additions & 0 deletions apps/api/src/app/routes/__chainId/yield/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface PoolInfo {
apy: number
tvl: number
feeTier: number
volume24h: number
}
11 changes: 11 additions & 0 deletions apps/api/src/app/routes/__chainId/yield/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function trimDoubleQuotes(value: string): string {
if (value[0] === '"') {
return trimDoubleQuotes(value.slice(1))
}

if (value[value.length - 1] === '"') {
return trimDoubleQuotes(value.slice(0, -1))
}

return value
}
16 changes: 16 additions & 0 deletions apps/api/src/datasource.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as dotenv from 'dotenv';
import { DataSource } from 'typeorm';

dotenv.config();

export const cowAnalyticsDb = new DataSource({
type: 'postgres',
host: process.env.COW_ANALYTICS_DATABASE_HOST,
port: Number(process.env.COW_ANALYTICS_DATABASE_PORT),
username: process.env.COW_ANALYTICS_DATABASE_USERNAME,
password: process.env.COW_ANALYTICS_DATABASE_PASSWORD,
database: process.env.COW_ANALYTICS_DATABASE_NAME,
entities: ['src/app/data/*.ts'],
});

cowAnalyticsDb.initialize();
2 changes: 1 addition & 1 deletion apps/api/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["node"]
"types": ["node"],
},
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
"include": ["src/**/*.ts"]
Expand Down
1 change: 1 addition & 0 deletions apps/api/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
}
],
"compilerOptions": {
"strictPropertyInitialization": false,
"esModuleInterop": true
}
}
Loading

0 comments on commit 6e1e26a

Please # to comment.