Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(yield): return liquidity pools info #99

Merged
merged 8 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading