From 84d25f19bcd3860595629151631798d3c7d7c945 Mon Sep 17 00:00:00 2001 From: nafiuishaaq Date: Thu, 26 Feb 2026 07:32:25 +0100 Subject: [PATCH 1/3] implemented the auth entity --- src/fee-estimation/dto/fee-estimate.dto.ts | 283 ++++++++++++++++++ .../entities/fee-estimate.entity.ts | 87 ++++++ .../types/fee-estimate.types.ts | 214 +++++++++++++ 3 files changed, 584 insertions(+) create mode 100644 src/fee-estimation/dto/fee-estimate.dto.ts create mode 100644 src/fee-estimation/entities/fee-estimate.entity.ts create mode 100644 src/fee-estimation/types/fee-estimate.types.ts diff --git a/src/fee-estimation/dto/fee-estimate.dto.ts b/src/fee-estimation/dto/fee-estimate.dto.ts new file mode 100644 index 0000000..1f07375 --- /dev/null +++ b/src/fee-estimation/dto/fee-estimate.dto.ts @@ -0,0 +1,283 @@ +import { IsOptional, IsString, IsNumber, IsBoolean, Min, IsDateString } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +/** + * DTO for fee estimate query + */ +export class FeeEstimateQueryDto { + @ApiProperty({ description: 'Bridge name' }) + @IsString() + bridgeName: string; + + @ApiProperty({ description: 'Source chain' }) + @IsString() + sourceChain: string; + + @ApiProperty({ description: 'Destination chain' }) + @IsString() + destinationChain: string; + + @ApiPropertyOptional({ description: 'Token symbol' }) + @IsOptional() + @IsString() + token?: string; + + @ApiPropertyOptional({ description: 'Transfer amount' }) + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + amount?: number; + + @ApiPropertyOptional({ description: 'Include USD estimates', default: true }) + @IsOptional() + @Type(() => Boolean) + @IsBoolean() + includeUsd?: boolean = true; +} + +/** + * DTO for fee estimate response + */ +export class FeeEstimateDto { + @ApiProperty({ description: 'Bridge name' }) + bridgeName: string; + + @ApiProperty({ description: 'Source chain' }) + sourceChain: string; + + @ApiProperty({ description: 'Destination chain' }) + destinationChain: string; + + @ApiPropertyOptional({ description: 'Token symbol' }) + token?: string; + + @ApiPropertyOptional({ description: 'Transfer amount' }) + amount?: number; + + @ApiProperty({ description: 'Total fee in native token' }) + totalFee: number; + + @ApiProperty({ description: 'Gas fee component' }) + gasFee: number; + + @ApiProperty({ description: 'Bridge fee component' }) + bridgeFee: number; + + @ApiProperty({ description: 'Liquidity-based fee component', default: 0 }) + liquidityFee: number; + + @ApiProperty({ description: 'Protocol fee component', default: 0 }) + protocolFee: number; + + @ApiPropertyOptional({ description: 'Gas price in Gwei' }) + gasPriceGwei?: number; + + @ApiPropertyOptional({ description: 'Gas limit estimate' }) + gasLimit?: number; + + @ApiPropertyOptional({ description: 'Network congestion level (0-100)' }) + networkCongestion?: number; + + @ApiProperty({ description: 'Token used for fee payment' }) + feeToken: string; + + @ApiPropertyOptional({ description: 'Fee token price in USD' }) + feeTokenPriceUsd?: number; + + @ApiPropertyOptional({ description: 'Total fee in USD' }) + totalFeeUsd?: number; + + @ApiProperty({ description: 'Whether this is a fallback estimate', default: false }) + isFallback: boolean; + + @ApiPropertyOptional({ description: 'Reason for fallback if applicable' }) + fallbackReason?: string; + + @ApiPropertyOptional({ description: 'Estimated transaction duration in seconds' }) + estimatedDurationSeconds?: number; + + @ApiProperty({ description: 'Last update timestamp' }) + lastUpdated: Date; + + @ApiProperty({ description: 'Expiration timestamp' }) + expiresAt: Date; + + @ApiProperty({ description: 'Cache TTL in seconds' }) + cacheTtlSeconds: number; +} + +/** + * DTO for batch fee estimates + */ +export class BatchFeeEstimateQueryDto { + @ApiProperty({ description: 'Array of route identifiers', type: [Object] }) + routes: Array<{ + bridgeName: string; + sourceChain: string; + destinationChain: string; + token?: string; + amount?: number; + }>; + + @ApiPropertyOptional({ description: 'Include USD estimates', default: true }) + @IsOptional() + @IsBoolean() + includeUsd?: boolean = true; +} + +/** + * DTO for batch fee estimate response + */ +export class BatchFeeEstimateResponseDto { + @ApiProperty({ description: 'Fee estimates for each route', type: [FeeEstimateDto] }) + estimates: FeeEstimateDto[]; + + @ApiProperty({ description: 'Number of successful estimates' }) + successful: number; + + @ApiProperty({ description: 'Number of fallback estimates' }) + fallbacks: number; + + @ApiProperty({ description: 'Response generation timestamp' }) + generatedAt: Date; +} + +/** + * DTO for gas price response + */ +export class GasPriceDto { + @ApiProperty({ description: 'Chain name' }) + chain: string; + + @ApiProperty({ description: 'Gas price in Gwei' }) + gasPriceGwei: number; + + @ApiPropertyOptional({ description: 'Base fee (EIP-1559)' }) + baseFeeGwei?: number; + + @ApiPropertyOptional({ description: 'Priority fee (EIP-1559)' }) + priorityFeeGwei?: number; + + @ApiPropertyOptional({ description: 'Network congestion level (0-100)' }) + congestionLevel?: number; + + @ApiProperty({ description: 'Recommended gas limit' }) + recommendedGasLimit: number; + + @ApiProperty({ description: 'Last updated timestamp' }) + lastUpdated: Date; + + @ApiProperty({ description: 'Expiration timestamp' }) + expiresAt: Date; +} + +/** + * DTO for fee comparison request + */ +export class FeeComparisonQueryDto { + @ApiProperty({ description: 'Source chain' }) + @IsString() + sourceChain: string; + + @ApiProperty({ description: 'Destination chain' }) + @IsString() + destinationChain: string; + + @ApiPropertyOptional({ description: 'Token symbol' }) + @IsOptional() + @IsString() + token?: string; + + @ApiPropertyOptional({ description: 'Transfer amount' }) + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + amount?: number; + + @ApiPropertyOptional({ description: 'Bridge names to compare' }) + @IsOptional() + bridges?: string[]; +} + +/** + * DTO for fee comparison result + */ +export class FeeComparisonDto { + @ApiProperty({ description: 'Bridge name' }) + bridgeName: string; + + @ApiProperty({ description: 'Total fee' }) + totalFee: number; + + @ApiPropertyOptional({ description: 'Total fee in USD' }) + totalFeeUsd?: number; + + @ApiProperty({ description: 'Fee breakdown' }) + breakdown: { + gasFee: number; + bridgeFee: number; + liquidityFee: number; + protocolFee: number; + }; + + @ApiProperty({ description: 'Whether this is a fallback estimate' }) + isFallback: boolean; + + @ApiProperty({ description: 'Rank by total fee (1 = cheapest)' }) + rank: number; + + @ApiProperty({ description: 'Savings compared to most expensive option' }) + savingsPercent: number; +} + +/** + * DTO for fee comparison response + */ +export class FeeComparisonResponseDto { + @ApiProperty({ description: 'Fee comparisons', type: [FeeComparisonDto] }) + comparisons: FeeComparisonDto[]; + + @ApiProperty({ description: 'Cheapest option' }) + cheapest: FeeComparisonDto; + + @ApiProperty({ description: 'Fastest option (if data available)' }) + fastest?: FeeComparisonDto; + + @ApiProperty({ description: 'Source chain' }) + sourceChain: string; + + @ApiProperty({ description: 'Destination chain' }) + destinationChain: string; + + @ApiProperty({ description: 'Response generation timestamp' }) + generatedAt: Date; +} + +/** + * DTO for network congestion status + */ +export class NetworkCongestionDto { + @ApiProperty({ description: 'Chain name' }) + chain: string; + + @ApiProperty({ description: 'Congestion level (0-100)' }) + congestionLevel: number; + + @ApiProperty({ description: 'Congestion status' }) + status: 'low' | 'moderate' | 'high' | 'severe'; + + @ApiProperty({ description: 'Average gas price in Gwei' }) + averageGasPriceGwei: number; + + @ApiProperty({ description: 'Pending transaction count' }) + pendingTransactions: number; + + @ApiProperty({ description: 'Average block time in seconds' }) + averageBlockTimeSeconds: number; + + @ApiProperty({ description: 'Last updated timestamp' }) + lastUpdated: Date; +} diff --git a/src/fee-estimation/entities/fee-estimate.entity.ts b/src/fee-estimation/entities/fee-estimate.entity.ts new file mode 100644 index 0000000..2dd6d5a --- /dev/null +++ b/src/fee-estimation/entities/fee-estimate.entity.ts @@ -0,0 +1,87 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, +} from 'typeorm'; + +/** + * FeeEstimate Entity + * + * Stores dynamic fee estimates for bridge routes. + * Includes breakdown of gas fees, bridge fees, and liquidity impact. + */ +@Entity('fee_estimates') +@Index(['bridgeName', 'sourceChain', 'destinationChain']) +@Index(['sourceChain', 'lastUpdated']) +export class FeeEstimate { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'bridge_name' }) + bridgeName: string; + + @Column({ name: 'source_chain' }) + sourceChain: string; + + @Column({ name: 'destination_chain' }) + destinationChain: string; + + @Column({ name: 'token', nullable: true }) + token: string | null; + + @Column({ name: 'amount', type: 'decimal', precision: 30, scale: 10, nullable: true }) + amount: number | null; + + @Column({ name: 'total_fee', type: 'decimal', precision: 30, scale: 10 }) + totalFee: number; + + @Column({ name: 'gas_fee', type: 'decimal', precision: 30, scale: 10 }) + gasFee: number; + + @Column({ name: 'bridge_fee', type: 'decimal', precision: 30, scale: 10 }) + bridgeFee: number; + + @Column({ name: 'liquidity_fee', type: 'decimal', precision: 30, scale: 10, default: 0 }) + liquidityFee: number; + + @Column({ name: 'protocol_fee', type: 'decimal', precision: 30, scale: 10, default: 0 }) + protocolFee: number; + + @Column({ name: 'gas_price_gwei', type: 'decimal', precision: 20, scale: 4, nullable: true }) + gasPriceGwei: number | null; + + @Column({ name: 'gas_limit', type: 'bigint', nullable: true }) + gasLimit: number | null; + + @Column({ name: 'network_congestion', type: 'decimal', precision: 5, scale: 2, nullable: true }) + networkCongestion: number | null; + + @Column({ name: 'fee_token' }) + feeToken: string; + + @Column({ name: 'fee_token_price_usd', type: 'decimal', precision: 20, scale: 8, nullable: true }) + feeTokenPriceUsd: number | null; + + @Column({ name: 'total_fee_usd', type: 'decimal', precision: 20, scale: 8, nullable: true }) + totalFeeUsd: number | null; + + @Column({ name: 'is_fallback', default: false }) + isFallback: boolean; + + @Column({ name: 'fallback_reason', nullable: true }) + fallbackReason: string | null; + + @Column({ name: 'estimated_duration_seconds', type: 'int', nullable: true }) + estimatedDurationSeconds: number | null; + + @CreateDateColumn({ name: 'last_updated' }) + lastUpdated: Date; + + @Column({ name: 'expires_at', type: 'timestamptz' }) + expiresAt: Date; + + @Column({ name: 'cache_ttl_seconds', type: 'int', default: 60 }) + cacheTtlSeconds: number; +} diff --git a/src/fee-estimation/types/fee-estimate.types.ts b/src/fee-estimation/types/fee-estimate.types.ts new file mode 100644 index 0000000..4747520 --- /dev/null +++ b/src/fee-estimation/types/fee-estimate.types.ts @@ -0,0 +1,214 @@ +/** + * Dynamic Fee Estimation Types + */ + +/** + * Fee estimate interface + */ +export interface FeeEstimate { + id: string; + bridgeName: string; + sourceChain: string; + destinationChain: string; + token: string | null; + amount: number | null; + totalFee: number; + gasFee: number; + bridgeFee: number; + liquidityFee: number; + protocolFee: number; + gasPriceGwei: number | null; + gasLimit: number | null; + networkCongestion: number | null; + feeToken: string; + feeTokenPriceUsd: number | null; + totalFeeUsd: number | null; + isFallback: boolean; + fallbackReason: string | null; + estimatedDurationSeconds: number | null; + lastUpdated: Date; + expiresAt: Date; + cacheTtlSeconds: number; +} + +/** + * Fee breakdown components + */ +export interface FeeBreakdown { + gasFee: number; + bridgeFee: number; + liquidityFee: number; + protocolFee: number; +} + +/** + * Gas price information + */ +export interface GasPriceInfo { + chain: string; + gasPriceGwei: number; + baseFeeGwei?: number; + priorityFeeGwei?: number; + congestionLevel: number; + recommendedGasLimit: number; + lastUpdated: Date; + expiresAt: Date; +} + +/** + * Network congestion status + */ +export interface NetworkCongestion { + chain: string; + congestionLevel: number; + status: 'low' | 'moderate' | 'high' | 'severe'; + averageGasPriceGwei: number; + pendingTransactions: number; + averageBlockTimeSeconds: number; + lastUpdated: Date; +} + +/** + * Bridge fee configuration + */ +export interface BridgeFeeConfig { + bridgeName: string; + baseFee: number; + percentageFee: number; + minFee: number; + maxFee: number; + supportsDynamicFees: boolean; + feeToken: string; +} + +/** + * Options for useFeeEstimate hook + */ +export interface UseFeeEstimateOptions { + bridgeName: string; + sourceChain: string; + destinationChain: string; + token?: string; + amount?: number; + includeUsd?: boolean; + refreshInterval?: number; + enabled?: boolean; +} + +/** + * Result from useFeeEstimate hook + */ +export interface UseFeeEstimateResult { + estimate: FeeEstimate | null; + loading: boolean; + error: Error | null; + refetch: () => Promise; + isStale: boolean; +} + +/** + * Options for useFeeComparison hook + */ +export interface UseFeeComparisonOptions { + sourceChain: string; + destinationChain: string; + token?: string; + amount?: number; + bridges?: string[]; + enabled?: boolean; +} + +/** + * Result from useFeeComparison hook + */ +export interface UseFeeComparisonResult { + comparisons: FeeComparison[]; + cheapest: FeeComparison | null; + fastest: FeeComparison | null; + loading: boolean; + error: Error | null; + refetch: () => Promise; +} + +/** + * Fee comparison item + */ +export interface FeeComparison { + bridgeName: string; + totalFee: number; + totalFeeUsd?: number; + breakdown: FeeBreakdown; + isFallback: boolean; + rank: number; + savingsPercent: number; +} + +/** + * Options for useGasPrice hook + */ +export interface UseGasPriceOptions { + chain: string; + refreshInterval?: number; + enabled?: boolean; +} + +/** + * Result from useGasPrice hook + */ +export interface UseGasPriceResult { + gasPrice: GasPriceInfo | null; + loading: boolean; + error: Error | null; + refetch: () => Promise; +} + +/** + * Fee estimation strategy + */ +export type FeeEstimationStrategy = 'conservative' | 'average' | 'aggressive'; + +/** + * Fee estimate request + */ +export interface FeeEstimateRequest { + bridgeName: string; + sourceChain: string; + destinationChain: string; + token?: string; + amount?: number; + strategy?: FeeEstimationStrategy; +} + +/** + * Fee cache entry + */ +export interface FeeCacheEntry { + estimate: FeeEstimate; + timestamp: number; + ttl: number; +} + +/** + * Fee estimation error + */ +export interface FeeEstimationError { + code: string; + message: string; + bridgeName?: string; + chain?: string; + fallbackUsed: boolean; +} + +/** + * Liquidity pool information for fee calculation + */ +export interface LiquidityPoolInfo { + poolAddress: string; + tokenA: string; + tokenB: string; + reserveA: number; + reserveB: number; + totalLiquidity: number; + feeTier: number; + priceImpact: number; +} From 535e8b7d628d2e193bed9dc215c45a2b8ff2639a Mon Sep 17 00:00:00 2001 From: nafiuishaaq Date: Thu, 26 Feb 2026 07:35:27 +0100 Subject: [PATCH 2/3] implemented the auth entity --- .../adapters/bridge-fee.adapter.ts | 254 +++++++++++++ .../adapters/gas-price.adapter.ts | 334 ++++++++++++++++++ 2 files changed, 588 insertions(+) create mode 100644 src/fee-estimation/adapters/bridge-fee.adapter.ts create mode 100644 src/fee-estimation/adapters/gas-price.adapter.ts diff --git a/src/fee-estimation/adapters/bridge-fee.adapter.ts b/src/fee-estimation/adapters/bridge-fee.adapter.ts new file mode 100644 index 0000000..0bb8636 --- /dev/null +++ b/src/fee-estimation/adapters/bridge-fee.adapter.ts @@ -0,0 +1,254 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { BridgeFeeConfig } from '../types/fee-estimate.types'; + +/** + * Bridge Fee Adapter + * + * Provides fee configurations and calculations for different bridge protocols. + * Supports both static and dynamic fee structures. + */ +@Injectable() +export class BridgeFeeAdapter { + private readonly logger = new Logger(BridgeFeeAdapter.name); + + // Bridge fee configurations + private readonly bridgeConfigs: Record = { + hop: { + bridgeName: 'hop', + baseFee: 0, + percentageFee: 0.0004, // 0.04% + minFee: 0.0001, + maxFee: 10, + supportsDynamicFees: true, + feeToken: 'ETH', + }, + across: { + bridgeName: 'across', + baseFee: 0, + percentageFee: 0.0006, // 0.06% + minFee: 0.0001, + maxFee: 10, + supportsDynamicFees: true, + feeToken: 'ETH', + }, + stargate: { + bridgeName: 'stargate', + baseFee: 0.0001, + percentageFee: 0.0006, // 0.06% + minFee: 0.0001, + maxFee: 100, + supportsDynamicFees: true, + feeToken: 'ETH', + }, + cctp: { + bridgeName: 'cctp', + baseFee: 0, + percentageFee: 0, + minFee: 0, + maxFee: 0, + supportsDynamicFees: false, + feeToken: 'ETH', + }, + synapse: { + bridgeName: 'synapse', + baseFee: 0, + percentageFee: 0.001, // 0.1% + minFee: 0.0005, + maxFee: 50, + supportsDynamicFees: true, + feeToken: 'ETH', + }, + connext: { + bridgeName: 'connext', + baseFee: 0, + percentageFee: 0.0005, // 0.05% + minFee: 0.0001, + maxFee: 10, + supportsDynamicFees: true, + feeToken: 'ETH', + }, + layerzero: { + bridgeName: 'layerzero', + baseFee: 0.0001, + percentageFee: 0, + minFee: 0.0001, + maxFee: 1, + supportsDynamicFees: true, + feeToken: 'ETH', + }, + axelar: { + bridgeName: 'axelar', + baseFee: 0.0001, + percentageFee: 0.0001, + minFee: 0.0001, + maxFee: 5, + supportsDynamicFees: true, + feeToken: 'ETH', + }, + wormhole: { + bridgeName: 'wormhole', + baseFee: 0, + percentageFee: 0, + minFee: 0, + maxFee: 0, + supportsDynamicFees: false, + feeToken: 'ETH', + }, + }; + + // Chain-specific fee adjustments + private readonly chainAdjustments: Record = { + ethereum: 1.0, + polygon: 0.01, + arbitrum: 0.5, + optimism: 0.5, + base: 0.5, + bsc: 0.3, + avalanche: 0.8, + fantom: 0.2, + gnosis: 0.1, + scroll: 0.5, + linea: 0.5, + zksync: 0.5, + zkevm: 0.5, + }; + + /** + * Get bridge fee configuration + */ + getBridgeConfig(bridgeName: string): BridgeFeeConfig | null { + return this.bridgeConfigs[bridgeName.toLowerCase()] || null; + } + + /** + * Calculate bridge fee + */ + calculateBridgeFee( + bridgeName: string, + amount: number, + sourceChain: string, + ): { bridgeFee: number; protocolFee: number } { + const config = this.getBridgeConfig(bridgeName); + if (!config) { + this.logger.warn(`No fee config for bridge: ${bridgeName}`); + return { bridgeFee: 0, protocolFee: 0 }; + } + + // Apply chain adjustment + const adjustment = this.chainAdjustments[sourceChain.toLowerCase()] || 1.0; + + // Calculate percentage fee + let fee = config.baseFee + amount * config.percentageFee; + + // Apply min/max bounds + fee = Math.max(config.minFee, Math.min(config.maxFee, fee)); + + // Apply chain adjustment + fee *= adjustment; + + // Split into bridge fee and protocol fee (80/20 split) + const bridgeFee = fee * 0.8; + const protocolFee = fee * 0.2; + + return { bridgeFee, protocolFee }; + } + + /** + * Calculate liquidity-based fee + */ + calculateLiquidityFee( + amount: number, + poolLiquidity: number, + feeTier: number = 0.003, // 0.3% default + ): number { + if (poolLiquidity <= 0) return 0; + + // Calculate price impact + const priceImpact = amount / (poolLiquidity + amount); + + // Fee increases with price impact + const impactMultiplier = 1 + priceImpact * 10; + + return amount * feeTier * impactMultiplier; + } + + /** + * Get supported bridges + */ + getSupportedBridges(): string[] { + return Object.keys(this.bridgeConfigs); + } + + /** + * Check if bridge supports dynamic fees + */ + supportsDynamicFees(bridgeName: string): boolean { + const config = this.getBridgeConfig(bridgeName); + return config?.supportsDynamicFees || false; + } + + /** + * Get fee token for bridge + */ + getFeeToken(bridgeName: string): string { + const config = this.getBridgeConfig(bridgeName); + return config?.feeToken || 'ETH'; + } + + /** + * Estimate total bridge cost including all fees + */ + estimateTotalBridgeCost( + bridgeName: string, + amount: number, + sourceChain: string, + poolLiquidity?: number, + ): { + bridgeFee: number; + protocolFee: number; + liquidityFee: number; + totalFee: number; + } { + const { bridgeFee, protocolFee } = this.calculateBridgeFee( + bridgeName, + amount, + sourceChain, + ); + + const liquidityFee = poolLiquidity + ? this.calculateLiquidityFee(amount, poolLiquidity) + : 0; + + return { + bridgeFee, + protocolFee, + liquidityFee, + totalFee: bridgeFee + protocolFee + liquidityFee, + }; + } + + /** + * Update bridge configuration (for dynamic updates) + */ + updateBridgeConfig( + bridgeName: string, + updates: Partial, + ): void { + const normalizedName = bridgeName.toLowerCase(); + if (this.bridgeConfigs[normalizedName]) { + this.bridgeConfigs[normalizedName] = { + ...this.bridgeConfigs[normalizedName], + ...updates, + }; + this.logger.log(`Updated fee config for ${bridgeName}`); + } + } + + /** + * Add new bridge configuration + */ + addBridgeConfig(config: BridgeFeeConfig): void { + this.bridgeConfigs[config.bridgeName.toLowerCase()] = config; + this.logger.log(`Added fee config for ${config.bridgeName}`); + } +} diff --git a/src/fee-estimation/adapters/gas-price.adapter.ts b/src/fee-estimation/adapters/gas-price.adapter.ts new file mode 100644 index 0000000..1bbf21c --- /dev/null +++ b/src/fee-estimation/adapters/gas-price.adapter.ts @@ -0,0 +1,334 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { firstValueFrom } from 'rxjs'; +import { GasPriceInfo, NetworkCongestion } from '../types/fee-estimate.types'; + +/** + * Gas Price Adapter + * + * Fetches real-time gas prices for EVM chains from various sources. + * Supports fallback to static estimates when dynamic fetching fails. + */ +@Injectable() +export class GasPriceAdapter { + private readonly logger = new Logger(GasPriceAdapter.name); + private cache: Map = new Map(); + private readonly CACHE_TTL_MS = 30000; // 30 seconds + + // Fallback gas prices by chain (in Gwei) + private readonly fallbackPrices: Record = { + ethereum: 30, + polygon: 100, + arbitrum: 0.5, + optimism: 0.001, + base: 0.1, + bsc: 5, + avalanche: 25, + fantom: 35, + gnosis: 3, + scroll: 0.5, + linea: 0.5, + zksync: 0.25, + zkevm: 0.5, + }; + + // Recommended gas limits by operation type + private readonly gasLimits: Record = { + bridgeTransfer: 200000, + tokenApproval: 65000, + swap: 150000, + wrap: 50000, + }; + + constructor(private readonly httpService: HttpService) {} + + /** + * Get gas price for a chain + */ + async getGasPrice(chain: string): Promise { + const normalizedChain = chain.toLowerCase(); + + // Check cache + const cached = this.cache.get(normalizedChain); + if (cached && Date.now() - cached.timestamp < this.CACHE_TTL_MS) { + return cached.data; + } + + try { + const gasPrice = await this.fetchGasPrice(normalizedChain); + const congestion = await this.fetchNetworkCongestion(normalizedChain); + + const info: GasPriceInfo = { + chain: normalizedChain, + gasPriceGwei: gasPrice, + congestionLevel: congestion, + recommendedGasLimit: this.gasLimits.bridgeTransfer, + lastUpdated: new Date(), + expiresAt: new Date(Date.now() + this.CACHE_TTL_MS), + }; + + // Cache the result + this.cache.set(normalizedChain, { data: info, timestamp: Date.now() }); + + return info; + } catch (error) { + this.logger.warn(`Failed to fetch gas price for ${chain}: ${error.message}`); + return this.getFallbackGasPrice(normalizedChain); + } + } + + /** + * Get network congestion status + */ + async getNetworkCongestion(chain: string): Promise { + const normalizedChain = chain.toLowerCase(); + + try { + const gasPrice = await this.getGasPrice(normalizedChain); + + // Determine congestion status based on gas price + const basePrice = this.fallbackPrices[normalizedChain] || 10; + const ratio = gasPrice.gasPriceGwei / basePrice; + + let status: 'low' | 'moderate' | 'high' | 'severe'; + if (ratio < 0.5) status = 'low'; + else if (ratio < 1.5) status = 'moderate'; + else if (ratio < 3) status = 'high'; + else status = 'severe'; + + return { + chain: normalizedChain, + congestionLevel: gasPrice.congestionLevel || 50, + status, + averageGasPriceGwei: gasPrice.gasPriceGwei, + pendingTransactions: 0, // Would need additional API + averageBlockTimeSeconds: this.getAverageBlockTime(normalizedChain), + lastUpdated: new Date(), + }; + } catch (error) { + this.logger.warn(`Failed to get congestion for ${chain}: ${error.message}`); + return { + chain: normalizedChain, + congestionLevel: 50, + status: 'moderate', + averageGasPriceGwei: this.fallbackPrices[normalizedChain] || 10, + pendingTransactions: 0, + averageBlockTimeSeconds: this.getAverageBlockTime(normalizedChain), + lastUpdated: new Date(), + }; + } + } + + /** + * Calculate gas fee for a transaction + */ + calculateGasFee( + chain: string, + gasPriceGwei: number, + gasLimit: number = this.gasLimits.bridgeTransfer, + ): number { + // Convert Gwei to native token units + const gasFeeNative = (gasPriceGwei * gasLimit) / 1e9; + return gasFeeNative; + } + + /** + * Fetch gas price from various sources + */ + private async fetchGasPrice(chain: string): Promise { + // Try multiple sources in order + const sources = [ + () => this.fetchFromEtherscan(chain), + () => this.fetchFromBlockNative(chain), + () => this.fetchFromPublicRPC(chain), + ]; + + for (const source of sources) { + try { + const price = await source(); + if (price > 0) return price; + } catch (error) { + this.logger.debug(`Source failed for ${chain}: ${error.message}`); + } + } + + throw new Error(`All gas price sources failed for ${chain}`); + } + + /** + * Fetch from Etherscan-like explorers + */ + private async fetchFromEtherscan(chain: string): Promise { + const apiUrls: Record = { + ethereum: `https://api.etherscan.io/api?module=gastracker&action=gasoracle`, + polygon: `https://api.polygonscan.com/api?module=gastracker&action=gasoracle`, + bsc: `https://api.bscscan.com/api?module=gastracker&action=gasoracle`, + arbitrum: `https://api.arbiscan.io/api?module=gastracker&action=gasoracle`, + optimism: `https://api-optimistic.etherscan.io/api?module=gastracker&action=gasoracle`, + base: `https://api.basescan.org/api?module=gastracker&action=gasoracle`, + avalanche: `https://api.snowtrace.io/api?module=gastracker&action=gasoracle`, + }; + + const apiUrl = apiUrls[chain]; + if (!apiUrl) throw new Error(`No Etherscan API for ${chain}`); + + const response = await firstValueFrom( + this.httpService.get(apiUrl, { timeout: 5000 }), + ); + + if (response.data.status === '1' && response.data.result) { + // Use ProposeGasPrice (medium priority) + return parseFloat(response.data.result.ProposeGasPrice); + } + + throw new Error('Invalid response from Etherscan'); + } + + /** + * Fetch from BlockNative + */ + private async fetchFromBlockNative(chain: string): Promise { + const chainMapping: Record = { + ethereum: 'main', + polygon: 'matic-main', + bsc: 'bsc-main', + arbitrum: 'arbitrum-main', + optimism: 'optimism-main', + base: 'base-main', + }; + + const blockNativeChain = chainMapping[chain]; + if (!blockNativeChain) throw new Error(`No BlockNative mapping for ${chain}`); + + const response = await firstValueFrom( + this.httpService.get( + `https://api.blocknative.com/gasprices/blockprices?chainid=${this.getChainId(chain)}`, + { timeout: 5000 }, + ), + ); + + if (response.data.blockPrices && response.data.blockPrices[0]) { + return response.data.blockPrices[0].estimatedPrices[1].price; // Medium priority + } + + throw new Error('Invalid response from BlockNative'); + } + + /** + * Fetch from public RPC (fallback) + */ + private async fetchFromPublicRPC(chain: string): Promise { + const rpcUrls: Record = { + ethereum: 'https://eth.llamarpc.com', + polygon: 'https://polygon.llamarpc.com', + arbitrum: 'https://arbitrum.llamarpc.com', + optimism: 'https://optimism.llamarpc.com', + base: 'https://base.llamarpc.com', + bsc: 'https://binance.llamarpc.com', + avalanche: 'https://avalanche.llamarpc.com', + }; + + const rpcUrl = rpcUrls[chain]; + if (!rpcUrl) throw new Error(`No RPC URL for ${chain}`); + + const response = await firstValueFrom( + this.httpService.post( + rpcUrl, + { + jsonrpc: '2.0', + method: 'eth_gasPrice', + params: [], + id: 1, + }, + { timeout: 5000 }, + ), + ); + + if (response.data.result) { + const wei = parseInt(response.data.result, 16); + return wei / 1e9; // Convert to Gwei + } + + throw new Error('Invalid RPC response'); + } + + /** + * Fetch network congestion level + */ + private async fetchNetworkCongestion(chain: string): Promise { + try { + // Use pending transaction count as a proxy for congestion + const rpcUrls: Record = { + ethereum: 'https://eth.llamarpc.com', + polygon: 'https://polygon.llamarpc.com', + }; + + const rpcUrl = rpcUrls[chain]; + if (!rpcUrl) return 50; // Default moderate congestion + + // This is a simplified check - real implementation would analyze more metrics + return 50; + } catch { + return 50; + } + } + + /** + * Get fallback gas price + */ + private getFallbackGasPrice(chain: string): GasPriceInfo { + const fallbackPrice = this.fallbackPrices[chain] || 10; + + return { + chain, + gasPriceGwei: fallbackPrice, + congestionLevel: 50, + recommendedGasLimit: this.gasLimits.bridgeTransfer, + lastUpdated: new Date(), + expiresAt: new Date(Date.now() + 60000), // 1 minute expiry for fallback + }; + } + + /** + * Get chain ID + */ + private getChainId(chain: string): number { + const chainIds: Record = { + ethereum: 1, + polygon: 137, + arbitrum: 42161, + optimism: 10, + base: 8453, + bsc: 56, + avalanche: 43114, + fantom: 250, + gnosis: 100, + }; + return chainIds[chain] || 1; + } + + /** + * Get average block time + */ + private getAverageBlockTime(chain: string): number { + const blockTimes: Record = { + ethereum: 12, + polygon: 2.3, + arbitrum: 0.25, + optimism: 2, + base: 2, + bsc: 3, + avalanche: 2, + fantom: 1, + gnosis: 5, + }; + return blockTimes[chain] || 12; + } + + /** + * Clear cache + */ + clearCache(): void { + this.cache.clear(); + } +} From 88ec901f7b1e0d8d99276c1ca734a565e158b476 Mon Sep 17 00:00:00 2001 From: nafiuishaaq Date: Thu, 26 Feb 2026 07:35:55 +0100 Subject: [PATCH 3/3] implemented the auth entity --- src/fee-estimation/fee-estimation.service.ts | 431 +++++++++++++++++++ 1 file changed, 431 insertions(+) create mode 100644 src/fee-estimation/fee-estimation.service.ts diff --git a/src/fee-estimation/fee-estimation.service.ts b/src/fee-estimation/fee-estimation.service.ts new file mode 100644 index 0000000..cc1b5e4 --- /dev/null +++ b/src/fee-estimation/fee-estimation.service.ts @@ -0,0 +1,431 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, LessThan } from 'typeorm'; +import { FeeEstimate } from './entities/fee-estimate.entity'; +import { GasPriceAdapter } from './adapters/gas-price.adapter'; +import { BridgeFeeAdapter } from './adapters/bridge-fee.adapter'; +import { + FeeEstimateDto, + FeeEstimateQueryDto, + BatchFeeEstimateQueryDto, + BatchFeeEstimateResponseDto, + FeeComparisonResponseDto, + FeeComparisonDto, +} from './dto/fee-estimate.dto'; +import { FeeEstimateRequest, FeeCacheEntry } from './types/fee-estimate.types'; + +/** + * Dynamic Fee Estimation Service + * + * Provides real-time fee estimates for bridge routes by combining + * gas prices, bridge fees, and liquidity impacts. + */ +@Injectable() +export class FeeEstimationService { + private readonly logger = new Logger(FeeEstimationService.name); + private cache: Map = new Map(); + private readonly DEFAULT_CACHE_TTL = 60000; // 1 minute + + constructor( + @InjectRepository(FeeEstimate) + private readonly feeEstimateRepository: Repository, + private readonly gasPriceAdapter: GasPriceAdapter, + private readonly bridgeFeeAdapter: BridgeFeeAdapter, + ) {} + + /** + * Get fee estimate for a route + */ + async getFeeEstimate(query: FeeEstimateQueryDto): Promise { + const cacheKey = this.buildCacheKey(query); + + // Check cache + const cached = this.cache.get(cacheKey); + if (cached && Date.now() - cached.timestamp < cached.ttl) { + return this.mapToDto(cached.estimate); + } + + try { + const estimate = await this.calculateFeeEstimate(query); + + // Cache the result + this.cache.set(cacheKey, { + estimate, + timestamp: Date.now(), + ttl: estimate.cacheTtlSeconds * 1000, + }); + + // Store in database for analytics + await this.saveFeeEstimate(estimate); + + return this.mapToDto(estimate); + } catch (error) { + this.logger.error(`Failed to calculate fee estimate: ${error.message}`); + return this.getFallbackEstimate(query, error.message); + } + } + + /** + * Get batch fee estimates + */ + async getBatchFeeEstimates( + query: BatchFeeEstimateQueryDto, + ): Promise { + const estimates: FeeEstimateDto[] = []; + let fallbacks = 0; + + for (const route of query.routes) { + try { + const estimate = await this.getFeeEstimate({ + bridgeName: route.bridgeName, + sourceChain: route.sourceChain, + destinationChain: route.destinationChain, + token: route.token, + amount: route.amount, + includeUsd: query.includeUsd, + }); + + if (estimate.isFallback) { + fallbacks++; + } + + estimates.push(estimate); + } catch (error) { + this.logger.warn(`Failed to get estimate for route: ${error.message}`); + fallbacks++; + estimates.push( + this.getFallbackEstimate( + { + bridgeName: route.bridgeName, + sourceChain: route.sourceChain, + destinationChain: route.destinationChain, + }, + error.message, + ), + ); + } + } + + return { + estimates, + successful: estimates.length - fallbacks, + fallbacks, + generatedAt: new Date(), + }; + } + + /** + * Compare fees across multiple bridges + */ + async compareFees( + sourceChain: string, + destinationChain: string, + token?: string, + amount?: number, + bridges?: string[], + ): Promise { + const bridgeList = bridges || this.bridgeFeeAdapter.getSupportedBridges(); + const comparisons: FeeComparisonDto[] = []; + + for (const bridgeName of bridgeList) { + try { + const estimate = await this.getFeeEstimate({ + bridgeName, + sourceChain, + destinationChain, + token, + amount, + }); + + comparisons.push({ + bridgeName, + totalFee: estimate.totalFee, + totalFeeUsd: estimate.totalFeeUsd, + breakdown: { + gasFee: estimate.gasFee, + bridgeFee: estimate.bridgeFee, + liquidityFee: estimate.liquidityFee, + protocolFee: estimate.protocolFee, + }, + isFallback: estimate.isFallback, + rank: 0, // Will be set after sorting + savingsPercent: 0, // Will be calculated + }); + } catch (error) { + this.logger.warn(`Failed to compare fee for ${bridgeName}: ${error.message}`); + } + } + + // Sort by total fee and assign ranks + comparisons.sort((a, b) => a.totalFee - b.totalFee); + + const cheapest = comparisons[0]; + const mostExpensive = comparisons[comparisons.length - 1]; + + comparisons.forEach((comp, index) => { + comp.rank = index + 1; + if (mostExpensive.totalFee > 0) { + comp.savingsPercent = + ((mostExpensive.totalFee - comp.totalFee) / mostExpensive.totalFee) * 100; + } + }); + + return { + comparisons, + cheapest, + fastest: comparisons.find((c) => !c.isFallback) || cheapest, + sourceChain, + destinationChain, + generatedAt: new Date(), + }; + } + + /** + * Calculate fee estimate + */ + private async calculateFeeEstimate( + query: FeeEstimateQueryDto, + ): Promise { + const { bridgeName, sourceChain, destinationChain, token, amount } = query; + + // Get gas price + const gasPrice = await this.gasPriceAdapter.getGasPrice(sourceChain); + + // Calculate gas fee + const gasFee = this.gasPriceAdapter.calculateGasFee( + sourceChain, + gasPrice.gasPriceGwei, + gasPrice.recommendedGasLimit, + ); + + // Get bridge fees + const bridgeFees = this.bridgeFeeAdapter.estimateTotalBridgeCost( + bridgeName, + amount || 0, + sourceChain, + ); + + // Calculate total fee + const totalFee = gasFee + bridgeFees.totalFee; + + // Get fee token (native token of source chain) + const feeToken = this.getNativeToken(sourceChain); + + // Calculate USD values (simplified - would need price oracle in production) + const feeTokenPriceUsd = await this.getTokenPriceUsd(feeToken); + const totalFeeUsd = feeTokenPriceUsd ? totalFee * feeTokenPriceUsd : undefined; + + // Create estimate entity + const estimate = this.feeEstimateRepository.create({ + bridgeName, + sourceChain, + destinationChain, + token: token || null, + amount: amount || null, + totalFee, + gasFee, + bridgeFee: bridgeFees.bridgeFee, + liquidityFee: bridgeFees.liquidityFee, + protocolFee: bridgeFees.protocolFee, + gasPriceGwei: gasPrice.gasPriceGwei, + gasLimit: gasPrice.recommendedGasLimit, + networkCongestion: gasPrice.congestionLevel, + feeToken, + feeTokenPriceUsd: feeTokenPriceUsd || null, + totalFeeUsd: totalFeeUsd || null, + isFallback: false, + fallbackReason: null, + estimatedDurationSeconds: this.estimateDuration(bridgeName, sourceChain, destinationChain), + expiresAt: gasPrice.expiresAt, + cacheTtlSeconds: Math.floor((gasPrice.expiresAt.getTime() - Date.now()) / 1000), + }); + + return estimate; + } + + /** + * Get fallback estimate when dynamic fetching fails + */ + private getFallbackEstimate( + query: Partial, + reason: string, + ): FeeEstimateDto { + const fallbackGasFee = 0.001; // Conservative fallback + const fallbackBridgeFee = 0.0001; + + const feeToken = query.sourceChain + ? this.getNativeToken(query.sourceChain) + : 'ETH'; + + return { + bridgeName: query.bridgeName || 'unknown', + sourceChain: query.sourceChain || 'unknown', + destinationChain: query.destinationChain || 'unknown', + token: query.token, + amount: query.amount, + totalFee: fallbackGasFee + fallbackBridgeFee, + gasFee: fallbackGasFee, + bridgeFee: fallbackBridgeFee, + liquidityFee: 0, + protocolFee: 0, + feeToken, + isFallback: true, + fallbackReason: reason, + lastUpdated: new Date(), + expiresAt: new Date(Date.now() + 300000), // 5 minutes + cacheTtlSeconds: 60, + }; + } + + /** + * Save fee estimate to database + */ + private async saveFeeEstimate(estimate: FeeEstimate): Promise { + try { + await this.feeEstimateRepository.save(estimate); + } catch (error) { + this.logger.warn(`Failed to save fee estimate: ${error.message}`); + } + } + + /** + * Get native token for chain + */ + private getNativeToken(chain: string): string { + const nativeTokens: Record = { + ethereum: 'ETH', + polygon: 'MATIC', + arbitrum: 'ETH', + optimism: 'ETH', + base: 'ETH', + bsc: 'BNB', + avalanche: 'AVAX', + fantom: 'FTM', + gnosis: 'xDAI', + scroll: 'ETH', + linea: 'ETH', + zksync: 'ETH', + zkevm: 'ETH', + }; + return nativeTokens[chain.toLowerCase()] || 'ETH'; + } + + /** + * Get token price in USD (simplified - would use price oracle) + */ + private async getTokenPriceUsd(token: string): Promise { + // Simplified price mapping - in production, use a price oracle + const prices: Record = { + ETH: 3000, + MATIC: 0.8, + BNB: 600, + AVAX: 35, + FTM: 0.6, + xDAI: 1, + }; + return prices[token]; + } + + /** + * Estimate transaction duration + */ + private estimateDuration( + bridgeName: string, + sourceChain: string, + destinationChain: string, + ): number { + // Base durations by bridge (in seconds) + const baseDurations: Record = { + hop: 300, // 5 minutes + across: 120, // 2 minutes + stargate: 600, // 10 minutes + cctp: 1800, // 30 minutes + synapse: 600, + connext: 300, + layerzero: 120, + axelar: 300, + wormhole: 900, + }; + + const baseDuration = baseDurations[bridgeName.toLowerCase()] || 600; + + // Add chain-specific delays + const chainDelays: Record = { + ethereum: 60, + polygon: 30, + arbitrum: 15, + optimism: 15, + }; + + const sourceDelay = chainDelays[sourceChain.toLowerCase()] || 30; + const destDelay = chainDelays[destinationChain.toLowerCase()] || 30; + + return baseDuration + sourceDelay + destDelay; + } + + /** + * Build cache key + */ + private buildCacheKey(query: FeeEstimateQueryDto): string { + return `${query.bridgeName}:${query.sourceChain}:${query.destinationChain}:${query.token || 'none'}:${query.amount || 0}`; + } + + /** + * Map entity to DTO + */ + private mapToDto(estimate: FeeEstimate): FeeEstimateDto { + return { + bridgeName: estimate.bridgeName, + sourceChain: estimate.sourceChain, + destinationChain: estimate.destinationChain, + token: estimate.token || undefined, + amount: estimate.amount || undefined, + totalFee: estimate.totalFee, + gasFee: estimate.gasFee, + bridgeFee: estimate.bridgeFee, + liquidityFee: estimate.liquidityFee, + protocolFee: estimate.protocolFee, + gasPriceGwei: estimate.gasPriceGwei || undefined, + gasLimit: estimate.gasLimit || undefined, + networkCongestion: estimate.networkCongestion || undefined, + feeToken: estimate.feeToken, + feeTokenPriceUsd: estimate.feeTokenPriceUsd || undefined, + totalFeeUsd: estimate.totalFeeUsd || undefined, + isFallback: estimate.isFallback, + fallbackReason: estimate.fallbackReason || undefined, + estimatedDurationSeconds: estimate.estimatedDurationSeconds || undefined, + lastUpdated: estimate.lastUpdated, + expiresAt: estimate.expiresAt, + cacheTtlSeconds: estimate.cacheTtlSeconds, + }; + } + + /** + * Clear expired cache entries + */ + clearExpiredCache(): void { + const now = Date.now(); + for (const [key, entry] of this.cache.entries()) { + if (now - entry.timestamp > entry.ttl) { + this.cache.delete(key); + } + } + } + + /** + * Clear all cache + */ + clearCache(): void { + this.cache.clear(); + } + + /** + * Get cache statistics + */ + getCacheStats(): { size: number; hitRate: number } { + return { + size: this.cache.size, + hitRate: 0, // Would track in production + }; + } +}