From 23175ef16db10ed6803ef4954421888a5cb09630 Mon Sep 17 00:00:00 2001 From: Harsh R <53080940+fullstackninja864@users.noreply.github.com> Date: Wed, 8 Nov 2023 08:24:30 +0530 Subject: [PATCH] feature(api): added api for faucet to allocate fund to user (#346) * 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 --- apps/server/package.json | 8 +- apps/server/src/AppConfig.ts | 129 +---------- apps/server/src/app.controller.spec.ts | 23 -- apps/server/src/app.module.ts | 15 +- .../faucet/AddressValidationInterceptor.ts | 14 ++ .../src/faucet/DefaultNetworkInterceptor.ts | 17 ++ apps/server/src/faucet/FaucetController.ts | 34 +++ apps/server/src/faucet/FaucetModule.ts | 14 ++ apps/server/src/faucet/FaucetService.ts | 36 ++++ apps/server/src/service/DST20TokenService.ts | 48 ----- apps/server/src/service/EVMProviderService.ts | 23 +- apps/server/src/service/EVMService.ts | 181 ---------------- apps/server/src/service/whaleApiService.ts | 25 --- apps/server/src/test/Faucet.spec.ts | 87 ++++++++ apps/server/src/test/FaucetContorller.spec.ts | 203 ++++++++++++++++++ apps/server/src/test/MetachainTestingApp.ts | 36 ++++ apps/server/src/test/TestingModuleApp.ts | 21 ++ .../test/containers/DeFiChainStubContainer.ts | 75 +++++++ apps/server/src/test/jest-e2e.json | 9 + package.json | 8 +- pnpm-lock.yaml | 122 ++++++++--- turbo.json | 10 +- 22 files changed, 696 insertions(+), 442 deletions(-) delete mode 100644 apps/server/src/app.controller.spec.ts create mode 100644 apps/server/src/faucet/AddressValidationInterceptor.ts create mode 100644 apps/server/src/faucet/DefaultNetworkInterceptor.ts create mode 100644 apps/server/src/faucet/FaucetController.ts create mode 100644 apps/server/src/faucet/FaucetModule.ts create mode 100644 apps/server/src/faucet/FaucetService.ts delete mode 100644 apps/server/src/service/DST20TokenService.ts delete mode 100644 apps/server/src/service/EVMService.ts delete mode 100644 apps/server/src/service/whaleApiService.ts create mode 100644 apps/server/src/test/Faucet.spec.ts create mode 100644 apps/server/src/test/FaucetContorller.spec.ts create mode 100644 apps/server/src/test/MetachainTestingApp.ts create mode 100644 apps/server/src/test/TestingModuleApp.ts create mode 100644 apps/server/src/test/containers/DeFiChainStubContainer.ts create mode 100644 apps/server/src/test/jest-e2e.json diff --git a/apps/server/package.json b/apps/server/package.json index 10c709d2..bc24f036 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -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", @@ -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", @@ -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" } } diff --git a/apps/server/src/AppConfig.ts b/apps/server/src/AppConfig.ts index b6c6ff8b..228e6fa7 100644 --- a/apps/server/src/AppConfig.ts +++ b/apps/server/src/AppConfig.ts @@ -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, }; } @@ -23,120 +19,7 @@ export type DeepPartial = T extends object export type AppConfig = DeepPartial>; 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; -} \ No newline at end of file diff --git a/apps/server/src/app.controller.spec.ts b/apps/server/src/app.controller.spec.ts deleted file mode 100644 index ccea57f3..00000000 --- a/apps/server/src/app.controller.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; - -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index 797782ef..58846d18 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -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({ @@ -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 {} diff --git a/apps/server/src/faucet/AddressValidationInterceptor.ts b/apps/server/src/faucet/AddressValidationInterceptor.ts new file mode 100644 index 00000000..7fe55505 --- /dev/null +++ b/apps/server/src/faucet/AddressValidationInterceptor.ts @@ -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(); + } +} diff --git a/apps/server/src/faucet/DefaultNetworkInterceptor.ts b/apps/server/src/faucet/DefaultNetworkInterceptor.ts new file mode 100644 index 00000000..ad19dfe4 --- /dev/null +++ b/apps/server/src/faucet/DefaultNetworkInterceptor.ts @@ -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(); + } +} diff --git a/apps/server/src/faucet/FaucetController.ts b/apps/server/src/faucet/FaucetController.ts new file mode 100644 index 00000000..de54b68c --- /dev/null +++ b/apps/server/src/faucet/FaucetController.ts @@ -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 { + 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; + } +} diff --git a/apps/server/src/faucet/FaucetModule.ts b/apps/server/src/faucet/FaucetModule.ts new file mode 100644 index 00000000..a6be6662 --- /dev/null +++ b/apps/server/src/faucet/FaucetModule.ts @@ -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 {} diff --git a/apps/server/src/faucet/FaucetService.ts b/apps/server/src/faucet/FaucetService.ts new file mode 100644 index 00000000..0bc4aa1d --- /dev/null +++ b/apps/server/src/faucet/FaucetService.ts @@ -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 { + 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 + } +} diff --git a/apps/server/src/service/DST20TokenService.ts b/apps/server/src/service/DST20TokenService.ts deleted file mode 100644 index 1281c471..00000000 --- a/apps/server/src/service/DST20TokenService.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import BigNumber from 'bignumber.js'; -import { ethers, formatUnits } from 'ethers'; - -import { DST20ABI } from '../AppConfig'; -import { EVMProviderService } from './EVMProviderService'; - -@Injectable() -export class DST20TokenService { - private readonly logger: Logger; - - constructor(private provider: EVMProviderService) { - this.logger = new Logger(DST20TokenService.name); - } - - async getTotalSupply(contractAddress: string, blockTag: number | string = 'latest'): Promise { - const tokenContract = new ethers.Contract(contractAddress, DST20ABI, this.provider); - const totalSupply = await tokenContract.totalSupply.staticCall({ blockTag }); - const decimals = await tokenContract.decimals(); - return formatUnits(totalSupply, decimals); // Convert to ether units - } - - async getDecimal(contractAddress: string): Promise { - const tokenContract = new ethers.Contract(contractAddress, DST20ABI, this.provider); - const decimal = await tokenContract.decimals(); - return new BigNumber(decimal).toNumber(); - } - - async getBalanceOf(contractAddress: string, address: string, blockTag: number | string = 'latest'): Promise { - const tokenContract = new ethers.Contract(contractAddress, DST20ABI, this.provider); - const balance = await tokenContract.balanceOf.staticCall(address, { blockTag }); - const decimals = await this.getDecimal(contractAddress); - return formatUnits(balance, decimals); // Convert to ether units - } - - /** - * Get DST20 contract address - * https://github.com/DeFiCh/ain/blob/f5a671362f9899080d0a0dddbbcdcecb7c19d9e3/lib/ain-contracts/src/lib.rs#L79 - */ - getAddressFromDST20TokenId(tokenId: string): string { - const parsedTokenId = BigInt(tokenId); - const numberStr = parsedTokenId.toString(16); // Convert parsedTokenId to hexadecimal - const paddedNumberStr = numberStr.padStart(38, '0'); // Pad with zeroes to the left - const finalStr = `ff${paddedNumberStr}`; - const ethAddress = ethers.getAddress(finalStr); - return ethAddress; - } -} diff --git a/apps/server/src/service/EVMProviderService.ts b/apps/server/src/service/EVMProviderService.ts index fe069ab3..542e5db9 100644 --- a/apps/server/src/service/EVMProviderService.ts +++ b/apps/server/src/service/EVMProviderService.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; +import { EnvironmentNetwork } from "@waveshq/walletkit-core"; import axios from 'axios'; import { ethers, JsonRpcProvider } from 'ethers'; @@ -9,8 +9,8 @@ export class EVMProviderService { evmRpcUrl: string; - constructor(private configService: ConfigService) { - this.evmRpcUrl = this.configService.getOrThrow('evmRpcUrl') || 'http://localhost:20551'; + constructor(network: EnvironmentNetwork) { + this.evmRpcUrl = this.getEthRpcUrl(network); this.provider = new JsonRpcProvider(this.evmRpcUrl); } @@ -34,4 +34,21 @@ export class EVMProviderService { const res = await axios(requestOptions); return res?.data?.result; } + + getEthRpcUrl(network: EnvironmentNetwork): string { + // TODO: Add proper ethereum RPC URLs for each network + switch (network) { + case EnvironmentNetwork.LocalPlayground: + return "http://localhost:19551"; + case EnvironmentNetwork.RemotePlayground: + case EnvironmentNetwork.DevNet: + case EnvironmentNetwork.Changi: + return "http://34.34.156.49:20551"; // TODO: add final eth rpc url for changi, devnet and remote playground + case EnvironmentNetwork.MainNet: + return "https://changi.dfi.team"; // TODO: add final eth rpc url for mainnet, with proper domain name + case EnvironmentNetwork.TestNet: + default: + return "https://eth.testnet.ocean.jellyfishsdk.com"; + } + } } diff --git a/apps/server/src/service/EVMService.ts b/apps/server/src/service/EVMService.ts deleted file mode 100644 index dd0147ff..00000000 --- a/apps/server/src/service/EVMService.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import BigNumber from 'bignumber.js'; -import { TransactionResponseParams } from 'ethers'; -import { decodeFunctionData } from 'viem'; - -import { BalanceMovementI, DST20ABI, DST20BalanceMovementOnBlockI } from '../AppConfig'; -import { DST20TokenService } from './DST20TokenService'; -import { EVMProviderService } from './EVMProviderService'; - -@Injectable() -export class EVMService { - private readonly logger: Logger; - - constructor( - private dst20TokenService: DST20TokenService, - private evmProviderService: EVMProviderService, - ) { - this.logger = new Logger(EVMService.name); - } - - async getEthBalance(address: string, blockHeight: number | string = 'latest'): Promise { - const res = await this.evmProviderService.evmRpc('eth_getBalance', [address, blockHeight]); - return new BigNumber(res, 16).dividedBy(new BigNumber(10).pow(18)).toString(); - } - - async getDFIBalanceMovementOnBlock(address: string, transactions: TransactionResponseParams[]): Promise { - const statsValue = await transactions.reduce( - async (stats: Promise, current: any) => { - const result = await stats; - const txReceipt = await this.evmProviderService.evmRpc('eth_getTransactionReceipt', [current.hash, true]); - // check if transaction is not reverted - if (txReceipt.status === '0x1') { - // txn happens from given address - if (current.from?.toLowerCase() === address.toLowerCase()) { - // calculate Transaction Fee = gas used * gas price - result.gasUsed = result.gasUsed.plus( - new BigNumber(current.gas, 16).times(new BigNumber(current.gasPrice, 16)), - ); - // calculate dfi transferred if any - result.transferredBalance = result.transferredBalance.plus(new BigNumber(current.value, 16)); - } - // txn happens to given address - if (current.to?.toLowerCase() === address.toLowerCase()) { - // calculate dfi received if any - result.receivedBalance = result.receivedBalance.plus(new BigNumber(current.value, 16)); - } - } - return Promise.resolve(result); - }, - Promise.resolve({ - gasUsed: new BigNumber(0), - transferredBalance: new BigNumber(0), - receivedBalance: new BigNumber(0), - }), - ); - // return movement - return new BigNumber(statsValue.transferredBalance) - .plus(statsValue.gasUsed) - .minus(statsValue.receivedBalance) - .dividedBy(new BigNumber(10).pow(18)); - } - - async getDFIBalanceDifferenceByBlockHash(address: string, transferBlockHash: string): Promise { - try { - // get block details by hash - const transferBlockDetails = await this.evmProviderService.evmRpc('eth_getBlockByHash', [ - transferBlockHash, - true, - ]); - // convert block number from hex to decimal - const transferBlockNumber = new BigNumber(transferBlockDetails.number, 16); - // get balance at current block - const currentBalance = await this.getEthBalance(address, transferBlockNumber.toNumber()); - // get balance at previous block - const prevBalance = await this.getEthBalance(address, transferBlockNumber.minus(1).toNumber()); - - // get balance movement at current block txn - const movement = await this.getDFIBalanceMovementOnBlock(address, transferBlockDetails.transactions); - const blockBalanceDiff = new BigNumber(currentBalance).minus(prevBalance); - // calculate balance difference happens due to token transfer - return blockBalanceDiff.plus(movement); - } catch (err) { - this.logger.error(err); - throw new Error(err.message); - } - } - - async getDST20BalanceMovementOnBlock( - contractAddress: string, - address: string, - transactions: TransactionResponseParams[], - ): Promise { - const tokenDecimal = await this.dst20TokenService.getDecimal(contractAddress); - const statsValue = await transactions.reduce( - async (stats: Promise, current: any) => { - // txn happens from given address - const result = await stats; - // check for contract call (transfer/ transferFrom) - if (current.to?.toLowerCase() === contractAddress.toLowerCase()) { - // check is txn is valid or not - const txReceipt = await this.evmProviderService.evmRpc('eth_getTransactionReceipt', [current.hash, true]); - // check if transaction is not reverted - if (txReceipt.status === '0x1') { - // calculate token transferred if any - const decodedData: any = decodeFunctionData({ - abi: DST20ABI, - data: current.input, - }); - // check for transfer function call of smart contract - if (decodedData.functionName === 'transfer') { - const [to, amount] = decodedData.args; - if (current.from.toLowerCase() === address.toLowerCase()) { - result.transferredBalance = result.transferredBalance.plus(amount ?? 0); - } - if (to?.toLowerCase() === address.toLowerCase()) { - result.receivedBalance = result.receivedBalance.plus(amount ?? 0); - } - } - // check for transferFrom function call of smart contract - if (decodedData.functionName === 'transferFrom') { - const [from, to, amount] = decodedData.args; - if (from?.toLowerCase() === address.toLowerCase()) { - result.transferredBalance = result.transferredBalance.plus(amount ?? 0); - } - if (to?.toLowerCase() === address.toLowerCase()) { - result.receivedBalance = result.receivedBalance.plus(amount ?? 0); - } - } - } - } - return Promise.resolve(result); - }, - Promise.resolve({ transferredBalance: new BigNumber(0), receivedBalance: new BigNumber(0) }), - ); - // return movement - return new BigNumber(statsValue.transferredBalance) - .minus(statsValue.receivedBalance) - .dividedBy(new BigNumber(10).pow(tokenDecimal)); - } - - async getDST20BalanceDifferenceByBlockHash( - tokenId: string, - address: string, - transferBlockHash: string, - ): Promise { - // TODO check eth_getTransactionReceipt to check txn is valid or not - try { - const contractAddress = this.dst20TokenService.getAddressFromDST20TokenId(tokenId); - // get block details by hash - const transferBlockDetails = await this.evmProviderService.evmRpc('eth_getBlockByHash', [ - transferBlockHash, - true, - ]); - // convert block number from hex to decimal - const transferBlockNumber = new BigNumber(transferBlockDetails.number, 16); - // get balance at current block - const currentBalance = await this.dst20TokenService.getBalanceOf( - contractAddress, - address, - transferBlockNumber.toNumber(), - ); - // get balance at previous block - const prevBalance = await this.dst20TokenService.getBalanceOf( - contractAddress, - address, - transferBlockNumber.minus(1).toNumber(), - ); - // get balance movement at current block txn - const movement = await this.getDST20BalanceMovementOnBlock( - contractAddress, - address, - transferBlockDetails.transactions, - ); - const blockBalanceDiff = new BigNumber(currentBalance).minus(prevBalance); - return blockBalanceDiff.plus(movement); - } catch (err) { - this.logger.error(err); - throw new Error(err.message); - } - } -} diff --git a/apps/server/src/service/whaleApiService.ts b/apps/server/src/service/whaleApiService.ts deleted file mode 100644 index 4f3c72bf..00000000 --- a/apps/server/src/service/whaleApiService.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { WhaleApiClient, WhaleApiClientOptions } from '@defichain/whale-api-client'; -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { EnvironmentNetwork, newOceanOptions, newWhaleAPIClient } from '@waveshq/walletkit-core'; - -@Injectable() -export class WhaleApiService { - whaleApi: WhaleApiClient; - - whaleOptions: WhaleApiClientOptions; - - network: EnvironmentNetwork; - - constructor(private configService: ConfigService) { - this.network = this.configService.getOrThrow('network') || EnvironmentNetwork.LocalPlayground; - this.whaleOptions = this.network === EnvironmentNetwork.LocalPlayground - ? ({ - url: this.configService.getOrThrow('whaleURL'), - network: 'regtest', - version: 'v0', - } as WhaleApiClientOptions) - : newOceanOptions(this.network); - this.whaleApi = newWhaleAPIClient(this.whaleOptions); - } -} diff --git a/apps/server/src/test/Faucet.spec.ts b/apps/server/src/test/Faucet.spec.ts new file mode 100644 index 00000000..7dceb685 --- /dev/null +++ b/apps/server/src/test/Faucet.spec.ts @@ -0,0 +1,87 @@ +import { TransferDomainType } from '@defichain/jellyfish-api-core/dist/category/account'; +import { NestFastifyApplication } from '@nestjs/platform-fastify'; +import { EnvironmentNetwork } from '@waveshq/walletkit-core'; +import { JsonRpcProvider, parseEther } from 'ethers'; +import * as request from 'supertest'; +import waitForExpect from 'wait-for-expect'; + +import { EVMProviderService } from '../service/EVMProviderService'; +import { DeFiChainStubContainer, StartedDeFiChainStubContainer } from './containers/DeFiChainStubContainer'; +import { MetachainTestingApp } from './MetachainTestingApp'; + +export async function waitForTxnToConfirm( + provider: JsonRpcProvider, + hash: string, + timeout: number = 30_000, +): Promise { + await waitForExpect(async () => { + const txn = await provider.getTransactionReceipt(hash); + expect(txn?.status).toStrictEqual(1); + }, timeout); + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); +} + +describe('Faucet (e2e)', () => { + let app: NestFastifyApplication; + let defichainContainer: StartedDeFiChainStubContainer; + let testing: MetachainTestingApp; + let evmRpcUrl: string + let dvmAddress; + let provider: JsonRpcProvider + const amount = '1' + const evmAddress = "0x9f1FF3f9A4F99f39Ec7F799F90e54bfC88B43FFA" + const evmPrivateKey = "0x814233c91d926169e9a6817db4de4f325cee91639ff6cfce74029ec9e568d2ad" + beforeAll(async () => { + defichainContainer = await new DeFiChainStubContainer().start(); + evmRpcUrl = await defichainContainer.getEvmURL(); + dvmAddress = await defichainContainer.defid.rpc.getNewAddress('legacy', 'legacy'); + + testing = new MetachainTestingApp(); + app = await testing.createNestApp({ + faucetAmountPerRequest: amount, + throttleTimePerAddress: '20', + privateKey: evmPrivateKey, + }); + provider = new JsonRpcProvider(evmRpcUrl); + await defichainContainer.playgroundClient.wallet.sendUtxo('20', dvmAddress); + await defichainContainer.playgroundClient.wallet.sendToken('0', '20', dvmAddress); + await defichainContainer.generateBlock(1); + await defichainContainer.playgroundRpcClient.account.transferDomain([ + { + src: { + address: dvmAddress, + amount: '10@0', + domain: TransferDomainType.DVM, + }, + dst: { + address: evmAddress, + amount: '10@0', + domain: TransferDomainType.EVM, + }, + }, + ]); + await defichainContainer.generateBlock(10); + await testing.start(); + }); + + afterAll(async () => { + await defichainContainer.stop(); + await testing.stop(); + app = undefined; + }); + + + it('/faucet/:address (GET) should send DFI to address', async () => { + // mock getEthRpcUrl method + EVMProviderService.prototype.getEthRpcUrl = jest.fn().mockReturnValue(evmRpcUrl); + const address = await defichainContainer.defid.rpc.getNewAddress('eth', 'eth'); + const response = await request(app.getHttpServer()) + .get(`/faucet/${address}?network=${EnvironmentNetwork.LocalPlayground}`) + .expect(200) + await waitForTxnToConfirm(provider, response.body.hash) + const balance = await provider.getBalance(address); + expect(parseEther(amount).toString()).toStrictEqual(balance.toString()); + }); +}); diff --git a/apps/server/src/test/FaucetContorller.spec.ts b/apps/server/src/test/FaucetContorller.spec.ts new file mode 100644 index 00000000..32a2a3a8 --- /dev/null +++ b/apps/server/src/test/FaucetContorller.spec.ts @@ -0,0 +1,203 @@ +import { CACHE_MANAGER, CacheModule } from '@nestjs/cache-manager'; +import { HttpException } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { APP_GUARD } from '@nestjs/core'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; +import { EnvironmentNetwork } from '@waveshq/walletkit-core'; +import { TransactionResponse } from 'ethers'; +import * as request from 'supertest'; + +import { FaucetController } from '../faucet/FaucetController'; +import { FaucetService } from '../faucet/FaucetService'; + + +describe('FaucetController (e2e)', () => { + let app; + let cacheManager; + let faucetService; + const faucetAmountPerRequest = '0.1'; + const throttleTimePerAddress = 3600; + const evmAddress = '0x9f1FF3f9A4F99f39Ec7F799F90e54bfC88B43FFA'; + const evmPrivateKey = "0x814233c91d926169e9a6817db4de4f325cee91639ff6cfce74029ec9e568d2ad" + const network = EnvironmentNetwork.LocalPlayground; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [ + ConfigModule.forFeature(() => ({ + faucetAmountPerRequest, + throttleTimePerAddress, + privateKey: evmPrivateKey, + })), + ThrottlerModule.forRoot([{ + ttl: 60_000, // Throttle time window set to 60 seconds + limit: 10, // Maximum 10 requests allowed within the time window + }]), + CacheModule.register(), + ], + controllers: [FaucetController], + providers: [ + FaucetService, + ConfigService, + { + provide: APP_GUARD, + useClass: ThrottlerGuard, + } + ], + }).compile(); + + app = moduleFixture.createNestApplication(); + cacheManager = app.get(CACHE_MANAGER); + faucetService = app.get(FaucetService); + + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + it('should return 200 and the expected response for valid request', async () => { + const invalidAddress = "0xInvalidAddress" + const response = await request(app.getHttpServer()) + .get(`/faucet/${invalidAddress}?network=${network}`) + .expect(400) + expect(response.body.message).toBe('Invalid Ethereum address'); + }); + + + it('should return 200 and the expected response for valid request', async () => { + const mockTransactionResponse = {} as TransactionResponse; + jest.spyOn(faucetService, 'sendFundsToUser').mockResolvedValueOnce(mockTransactionResponse); + + const response = await request(app.getHttpServer()) + .get(`/faucet/${evmAddress}?network=${network}`) + .expect(200) + expect(response.body).toEqual(mockTransactionResponse); + }); + + it('should return 403 for repeated requests from the same address', async () => { + const mockedKey = `FAUCET_${evmAddress}_${network}`; + // Mock the cacheManager to return true, simulating a cached response + jest.spyOn(cacheManager, 'get').mockImplementation(async (key) => { + if (key === mockedKey) { + return true; + } + return null; + }); + + const response = await request(app.getHttpServer()) + .get(`/faucet/${evmAddress}?network=${network}`) + .expect(403); + + // Check the error message in the response body + expect(response.body.message).toBe('Transfer already done, pleas try again later.'); + + // Check if the cacheManager get method was called with the correct key + expect(cacheManager.get).toHaveBeenCalledWith(mockedKey); + }); + + it('should cache the response for a request and return the cached response for subsequent requests within the TTL', async () => { + const mockedKey = `FAUCET_${evmAddress}_${network}`; + const mockTransactionResponse = {} as TransactionResponse; + jest.spyOn(faucetService, 'sendFundsToUser').mockResolvedValueOnce(mockTransactionResponse); + const mockCacheGet = jest.spyOn(cacheManager, 'get').mockImplementation(() => null); + const mockCacheSet = jest.spyOn(cacheManager, 'set').mockResolvedValue(true); + + await request(app.getHttpServer()) + .get(`/faucet/${evmAddress}?network=${network}`) + .expect(200); + + expect(mockCacheGet).toHaveBeenCalledWith(mockedKey); + expect(mockCacheSet).toHaveBeenCalledWith(mockedKey, true, { ttl: throttleTimePerAddress }); + }); + + it('should not cache the response for a request if the response is not successful', async () => { + const mockedKey = `FAUCET_${evmAddress}_${network}`; + const mockCacheGet = jest.spyOn(cacheManager, 'get').mockImplementation(() => null); + jest.spyOn(faucetService, 'sendFundsToUser').mockRejectedValue(new HttpException('Error', 500)); + const mockCacheSet = jest.spyOn(cacheManager, 'set').mockResolvedValue(true); + await request(app.getHttpServer()) + .get(`/faucet/${evmAddress}?network=${network}`) + .expect(500); + + expect(mockCacheGet).toHaveBeenCalledWith(mockedKey); + expect(mockCacheSet).not.toHaveBeenCalled(); + }); + + it('should reset the cache after the TTL expires', async () => { + const mockedKey = `FAUCET_${evmAddress}_${network}`; + // Mock the cacheManager to return true, simulating a cached response + jest.spyOn(cacheManager, 'get').mockImplementation(async (key) => { + if (key === mockedKey) { + return false; + } + return null; + }); + const mockTransactionResponse = {} as TransactionResponse; + + jest.spyOn(faucetService, 'sendFundsToUser').mockResolvedValueOnce(mockTransactionResponse); + + await request(app.getHttpServer()) + .get(`/faucet/${evmAddress}?network=${network}`) + .expect(200) + .expect((res) => { + expect(res.body).toEqual(mockTransactionResponse); + }); + }); + + it('should throttle requests from the same address within the throttle time frame', async () => { + const mockedKey = `FAUCET_${evmAddress}_${network}`; + const mockTransactionResponse = {} as TransactionResponse; + + // Mock the cacheManager to return false for the first request and true for subsequent requests + let callCount = 0; + jest.spyOn(cacheManager, 'get').mockImplementation(async (key) => { + if (key === mockedKey) { + callCount += 1; + return callCount > 1; + } + return null; + }); + jest.spyOn(faucetService, 'sendFundsToUser').mockResolvedValueOnce(mockTransactionResponse); + // Make multiple requests within the throttle time frame + const response1 = await request(app.getHttpServer()) + .get(`/faucet/${evmAddress}?network=${network}`) + .expect(200); + + const response2 = await request(app.getHttpServer()) + .get(`/faucet/${evmAddress}?network=${network}`) + .expect(403); + + // Check the response body and status codes + expect(response1.status).toBe(200); + expect(response2.status).toBe(403); + expect(cacheManager.get).toHaveBeenCalledWith(mockedKey); + expect(cacheManager.get).toHaveBeenCalledTimes(2) + }); + + it('should throttle requests from the same IP address within the throttle time frame', async () => { + // Wait for the throttle time frame to expire + await sleep(60_000); + const mockTransactionResponse = {} as TransactionResponse; + jest.spyOn(faucetService, 'sendFundsToUser').mockResolvedValue(mockTransactionResponse); + jest.spyOn(cacheManager, 'get').mockImplementation(() => null); + for (let i = 0; i < 10; i += 1) { + await request(app.getHttpServer()).get(`/faucet/${evmAddress}?network=${network}`).expect(200) + } + // Make multiple requests from the same IP address within the throttle time frame + await request(app.getHttpServer()).get(`/faucet/${evmAddress}?network=${network}`).expect(429) + // Wait for the throttle time frame to expire + await sleep(60_000); + // Make a new request after the throttle time frame has passed + await request(app.getHttpServer()).get(`/faucet/${evmAddress}?network=${network}`).expect(200); + }); +}); + +export const sleep = (time: number) => + new Promise((resolve) => { + setTimeout(() => { + resolve(''); + }, time); +}); diff --git a/apps/server/src/test/MetachainTestingApp.ts b/apps/server/src/test/MetachainTestingApp.ts new file mode 100644 index 00000000..7676c1b4 --- /dev/null +++ b/apps/server/src/test/MetachainTestingApp.ts @@ -0,0 +1,36 @@ +import { NestFastifyApplication } from '@nestjs/platform-fastify'; +import { Test, TestingModule } from '@nestjs/testing'; + +import { BuildTestConfigParams, TestingModuleApp } from './TestingModuleApp'; + +/** + * Testing app used for testing Server App behaviour through integration tests + */ +export class MetachainTestingApp { + app: NestFastifyApplication; + + async createTestingModule(config: BuildTestConfigParams): Promise { + const dynamicModule = TestingModuleApp.register(config); + return Test.createTestingModule({ + imports: [dynamicModule], + }) + .compile(); + } + + async createNestApp(config: BuildTestConfigParams): Promise { + const module = await this.createTestingModule(config); + this.app = module.createNestApplication(); + await new Promise(setImmediate); + + return this.app; + } + + async start(): Promise { + return this.app.init(); + } + + async stop(): Promise { + await this.app.close(); + this.app = undefined; + } +} diff --git a/apps/server/src/test/TestingModuleApp.ts b/apps/server/src/test/TestingModuleApp.ts new file mode 100644 index 00000000..365c90bc --- /dev/null +++ b/apps/server/src/test/TestingModuleApp.ts @@ -0,0 +1,21 @@ +import { DynamicModule, Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; + +import { AppModule } from '../app.module'; +import { AppConfig } from '../AppConfig'; + +@Module({}) +export class TestingModuleApp { + static register(config: AppConfig): DynamicModule { + return { + module: TestingModuleApp, + imports: [AppModule, ConfigModule.forFeature(() => config)], + }; + } +} + +export type BuildTestConfigParams = { + faucetAmountPerRequest: string; + throttleTimePerAddress: string; + privateKey: string; +}; diff --git a/apps/server/src/test/containers/DeFiChainStubContainer.ts b/apps/server/src/test/containers/DeFiChainStubContainer.ts new file mode 100644 index 00000000..d2bdad6f --- /dev/null +++ b/apps/server/src/test/containers/DeFiChainStubContainer.ts @@ -0,0 +1,75 @@ +import { PlaygroundApiClient, PlaygroundRpcClient } from '@defichain/playground-api-client'; +import { + NativeChainContainer, + PlaygroundApiContainer, + StartedNativeChainContainer, + StartedPlaygroundApiContainer, + StartedWhaleApiContainer, + WhaleApiContainer, +} from '@defichain/testcontainers'; +import { WhaleApiClient } from '@defichain/whale-api-client'; +import { Network } from '@stickyjs/testcontainers'; + +/** + * DeFiChain Container that runs all necessary containers (Playground, Whale, Ain). + * + * */ +export class DeFiChainStubContainer { + async start(): Promise { + const network = await new Network().start(); + const defid = await new NativeChainContainer() + .withNetwork(network) + .withPreconfiguredRegtestMasternode() + .addCmd('-ethrpcport=19551') + .addCmd('-ethrpcbind=0.0.0.0') + .withExposedPorts(19551, 19554) + .start(); + const whale = await new WhaleApiContainer().withNetwork(network).withNativeChain(defid, network).start(); + const playground = await new PlaygroundApiContainer().withNetwork(network).withNativeChain(defid, network).start(); + await playground.waitForReady(); + return new StartedDeFiChainStubContainer(defid, whale, playground); + } +} + +export class StartedDeFiChainStubContainer { + public playgroundRpcClient: PlaygroundRpcClient; + + public playgroundClient: PlaygroundApiClient; + + public whaleClient: WhaleApiClient; + + public static LOCAL_MNEMONIC = process.env.DEFICHAIN_PRIVATE_KEY; + + constructor( + public defid: StartedNativeChainContainer, + protected whale: StartedWhaleApiContainer, + protected playground: StartedPlaygroundApiContainer, + ) { + this.playgroundClient = new PlaygroundApiClient({ url: this.playground.getPlaygroundApiClientOptions().url }); + this.playgroundRpcClient = new PlaygroundRpcClient(this.playgroundClient); + this.whaleClient = new WhaleApiClient(this.whale.getWhaleApiClientOptions()); + } + + async stop(): Promise { + await this.whale.stop(); + await this.defid.stop(); + await this.playground.stop(); + } + + /** + * Please note that number of blocks generated can be 2-3 blocks off from the given `number`. + * eg. if you want to generate 35 blocks you might need to add a little allowance and generate 38 blocks instead + * @param number + */ + async generateBlock(number = 10): Promise { + await this.playgroundClient.rpc.call('generatetoaddress', [number, 'mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy'], 'number'); + } + + async getWhaleURL(): Promise { + return this.whale.getWhaleApiClientOptions().url; + } + + async getEvmURL(): Promise { + return `http://localhost:${this.defid.getMappedPort(19551)}`; + } +} diff --git a/apps/server/src/test/jest-e2e.json b/apps/server/src/test/jest-e2e.json new file mode 100644 index 00000000..e9d912f3 --- /dev/null +++ b/apps/server/src/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/package.json b/package.json index 8b540b8d..926e476f 100644 --- a/package.json +++ b/package.json @@ -18,12 +18,12 @@ "@waveshq/standard-defichain-jellyfishsdk": "^1.9.3" }, "devDependencies": { - "@stickyjs/turbo": "^1.3.3", - "eslint-config-next": "13.4.19", + "@stickyjs/turbo": "^1.3.4", + "eslint-config-next": "13.5.4", "husky": "^8.0.3", - "lint-staged": "^13.3.0" + "lint-staged": "^14.0.1" }, - "packageManager": "pnpm@8.7.5", + "packageManager": "pnpm@8.8.0", "engines": { "node": ">=18.0.0", "pnpm": ">=8.7.5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 494ad800..4a4dcb08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,17 +13,17 @@ importers: version: 1.9.3(@types/tar-fs@2.0.3) devDependencies: '@stickyjs/turbo': - specifier: ^1.3.3 + specifier: ^1.3.4 version: 1.3.4(turbo@1.10.16)(typanion@3.14.0) eslint-config-next: - specifier: 13.4.19 - version: 13.4.19(eslint@8.52.0)(typescript@5.2.2) + specifier: 13.5.4 + version: 13.5.4(eslint@8.52.0)(typescript@5.2.2) husky: specifier: ^8.0.3 version: 8.0.3 lint-staged: - specifier: ^13.3.0 - version: 13.3.0 + specifier: ^14.0.1 + version: 14.0.1 apps/server: dependencies: @@ -39,6 +39,9 @@ importers: '@defichain/whale-api-client': specifier: ^4.0.0-beta.11 version: 4.0.0-beta.14.1(defichain@4.0.0-beta.14.1) + '@nestjs/cache-manager': + specifier: ^2.1.1 + version: 2.1.1(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)(cache-manager@5.2.4)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/common': specifier: ^10.2.5 version: 10.2.8(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) @@ -51,6 +54,9 @@ importers: '@nestjs/platform-express': specifier: ^10.2.5 version: 10.2.8(@nestjs/common@10.2.8)(@nestjs/core@10.2.8) + '@nestjs/throttler': + specifier: ^5.0.1 + version: 5.0.1(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)(reflect-metadata@0.1.13) '@waveshq/standard-defichain-jellyfishsdk': specifier: ^2.6.1 version: 2.23.0(@types/tar-fs@2.0.3) @@ -63,6 +69,9 @@ importers: bignumber.js: specifier: ^9.1.2 version: 9.1.2 + cache-manager: + specifier: ^5.2.4 + version: 5.2.4 class-validator: specifier: ^0.14.0 version: 0.14.0 @@ -184,6 +193,9 @@ importers: typescript: specifier: ^5.2.2 version: 5.2.2 + wait-for-expect: + specifier: ^3.0.2 + version: 3.0.2 apps/web: dependencies: @@ -3170,6 +3182,22 @@ packages: tslib: 2.6.2 dev: false + /@nestjs/cache-manager@2.1.1(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)(cache-manager@5.2.4)(reflect-metadata@0.1.13)(rxjs@7.8.1): + resolution: {integrity: sha512-oYfRys4Ng0zp2HTUPNjH7gizf4vvG3PQZZ+3yGemb3xrF+p3JxDSK0cDq9NTjHzD5UmhjiyAftB9GkuL+t3r9g==} + peerDependencies: + '@nestjs/common': ^9.0.0 || ^10.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 + cache-manager: <=5 + reflect-metadata: ^0.1.12 + rxjs: ^7.0.0 + dependencies: + '@nestjs/common': 10.2.8(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.2.8(@nestjs/common@10.2.8)(@nestjs/platform-express@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) + cache-manager: 5.2.4 + reflect-metadata: 0.1.13 + rxjs: 7.8.1 + dev: false + /@nestjs/cli@10.2.1: resolution: {integrity: sha512-CAJAQwmxFZfB3RTvqz/eaXXWpyU+mZ4QSqfBYzjneTsPgF+uyOAW3yQpaLNn9Dfcv39R9UxSuAhayv6yuFd+Jg==} engines: {node: '>= 16.14'} @@ -3353,6 +3381,19 @@ packages: tslib: 2.6.2 dev: true + /@nestjs/throttler@5.0.1(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)(reflect-metadata@0.1.13): + resolution: {integrity: sha512-6gHmKmus7LDgzK/wRh7W9cT3gtZw1rY4mLVymvx6pYyRA46A9fMvjmZNQKJbo02jGg2R7gNo+3pyfvNnjDqg4A==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + reflect-metadata: ^0.1.13 + dependencies: + '@nestjs/common': 10.2.8(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.2.8(@nestjs/common@10.2.8)(@nestjs/platform-express@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) + md5: 2.3.0 + reflect-metadata: 0.1.13 + dev: false + /@netlify/blobs@2.2.0: resolution: {integrity: sha512-j2C0+IvWj9CLNGPoiA7ETquMFDExZTrv4CarjfE6Au0eY3zlinnnTVae7DE+VQFK+U0CDM/O0VvelNy1QbsdwQ==} engines: {node: ^14.16.0 || >=16.0.0} @@ -3700,6 +3741,12 @@ packages: glob: 7.1.7 dev: true + /@next/eslint-plugin-next@13.5.4: + resolution: {integrity: sha512-vI94U+D7RNgX6XypSyjeFrOzxGlZyxOplU0dVE5norIfZGn/LDjJYPHdvdsR5vN1eRtl6PDAsOHmycFEOljK5A==} + dependencies: + glob: 7.1.7 + dev: true + /@next/eslint-plugin-next@14.0.1: resolution: {integrity: sha512-bLjJMwXdzvhnQOnxvHoTTUh/+PYk6FF/DCgHi4BXwXCINer+o1ZYfL9aVeezj/oI7wqGJOqwGIXrlBvPbAId3w==} dependencies: @@ -3977,6 +4024,7 @@ packages: dependencies: is-glob: 4.0.3 micromatch: 4.0.5 + napi-wasm: 1.1.0 bundledDependencies: - napi-wasm @@ -6859,6 +6907,13 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + /cache-manager@5.2.4: + resolution: {integrity: sha512-gkuCjug16NdGvKm/sydxGVx17uffrSWcEe2xraBtwRCgdYcFxwJAla4OYpASAZT2yhSoxgDiWL9XH6IAChcZJA==} + dependencies: + lodash.clonedeep: 4.5.0 + lru-cache: 10.0.1 + dev: false + /cachedir@2.4.0: resolution: {integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==} engines: {node: '>=6'} @@ -6969,6 +7024,10 @@ packages: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true + /charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + dev: false + /check-more-types@2.24.0: resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} engines: {node: '>= 0.8.0'} @@ -7539,6 +7598,10 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + dev: false + /crypto-random-string@2.0.0: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} @@ -8468,8 +8531,8 @@ packages: - supports-color dev: true - /eslint-config-next@13.4.19(eslint@8.52.0)(typescript@5.2.2): - resolution: {integrity: sha512-WE8367sqMnjhWHvR5OivmfwENRQ1ixfNE9hZwQqNCsd+iM3KnuMc1V8Pt6ytgjxjf23D+xbesADv9x3xaKfT3g==} + /eslint-config-next@13.5.4(eslint@8.52.0)(typescript@5.2.2): + resolution: {integrity: sha512-FzQGIj4UEszRX7fcRSJK6L1LrDiVZvDFW320VVntVKh3BSU8Fb9kpaoxQx0cdFgf3MQXdeSbrCXJ/5Z/NndDkQ==} peerDependencies: eslint: ^7.23.0 || ^8.0.0 typescript: '>=3.3.1' @@ -8477,7 +8540,7 @@ packages: typescript: optional: true dependencies: - '@next/eslint-plugin-next': 13.4.19 + '@next/eslint-plugin-next': 13.5.4 '@rushstack/eslint-patch': 1.5.1 '@typescript-eslint/parser': 6.9.1(eslint@8.52.0)(typescript@5.2.2) eslint: 8.52.0 @@ -10439,6 +10502,10 @@ packages: has-tostringtag: 1.0.0 dev: true + /is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + dev: false + /is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -11699,26 +11766,6 @@ packages: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true - /lint-staged@13.3.0: - resolution: {integrity: sha512-mPRtrYnipYYv1FEE134ufbWpeggNTo+O/UPzngoaKzbzHAthvR55am+8GfHTnqNRQVRRrYQLGW9ZyUoD7DsBHQ==} - engines: {node: ^16.14.0 || >=18.0.0} - hasBin: true - dependencies: - chalk: 5.3.0 - commander: 11.0.0 - debug: 4.3.4(supports-color@5.5.0) - execa: 7.2.0 - lilconfig: 2.1.0 - listr2: 6.6.1 - micromatch: 4.0.5 - pidtree: 0.6.0 - string-argv: 0.3.2 - yaml: 2.3.1 - transitivePeerDependencies: - - enquirer - - supports-color - dev: true - /lint-staged@14.0.1: resolution: {integrity: sha512-Mw0cL6HXnHN1ag0mN/Dg4g6sr8uf8sn98w2Oc1ECtFto9tvRF7nkXGJRbx8gPlHyoR0pLyBr2lQHbWwmUHe1Sw==} engines: {node: ^16.14.0 || >=18.0.0} @@ -11876,6 +11923,10 @@ packages: p-locate: 6.0.0 dev: true + /lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + dev: false + /lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} dev: true @@ -12061,6 +12112,14 @@ packages: inherits: 2.0.4 safe-buffer: 5.2.1 + /md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + dev: false + /media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -12322,6 +12381,9 @@ packages: /napi-build-utils@1.0.2: resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + /napi-wasm@1.1.0: + resolution: {integrity: sha512-lHwIAJbmLSjF9VDRm9GoVOy9AGp3aIvkjv+Kvz9h16QR3uSVYH78PNQUnT2U4X53mhlnV2M7wrhibQ3GHicDmg==} + /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true @@ -16102,6 +16164,10 @@ packages: - zod dev: false + /wait-for-expect@3.0.2: + resolution: {integrity: sha512-cfS1+DZxuav1aBYbaO/kE06EOS8yRw7qOFoD3XtjTkYvCvh3zUvNST8DXK/nPaeqIzIv3P3kL3lRJn8iwOiSag==} + dev: true + /walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: diff --git a/turbo.json b/turbo.json index ec4d016d..d6e38f6b 100644 --- a/turbo.json +++ b/turbo.json @@ -6,10 +6,14 @@ "dependsOn": ["^build"], "outputs": [".next/**", "!.next/cache/**"] }, - "lint": {}, + "lint": { + "outputs": [] + }, + "clean": { + "cache": false + }, "dev": { - "cache": false, - "persistent": true + "cache": false } } }