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
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class CreateBridgeReliabilityTables1700000000000 implements MigrationInterface {
name = 'CreateBridgeReliabilityTables1700000000000';

public async up(queryRunner: QueryRunner): Promise<void> {
// ── Enums ────────────────────────────────────────────────────────────────
await queryRunner.query(`
CREATE TYPE "public"."transaction_outcome_enum" AS ENUM(
'SUCCESS', 'FAILED', 'TIMEOUT', 'CANCELLED'
)
`);

await queryRunner.query(`
CREATE TYPE "public"."reliability_tier_enum" AS ENUM(
'HIGH', 'MEDIUM', 'LOW'
)
`);

// ── bridge_transaction_events ─────────────────────────────────────────────
await queryRunner.query(`
CREATE TABLE "bridge_transaction_events" (
"id" UUID NOT NULL DEFAULT uuid_generate_v4(),
"bridgeName" VARCHAR(100) NOT NULL,
"sourceChain" VARCHAR(50) NOT NULL,
"destinationChain" VARCHAR(50) NOT NULL,
"outcome" "public"."transaction_outcome_enum" NOT NULL,
"transactionHash" VARCHAR(255),
"failureReason" TEXT,
"durationMs" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT "PK_bridge_transaction_events" PRIMARY KEY ("id")
)
`);

await queryRunner.query(`
CREATE INDEX "IDX_bte_bridge_name"
ON "bridge_transaction_events" ("bridgeName")
`);

await queryRunner.query(`
CREATE INDEX "IDX_bte_route_created"
ON "bridge_transaction_events" ("bridgeName", "sourceChain", "destinationChain", "createdAt")
`);

// ── bridge_reliability_metrics ────────────────────────────────────────────
await queryRunner.query(`
CREATE TABLE "bridge_reliability_metrics" (
"id" UUID NOT NULL DEFAULT uuid_generate_v4(),
"bridgeName" VARCHAR(100) NOT NULL,
"sourceChain" VARCHAR(50) NOT NULL,
"destinationChain" VARCHAR(50) NOT NULL,
"totalAttempts" INTEGER NOT NULL DEFAULT 0,
"successfulTransfers" INTEGER NOT NULL DEFAULT 0,
"failedTransfers" INTEGER NOT NULL DEFAULT 0,
"timeoutCount" INTEGER NOT NULL DEFAULT 0,
"reliabilityPercent" DECIMAL(5,2) NOT NULL DEFAULT 0,
"reliabilityScore" DECIMAL(5,2) NOT NULL DEFAULT 0,
"reliabilityTier" "public"."reliability_tier_enum" NOT NULL DEFAULT 'LOW',
"windowConfig" JSONB,
"lastComputedAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT "PK_bridge_reliability_metrics" PRIMARY KEY ("id"),
CONSTRAINT "UQ_bridge_route" UNIQUE ("bridgeName", "sourceChain", "destinationChain")
)
`);

await queryRunner.query(`
CREATE INDEX "IDX_brm_bridge_name"
ON "bridge_reliability_metrics" ("bridgeName")
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS "bridge_reliability_metrics"`);
await queryRunner.query(`DROP TABLE IF EXISTS "bridge_transaction_events"`);
await queryRunner.query(`DROP TYPE IF EXISTS "public"."reliability_tier_enum"`);
await queryRunner.query(`DROP TYPE IF EXISTS "public"."transaction_outcome_enum"`);
}
}
56 changes: 56 additions & 0 deletions src/reliability-score/bridge-reliability-metric.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
UpdateDateColumn,
Index,
Unique,
} from 'typeorm';
import { ReliabilityTier } from '../enums/reliability.enum';

@Entity('bridge_reliability_metrics')
@Unique(['bridgeName', 'sourceChain', 'destinationChain'])
export class BridgeReliabilityMetric {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ length: 100 })
@Index()
bridgeName: string;

@Column({ length: 50 })
sourceChain: string;

@Column({ length: 50 })
destinationChain: string;

@Column({ type: 'int', default: 0 })
totalAttempts: number;

@Column({ type: 'int', default: 0 })
successfulTransfers: number;

@Column({ type: 'int', default: 0 })
failedTransfers: number;

@Column({ type: 'int', default: 0 })
timeoutCount: number;

@Column({ type: 'decimal', precision: 5, scale: 2, default: 0 })
reliabilityPercent: number;

@Column({ type: 'decimal', precision: 5, scale: 2, default: 0 })
reliabilityScore: number;

@Column({ type: 'enum', enum: ReliabilityTier, default: ReliabilityTier.LOW })
reliabilityTier: ReliabilityTier;

@Column({ type: 'jsonb', nullable: true })
windowConfig: {
mode: string;
size: number;
} | null;

@UpdateDateColumn()
lastComputedAt: Date;
}
83 changes: 83 additions & 0 deletions src/reliability-score/bridge-reliability.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Query,
} from '@nestjs/common';
import {
ApiBody,
ApiOkResponse,
ApiOperation,
ApiTags,
} from '@nestjs/swagger';
import { BridgeReliabilityService } from './bridge-reliability.service';
import {
BridgeReliabilityResponseDto,
GetReliabilityDto,
RecordBridgeEventDto,
ReliabilityRankingFactorDto,
} from './dto/reliability.dto';
import { BridgeReliabilityMetric } from './entities/bridge-reliability-metric.entity';

@ApiTags('Bridge Reliability')
@Controller('bridge-reliability')
export class BridgeReliabilityController {
constructor(private readonly reliabilityService: BridgeReliabilityService) {}

/**
* Record a bridge transaction outcome (called internally by bridge adapters).
*/
@Post('events')
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Record a bridge transaction outcome' })
@ApiBody({ type: RecordBridgeEventDto })
async recordEvent(@Body() dto: RecordBridgeEventDto) {
return this.reliabilityService.recordEvent(dto);
}

/**
* Get reliability score for a specific bridge route.
*/
@Get()
@ApiOperation({ summary: 'Get reliability score for a bridge route' })
@ApiOkResponse({ type: BridgeReliabilityResponseDto })
async getReliability(
@Query() dto: GetReliabilityDto,
): Promise<BridgeReliabilityResponseDto> {
return this.reliabilityService.getReliability(dto);
}

/**
* Get all cached reliability metrics (for admin / ranking engine).
*/
@Get('all')
@ApiOperation({ summary: 'List all bridge reliability metrics (admin)' })
async getAllMetrics(): Promise<BridgeReliabilityMetric[]> {
return this.reliabilityService.getAllMetrics();
}

/**
* Get ranking adjustment factors for all bridges on a route.
* Called by Smart Bridge Ranking engine (Issue #5).
*/
@Get('ranking-factors')
@ApiOperation({
summary: 'Get reliability ranking factors for a route',
description: 'Returns reliability-adjusted scores for all bridges on a route.',
})
async getRankingFactors(
@Query('sourceChain') sourceChain: string,
@Query('destinationChain') destinationChain: string,
@Query('threshold') threshold?: number,
@Query('ignoreReliability') ignoreReliability?: boolean,
): Promise<ReliabilityRankingFactorDto[]> {
return this.reliabilityService.getBulkReliabilityFactors(
sourceChain,
destinationChain,
{ threshold, ignoreReliability },
);
}
}
17 changes: 17 additions & 0 deletions src/reliability-score/bridge-reliability.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BridgeTransactionEvent } from './entities/bridge-transaction-event.entity';
import { BridgeReliabilityMetric } from './entities/bridge-reliability-metric.entity';
import { BridgeReliabilityService } from './bridge-reliability.service';
import { BridgeReliabilityController } from './bridge-reliability.controller';
import { ReliabilityCalculatorService } from './reliability-calculator.service';

@Module({
imports: [
TypeOrmModule.forFeature([BridgeTransactionEvent, BridgeReliabilityMetric]),
],
controllers: [BridgeReliabilityController],
providers: [BridgeReliabilityService, ReliabilityCalculatorService],
exports: [BridgeReliabilityService, ReliabilityCalculatorService],
})
export class BridgeReliabilityModule {}
Loading
Loading