Skip to content

Commit

Permalink
feat: add simulation and token holder endpoints (#88)
Browse files Browse the repository at this point in the history
* add token holder repository

* feat: add token holder endpoint

* add tenderly bundle simulation endpoint

* refactor: remove unused code

* feat: add ethplorer token holder repository

* add null value cache on token holder

* chore: add fallback strategy on token holder

* refactor: add generic fallback repository

* refactor: add generic cache repository factory

* revert: usdPrice changes

* refactor: rename tenderly endpoint to simulation

* rename tenderly repository and service to simulation

* refactor cache factory to receive converfns

* fix import typo

* revert refactoring to the fallback and cache factories
  • Loading branch information
yvesfracari authored Oct 17, 2024
1 parent 4eaa935 commit 4a913cb
Show file tree
Hide file tree
Showing 30 changed files with 1,880 additions and 27 deletions.
13 changes: 12 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,15 @@
# JWT_CERT_PASSPHRASE=secret

# Authorized domains
# AUTHORIZED_ORIGINS=cow.fi
# AUTHORIZED_ORIGINS=cow.fi

# GOLD RUSH
# GOLD_RUSH_API_KEY=

# TENDERLY
# TENDERLY_API_KEY=
# TENDERLY_ORG_NAME=
# TENDERLY_PROJECT_NAME=

# ETHPLORER
# ETHPLORER_API_KEY=
65 changes: 65 additions & 0 deletions apps/api/src/app/inversify.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import {
Erc20Repository,
Erc20RepositoryCache,
Erc20RepositoryViem,
SimulationRepository,
SimulationRepositoryTenderly,
TokenHolderRepository,
TokenHolderRepositoryCache,
TokenHolderRepositoryEthplorer,
TokenHolderRepositoryFallback,
TokenHolderRepositoryGoldRush,
UsdRepository,
UsdRepositoryCache,
UsdRepositoryCoingecko,
Expand All @@ -14,6 +21,8 @@ import {
cowApiClients,
erc20RepositorySymbol,
redisClient,
tenderlyRepositorySymbol,
tokenHolderRepositorySymbol,
usdRepositorySymbol,
viemClients,
} from '@cowprotocol/repositories';
Expand All @@ -25,11 +34,16 @@ const CACHE_TOKEN_INFO_SECONDS = ms('24h') / 1000; // 24h

import { Container } from 'inversify';
import {
SimulationService,
SlippageService,
SlippageServiceMain,
TokenHolderService,
TokenHolderServiceMain,
UsdService,
UsdServiceMain,
simulationServiceSymbol,
slippageServiceSymbol,
tokenHolderServiceSymbol,
usdServiceSymbol,
} from '@cowprotocol/services';
import ms from 'ms';
Expand Down Expand Up @@ -86,16 +100,55 @@ function getUsdRepository(
]);
}

function getTokenHolderRepositoryEthplorer(
cacheRepository: CacheRepository
): TokenHolderRepository {
return new TokenHolderRepositoryCache(
new TokenHolderRepositoryEthplorer(),
cacheRepository,
'tokenHolderEthplorer',
DEFAULT_CACHE_VALUE_SECONDS,
DEFAULT_CACHE_NULL_SECONDS
);
}

function getTokenHolderRepositoryGoldRush(
cacheRepository: CacheRepository
): TokenHolderRepository {
return new TokenHolderRepositoryCache(
new TokenHolderRepositoryGoldRush(),
cacheRepository,
'tokenHolderGoldRush',
DEFAULT_CACHE_VALUE_SECONDS,
DEFAULT_CACHE_NULL_SECONDS
);
}

function getTokenHolderRepository(
cacheRepository: CacheRepository
): TokenHolderRepository {
return new TokenHolderRepositoryFallback([
getTokenHolderRepositoryGoldRush(cacheRepository),
getTokenHolderRepositoryEthplorer(cacheRepository),
]);
}

function getApiContainer(): Container {
const apiContainer = new Container();
// Repositories
const cacheRepository = getCacheRepository(apiContainer);
const erc20Repository = getErc20Repository(cacheRepository);
const simulationRepository = new SimulationRepositoryTenderly();
const tokenHolderRepository = getTokenHolderRepository(cacheRepository);

apiContainer
.bind<Erc20Repository>(erc20RepositorySymbol)
.toConstantValue(erc20Repository);

apiContainer
.bind<SimulationRepository>(tenderlyRepositorySymbol)
.toConstantValue(simulationRepository);

apiContainer
.bind<CacheRepository>(cacheRepositorySymbol)
.toConstantValue(cacheRepository);
Expand All @@ -104,13 +157,25 @@ function getApiContainer(): Container {
.bind<UsdRepository>(usdRepositorySymbol)
.toConstantValue(getUsdRepository(cacheRepository, erc20Repository));

apiContainer
.bind<TokenHolderRepository>(tokenHolderRepositorySymbol)
.toConstantValue(tokenHolderRepository);

// Services
apiContainer
.bind<SlippageService>(slippageServiceSymbol)
.to(SlippageServiceMain);

apiContainer
.bind<TokenHolderService>(tokenHolderServiceSymbol)
.to(TokenHolderServiceMain);

apiContainer.bind<UsdService>(usdServiceSymbol).to(UsdServiceMain);

apiContainer
.bind<SimulationService>(simulationServiceSymbol)
.to(SimulationService);

return apiContainer;
}

Expand Down
15 changes: 15 additions & 0 deletions apps/api/src/app/plugins/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ const schema = {
COINGECKO_API_KEY: {
type: 'string',
},
GOLD_RUSH_API_KEY: {
type: 'string',
},
TENDERLY_API_KEY: {
type: 'string',
},
TENDERLY_PROJECT: {
type: 'string',
},
TENDERLY_ACCOUNT: {
type: 'string',
},
ETHPLORER_API_KEY: {
type: 'string',
},
},
};

Expand Down
138 changes: 138 additions & 0 deletions apps/api/src/app/routes/__chainId/simulation/simulateBundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import {
SimulationService,
simulationServiceSymbol,
} from '@cowprotocol/services';
import { FastifyPluginAsync } from 'fastify';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { AddressSchema, ChainIdSchema } from '../../../schemas';
import { apiContainer } from '../../../inversify.config';

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

const successSchema = {
type: 'array',
items: {
type: 'object',
required: ['status', 'id', 'link'],
additionalProperties: false,
properties: {
status: {
title: 'Status',
description: 'If the transaction was successful.',
type: 'boolean',
},
id: {
title: 'ID',
description: 'Tenderly ID of the transaction.',
type: 'string',
},
link: {
title: 'Link',
description: 'Link to the transaction on Tenderly.',
type: 'string',
},
},
},
} as const satisfies JSONSchema;

const bodySchema = {
type: 'array',
items: {
type: 'object',
required: ['from', 'to', 'input'],
additionalProperties: false,
properties: {
from: AddressSchema,
to: AddressSchema,
value: {
title: 'Value',
description: 'Amount of native coin to send.',
type: 'string',
},
input: {
title: 'Input',
description: 'Transaction data.',
type: 'string',
},
gas: {
title: 'Gas',
description: 'Transaction gas limit.',
type: 'number',
},
gas_price: {
title: 'Gas price',
description: 'Gas price.',
type: 'string',
},
},
},
} as const satisfies JSONSchema;

const errorSchema = {
type: 'object',
required: ['message'],
additionalProperties: false,
properties: {
message: {
title: 'Message',
description: 'Message describing the error.',
type: 'string',
},
},
} as const satisfies JSONSchema;

type RouteSchema = FromSchema<typeof paramsSchema>;
type SuccessSchema = FromSchema<typeof successSchema>;
type ErrorSchema = FromSchema<typeof errorSchema>;
type BodySchema = FromSchema<typeof bodySchema>;

const tenderlyService: SimulationService = apiContainer.get(
simulationServiceSymbol
);

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

const simulationResult =
await tenderlyService.postTenderlyBundleSimulation(
chainId,
request.body
);

if (simulationResult === null) {
reply.code(404).send({ message: 'Token holders not found' });
return;
}
fastify.log.info(
`Post Tenderly bundle of ${request.body.length} simulation on chain ${chainId}`
);

reply.send(simulationResult);
}
);
};

export default root;
Loading

0 comments on commit 4a913cb

Please sign in to comment.