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

feature(api): added api for faucet to allocate fund to user #346

Merged
merged 5 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
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.

9 changes: 8 additions & 1 deletion apps/server/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { 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,6 +15,11 @@ 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],
Expand Down
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, CacheInterceptor, } 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, CacheInterceptor, DefaultNetworkInterceptor)
async sendFunds(@Param('address') address: string, @Query('network') network: EnvironmentNetwork): Promise<TransactionResponse> {
const key = address;
lykalabrada marked this conversation as resolved.
Show resolved Hide resolved
const isCached = await this.cacheManager.get(key);
if (isCached) {
throw new HttpException('Transfer already done, pleas try again later.', 403);
fullstackninja864 marked this conversation as resolved.
Show resolved Hide resolved
}
const amountToSend: string = this.configService.getOrThrow('faucetAmountPerRequest'); // Amount to send in DFI
const ttl = +this.configService.getOrThrow('faucetAmountPerRequest')
lykalabrada marked this conversation as resolved.
Show resolved Hide resolved
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
Loading