Skip to content

Commit

Permalink
feature(api): added api for faucet to allocate fund to user (#346)
Browse files Browse the repository at this point in the history
* feature(api): added api for faucet to allocate fund to user

* added e2e test

* fixed pr comments

* added e2e for ratelimiting

* added invalid address test case
  • Loading branch information
fullstackninja864 authored Nov 8, 2023
1 parent 7abc99f commit 23175ef
Show file tree
Hide file tree
Showing 22 changed files with 696 additions and 442 deletions.
8 changes: 6 additions & 2 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"prettier": "npx prettier --write .",
"start": "nest start",
"start:debug": "nest start --debug --watch",
"start:dev": "nest start --watch",
"dev": "NODE_ENV=dev nest start --watch",
"start:prod": "node dist/main",
"test": "jest",
"test:cov": "jest --coverage",
Expand Down Expand Up @@ -56,14 +56,17 @@
"@defichain/jellyfish-network": "^4.0.0-beta.11",
"@defichain/jellyfish-transaction": "^4.0.0-beta.11",
"@defichain/whale-api-client": "^4.0.0-beta.11",
"@nestjs/cache-manager": "^2.1.1",
"@nestjs/common": "^10.2.5",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.2.5",
"@nestjs/platform-express": "^10.2.5",
"@nestjs/throttler": "^5.0.1",
"@waveshq/standard-defichain-jellyfishsdk": "^2.6.1",
"@waveshq/walletkit-core": "^1.3.4",
"axios": "^1.5.0",
"bignumber.js": "^9.1.2",
"cache-manager": "^5.2.4",
"class-validator": "^0.14.0",
"ethers": "^6.7.1",
"express": "^4.18.2",
Expand Down Expand Up @@ -105,6 +108,7 @@
"ts-loader": "^9.4.4",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.2.2"
"typescript": "^5.2.2",
"wait-for-expect": "^3.0.2"
}
}
129 changes: 6 additions & 123 deletions apps/server/src/AppConfig.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import BigNumber from 'bignumber.js';
import * as Joi from 'joi';

export const DATABASE_URL = 'DATABASE_URL';

export function appConfig() {
return {
dbUrl: process.env.DATABASE_URL,
dvmActivationHeight: process.env.DVM_ACTIVATION_HEIGHT,
evmRpcUrl: process.env.EVM_RPC_URL,
network: process.env.NETWORK,
whaleURL: process.env.DEFICHAIN_WHALE_URL,
slackWebhookUrl: process.env.SLACK_WEBHOOK_URL,
faucetAmountPerRequest: process.env.FAUCET_AMOUNT_PER_REQUEST || '0.01',
throttleTimePerAddress: process.env.THROTTLE_TIME_PER_ADDRESS || '86400', // 24 * 60 * 60 (1 Day)
privateKey: process.env.PRIVATE_KEY,
};
}

Expand All @@ -23,120 +19,7 @@ export type DeepPartial<T> = T extends object
export type AppConfig = DeepPartial<ReturnType<typeof appConfig>>;

export const ENV_VALIDATION_SCHEMA = Joi.object({
DATABASE_URL: Joi.string(),
DVM_ACTIVATION_HEIGHT: Joi.string(),
EVM_RPC_URL: Joi.string(),
NETWORK: Joi.string(),
PRIVATE_KEY: Joi.string().required(),
FAUCET_AMOUNT_PER_REQUEST: Joi.string(),
THROTTLE_TIME_PER_ADDRESS: Joi.string(),
});

export const DST20ABI = [
{
inputs: [],
name: 'totalSupply',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'decimals',
outputs: [
{
internalType: 'uint8',
name: '',
type: 'uint8',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: 'account',
type: 'address',
},
],
name: 'balanceOf',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: 'from',
type: 'address',
},
{
internalType: 'address',
name: 'to',
type: 'address',
},
{
internalType: 'uint256',
name: 'amount',
type: 'uint256',
},
],
name: 'transferFrom',
outputs: [
{
internalType: 'bool',
name: '',
type: 'bool',
},
],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: 'to',
type: 'address',
},
{
internalType: 'uint256',
name: 'amount',
type: 'uint256',
},
],
name: 'transfer',
outputs: [
{
internalType: 'bool',
name: '',
type: 'bool',
},
],
stateMutability: 'nonpayable',
type: 'function',
},
];

export interface DST20BalanceMovementOnBlockI {
transferredBalance: BigNumber;
receivedBalance: BigNumber;
}

export interface BalanceMovementI {
gasUsed: BigNumber;
transferredBalance: BigNumber;
receivedBalance: BigNumber;
}
23 changes: 0 additions & 23 deletions apps/server/src/app.controller.spec.ts

This file was deleted.

15 changes: 13 additions & 2 deletions apps/server/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { APP_GUARD } from '@nestjs/core';
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';

import {AppController} from "./app.controller"
import { AppController } from "./app.controller"
import { AppService } from './app.service';
import { appConfig, ENV_VALIDATION_SCHEMA } from './AppConfig';
import { FaucetModule } from './faucet/FaucetModule';


@Module({
Expand All @@ -13,8 +16,16 @@ import { appConfig, ENV_VALIDATION_SCHEMA } from './AppConfig';
load: [appConfig],
validationSchema: ENV_VALIDATION_SCHEMA,
}),
ThrottlerModule.forRoot([{
ttl: 60_000, // Throttle time window set to 60 seconds
limit: 10, // Maximum 10 requests allowed within the time window
}]),
FaucetModule,
],
controllers: [AppController],
providers: [AppService],
providers: [AppService, {
provide: APP_GUARD,
useClass: ThrottlerGuard,
}],
})
export class AppModule {}
14 changes: 14 additions & 0 deletions apps/server/src/faucet/AddressValidationInterceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CallHandler, ExecutionContext, HttpException, Injectable, NestInterceptor } from "@nestjs/common";
import { isAddress } from 'ethers';

@Injectable()
export class AddressValidationInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
const request = context.switchToHttp().getRequest();
const { address } = request.params;
if (!isAddress(address)) {
throw new HttpException('Invalid Ethereum address', 400);
}
return next.handle();
}
}
17 changes: 17 additions & 0 deletions apps/server/src/faucet/DefaultNetworkInterceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { EnvironmentNetwork, getEnvironment } from "@waveshq/walletkit-core";

@Injectable()
export class DefaultNetworkInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
const request = context.switchToHttp().getRequest();
const { network } = request.query;
const { networks } = getEnvironment(process.env.NODE_ENV);

if (!network || !networks.includes(network) ) {
request.query.network = EnvironmentNetwork.MainNet; // Set your default network here
}

return next.handle();
}
}
34 changes: 34 additions & 0 deletions apps/server/src/faucet/FaucetController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Controller, Get, HttpException, Inject, Param, Query, UseInterceptors } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { EnvironmentNetwork } from '@waveshq/walletkit-core';
import { TransactionResponse } from 'ethers';

import { AddressValidationInterceptor } from './AddressValidationInterceptor';
import { DefaultNetworkInterceptor } from './DefaultNetworkInterceptor';
import { FaucetService } from './FaucetService';


@Controller('faucet')
export class FaucetController {
constructor(
@Inject(CACHE_MANAGER) private cacheManager: any,
private readonly faucetService: FaucetService,
private configService: ConfigService,
) {}

@Get(':address')
@UseInterceptors(AddressValidationInterceptor, DefaultNetworkInterceptor)
async sendFunds(@Param('address') address: string, @Query('network') network: EnvironmentNetwork): Promise<TransactionResponse> {
const key = `FAUCET_${address}_${network}`;
const isCached = await this.cacheManager.get(key);
if (isCached) {
throw new HttpException('Transfer already done, pleas try again later.', 403);
}
const amountToSend: string = this.configService.getOrThrow('faucetAmountPerRequest'); // Amount to send in DFI
const ttl = +this.configService.getOrThrow('throttleTimePerAddress')
const response = await this.faucetService.sendFundsToUser(address, amountToSend, network);
await this.cacheManager.set(key, true, { ttl });
return response;
}
}
14 changes: 14 additions & 0 deletions apps/server/src/faucet/FaucetModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CacheModule } from '@nestjs/cache-manager';
import { Module } from '@nestjs/common';

import { FaucetController } from './FaucetController';
import { FaucetService } from './FaucetService';

@Module({
imports: [
CacheModule.register(),
],
controllers: [FaucetController],
providers: [FaucetService],
})
export class FaucetModule {}
36 changes: 36 additions & 0 deletions apps/server/src/faucet/FaucetService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* eslint-disable guard-for-in */
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { EnvironmentNetwork } from '@waveshq/walletkit-core';
import { ethers, parseEther,TransactionResponse } from 'ethers';

import { EVMProviderService } from '../service/EVMProviderService';

@Injectable()
export class FaucetService {
private readonly logger: Logger;

private readonly privateKey: string;

constructor(
private configService: ConfigService,
) {
this.logger = new Logger(FaucetService.name);
this.privateKey = this.configService.getOrThrow('privateKey')
}

async sendFundsToUser(address: string, amount: string, network: EnvironmentNetwork): Promise<TransactionResponse> {
const evmProviderService = new EVMProviderService(network)
const wallet = new ethers.Wallet(this.privateKey, evmProviderService.provider);
const nonce = await evmProviderService.provider.getTransactionCount(wallet.address);
const tx = {
to: address,
value: parseEther(amount),
nonce
};
this.logger.log(`Initiating transfer of ${amount} DFI ${network} to address ${address}`)
const response = await wallet.sendTransaction(tx)
this.logger.log(`Transfer done to address ${address} of amount ${amount} DFI ${network} with txn hash ${response.hash} at ${new Date().toTimeString()}.`)
return response
}
}
Loading

0 comments on commit 23175ef

Please sign in to comment.