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
12 changes: 7 additions & 5 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,11 @@ import { InsuranceTransaction } from './protection/entities/insurance-transactio
import { LiquidationEvent } from './protection/entities/liquidation-event.entity';
import { ProtectionModule } from './protection/protection.module';

// Blockchain Domain — Stellar & Cross-Chain Bridge (issues #381 & #386)
import { BlockchainModule } from './blockchain/blockchain.module';
// Blockchain — Cross-Chain Bridge (issue #386)
import { CrossChainBridgeModule } from './blockchain/cross-chain-bridge.module';
import { CrossChainBridge } from './blockchain/entities/cross-chain-bridge.entity';
import { BlockchainTransaction } from './blockchain/entities/blockchain-transaction.entity';
import { WalletAddress } from './blockchain/entities/wallet-address.entity';
import { CrossChainBridge } from './blockchain/entities/cross-chain-bridge.entity';

@Module({
imports: [
Expand Down Expand Up @@ -167,10 +167,10 @@ import { CrossChainBridge } from './blockchain/entities/cross-chain-bridge.entit
InsuranceFundTier,
InsuranceTransaction,
LiquidationEvent,
// Blockchain — Stellar & Cross-Chain Bridge (issues #381 & #386)
// Blockchain — Cross-Chain Bridge (issue #386)
CrossChainBridge,
BlockchainTransaction,
WalletAddress,
CrossChainBridge,
],
synchronize: configService.get<boolean>('DB_SYNCHRONIZE', true),
logging: configService.get<boolean>('DB_LOGGING', false),
Expand All @@ -192,6 +192,8 @@ import { CrossChainBridge } from './blockchain/entities/cross-chain-bridge.entit
// ── Protection Domain — Insurance Fund (issue #380) ──
ProtectionModule,

// ── Blockchain — Cross-Chain Bridge (issue #386) ──
CrossChainBridgeModule,
// ── Blockchain Domain — Stellar & Cross-Chain Bridge (issues #381 & #386) ──
BlockchainModule,

Expand Down
17 changes: 3 additions & 14 deletions src/blockchain/blockchain.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,11 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BlockchainTransaction } from './entities/blockchain-transaction.entity';
import { WalletAddress } from './entities/wallet-address.entity';
import { CrossChainBridge } from './entities/cross-chain-bridge.entity';
import { StellarService } from './services/stellar.service';
import { EthereumService } from './services/ethereum.service';
import { CrossChainBridgeService } from './services/cross-chain-bridge.service';
import { BlockchainController } from './blockchain.controller';

@Module({
imports: [
TypeOrmModule.forFeature([
BlockchainTransaction,
WalletAddress,
CrossChainBridge,
]),
],
controllers: [BlockchainController],
providers: [StellarService, EthereumService, CrossChainBridgeService],
exports: [StellarService, EthereumService, CrossChainBridgeService],
imports: [TypeOrmModule.forFeature([BlockchainTransaction, WalletAddress])],
providers: [StellarService],
exports: [StellarService],
})
export class BlockchainModule {}
50 changes: 50 additions & 0 deletions src/blockchain/cross-chain-bridge.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Controller, Post, Get, Body, Param, Request, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { CrossChainBridgeService } from './services/cross-chain-bridge.service';
import { BridgeTransferDto } from './dto/bridge.dto';

@ApiTags('cross-chain-bridge')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('bridge')
export class CrossChainBridgeController {
constructor(private readonly bridgeService: CrossChainBridgeService) {}

@Post()
@ApiOperation({ summary: 'Initiate a cross-chain bridge transfer' })
async initiateBridge(@Request() req, @Body() dto: BridgeTransferDto) {
return this.bridgeService.initiateBridge(
req.user.id,
dto.sourceNetwork,
dto.destinationNetwork,
dto.sourceAddress,
dto.destinationAddress,
dto.amount,
);
}

@Post(':id/approve')
@ApiOperation({ summary: 'Add a multi-sig approval to a bridge operation' })
async approveBridge(@Param('id') id: string) {
return this.bridgeService.addApproval(id);
}

@Get('health')
@ApiOperation({ summary: 'Check bridge fund health and reserve status' })
async bridgeHealth() {
return this.bridgeService.getBridgeHealth();
}

@Get('history')
@ApiOperation({ summary: 'Get cross-chain bridge history for current user' })
async bridgeHistory(@Request() req) {
return this.bridgeService.getBridgeHistory(req.user.id);
}

@Get(':id')
@ApiOperation({ summary: 'Get bridge transfer status by ID' })
async getBridge(@Param('id') id: string) {
return this.bridgeService.getBridgeById(id);
}
}
14 changes: 14 additions & 0 deletions src/blockchain/cross-chain-bridge.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CrossChainBridge } from './entities/cross-chain-bridge.entity';
import { CrossChainBridgeService } from './services/cross-chain-bridge.service';
import { CrossChainBridgeController } from './cross-chain-bridge.controller';
import { BlockchainModule } from './blockchain.module';

@Module({
imports: [TypeOrmModule.forFeature([CrossChainBridge]), BlockchainModule],
controllers: [CrossChainBridgeController],
providers: [CrossChainBridgeService],
exports: [CrossChainBridgeService],
})
export class CrossChainBridgeModule {}
27 changes: 27 additions & 0 deletions src/blockchain/dto/bridge.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { IsString, IsNotEmpty, IsNumberString, IsEnum } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { BlockchainNetwork } from '../entities/blockchain-transaction.entity';

export class BridgeTransferDto {
@ApiProperty({ enum: BlockchainNetwork })
@IsEnum(BlockchainNetwork)
sourceNetwork: BlockchainNetwork;

@ApiProperty({ enum: BlockchainNetwork })
@IsEnum(BlockchainNetwork)
destinationNetwork: BlockchainNetwork;

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

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

@ApiProperty({ example: '50.00' })
@IsNumberString()
amount: string;
}
2 changes: 1 addition & 1 deletion src/blockchain/entities/cross-chain-bridge.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
UpdateDateColumn,
Index,
} from 'typeorm';
import { BlockchainNetwork } from './blockchain-transaction.entity';
import { BlockchainNetwork } from '../../blockchain/entities/blockchain-transaction.entity';

export enum BridgeStatus {
INITIATED = 'initiated',
Expand Down
50 changes: 21 additions & 29 deletions src/blockchain/services/cross-chain-bridge.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,11 @@ import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {
CrossChainBridge,
BridgeStatus,
} from '../entities/cross-chain-bridge.entity';
import {
BlockchainNetwork,
} from '../entities/blockchain-transaction.entity';
import { CrossChainBridge, BridgeStatus } from '../entities/cross-chain-bridge.entity';
import { BlockchainNetwork } from '../entities/blockchain-transaction.entity';
import { BlockchainException } from '../../error/exceptions/blockchain.exception';
import { StellarService } from './stellar.service';

/** Minimum bridge reserve in USDC before an alert is raised */
const BRIDGE_RESERVE_THRESHOLD = 10_000;

@Injectable()
Expand All @@ -29,7 +23,6 @@ export class CrossChainBridgeService {
this.multisigThreshold = this.configService.get<number>('BRIDGE_MULTISIG_THRESHOLD', 2);
}

/** Initiate a cross-chain bridge transfer */
async initiateBridge(
userId: string,
sourceNetwork: BlockchainNetwork,
Expand All @@ -53,13 +46,19 @@ export class CrossChainBridgeService {
return this.bridgeRepo.save(record);
}

/** Add a multi-sig approval; executes the bridge once threshold is reached */
async addApproval(bridgeId: string): Promise<CrossChainBridge> {
const bridge = await this.bridgeRepo.findOne({ where: { id: bridgeId } });
if (!bridge) throw BlockchainException.transactionFailed({ reason: 'Bridge record not found', bridgeId });

if (bridge.status !== BridgeStatus.INITIATED && bridge.status !== BridgeStatus.SOURCE_CONFIRMED) {
throw BlockchainException.transactionFailed({ reason: 'Bridge is not awaiting approvals', bridgeId });
if (!bridge)
throw BlockchainException.transactionFailed({ reason: 'Bridge record not found', bridgeId });

if (
bridge.status !== BridgeStatus.INITIATED &&
bridge.status !== BridgeStatus.SOURCE_CONFIRMED
) {
throw BlockchainException.transactionFailed({
reason: 'Bridge is not awaiting approvals',
bridgeId,
});
}

bridge.multisigApprovals += 1;
Expand All @@ -73,7 +72,6 @@ export class CrossChainBridgeService {
return this.bridgeRepo.save(bridge);
}

/** Execute the bridge once multi-sig threshold is met */
private async executeBridge(bridge: CrossChainBridge): Promise<CrossChainBridge> {
try {
bridge.status = BridgeStatus.DESTINATION_PENDING;
Expand All @@ -88,8 +86,7 @@ export class CrossChainBridgeService {
);
bridge.destinationTxHash = tx.txHash;
} else {
// Ethereum destination: log intent and mark pending
// Actual ETH signing requires a hot-wallet signer configured externally
// Ethereum destination: log intent and mark pending (hot-wallet signer external)
this.logger.log(
`Bridge ${bridge.id}: ETH destination tx queued for ${bridge.destinationAddress}`,
);
Expand All @@ -108,7 +105,6 @@ export class CrossChainBridgeService {
return this.bridgeRepo.save(bridge);
}

/** Auto-refund a failed bridge back to source via Stellar */
private async refundBridge(bridge: CrossChainBridge): Promise<void> {
try {
if (bridge.sourceNetwork === BlockchainNetwork.STELLAR) {
Expand All @@ -127,17 +123,16 @@ export class CrossChainBridgeService {
}
}

/** Returns bridge health: whether reserves are above threshold */
async getBridgeHealth(): Promise<{ healthy: boolean; alertMessage?: string }> {
const pendingBridges = await this.bridgeRepo.count({
where: { status: BridgeStatus.BRIDGE_PROCESSING },
});

const totalLockedResult = await this.bridgeRepo
.createQueryBuilder('b')
.select('COALESCE(SUM(CAST(b.amount AS DECIMAL)), 0)', 'total')
.where('b.status IN (:...statuses)', {
statuses: [BridgeStatus.INITIATED, BridgeStatus.SOURCE_CONFIRMED, BridgeStatus.BRIDGE_PROCESSING],
statuses: [
BridgeStatus.INITIATED,
BridgeStatus.SOURCE_CONFIRMED,
BridgeStatus.BRIDGE_PROCESSING,
],
})
.getRawOne<{ total: string }>();

Expand All @@ -148,7 +143,7 @@ export class CrossChainBridgeService {
healthy,
alertMessage: healthy
? undefined
: `Bridge reserves critically low: ${totalLocked} USDC locked across ${pendingBridges} pending operations`,
: `Bridge reserves critically low: ${totalLocked} USDC locked in pending operations`,
};
}

Expand All @@ -157,9 +152,6 @@ export class CrossChainBridgeService {
}

async getBridgeHistory(userId: string): Promise<CrossChainBridge[]> {
return this.bridgeRepo.find({
where: { userId },
order: { createdAt: 'DESC' },
});
return this.bridgeRepo.find({ where: { userId }, order: { createdAt: 'DESC' } });
}
}
Loading
Loading