Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 68 additions & 41 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { RateLimitingModule } from './modules/rate-limiting/rate-limiting.module
import { PortfolioModule } from './modules/portfolio/portfolio.module';
import { WebhookModule } from './modules/webhooks/webhook.module';
import { TransactionCoordinatorModule } from './modules/transaction-coordinator/transaction-coordinator.module';
import { LiquidityAggregatorModule } from './modules/liquidity-aggregator/liquidity-aggregator.module';

@Module({
imports: [
Expand Down Expand Up @@ -61,6 +62,7 @@ import { TransactionCoordinatorModule } from './modules/transaction-coordinator/
PortfolioModule,
WebhookModule,
TransactionCoordinatorModule,
LiquidityAggregatorModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
30 changes: 30 additions & 0 deletions src/modules/liquidity-aggregator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Liquidity Aggregator

The Liquidity Aggregator module centralizes pool discovery, pricing, route planning, and arbitrage evaluation across the trading stack.

## Responsibilities

- Register and refresh liquidity pools
- Track pool snapshots and route cache state
- Compute prices and estimate price impact for candidate paths
- Build and analyze route graphs and multi-route splits
- Detect arbitrage opportunities and simulate execution outcomes
- Expose the orchestration API through the Nest controller

## Core flow

1. Pools are registered and monitored for health.
2. Price and route data are normalized into a graph model.
3. The module evaluates routes, split paths, and execution simulations.
4. Alerts and arbitrage signals are surfaced through the API layer.

## Main services

- `PoolRegistryService`
- `PriceOracleService`
- `RouteGraphService`
- `PriceImpactService`
- `MultiRouteSplitterService`
- `LiquidityMonitoringService`
- `RouteExecutionSimulatorService`
- `LiquidityAggregatorService`
288 changes: 288 additions & 0 deletions src/modules/liquidity-aggregator/dto/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
import {
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
IsNumber,
IsDateString,
IsArray,
IsUUID,
Min,
Max,
ValidateNested,
} from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { PoolType, PoolStatus } from '../entities/liquidity-pool.entity';

// ─── Pool Registry DTOs ───────────────────────────────────────────────────────

export class RegisterPoolDto {
@ApiProperty()
@IsString()
@IsNotEmpty()
name: string;

@ApiProperty({ enum: PoolType })
@IsEnum(PoolType)
type: PoolType;

@ApiProperty()
@IsString()
@IsNotEmpty()
assetCodeA: string;

@ApiPropertyOptional()
@IsOptional()
@IsString()
assetIssuerA?: string;

@ApiProperty()
@IsString()
@IsNotEmpty()
assetCodeB: string;

@ApiPropertyOptional()
@IsOptional()
@IsString()
assetIssuerB?: string;

@ApiPropertyOptional()
@IsOptional()
@IsString()
onChainAddress?: string;

@ApiPropertyOptional()
@IsOptional()
@IsNumber()
feeRate?: number;

@ApiPropertyOptional()
@IsOptional()
config?: Record<string, any>;
}

export class QueryPoolsDto {
@ApiPropertyOptional({ enum: PoolType })
@IsOptional()
@IsEnum(PoolType)
type?: PoolType;

@ApiPropertyOptional({ enum: PoolStatus })
@IsOptional()
@IsEnum(PoolStatus)
status?: PoolStatus;

@ApiPropertyOptional()
@IsOptional()
@IsString()
assetCode?: string;

@ApiPropertyOptional({ default: 1 })
@IsOptional()
@IsNumber()
@Min(1)
page?: number = 1;

@ApiPropertyOptional({ default: 20 })
@IsOptional()
@IsNumber()
@Min(1)
@Max(100)
limit?: number = 20;
}

export class RefreshPoolDto {
@ApiPropertyOptional({ description: 'Specific pool ID to refresh. Omit to refresh all active pools.' })
@IsOptional()
@IsUUID()
poolId?: string;
}

// ─── Price Oracle DTOs ────────────────────────────────────────────────────────

export class GetPriceDto {
@ApiProperty({ description: 'Token to price (asset code)' })
@IsString()
@IsNotEmpty()
tokenIn: string;

@ApiPropertyOptional({ description: 'Token issuer for non-native assets' })
@IsOptional()
@IsString()
tokenInIssuer?: string;

@ApiProperty({ description: 'Denomination token (asset code)' })
@IsString()
@IsNotEmpty()
tokenOut: string;

@ApiPropertyOptional({ description: 'Token issuer for non-native assets' })
@IsOptional()
@IsString()
tokenOutIssuer?: string;
}

export class GetBatchPricesDto {
@ApiProperty({ type: [GetPriceDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => GetPriceDto)
pairs: GetPriceDto[];
}

// ─── Route Finding DTOs ───────────────────────────────────────────────────────

export class FindRouteDto {
@ApiProperty({ description: 'Token to swap from' })
@IsString()
@IsNotEmpty()
tokenIn: string;

@ApiPropertyOptional()
@IsOptional()
@IsString()
tokenInIssuer?: string;

@ApiProperty({ description: 'Token to receive' })
@IsString()
@IsNotEmpty()
tokenOut: string;

@ApiPropertyOptional()
@IsOptional()
@IsString()
tokenOutIssuer?: string;

@ApiProperty({ description: 'Amount of tokenIn to swap' })
@IsNumber()
@Min(0)
amountIn: number;

@ApiPropertyOptional({ description: 'Maximum number of hops', default: 4 })
@IsOptional()
@IsNumber()
@Min(1)
@Max(6)
maxHops?: number = 4;

@ApiPropertyOptional({ description: 'Maximum acceptable price impact (0-1)', default: 0.05 })
@IsOptional()
@IsNumber()
@Min(0)
@Max(1)
maxPriceImpact?: number = 0.05;
}

export class FindMultiRouteDto extends FindRouteDto {
@ApiPropertyOptional({ description: 'Number of alternative routes to return', default: 3 })
@IsOptional()
@IsNumber()
@Min(1)
@Max(10)
topN?: number = 3;
}

export class SplitRouteDto extends FindRouteDto {
@ApiPropertyOptional({ description: 'Number of splits across pools', default: 3 })
@IsOptional()
@IsNumber()
@Min(2)
@Max(10)
numSplits?: number = 3;
}

// ─── Price Impact DTOs ────────────────────────────────────────────────────────

export class EstimatePriceImpactDto {
@ApiProperty()
@IsString()
@IsNotEmpty()
tokenIn: string;

@ApiPropertyOptional()
@IsOptional()
@IsString()
tokenInIssuer?: string;

@ApiProperty()
@IsString()
@IsNotEmpty()
tokenOut: string;

@ApiPropertyOptional()
@IsOptional()
@IsString()
tokenOutIssuer?: string;

@ApiProperty({ description: 'Input amount to estimate impact for' })
@IsNumber()
@Min(0)
amountIn: number;

@ApiPropertyOptional({ description: 'Specific pool ID. If omitted, estimates across all matching pools.' })
@IsOptional()
@IsUUID()
poolId?: string;
}

// ─── Arbitrage DTOs ───────────────────────────────────────────────────────────

export class QueryArbitrageDto {
@ApiPropertyOptional()
@IsOptional()
@IsString()
asset?: string;

@ApiPropertyOptional({ description: 'Minimum spread % to report', default: 0.1 })
@IsOptional()
@IsNumber()
@Min(0)
minSpreadPercent?: number = 0.1;

@ApiPropertyOptional({ default: 1 })
@IsOptional()
@IsNumber()
@Min(1)
page?: number = 1;

@ApiPropertyOptional({ default: 20 })
@IsOptional()
@IsNumber()
@Min(1)
@Max(100)
limit?: number = 20;
}

// ─── Simulation DTOs ──────────────────────────────────────────────────────────

export class SimulateRouteDto {
@ApiProperty({ description: 'Route pool IDs to simulate (in order)' })
@IsArray()
poolPath: string[];

@ApiProperty({ description: 'Input amount' })
@IsNumber()
@Min(0)
amountIn: number;

@ApiPropertyOptional({ description: 'Expected minimum output. Simulation fails if route delivers less.' })
@IsOptional()
@IsNumber()
@Min(0)
minAmountOut?: number;
}

// ─── Monitoring DTOs ──────────────────────────────────────────────────────────

export class PoolAlertDto {
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
dateFrom?: string;

@ApiPropertyOptional()
@IsOptional()
@IsDateString()
dateTo?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Column, Entity, Index } from 'typeorm';
import { BaseEntity } from '@app/common';

export enum ArbitrageStatus {
DETECTED = 'detected',
SIMULATED = 'simulated',
EXECUTED = 'executed',
EXPIRED = 'expired',
INVALIDATED = 'invalidated',
}

/**
* A detected arbitrage opportunity spanning two or more pools. Records
* the full cycle of detection → simulation → execution/expiry.
*/
@Entity('arbitrage_opportunities')
export class ArbitrageOpportunity extends BaseEntity {
/** The asset pair where arbitrage exists. */
@Index()
@Column({ type: 'varchar' })
baseAsset: string;

@Column({ type: 'varchar' })
quoteAsset: string;

/** Ordered list of pool IDs forming the arbitrage cycle. */
@Column({ type: 'jsonb' })
cyclePools: string[];

/** Theoretical profit in quote asset units. */
@Column({ type: 'numeric', precision: 30, scale: 7 })
estimatedProfit: string;

/** Maximum profitable input size before price convergence erases the edge. */
@Column({ type: 'numeric', precision: 30, scale: 7 })
maxProfitableSize: string;

/** Spread percentage that enables the arb (buy low / sell high across pools). */
@Column({ type: 'numeric', precision: 10, scale: 6 })
spreadPercent: string;

/** Estimated gas cost to execute the full cycle. */
@Column({ type: 'numeric', precision: 20, scale: 0 })
estimatedGasCost: string;

@Column({
type: 'enum',
enum: ArbitrageStatus,
default: ArbitrageStatus.DETECTED,
})
status: ArbitrageStatus;

/** When this opportunity was first detected. */
@Index()
@Column({ type: 'timestamptz' })
detectedAt: Date;

/** TTL — opportunities older than this are pruned. */
@Column({ type: 'timestamptz' })
expiresAt: Date;
}
Loading
Loading