mirror of
https://github.com/tornadocash/tornado-pool-relayer
synced 2024-02-02 15:04:09 +01:00
refactor: spread across services & added docker
This commit is contained in:
parent
268dd097b7
commit
79af9c7ce0
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
.git
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -9,6 +9,7 @@ npm-debug.log*
|
|||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
.env
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@ -31,4 +32,4 @@ lerna-debug.log*
|
|||||||
!.vscode/settings.json
|
!.vscode/settings.json
|
||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
|
"printWidth": 140
|
||||||
}
|
}
|
||||||
|
9
Dockerfile
Normal file
9
Dockerfile
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
FROM node:12
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json yarn.lock ./
|
||||||
|
RUN yarn && yarn cache clean --force
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
ENTRYPOINT ["yarn"]
|
63
docker-compose.yml
Normal file
63
docker-compose.yml
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
version: '2'
|
||||||
|
|
||||||
|
services:
|
||||||
|
server:
|
||||||
|
image: tornadocash/relayer
|
||||||
|
restart: always
|
||||||
|
command: start:prod
|
||||||
|
env_file: .env
|
||||||
|
environment:
|
||||||
|
REDIS_URL: redis://redis/0
|
||||||
|
nginx_proxy_read_timeout: 600
|
||||||
|
depends_on: [redis]
|
||||||
|
links:
|
||||||
|
- redis
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis
|
||||||
|
restart: always
|
||||||
|
command: [redis-server, --appendonly, 'yes']
|
||||||
|
volumes:
|
||||||
|
- redis:/data
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: nginx
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
- 443:443
|
||||||
|
volumes:
|
||||||
|
- conf:/etc/nginx/conf.d
|
||||||
|
- vhost:/etc/nginx/vhost.d
|
||||||
|
- html:/usr/share/nginx/html
|
||||||
|
- certs:/etc/nginx/certs
|
||||||
|
logging:
|
||||||
|
driver: none
|
||||||
|
|
||||||
|
dockergen:
|
||||||
|
image: poma/docker-gen
|
||||||
|
container_name: dockergen
|
||||||
|
restart: always
|
||||||
|
command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
|
||||||
|
volumes_from:
|
||||||
|
- nginx
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
|
||||||
|
letsencrypt:
|
||||||
|
image: jrcs/letsencrypt-nginx-proxy-companion
|
||||||
|
container_name: letsencrypt
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
NGINX_DOCKER_GEN_CONTAINER: dockergen
|
||||||
|
volumes_from:
|
||||||
|
- nginx
|
||||||
|
- dockergen
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
conf:
|
||||||
|
vhost:
|
||||||
|
html:
|
||||||
|
certs:
|
||||||
|
redis:
|
18
example.env
18
example.env
@ -1,3 +1,17 @@
|
|||||||
RPC_URL=
|
# DNS settings
|
||||||
PRIVATE_KEY=
|
VIRTUAL_HOST=
|
||||||
|
LETSENCRYPT_HOST=
|
||||||
|
|
||||||
|
# server settings
|
||||||
|
PORT=8000
|
||||||
|
|
||||||
|
#commision for service
|
||||||
SERVICE_FEE=0.05
|
SERVICE_FEE=0.05
|
||||||
|
|
||||||
|
# bull settings
|
||||||
|
REDIS_URL=redis://127.0.0.1:6379
|
||||||
|
|
||||||
|
# tx-manager settings
|
||||||
|
RPC_URL=
|
||||||
|
CHAIN_ID=1
|
||||||
|
PRIVATE_KEY=
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "nest start --watch",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "yarn prebuild; yarn build; node dist/main",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
|
@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
|
|||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
|
||||||
import { baseConfig } from '@/config';
|
import { baseConfig } from '@/config';
|
||||||
import { QueueModule, StatusModule } from '@/modules';
|
import { QueueModule, ApiModule } from '@/modules';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -10,8 +10,8 @@ import { QueueModule, StatusModule } from '@/modules';
|
|||||||
load: [baseConfig],
|
load: [baseConfig],
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
}),
|
}),
|
||||||
|
ApiModule,
|
||||||
QueueModule,
|
QueueModule,
|
||||||
StatusModule,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { registerAs } from '@nestjs/config';
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
export default registerAs('bull', () => ({
|
export default registerAs('bull', () => ({
|
||||||
|
name: 'withdrawal',
|
||||||
redis: {
|
redis: {
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 6379,
|
port: 6379,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export const baseConfig = () => ({
|
export const baseConfig = () => ({
|
||||||
|
gasLimit: 600000,
|
||||||
|
serviceFee: process.env.SERVICE_FEE,
|
||||||
|
chainId: process.env.CHAIN_ID,
|
||||||
port: parseInt(process.env.PORT, 10) || 8080,
|
port: parseInt(process.env.PORT, 10) || 8080,
|
||||||
gasLimit: 400000,
|
|
||||||
fee: process.env.SERVICE_FEE,
|
|
||||||
});
|
});
|
||||||
|
@ -3,7 +3,7 @@ import { ChainId } from '@/types';
|
|||||||
export const CONTRACT_NETWORKS: { [chainId in ChainId]: string } = {
|
export const CONTRACT_NETWORKS: { [chainId in ChainId]: string } = {
|
||||||
[ChainId.MAINNET]: '0x8Bfac9EF3d73cE08C7CEC339C0fE3B2e57814c1E',
|
[ChainId.MAINNET]: '0x8Bfac9EF3d73cE08C7CEC339C0fE3B2e57814c1E',
|
||||||
[ChainId.GOERLI]: '0x20a2D506cf52453D681F9E8E814A3437c6242B9e',
|
[ChainId.GOERLI]: '0x20a2D506cf52453D681F9E8E814A3437c6242B9e',
|
||||||
[ChainId.OPTIMISM]: '0xc436071dE853A4421c57ddD0CDDC116C735aa8b5',
|
[ChainId.OPTIMISM]: '0x1Ed4dcDB4b78985008199f451E88C6448C4EDd94',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RPC_LIST: { [chainId in ChainId]: string } = {
|
export const RPC_LIST: { [chainId in ChainId]: string } = {
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import { ChainId } from '@/types';
|
|
||||||
import { CONTRACT_NETWORKS } from '@/constants';
|
|
||||||
import { getProviderWithSigner } from '@/services';
|
|
||||||
|
|
||||||
import { TornadoPool__factory as TornadoPoolFactory } from '@/artifacts';
|
|
||||||
|
|
||||||
export function getTornadoPool(chainId: ChainId) {
|
|
||||||
const provider = getProviderWithSigner(chainId);
|
|
||||||
return TornadoPoolFactory.connect(CONTRACT_NETWORKS[chainId], provider);
|
|
||||||
}
|
|
31
src/modules/api/api.controller.ts
Normal file
31
src/modules/api/api.controller.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Controller, Body, Param, Get, Post } from '@nestjs/common';
|
||||||
|
import { Job } from 'bull';
|
||||||
|
|
||||||
|
import { ApiService } from './api.service';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class ApiController {
|
||||||
|
constructor(private readonly service: ApiService) {}
|
||||||
|
|
||||||
|
@Get('/api')
|
||||||
|
async status(): Promise<Health> {
|
||||||
|
return await this.service.status();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/')
|
||||||
|
async main(): Promise<string> {
|
||||||
|
return this.service.main();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/job/:jobId')
|
||||||
|
async getJob(@Param('jobId') jobId: string): Promise<Job> {
|
||||||
|
return await this.service.getJob(jobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/withdrawal')
|
||||||
|
async withdrawal(_, @Body() { body }: any): Promise<string> {
|
||||||
|
console.log('body', body);
|
||||||
|
|
||||||
|
return await this.service.withdrawal(JSON.parse(body));
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,15 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
|
||||||
import { StatusService } from './stat.service';
|
import { ApiService } from './api.service';
|
||||||
import { StatusController } from './stat.controller';
|
import { ApiController } from './api.controller';
|
||||||
|
|
||||||
import { QueueModule } from '@/modules';
|
import { QueueModule } from '@/modules';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule, QueueModule],
|
imports: [ConfigModule, QueueModule],
|
||||||
providers: [StatusService],
|
providers: [ApiService],
|
||||||
controllers: [StatusController],
|
controllers: [ApiController],
|
||||||
exports: [],
|
exports: [],
|
||||||
})
|
})
|
||||||
export class StatusModule {}
|
export class ApiModule {}
|
@ -1,9 +1,9 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Queue, Job } from 'bull';
|
||||||
import { Queue } from 'bull';
|
|
||||||
import { InjectQueue } from '@nestjs/bull';
|
import { InjectQueue } from '@nestjs/bull';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
class StatusService {
|
class ApiService {
|
||||||
constructor(@InjectQueue('withdrawal') private withdrawalQueue: Queue) {}
|
constructor(@InjectQueue('withdrawal') private withdrawalQueue: Queue) {}
|
||||||
|
|
||||||
async status(): Promise<Health> {
|
async status(): Promise<Health> {
|
||||||
@ -17,11 +17,15 @@ class StatusService {
|
|||||||
return `This is <a href=https://tornado.cash>tornado.cash</a> Relayer service. Check the <a href=/status>/status</a> for settings`;
|
return `This is <a href=https://tornado.cash>tornado.cash</a> Relayer service. Check the <a href=/status>/status</a> for settings`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async withdrawal(data): Promise<string> {
|
async withdrawal(data: any): Promise<string> {
|
||||||
const job = await this.withdrawalQueue.add(data)
|
const job = await this.withdrawalQueue.add(data);
|
||||||
|
|
||||||
return String(job.id);
|
return String(job.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getJob(id: string): Promise<Job> {
|
||||||
|
return await this.withdrawalQueue.getJob(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { StatusService };
|
export { ApiService };
|
@ -1,4 +1,4 @@
|
|||||||
export class CreateStatusDto {
|
export class CreateApiDto {
|
||||||
error: boolean;
|
error: boolean;
|
||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
1
src/modules/api/index.ts
Normal file
1
src/modules/api/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { ApiModule } from './api.module';
|
@ -1,2 +1,2 @@
|
|||||||
export * from './queue';
|
export * from './queue';
|
||||||
export * from './status';
|
export * from './api';
|
||||||
|
@ -10,7 +10,6 @@ import {
|
|||||||
} from '@nestjs/bull';
|
} from '@nestjs/bull';
|
||||||
import { Injectable, OnModuleDestroy } from '@nestjs/common';
|
import { Injectable, OnModuleDestroy } from '@nestjs/common';
|
||||||
import { Job, Queue } from 'bull';
|
import { Job, Queue } from 'bull';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@Processor()
|
@Processor()
|
||||||
@ -54,17 +53,11 @@ export class BaseProcessor<T = object> implements OnModuleDestroy {
|
|||||||
return this.updateTask(job);
|
return this.updateTask(job);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateTask(job: Job<T>) {
|
private async updateTask(job: Job<T>) {
|
||||||
const currentJob = await this.queue.getJob(job.id);
|
const currentJob = await this.queue.getJob(job.id);
|
||||||
await currentJob.update(job.data);
|
await currentJob.update(job.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createTask({ request }) {
|
|
||||||
const id = uuid();
|
|
||||||
await this.queue.add({ ...request, id });
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async onModuleDestroy() {
|
async onModuleDestroy() {
|
||||||
if (this.queue) {
|
if (this.queue) {
|
||||||
await this.queue.close();
|
await this.queue.close();
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
import { BullModule } from '@nestjs/bull';
|
import { BullModule } from '@nestjs/bull';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { GasPriceService, ProviderService } from '@/services';
|
||||||
|
|
||||||
import { WithdrawalProcessor } from './withdrawal.processor';
|
import { WithdrawalProcessor } from './withdrawal.processor';
|
||||||
|
|
||||||
import bullConfig from '@/config/bull.config';
|
import bullConfig from '@/config/bull.config';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [BullModule.registerQueue(bullConfig())],
|
||||||
BullModule.registerQueue({
|
providers: [GasPriceService, ProviderService, WithdrawalProcessor],
|
||||||
...bullConfig(),
|
|
||||||
name: 'withdrawal',
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
providers: [WithdrawalProcessor],
|
|
||||||
exports: [BullModule],
|
exports: [BullModule],
|
||||||
})
|
})
|
||||||
export class QueueModule {}
|
export class QueueModule {}
|
||||||
|
@ -7,11 +7,11 @@ import { ConfigService } from '@nestjs/config';
|
|||||||
import { InjectQueue, Process, Processor } from '@nestjs/bull';
|
import { InjectQueue, Process, Processor } from '@nestjs/bull';
|
||||||
|
|
||||||
import { toWei } from '@/utilities';
|
import { toWei } from '@/utilities';
|
||||||
import { getGasPrice } from '@/services';
|
import { GasPriceService, ProviderService } from '@/services';
|
||||||
import { getTornadoPool } from '@/contracts';
|
|
||||||
import txMangerConfig from '@/config/txManager.config';
|
import txMangerConfig from '@/config/txManager.config';
|
||||||
|
|
||||||
import { BaseProcessor } from './base.processor';
|
import { BaseProcessor } from './base.processor';
|
||||||
|
import { ChainId } from '@/types';
|
||||||
|
|
||||||
export interface Withdrawal {
|
export interface Withdrawal {
|
||||||
args: string[];
|
args: string[];
|
||||||
@ -27,6 +27,8 @@ export interface Withdrawal {
|
|||||||
export class WithdrawalProcessor extends BaseProcessor<Withdrawal> {
|
export class WithdrawalProcessor extends BaseProcessor<Withdrawal> {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectQueue('withdrawal') public withdrawalQueue: Queue,
|
@InjectQueue('withdrawal') public withdrawalQueue: Queue,
|
||||||
|
private gasPriceService: GasPriceService,
|
||||||
|
private providerService: ProviderService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
@ -49,12 +51,12 @@ export class WithdrawalProcessor extends BaseProcessor<Withdrawal> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async submitTx(job: Job<Withdrawal>) {
|
async submitTx(job: Job<Withdrawal>) {
|
||||||
const txManager = new TxManager(txMangerConfig());
|
|
||||||
|
|
||||||
const prepareTx = await this.prepareTransaction(job.data);
|
|
||||||
const tx = await txManager.createTx(prepareTx);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const txManager = new TxManager(txMangerConfig());
|
||||||
|
|
||||||
|
const prepareTx = await this.prepareTransaction(job.data);
|
||||||
|
const tx = await txManager.createTx(prepareTx);
|
||||||
|
|
||||||
const receipt = await tx
|
const receipt = await tx
|
||||||
.send()
|
.send()
|
||||||
.on('transactionHash', async (txHash: string) => {
|
.on('transactionHash', async (txHash: string) => {
|
||||||
@ -88,44 +90,52 @@ export class WithdrawalProcessor extends BaseProcessor<Withdrawal> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async prepareTransaction({ proof, args, amount }) {
|
async prepareTransaction({ proof, args }) {
|
||||||
const contract = getTornadoPool(5);
|
const chainId = this.configService.get<number>('chainId');
|
||||||
|
|
||||||
|
const contract = this.providerService.getTornadoPool();
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const data = contract.interface.encodeFunctionData('transaction', [
|
const data = contract.interface.encodeFunctionData('transaction', [proof, ...args]);
|
||||||
proof,
|
|
||||||
...args,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const gasLimit = this.configService.get<number>('gasLimit');
|
let gasLimit = this.configService.get<BigNumber>('gasLimit');
|
||||||
|
|
||||||
|
// need because optimism has dynamic gas limit
|
||||||
|
if (chainId === ChainId.OPTIMISM) {
|
||||||
|
// @ts-ignore
|
||||||
|
gasLimit = await contract.estimateGas.transaction(proof, ...args, {
|
||||||
|
value: BigNumber.from(0)._hex,
|
||||||
|
from: '0x1a5245ea5210C3B57B7Cfdf965990e63534A7b52',
|
||||||
|
gasPrice: toWei('0.015', 'gwei'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { fast } = await this.gasPriceService.getGasPrice();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
gasLimit,
|
gasLimit,
|
||||||
value: BigNumber.from(0)._hex,
|
|
||||||
to: contract.address,
|
to: contract.address,
|
||||||
|
gasPrice: fast.toString(),
|
||||||
|
value: BigNumber.from(0)._hex,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkFee({ fee, amount }) {
|
async checkFee({ fee, amount }) {
|
||||||
const gasLimit = this.configService.get<number>('gasLimit');
|
const { gasLimit, serviceFee } = this.configService.get('');
|
||||||
|
|
||||||
const { fast } = await getGasPrice(5);
|
const { fast } = await this.gasPriceService.getGasPrice();
|
||||||
|
|
||||||
const expense = BigNumber.from(toWei(fast.toString(), 'gwei')).mul(
|
const expense = BigNumber.from(toWei(fast.toString(), 'gwei')).mul(gasLimit);
|
||||||
gasLimit,
|
|
||||||
);
|
|
||||||
|
|
||||||
const serviceFee = this.configService.get<number>('fee');
|
const feePercent = BigNumber.from(amount)
|
||||||
const feePercent = BigNumber.from(amount).mul(serviceFee * 1e10).div(100 * 1e10)
|
.mul(serviceFee * 1e10)
|
||||||
|
.div(100 * 1e10);
|
||||||
|
|
||||||
const desiredFee = expense.add(feePercent);
|
const desiredFee = expense.add(feePercent);
|
||||||
|
|
||||||
if (BigNumber.from(fee).lt(desiredFee)) {
|
if (BigNumber.from(fee).lt(desiredFee)) {
|
||||||
throw new Error(
|
throw new Error('Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.');
|
||||||
'Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export { StatusModule } from './stat.module';
|
|
@ -1,25 +0,0 @@
|
|||||||
import { Controller, Body, Get, Post } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { StatusService } from './stat.service';
|
|
||||||
|
|
||||||
@Controller()
|
|
||||||
export class StatusController {
|
|
||||||
constructor(private readonly service: StatusService) {}
|
|
||||||
|
|
||||||
@Get('/status')
|
|
||||||
async status(): Promise<Health> {
|
|
||||||
return await this.service.status();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('/')
|
|
||||||
async main(): Promise<string> {
|
|
||||||
return this.service.main();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('/withdrawal')
|
|
||||||
async withdrawal(_, @Body() { body }: any): Promise<string> {
|
|
||||||
console.log('body', body)
|
|
||||||
|
|
||||||
return await this.service.withdrawal(JSON.parse(body))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
import { ethers } from 'ethers';
|
|
||||||
|
|
||||||
import { ChainId } from '@/types';
|
|
||||||
import { RPC_LIST } from '@/constants';
|
|
||||||
|
|
||||||
interface Options {
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Provider {
|
|
||||||
public provider: ethers.providers.JsonRpcProvider;
|
|
||||||
|
|
||||||
constructor(options: Options) {
|
|
||||||
this.provider = new ethers.providers.JsonRpcProvider(options.url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getProvider(chainId: ChainId): Provider {
|
|
||||||
return new Provider({ url: RPC_LIST[chainId] });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getProviderWithSigner(
|
|
||||||
chainId: ChainId,
|
|
||||||
): ethers.providers.BaseProvider {
|
|
||||||
return ethers.providers.getDefaultProvider(RPC_LIST[chainId]);
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
import { Wallet, PopulatedTransaction } from 'ethers';
|
|
||||||
import {
|
|
||||||
FlashbotsBundleProvider,
|
|
||||||
FlashbotsBundleResolution,
|
|
||||||
} from '@flashbots/ethers-provider-bundle';
|
|
||||||
|
|
||||||
import { ChainId } from '@/types';
|
|
||||||
import { numbers } from '@/constants';
|
|
||||||
import { getProviderWithSigner } from '@/services';
|
|
||||||
|
|
||||||
const authSigner = Wallet.createRandom();
|
|
||||||
|
|
||||||
const FLASH_BOT_RPC: { [key in ChainId]: { name: string; url: string } } = {
|
|
||||||
[ChainId.GOERLI]: {
|
|
||||||
url: 'https://relay-goerli.flashbots.net/',
|
|
||||||
name: 'goerli',
|
|
||||||
},
|
|
||||||
[ChainId.MAINNET]: {
|
|
||||||
url: '',
|
|
||||||
name: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
async function sendFlashBotTransaction(
|
|
||||||
transaction: PopulatedTransaction,
|
|
||||||
chainId: ChainId,
|
|
||||||
) {
|
|
||||||
const provider = getProviderWithSigner(chainId);
|
|
||||||
|
|
||||||
const { url, name } = FLASH_BOT_RPC[chainId];
|
|
||||||
|
|
||||||
const flashBotsProvider = await FlashbotsBundleProvider.create(
|
|
||||||
provider,
|
|
||||||
authSigner,
|
|
||||||
url,
|
|
||||||
name,
|
|
||||||
);
|
|
||||||
|
|
||||||
const nonce = await provider.getTransactionCount(authSigner.address);
|
|
||||||
|
|
||||||
const mergedTx = {
|
|
||||||
...transaction,
|
|
||||||
nonce,
|
|
||||||
from: authSigner.address,
|
|
||||||
};
|
|
||||||
|
|
||||||
const signedTransaction = await authSigner.signTransaction(mergedTx);
|
|
||||||
|
|
||||||
const TIME_10_BLOCK = 130;
|
|
||||||
|
|
||||||
const blockNumber = await provider.getBlockNumber();
|
|
||||||
const minTimestamp = (await provider.getBlock(blockNumber)).timestamp;
|
|
||||||
|
|
||||||
const maxTimestamp = minTimestamp + TIME_10_BLOCK;
|
|
||||||
const targetBlockNumber = blockNumber + numbers.TWO;
|
|
||||||
|
|
||||||
const simulation = await flashBotsProvider.simulate(
|
|
||||||
[signedTransaction],
|
|
||||||
targetBlockNumber,
|
|
||||||
);
|
|
||||||
|
|
||||||
if ('error' in simulation) {
|
|
||||||
console.log(`Simulation Error: ${simulation.error.message}`);
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
`Simulation Success: ${JSON.stringify(simulation, null, numbers.TWO)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const bundleSubmission = await flashBotsProvider.sendBundle(
|
|
||||||
[{ signedTransaction }],
|
|
||||||
targetBlockNumber,
|
|
||||||
{
|
|
||||||
minTimestamp,
|
|
||||||
maxTimestamp,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if ('error' in bundleSubmission) {
|
|
||||||
throw new Error(bundleSubmission.error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const waitResponse = await bundleSubmission.wait();
|
|
||||||
const bundleSubmissionSimulation = await bundleSubmission.simulate();
|
|
||||||
console.log({
|
|
||||||
bundleSubmissionSimulation,
|
|
||||||
waitResponse: FlashbotsBundleResolution[waitResponse],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export { sendFlashBotTransaction };
|
|
@ -1,3 +1,2 @@
|
|||||||
export * from './ether';
|
export * from './oracle.service';
|
||||||
export * from './oracle';
|
export * from './provider.service';
|
||||||
export * from './flashbot';
|
|
||||||
|
45
src/services/oracle.service.ts
Normal file
45
src/services/oracle.service.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
import { GasPriceOracle } from 'gas-price-oracle';
|
||||||
|
import { GasPrice } from 'gas-price-oracle/lib/types';
|
||||||
|
|
||||||
|
import { ChainId } from '@/types';
|
||||||
|
import { toWei } from '@/utilities';
|
||||||
|
import { RPC_LIST, numbers } from '@/constants';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GasPriceService {
|
||||||
|
private readonly chainId: number;
|
||||||
|
|
||||||
|
constructor(private configService: ConfigService) {
|
||||||
|
this.chainId = this.configService.get<number>('chainId');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGasPrice(): Promise<GasPrice> {
|
||||||
|
if (this.chainId === ChainId.OPTIMISM) {
|
||||||
|
return GasPriceService.getOptimismPrice();
|
||||||
|
}
|
||||||
|
|
||||||
|
const TIMER = 10;
|
||||||
|
const INTERVAL = TIMER * numbers.SECOND;
|
||||||
|
|
||||||
|
const instance = new GasPriceOracle({
|
||||||
|
timeout: INTERVAL,
|
||||||
|
defaultRpc: RPC_LIST[ChainId.MAINNET],
|
||||||
|
});
|
||||||
|
|
||||||
|
return await instance.gasPrices();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getOptimismPrice() {
|
||||||
|
const OPTIMISM_GAS = toWei('0.015', 'gwei').toNumber();
|
||||||
|
|
||||||
|
return {
|
||||||
|
fast: OPTIMISM_GAS,
|
||||||
|
low: OPTIMISM_GAS,
|
||||||
|
instant: OPTIMISM_GAS,
|
||||||
|
standard: OPTIMISM_GAS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,29 +0,0 @@
|
|||||||
import { GasPriceOracle } from 'gas-price-oracle';
|
|
||||||
import { GasPrice } from 'gas-price-oracle/lib/types';
|
|
||||||
|
|
||||||
import { ChainId } from '@/types';
|
|
||||||
import { RPC_LIST, numbers } from '@/constants';
|
|
||||||
|
|
||||||
const SECONDS = 10;
|
|
||||||
const TEN_SECOND = SECONDS * numbers.SECOND;
|
|
||||||
|
|
||||||
const OPTIMISM_GAS_PRICE = {
|
|
||||||
fast: 0.015,
|
|
||||||
low: 0.015,
|
|
||||||
instant: 0.015,
|
|
||||||
standard: 0.015,
|
|
||||||
};
|
|
||||||
|
|
||||||
const getGasPrice = async (chainId: ChainId): Promise<GasPrice> => {
|
|
||||||
if (chainId === ChainId.OPTIMISM) {
|
|
||||||
return OPTIMISM_GAS_PRICE;
|
|
||||||
}
|
|
||||||
|
|
||||||
const instance = new GasPriceOracle({
|
|
||||||
timeout: TEN_SECOND,
|
|
||||||
defaultRpc: RPC_LIST[ChainId.MAINNET],
|
|
||||||
});
|
|
||||||
return await instance.gasPrices();
|
|
||||||
};
|
|
||||||
|
|
||||||
export { getGasPrice };
|
|
27
src/services/provider.service.ts
Normal file
27
src/services/provider.service.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { ethers } from 'ethers';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
import { CONTRACT_NETWORKS, RPC_LIST } from '@/constants';
|
||||||
|
import { TornadoPool__factory as TornadoPoolFactory } from '@/artifacts';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ProviderService {
|
||||||
|
private readonly chainId: number;
|
||||||
|
|
||||||
|
constructor(private configService: ConfigService) {
|
||||||
|
this.chainId = this.configService.get<number>('chainId');
|
||||||
|
}
|
||||||
|
|
||||||
|
getProvider() {
|
||||||
|
return new ethers.providers.JsonRpcProvider(RPC_LIST[this.chainId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getProviderWithSigner() {
|
||||||
|
return ethers.providers.getDefaultProvider(RPC_LIST[this.chainId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTornadoPool() {
|
||||||
|
return TornadoPoolFactory.connect(CONTRACT_NETWORKS[this.chainId], this.getProviderWithSigner());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user