diff --git a/libs/bridge-core/dist/api.js b/libs/bridge-core/dist/api.js index 74e9a02..9b7abee 100644 --- a/libs/bridge-core/dist/api.js +++ b/libs/bridge-core/dist/api.js @@ -48,9 +48,8 @@ async function callApi(request) { return { success: false, error: { - isTransient: err.isTransient !== false, // Assume transient unless specified + code: err.code || 'UNKNOWN_ERROR', message: err.message || 'Circuit breaker opened', - details: err, }, }; } @@ -61,10 +60,10 @@ async function callApi(request) { */ async function mockApiCall(request) { console.log(`Calling API for provider: ${request.provider.name}`); - if (request.provider.name === 'Stellar') { + if (request.provider.name === 'stellar') { // Consistently fail for Stellar to test circuit breaker const err = new Error('Transient failure'); - err.isTransient = true; + err.code = 'TRANSIENT_ERROR'; throw err; } // LayerZero will have random failures diff --git a/libs/bridge-core/dist/index.d.ts b/libs/bridge-core/dist/index.d.ts index f3b202c..481d113 100644 --- a/libs/bridge-core/dist/index.d.ts +++ b/libs/bridge-core/dist/index.d.ts @@ -1,2 +1,55 @@ +/** + * @bridgewise/bridge-core + * + * Central aggregation logic for multi-chain bridge route discovery. + * Provides a unified interface to query routes from multiple bridge providers + * including Stellar/Soroban, LayerZero, and Hop Protocol. + */ +import { BridgeAggregator } from './aggregator'; +import type { RouteRequest } from './types'; export * from './types'; -export * from './api'; +export type { BridgeAdapter } from './adapters/base'; +export { BaseBridgeAdapter } from './adapters/base'; +export { HopAdapter } from './adapters/hop'; +export { LayerZeroAdapter } from './adapters/layerzero'; +export { StellarAdapter } from './adapters/stellar'; +export * from './fee-estimation'; +export * from './error-codes'; +export { BridgeAggregator } from './aggregator'; +export type { AggregatorConfig } from './aggregator'; +export { BridgeValidator } from './validator'; +export type { ValidationError, ValidationResult, BridgeExecutionRequest, } from './validator'; +/** + * Main function to get aggregated bridge routes + * + * @example + * ```typescript + * import { getBridgeRoutes } from '@bridgewise/bridge-core'; + * + * const routes = await getBridgeRoutes({ + * sourceChain: 'ethereum', + * targetChain: 'polygon', + * assetAmount: '1000000000000000000', // 1 ETH in wei + * slippageTolerance: 0.5 + * }); + * + * console.log(`Found ${routes.routes.length} routes`); + * routes.routes.forEach(route => { + * console.log(`${route.provider}: ${route.feePercentage}% fee, ${route.estimatedTime}s`); + * }); + * ``` + */ +export declare function getBridgeRoutes(request: RouteRequest, config?: { + providers?: { + hop?: boolean; + layerzero?: boolean; + stellar?: boolean; + }; + layerZeroApiKey?: string; + timeout?: number; +}): Promise; +declare const _default: { + BridgeAggregator: typeof BridgeAggregator; + getBridgeRoutes: typeof getBridgeRoutes; +}; +export default _default; diff --git a/libs/bridge-core/dist/index.js b/libs/bridge-core/dist/index.js index 7160f89..d5c5020 100644 --- a/libs/bridge-core/dist/index.js +++ b/libs/bridge-core/dist/index.js @@ -1,4 +1,11 @@ "use strict"; +/** + * @bridgewise/bridge-core + * + * Central aggregation logic for multi-chain bridge route discovery. + * Provides a unified interface to query routes from multiple bridge providers + * including Stellar/Soroban, LayerZero, and Hop Protocol. + */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); @@ -14,5 +21,55 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); +exports.BridgeValidator = exports.BridgeAggregator = exports.StellarAdapter = exports.LayerZeroAdapter = exports.HopAdapter = exports.BaseBridgeAdapter = void 0; +exports.getBridgeRoutes = getBridgeRoutes; +const aggregator_1 = require("./aggregator"); +// Types __exportStar(require("./types"), exports); -__exportStar(require("./api"), exports); +var base_1 = require("./adapters/base"); +Object.defineProperty(exports, "BaseBridgeAdapter", { enumerable: true, get: function () { return base_1.BaseBridgeAdapter; } }); +var hop_1 = require("./adapters/hop"); +Object.defineProperty(exports, "HopAdapter", { enumerable: true, get: function () { return hop_1.HopAdapter; } }); +var layerzero_1 = require("./adapters/layerzero"); +Object.defineProperty(exports, "LayerZeroAdapter", { enumerable: true, get: function () { return layerzero_1.LayerZeroAdapter; } }); +var stellar_1 = require("./adapters/stellar"); +Object.defineProperty(exports, "StellarAdapter", { enumerable: true, get: function () { return stellar_1.StellarAdapter; } }); +// Fee Estimation +__exportStar(require("./fee-estimation"), exports); +// Error Codes and Mapping +__exportStar(require("./error-codes"), exports); +// Aggregator +var aggregator_2 = require("./aggregator"); +Object.defineProperty(exports, "BridgeAggregator", { enumerable: true, get: function () { return aggregator_2.BridgeAggregator; } }); +// Validator +var validator_1 = require("./validator"); +Object.defineProperty(exports, "BridgeValidator", { enumerable: true, get: function () { return validator_1.BridgeValidator; } }); +/** + * Main function to get aggregated bridge routes + * + * @example + * ```typescript + * import { getBridgeRoutes } from '@bridgewise/bridge-core'; + * + * const routes = await getBridgeRoutes({ + * sourceChain: 'ethereum', + * targetChain: 'polygon', + * assetAmount: '1000000000000000000', // 1 ETH in wei + * slippageTolerance: 0.5 + * }); + * + * console.log(`Found ${routes.routes.length} routes`); + * routes.routes.forEach(route => { + * console.log(`${route.provider}: ${route.feePercentage}% fee, ${route.estimatedTime}s`); + * }); + * ``` + */ +async function getBridgeRoutes(request, config) { + const aggregator = new aggregator_1.BridgeAggregator(config); + return aggregator.getRoutes(request); +} +// Default export +exports.default = { + BridgeAggregator: aggregator_1.BridgeAggregator, + getBridgeRoutes, +}; diff --git a/libs/bridge-core/dist/types.d.ts b/libs/bridge-core/dist/types.d.ts index c8e999c..7618126 100644 --- a/libs/bridge-core/dist/types.d.ts +++ b/libs/bridge-core/dist/types.d.ts @@ -1,18 +1,128 @@ -export interface BridgeProvider { - name: string; - apiUrl: string; +/** + * Supported chain identifiers + */ +export type ChainId = 'ethereum' | 'stellar' | 'polygon' | 'arbitrum' | 'optimism' | 'base' | 'gnosis' | 'nova' | 'bsc' | 'avalanche'; +/** + * Bridge provider identifiers + */ +export type BridgeProvider = 'stellar' | 'layerzero' | 'hop'; +/** + * Fee breakdown components + */ +export interface FeeBreakdown { + /** Network fee (in smallest unit) */ + networkFee: string; + /** Bridge protocol fee (in smallest unit) */ + bridgeFee: string; + /** Slippage fee (in smallest unit) */ + slippageFee?: string; } -export interface ApiRequest { +/** + * Unified bridge route response + */ +export interface BridgeRoute { + /** Unique identifier for this route */ + id: string; + /** Bridge provider name */ + provider: BridgeProvider; + /** Source chain identifier */ + sourceChain: ChainId; + /** Target chain identifier */ + targetChain: ChainId; + /** Input amount (in smallest unit, e.g., wei) */ + inputAmount: string; + /** Output amount after fees (in smallest unit) */ + outputAmount: string; + /** Total fees charged (in smallest unit) */ + fee: string; + /** Fee percentage (0-100) */ + feePercentage: number; + /** Estimated time to complete bridge (in seconds) */ + estimatedTime: number; + /** Minimum amount out (for slippage protection) */ + minAmountOut: string; + /** Maximum amount out */ + maxAmountOut: string; + /** Transaction deadline timestamp (Unix epoch in seconds) */ + deadline?: number; + /** Bridge-specific transaction data */ + transactionData?: { + /** Contract address to interact with */ + contractAddress?: string; + /** Encoded calldata */ + calldata?: string; + /** Value to send with transaction */ + value?: string; + /** Gas estimate */ + gasEstimate?: string; + }; + /** Additional metadata */ + metadata?: { + /** Route description */ + description?: string; + /** Risk level (1-5, 1 being safest) */ + riskLevel?: number; + /** Fee breakdown */ + feeBreakdown?: FeeBreakdown; + /** Bridge-specific data */ + [key: string]: unknown; + }; +} +/** + * Request parameters for route discovery + */ +export interface RouteRequest { + /** Source chain identifier */ + sourceChain: ChainId; + /** Target chain identifier */ + targetChain: ChainId; + /** Amount to bridge (in smallest unit, e.g., wei) */ + assetAmount: string; + /** Optional: Token contract address on source chain */ + tokenAddress?: string; + /** Optional: Slippage tolerance (0-100, default: 0.5) */ + slippageTolerance?: number; + /** Optional: Recipient address */ + recipientAddress?: string; +} +/** + * Aggregated routes response + */ +export interface AggregatedRoutes { + /** Array of available routes, sorted by best option first */ + routes: BridgeRoute[]; + /** Timestamp when routes were fetched */ + timestamp: number; + /** Total number of providers queried */ + providersQueried: number; + /** Number of successful responses */ + providersResponded: number; +} +/** + * Error response from a bridge provider + */ +export interface BridgeError { provider: BridgeProvider; - payload: unknown; + error: string; + code?: string; +} +/** + * API request for bridge provider + */ +export interface ApiRequest { + provider: { + name: BridgeProvider; + }; + [key: string]: unknown; } +/** + * API response from bridge provider + */ export interface ApiResponse { success: boolean; data?: unknown; - error?: ApiError; -} -export interface ApiError { - isTransient: boolean; - message: string; - details?: unknown; + error?: { + code: string; + message: string; + }; } diff --git a/libs/bridge-core/src/adapters/mock-rpc.ts b/libs/bridge-core/src/adapters/mock-rpc.ts new file mode 100644 index 0000000..46d2382 --- /dev/null +++ b/libs/bridge-core/src/adapters/mock-rpc.ts @@ -0,0 +1,443 @@ +/** + * Mock Stellar RPC server for integration testing + * Simulates various scenarios and edge cases + */ + +import express, { Express, Request, Response } from 'express'; + +export interface MockRpcConfig { + port?: number; + networkLatency?: number; // in ms + failureRate?: number; // 0-1 percentage + customResponses?: Record; +} + +export class MockStellarRpc { + private app: Express; + private server: any; + private config: Required; + private requestCount = 0; + private failingUntil = 0; // timestamp until which requests should fail + + constructor(config: MockRpcConfig = {}) { + this.config = { + port: config.port || 8545, + networkLatency: config.networkLatency || 100, + failureRate: config.failureRate || 0, + customResponses: config.customResponses || {}, + }; + + this.app = express(); + this.setupRoutes(); + } + + private setupRoutes(): void { + this.app.use(express.json()); + + // Health check endpoint + this.app.get('/health', (req: Request, res: Response) => { + res.json({ status: 'ok', ledger: this.getMockLedgerInfo() }); + }); + + // Main RPC endpoint + this.app.post('/', (req: Request, res: Response) => { + this.handleRpcRequest(req, res); + }); + + // Horizon-compatible endpoints + this.app.get('/ledgers', (req: Request, res: Response) => { + this.handleHorizonLedgers(req, res); + }); + + this.app.get('/accounts/:accountId', (req: Request, res: Response) => { + this.handleHorizonAccount(req, res); + }); + } + + private async handleRpcRequest(req: Request, res: Response): Promise { + this.requestCount++; + const { method, params } = req.body; + + // Simulate network latency + if (this.config.networkLatency > 0) { + await this.delay(this.config.networkLatency); + } + + // Check if we should simulate a failure + if (this.shouldFail()) { + return this.sendError(res, -32603, 'Internal error', { requestId: this.requestCount }); + } + + // Route to specific handler + switch (method) { + case 'getSorobanTransaction': + return this.handleGetSorobanTransaction(res, params); + + case 'submitTransaction': + return this.handleSubmitTransaction(res, params); + + case 'getLatestLedger': + return this.handleGetLatestLedger(res); + + case 'getLedger': + return this.handleGetLedger(res, params); + + case 'getAccount': + return this.handleGetAccount(res, params); + + case 'getContractData': + return this.handleGetContractData(res, params); + + case 'invokeHostFunction': + return this.handleInvokeHostFunction(res, params); + + default: + return this.sendError(res, -32601, 'Method not found', { method }); + } + } + + private handleGetLatestLedger(res: Response): void { + res.json({ + jsonrpc: '2.0', + result: { + id: '4294967296', + pagingToken: '18446744073709551616', + hash: 'a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3', + prevHash: 'b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2', + sequence: 50000000, + closedAt: new Date().toISOString(), + totalCoins: '50000000000.0000000', + baseFeeInStroops: 100, + baseReserveInStroops: 5000000, + maxTxSetSize: 1000, + protocolVersion: 20, + headerXDR: 'AAAAGgAAAO8pFjM0...', + }, + id: 'test-request', + }); + } + + private handleGetLedger(res: Response, params: any): void { + const ledgerId = params?.[0] || 50000000; + + res.json({ + jsonrpc: '2.0', + result: { + id: ledgerId.toString(), + pagingToken: ledgerId.toString(), + hash: 'c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4', + prevHash: 'b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2', + sequence: ledgerId, + closedAt: new Date(Date.now() - 5000).toISOString(), + totalCoins: '50000000000.0000000', + baseFeeInStroops: 100, + baseReserveInStroops: 5000000, + maxTxSetSize: 1000, + protocolVersion: 20, + headerXDR: 'AAAAGgAAAO8pFjM0...', + }, + id: 'test-request', + }); + } + + private handleGetAccount(res: Response, params: any): void { + const accountId = params?.[0] || 'GBRPYHIL2CI3WHZSRXUJOUPJMSUC3SM7DM7V4T5DYKU2QC34EHJQUHOG'; + + // Simulate account not found for specific test addresses + if (accountId.includes('NOTFOUND')) { + return this.sendError(res, -32000, 'Account not found', { accountId }); + } + + res.json({ + jsonrpc: '2.0', + result: { + id: accountId, + accountId, + balances: [ + { + balance: '1000.0000000', + buyingLiabilities: '0.0000000', + sellingLiabilities: '0.0000000', + assetType: 'native', + }, + ], + signers: [ + { + weight: 1, + key: accountId, + type: 'ed25519_public_key', + }, + ], + numSubentries: 0, + inflationDestination: null, + homeDomain: null, + lastModifiedLedger: 50000000, + lastModifiedTime: Math.floor(Date.now() / 1000).toString(), + thresholds: { + lowThreshold: 0, + medThreshold: 0, + highThreshold: 0, + }, + flags: { + authRequired: false, + authRevocable: false, + authImmutable: false, + clawbackEnabled: false, + }, + sequenceNumber: '123456789', + subentryCount: 0, + }, + id: 'test-request', + }); + } + + private handleSubmitTransaction(res: Response, params: any): void { + const txXdr = params?.[0]; + + if (!txXdr) { + return this.sendError(res, -32602, 'Invalid params', { message: 'Transaction XDR required' }); + } + + // Simulate transaction submission + res.json({ + jsonrpc: '2.0', + result: { + hash: 'e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3', + status: 'PENDING', + latestLedger: 50000000, + oldestLedger: 49999900, + }, + id: 'test-request', + }); + } + + private handleGetSorobanTransaction(res: Response, params: any): void { + const hash = params?.[0]; + + if (!hash) { + return this.sendError(res, -32602, 'Invalid params', { message: 'Transaction hash required' }); + } + + res.json({ + jsonrpc: '2.0', + result: { + status: 'SUCCESS', + latestLedger: 50000001, + oldestLedger: 49999900, + resultXdr: + 'AAAAAgAAAABmzWfcQvp/fwNcrvZs0HdWxvLAIDj51MhYnzYY2RQYAAAAZGF0YQAAAAo=', + }, + id: 'test-request', + }); + } + + private handleGetContractData(res: Response, params: any): void { + const contractId = params?.[0]; + + if (!contractId) { + return this.sendError(res, -32602, 'Invalid params', { message: 'Contract ID required' }); + } + + // Simulate contract not found for specific test IDs + if (contractId.includes('NOTFOUND')) { + return this.sendError(res, -32000, 'Contract not found', { contractId }); + } + + res.json({ + jsonrpc: '2.0', + result: { + xdr: 'AAAACgAAAABmzWfcQvp/fwNcrvZs0HdWxvLAIDj51MhYnzYY2RQYAAAAZGF0YQ==', + lastModifiedLedgerSeq: 50000000, + latestLedger: 50000000, + }, + id: 'test-request', + }); + } + + private handleInvokeHostFunction(res: Response, params: any): void { + const functionXdr = params?.[0]; + + if (!functionXdr) { + return this.sendError(res, -32602, 'Invalid params', { message: 'Function XDR required' }); + } + + // Simulate contract invocation + res.json({ + jsonrpc: '2.0', + result: { + transactionHash: + 'f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4', + resultXdr: 'AAAACgAAAABmzWfcQvp/fwNcrvZs0HdWxvLAIDj51MhYnzYY2RQYAAAAZGF0YQ==', + status: 'PENDING', + }, + id: 'test-request', + }); + } + + private handleHorizonLedgers(req: Request, res: Response): void { + res.json({ + _embedded: { + records: [ + { + id: '4294967296', + paging_token: '4294967296', + hash: 'a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3', + prev_hash: 'b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2', + sequence: 50000000, + transaction_count: 100, + operation_count: 500, + closed_at: new Date().toISOString(), + total_coins: '50000000000.0000000', + base_fee_in_stroops: 100, + base_reserve_in_stroops: 5000000, + max_tx_set_size: 1000, + protocol_version: 20, + }, + ], + }, + }); + } + private handleHorizonAccount(req: Request, res: Response): void { + const accountId = req.params.accountId; + + if (accountId.includes('NOTFOUND')) { + res.status(404).json({ + type: 'https://stellar.org/errors/not-found', + title: 'Resource Missing', + status: 404, + detail: 'The resource at the url requested was not found', + }); + return; + } + + res.json({ + id: accountId, + account_id: accountId, + balances: [ + { + balance: '1000.0000000', + buying_liabilities: '0.0000000', + selling_liabilities: '0.0000000', + asset_type: 'native', + }, + ], + signers: [ + { + weight: 1, + key: accountId, + type: 'ed25519_public_key', + }, + ], + num_subentries: 0, + inflation_destination: null, + home_domain: null, + last_modified_ledger: 50000000, + last_modified_time: new Date().toISOString(), + thresholds: { + low_threshold: 0, + med_threshold: 0, + high_threshold: 0, + }, + flags: { + auth_required: false, + auth_revocable: false, + auth_immutable: false, + clawback_enabled: false, + }, + sequence: '123456789', + subentry_count: 0, + }); + } + + private shouldFail(): boolean { + // If failure window is active, fail this request + if (this.failingUntil > Date.now()) { + return true; + } + + // Otherwise, check failure rate + return Math.random() < this.config.failureRate; + } + + /** + * Simulate temporary failures (e.g., network issues) + */ + setFailureWindow(durationMs: number): void { + this.failingUntil = Date.now() + durationMs; + } + + /** + * Reset to healthy state + */ + reset(): void { + this.failingUntil = 0; + this.requestCount = 0; + } + + /** + * Get current request count + */ + getRequestCount(): number { + return this.requestCount; + } + + private sendError(res: Response, code: number, message: string, data?: unknown): void { + res.status(200).json({ + jsonrpc: '2.0', + error: { + code, + message, + data, + }, + id: 'test-request', + }); + } + + private getMockLedgerInfo(): any { + return { + sequence: 50000000, + hash: 'a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3', + closedAt: new Date().toISOString(), + baseFeeInStroops: 100, + }; + } + + private delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + /** + * Start the mock RPC server + */ + async start(): Promise { + return new Promise((resolve) => { + this.server = this.app.listen(this.config.port, () => { + console.log(`[MockStellarRpc] Server running on port ${this.config.port}`); + resolve(); + }); + }); + } + + /** + * Stop the mock RPC server + */ + async stop(): Promise { + return new Promise((resolve, reject) => { + if (this.server) { + this.server.close((err: any) => { + if (err) reject(err); + else resolve(); + }); + } else { + resolve(); + } + }); + } + + /** + * Get the base URL of the mock RPC server + */ + getBaseUrl(): string { + return `http://localhost:${this.config.port}`; + } +} diff --git a/libs/bridge-core/src/adapters/stellar.integration.spec.ts b/libs/bridge-core/src/adapters/stellar.integration.spec.ts new file mode 100644 index 0000000..1bb53e3 --- /dev/null +++ b/libs/bridge-core/src/adapters/stellar.integration.spec.ts @@ -0,0 +1,413 @@ +/** + * Integration tests for Stellar/Soroban bridge adapter + * Tests fee estimation, latency estimation, and error mapping with mock RPC + */ + +import { StellarAdapter } from './stellar'; +import { MockStellarRpc } from './mock-rpc'; +import { BridgeErrorCode, ErrorMapper, STELLAR_ERROR_MAPPING } from '../error-codes'; +import { StellarFees, LatencyEstimation } from '../fee-estimation'; + +describe('StellarAdapter Integration Tests', () => { + let adapter: StellarAdapter; + let mockRpc: MockStellarRpc; + const MOCK_RPC_PORT = 18545; + + beforeAll(async () => { + // Start mock RPC server + mockRpc = new MockStellarRpc({ + port: MOCK_RPC_PORT, + networkLatency: 50, // 50ms simulated latency + failureRate: 0, // No random failures by default + }); + + await mockRpc.start(); + }); + + beforeEach(() => { + // Create adapter pointing to mock RPC + adapter = new StellarAdapter( + `http://localhost:${MOCK_RPC_PORT}`, + `http://localhost:${MOCK_RPC_PORT}`, + 'testnet' + ); + + // Reset mock state + mockRpc.reset(); + }); + + afterAll(async () => { + await mockRpc.stop(); + }); + + describe('Fee Estimation', () => { + it('should calculate accurate fees for Stellar to EVM bridge', () => { + const inputAmount = 1000000000n; // 100 XLM in stroops + const fees = StellarFees.estimateFees(inputAmount, true, 0.5); + + expect(fees).toBeDefined(); + expect(fees.networkFee).toBeGreaterThan(0n); + expect(fees.bridgeFee).toBeGreaterThan(0n); + expect(fees.totalFee).toBeLessThan(inputAmount); + expect(fees.feePercentage).toBeGreaterThan(0); + expect(fees.feePercentage).toBeLessThanOrEqual(100); + }); + + it('should calculate accurate fees for EVM to Stellar bridge', () => { + const inputAmount = 1000000000n; // 1 USDC in smallest units + const fees = StellarFees.estimateFees(inputAmount, false, 0.5); + + expect(fees).toBeDefined(); + expect(fees.networkFee).toBeGreaterThan(0n); + expect(fees.bridgeFee).toBeGreaterThan(0n); + // EVM to Stellar should have slightly higher bridge fee + expect(fees.bridgeFee).toBeGreaterThan( + StellarFees.estimateFees(inputAmount, true, 0.5).bridgeFee + ); + }); + + it('should break down fees into network, bridge, and slippage components', () => { + const inputAmount = 5000000000n; // 500 XLM + const fees = StellarFees.estimateFees(inputAmount, true, 1.0); // 1% slippage + + expect(fees.networkFee).toBeGreaterThan(0n); + expect(fees.bridgeFee).toBeGreaterThan(0n); + expect(fees.slippageFee).toBeGreaterThan(0n); + expect(fees.totalFee).toBe(fees.networkFee + fees.bridgeFee + fees.slippageFee); + }); + + it('should respect slippage tolerance in fee calculations', () => { + const inputAmount = 1000000000n; + + const lowSlippageFees = StellarFees.estimateFees(inputAmount, true, 0.1); + const highSlippageFees = StellarFees.estimateFees(inputAmount, true, 1.0); + + expect(highSlippageFees.slippageFee).toBeGreaterThan(lowSlippageFees.slippageFee); + expect(highSlippageFees.totalFee).toBeGreaterThan(lowSlippageFees.totalFee); + }); + + it('should reject dust amounts', () => { + // Stellar minimum is 1 XLM + const dustAmount = 100n; // Less than 1 XLM + + const isValid = StellarFees.isValidAmount(dustAmount, true); + expect(isValid).toBe(false); + }); + + it('should accept valid amounts', () => { + const validAmount = 10000000n; // 1 XLM + + const isValid = StellarFees.isValidAmount(validAmount, true); + expect(isValid).toBe(true); + }); + + it('should calculate correct minimum amount out with slippage', () => { + const outputAmount = 100000000n; + const slippagePercentage = 0.5; + + const minAmountOut = StellarFees.calculateMinAmountOut(outputAmount, slippagePercentage); + + expect(minAmountOut).toBeLessThan(outputAmount); + expect(minAmountOut).toBeGreaterThan(0n); + + const slippageAmount = outputAmount - minAmountOut; + const expectedSlippage = (outputAmount * BigInt(50)) / 10000n; // 0.5% = 50 basis points + expect(slippageAmount).toBe(expectedSlippage); + }); + }); + + describe('Latency Estimation', () => { + it('should estimate latency for Stellar to Ethereum bridge', () => { + const estimate = LatencyEstimation.estimateLatency('stellar', 'ethereum'); + + expect(estimate).toBeDefined(); + expect(estimate.estimatedSeconds).toBeGreaterThan(0); + expect(estimate.confidence).toBeGreaterThanOrEqual(40); + expect(estimate.confidence).toBeLessThanOrEqual(100); + expect(estimate.breakdown).toBeDefined(); + }); + + it('should estimate latency for Stellar to L2 chain bridge', () => { + const estimateL1 = LatencyEstimation.estimateLatency('stellar', 'ethereum'); + const estimateL2 = LatencyEstimation.estimateLatency('stellar', 'optimism'); + + expect(estimateL2.estimatedSeconds).toBeLessThan(estimateL1.estimatedSeconds); + }); + + it('should account for network load in latency estimation', () => { + const lowLoadEstimate = LatencyEstimation.estimateLatency('stellar', 'ethereum', 0.1); + const highLoadEstimate = LatencyEstimation.estimateLatency('stellar', 'ethereum', 0.9); + + expect(highLoadEstimate.estimatedSeconds).toBeGreaterThan(lowLoadEstimate.estimatedSeconds); + expect(highLoadEstimate.confidence).toBeLessThan(lowLoadEstimate.confidence); + }); + + it('should provide detailed breakdown of latency components', () => { + const estimate = LatencyEstimation.estimateLatency('stellar', 'polygon'); + + expect(estimate.breakdown.networkLatency).toBeGreaterThan(0); + expect(estimate.breakdown.blockTime).toBeGreaterThan(0); + expect(estimate.breakdown.bridgeProcessing).toBeGreaterThan(0); + expect(estimate.breakdown.confirmationTime).toBeGreaterThan(0); + }); + + it('should format latency estimate as human-readable string', () => { + const estimate = LatencyEstimation.estimateLatency('stellar', 'ethereum'); + const formatted = LatencyEstimation.formatEstimate(estimate); + + expect(formatted).toBeDefined(); + expect(formatted).toContain('confidence'); + }); + }); + + describe('Error Mapping', () => { + it('should map RPC timeout errors to standard code', () => { + const errorMapper = new ErrorMapper(STELLAR_ERROR_MAPPING); + const error = new Error('Request timeout'); + + const mapped = errorMapper.mapError(error); + + expect(mapped.code).toBe(BridgeErrorCode.RPC_TIMEOUT); + }); + + it('should map connection refused errors to standard code', () => { + const errorMapper = new ErrorMapper(STELLAR_ERROR_MAPPING); + const error = new Error('ECONNREFUSED: Connection refused'); + + const mapped = errorMapper.mapError(error); + + expect(mapped.code).toBe(BridgeErrorCode.RPC_CONNECTION_FAILED); + }); + + it('should map account not found errors', () => { + const errorMapper = new ErrorMapper(STELLAR_ERROR_MAPPING); + const error = new Error('Account not found on network'); + + const mapped = errorMapper.mapError(error); + + expect(mapped.code).toBe(BridgeErrorCode.ACCOUNT_NOT_FOUND); + }); + + it('should map insufficient balance errors', () => { + const errorMapper = new ErrorMapper(STELLAR_ERROR_MAPPING); + const error = new Error('Insufficient balance for operation'); + + const mapped = errorMapper.mapError(error); + + expect(mapped.code).toBe(BridgeErrorCode.INSUFFICIENT_BALANCE); + }); + + it('should map sequence mismatch errors', () => { + const errorMapper = new ErrorMapper(STELLAR_ERROR_MAPPING); + const error = new Error('tx_bad_seq: Transaction sequence number is too high'); + + const mapped = errorMapper.mapError(error); + + expect(mapped.code).toBe(BridgeErrorCode.ACCOUNT_SEQUENCE_MISMATCH); + }); + + it('should map contract not found errors', () => { + const errorMapper = new ErrorMapper(STELLAR_ERROR_MAPPING); + const error = new Error('ContractNotFound: Contract does not exist'); + + const mapped = errorMapper.mapError(error); + + expect(mapped.code).toBe(BridgeErrorCode.CONTRACT_NOT_FOUND); + }); + + it('should map contract invocation failure errors', () => { + const errorMapper = new ErrorMapper(STELLAR_ERROR_MAPPING); + const error = new Error('InvokeHostFunctionFailed: Contract call failed'); + + const mapped = errorMapper.mapError(error); + + expect(mapped.code).toBe(BridgeErrorCode.CONTRACT_INVOCATION_FAILED); + }); + + it('should map rate limit errors', () => { + const errorMapper = new ErrorMapper(STELLAR_ERROR_MAPPING); + const error = new Error('429: Too many requests'); + + const mapped = errorMapper.mapError(error); + + expect(mapped.code).toBe(BridgeErrorCode.RATE_LIMIT_EXCEEDED); + }); + + it('should map unknown errors to UNKNOWN_ERROR code', () => { + const errorMapper = new ErrorMapper(STELLAR_ERROR_MAPPING); + const error = new Error('Some completely unknown error'); + + const mapped = errorMapper.mapError(error); + + expect(mapped.code).toBe(BridgeErrorCode.UNKNOWN_ERROR); + }); + + it('should handle non-Error objects', () => { + const errorMapper = new ErrorMapper(STELLAR_ERROR_MAPPING); + const error = { message: 'Timeout', code: 'TIMEOUT' }; + + const mapped = errorMapper.mapError(error); + + expect(mapped).toBeDefined(); + expect(mapped.originalError).toBe(error); + }); + + it('should extract original error message', () => { + const errorMapper = new ErrorMapper(STELLAR_ERROR_MAPPING); + const originalMessage = 'RPC timeout after 10s'; + const error = new Error(originalMessage); + + const mapped = errorMapper.mapError(error); + + expect(mapped.details?.originalMessage).toBe(originalMessage); + }); + }); + + describe('Route Fetching', () => { + it('should fetch routes from Stellar to Ethereum', async () => { + const routes = await adapter.fetchRoutes({ + sourceChain: 'stellar', + targetChain: 'ethereum', + assetAmount: '10000000000', // 1000 XLM + }); + + expect(routes.length).toBeGreaterThan(0); + const route = routes[0]; + expect(route.provider).toBe('stellar'); + expect(route.sourceChain).toBe('stellar'); + expect(route.targetChain).toBe('ethereum'); + expect(route.estimatedTime).toBeGreaterThan(0); + }); + + it('should fetch routes from Ethereum to Stellar', async () => { + const routes = await adapter.fetchRoutes({ + sourceChain: 'ethereum', + targetChain: 'stellar', + assetAmount: '1000000000', // 1 USDC + }); + + expect(routes.length).toBeGreaterThan(0); + const route = routes[0]; + expect(route.sourceChain).toBe('ethereum'); + expect(route.targetChain).toBe('stellar'); + }); + + it('should include fee breakdown in route metadata', async () => { + const routes = await adapter.fetchRoutes({ + sourceChain: 'stellar', + targetChain: 'polygon', + assetAmount: '5000000000', // 500 XLM + }); + + expect(routes.length).toBeGreaterThan(0); + const route = routes[0]; + expect(route.metadata?.feeBreakdown).toBeDefined(); + expect(route.metadata?.feeBreakdown?.networkFee).toBeDefined(); + expect(route.metadata?.feeBreakdown?.bridgeFee).toBeDefined(); + }); + + it('should include latency information in route metadata', async () => { + const routes = await adapter.fetchRoutes({ + sourceChain: 'stellar', + targetChain: 'arbitrum', + assetAmount: '1000000000', + }); + + expect(routes.length).toBeGreaterThan(0); + const route = routes[0]; + expect(route.metadata?.latencyConfidence).toBeDefined(); + expect(route.metadata?.latencyBreakdown).toBeDefined(); + }); + + it('should reject dust amounts', async () => { + const routes = await adapter.fetchRoutes({ + sourceChain: 'stellar', + targetChain: 'ethereum', + assetAmount: '100', // Less than 1 XLM + }); + + expect(routes.length).toBe(0); + }); + + it('should apply slippage tolerance to minimum amount out', async () => { + const routes = await adapter.fetchRoutes({ + sourceChain: 'stellar', + targetChain: 'ethereum', + assetAmount: '1000000000', + slippageTolerance: 1.0, // 1% slippage + }); + + expect(routes.length).toBeGreaterThan(0); + const route = routes[0]; + expect(BigInt(route.minAmountOut)).toBeLessThan(BigInt(route.outputAmount)); + }); + }); + + describe('Mock RPC Integration', () => { + it('should simulate network latency', async () => { + const mockRpcWithLatency = new MockStellarRpc({ + port: MOCK_RPC_PORT + 1, + networkLatency: 500, // 500ms latency + }); + + await mockRpcWithLatency.start(); + + const startTime = Date.now(); + const adapterWithLatency = new StellarAdapter( + `http://localhost:${MOCK_RPC_PORT + 1}`, + `http://localhost:${MOCK_RPC_PORT + 1}` + ); + + // This would make an RPC call through the adapter + // For now, we're just testing that the mock is properly configured + expect(startTime).toBeDefined(); + + await mockRpcWithLatency.stop(); + }); + + it('should simulate RPC failures', async () => { + mockRpc.setFailureWindow(1000); // Fail for 1 second + + const routes = await adapter.fetchRoutes({ + sourceChain: 'stellar', + targetChain: 'ethereum', + assetAmount: '1000000000', + }); + + // Should return empty array due to error + expect(Array.isArray(routes)).toBe(true); + + // Reset state + mockRpc.reset(); + }); + + it('should track request count in mock RPC', async () => { + const initialCount = mockRpc.getRequestCount(); + + await adapter.fetchRoutes({ + sourceChain: 'stellar', + targetChain: 'ethereum', + assetAmount: '1000000000', + }); + + const finalCount = mockRpc.getRequestCount(); + expect(finalCount).toBeGreaterThanOrEqual(initialCount); + }); + }); + + describe('Chain Pair Support', () => { + it('should support Stellar to major EVM chains', () => { + const chains = ['ethereum', 'polygon', 'arbitrum', 'optimism', 'base']; + + chains.forEach((chain) => { + expect(adapter.supportsChainPair('stellar', chain)).toBe(true); + expect(adapter.supportsChainPair(chain, 'stellar')).toBe(true); + }); + }); + + it('should not support unsupported chain pairs', () => { + expect(adapter.supportsChainPair('stellar', 'avalanche')).toBe(false); + expect(adapter.supportsChainPair('ethereum', 'polygon')).toBe(false); + }); + }); +}); diff --git a/libs/bridge-core/src/adapters/stellar.ts b/libs/bridge-core/src/adapters/stellar.ts index 5951781..3aeda3f 100644 --- a/libs/bridge-core/src/adapters/stellar.ts +++ b/libs/bridge-core/src/adapters/stellar.ts @@ -1,6 +1,8 @@ import axios, { AxiosInstance } from 'axios'; import { BaseBridgeAdapter } from './base'; import { BridgeRoute, RouteRequest, BridgeProvider, ChainId } from '../types'; +import { ErrorMapper, STELLAR_ERROR_MAPPING, BridgeErrorCode, StandardBridgeError } from '../error-codes'; +import { StellarFees, LatencyEstimation, LatencyEstimate } from '../fee-estimation'; /** * Stellar/Soroban bridge adapter @@ -10,6 +12,7 @@ export class StellarAdapter extends BaseBridgeAdapter { readonly provider: BridgeProvider = 'stellar'; private readonly rpcClient: AxiosInstance; private readonly horizonClient: AxiosInstance; + private readonly errorMapper: ErrorMapper; // Stellar network configuration private readonly network: 'mainnet' | 'testnet'; @@ -21,6 +24,7 @@ export class StellarAdapter extends BaseBridgeAdapter { ) { super(); this.network = network; + this.errorMapper = new ErrorMapper(STELLAR_ERROR_MAPPING); this.rpcClient = axios.create({ baseURL: rpcUrl, @@ -84,27 +88,45 @@ export class StellarAdapter extends BaseBridgeAdapter { */ private async fetchRoutesFromStellar(request: RouteRequest): Promise { try { + // Validate amount + const inputAmount = BigInt(request.assetAmount); + if (!StellarFees.isValidAmount(inputAmount, true)) { + return []; + } + // Query Soroban bridge contract for quote - // In a real implementation, this would call the bridge contract's quote function const bridgeContractAddress = await this.getBridgeContractAddress(request.targetChain); if (!bridgeContractAddress) { return []; } - // Estimate fees and output amount - // Stellar bridges typically have very low fees - const inputAmount = BigInt(request.assetAmount); - - // Stellar fees are typically very low (~0.00001 XLM per operation) - // For cross-chain bridges, estimate ~0.1% fee - const fee = inputAmount / 1000n; - const outputAmount = inputAmount - fee; - + // Estimate fees using accurate Stellar fee model + const feeEstimate = StellarFees.estimateFees( + inputAmount, + true, // isFromStellar + request.slippageTolerance || 0.5 + ); + + const outputAmount = inputAmount - feeEstimate.totalFee; + + // Validate minimum output + if (outputAmount <= 0n) { + return []; + } + + // Estimate latency + const latencyEstimate = LatencyEstimation.estimateLatency('stellar', request.targetChain); + // Get current ledger info for deadline calculation const ledgerInfo = await this.getCurrentLedger(); const deadline = ledgerInfo ? ledgerInfo.closeTime + 300 : undefined; // 5 minutes from now - + + const minAmountOut = StellarFees.calculateMinAmountOut( + outputAmount, + request.slippageTolerance || 0.5 + ); + const route: BridgeRoute = { id: this.generateRouteId(this.provider, request.sourceChain, request.targetChain, 0), provider: this.provider, @@ -112,10 +134,10 @@ export class StellarAdapter extends BaseBridgeAdapter { targetChain: request.targetChain, inputAmount: inputAmount.toString(), outputAmount: outputAmount.toString(), - fee: fee.toString(), - feePercentage: this.calculateFeePercentage(inputAmount.toString(), outputAmount.toString()), - estimatedTime: this.estimateBridgeTime(request.targetChain), - minAmountOut: this.calculateMinAmountOut(outputAmount.toString(), request.slippageTolerance), + fee: feeEstimate.totalFee.toString(), + feePercentage: feeEstimate.feePercentage, + estimatedTime: latencyEstimate.estimatedSeconds, + minAmountOut: minAmountOut.toString(), maxAmountOut: outputAmount.toString(), deadline, transactionData: { @@ -127,12 +149,20 @@ export class StellarAdapter extends BaseBridgeAdapter { riskLevel: 1, // Stellar is considered very safe network: this.network, bridgeContract: bridgeContractAddress, + feeBreakdown: { + networkFee: feeEstimate.networkFee.toString(), + bridgeFee: feeEstimate.bridgeFee.toString(), + slippageFee: feeEstimate.slippageFee.toString(), + }, + latencyConfidence: latencyEstimate.confidence, + latencyBreakdown: latencyEstimate.breakdown, }, }; return [route]; } catch (error) { - console.error(`[StellarAdapter] Error fetching routes from Stellar:`, error); + const mappedError = this.errorMapper.mapError(error); + console.error(`[StellarAdapter] Error fetching routes from Stellar:`, mappedError); return []; } } @@ -142,15 +172,37 @@ export class StellarAdapter extends BaseBridgeAdapter { */ private async fetchRoutesToStellar(request: RouteRequest): Promise { try { + // Validate amount + const inputAmount = BigInt(request.assetAmount); + if (!StellarFees.isValidAmount(inputAmount, false)) { + return []; + } + // For bridging TO Stellar, we need to query the source chain's bridge contract // This is a simplified implementation - in production, you'd query the actual bridge contract - const inputAmount = BigInt(request.assetAmount); - - // Estimate fees (typically 0.1-0.5% for cross-chain bridges) - const fee = inputAmount / 500n; // ~0.2% fee - const outputAmount = inputAmount - fee; - + // Estimate fees using accurate fee model + const feeEstimate = StellarFees.estimateFees( + inputAmount, + false, // isFromStellar (bridging TO Stellar) + request.slippageTolerance || 0.5 + ); + + const outputAmount = inputAmount - feeEstimate.totalFee; + + // Validate minimum output + if (outputAmount <= 0n) { + return []; + } + + // Estimate latency + const latencyEstimate = LatencyEstimation.estimateLatency(request.sourceChain, 'stellar'); + + const minAmountOut = StellarFees.calculateMinAmountOut( + outputAmount, + request.slippageTolerance || 0.5 + ); + const route: BridgeRoute = { id: this.generateRouteId(this.provider, request.sourceChain, request.targetChain, 0), provider: this.provider, @@ -158,10 +210,10 @@ export class StellarAdapter extends BaseBridgeAdapter { targetChain: request.targetChain, inputAmount: inputAmount.toString(), outputAmount: outputAmount.toString(), - fee: fee.toString(), - feePercentage: this.calculateFeePercentage(inputAmount.toString(), outputAmount.toString()), - estimatedTime: this.estimateBridgeTime(request.sourceChain), - minAmountOut: this.calculateMinAmountOut(outputAmount.toString(), request.slippageTolerance), + fee: feeEstimate.totalFee.toString(), + feePercentage: feeEstimate.feePercentage, + estimatedTime: latencyEstimate.estimatedSeconds, + minAmountOut: minAmountOut.toString(), maxAmountOut: outputAmount.toString(), transactionData: { contractAddress: request.tokenAddress, // Bridge contract on source chain @@ -171,12 +223,20 @@ export class StellarAdapter extends BaseBridgeAdapter { description: `Bridge from ${request.sourceChain} to Stellar via Soroban`, riskLevel: 1, network: this.network, + feeBreakdown: { + networkFee: feeEstimate.networkFee.toString(), + bridgeFee: feeEstimate.bridgeFee.toString(), + slippageFee: feeEstimate.slippageFee.toString(), + }, + latencyConfidence: latencyEstimate.confidence, + latencyBreakdown: latencyEstimate.breakdown, }, }; return [route]; } catch (error) { - console.error(`[StellarAdapter] Error fetching routes to Stellar:`, error); + const mappedError = this.errorMapper.mapError(error); + console.error(`[StellarAdapter] Error fetching routes to Stellar:`, mappedError); return []; } } @@ -230,21 +290,24 @@ export class StellarAdapter extends BaseBridgeAdapter { * Estimate bridge time based on target chain */ private estimateBridgeTime(chain: ChainId): number { - // Stellar bridges are typically fast (1-5 minutes) - // L2 chains are faster than L1 - if (chain === 'ethereum') { - return 5 * 60; // 5 minutes for L1 - } - return 2 * 60; // 2 minutes for L2 chains + const latencyEstimate = LatencyEstimation.estimateLatency('stellar', chain); + return latencyEstimate.estimatedSeconds; } - + + /** + * Map Stellar RPC errors to standard error codes + */ + mapError(error: unknown): StandardBridgeError { + return this.errorMapper.mapError(error); + } + /** * Calculate minimum amount out with slippage */ private calculateMinAmountOut(amountOut: string, slippageTolerance?: number): string { - const slippage = slippageTolerance || 0.5; - const amount = BigInt(amountOut); - const slippageAmount = (amount * BigInt(Math.floor(slippage * 100))) / 10000n; - return (amount - slippageAmount).toString(); + return StellarFees.calculateMinAmountOut( + BigInt(amountOut), + slippageTolerance || 0.5 + ).toString(); } } diff --git a/libs/bridge-core/src/api.ts b/libs/bridge-core/src/api.ts index b1c9c25..d3c37e0 100644 --- a/libs/bridge-core/src/api.ts +++ b/libs/bridge-core/src/api.ts @@ -26,7 +26,7 @@ function getBreaker(providerName: string): opossum { breaker.on('open', () => console.log(`[${providerName}] Circuit breaker opened.`)); breaker.on('halfOpen', () => console.log(`[${providerName}] Circuit breaker is half-open.`)); breaker.on('close', () => console.log(`[${providerName}] Circuit breaker closed.`)); - breaker.on('fallback', (result) => console.log(`[${providerName}] Fallback executed with result:`, result)); + breaker.on('fallback', (result: any) => console.log(`[${providerName}] Fallback executed with result:`, result)); breakers.set(providerName, breaker); } @@ -48,9 +48,8 @@ export async function callApi(request: ApiRequest): Promise { return { success: false, error: { - isTransient: err.isTransient !== false, // Assume transient unless specified + code: err.code || 'UNKNOWN_ERROR', message: err.message || 'Circuit breaker opened', - details: err, }, }; } @@ -63,10 +62,10 @@ export async function callApi(request: ApiRequest): Promise { async function mockApiCall(request: ApiRequest): Promise { console.log(`Calling API for provider: ${request.provider.name}`); - if (request.provider.name === 'Stellar') { + if (request.provider.name === 'stellar') { // Consistently fail for Stellar to test circuit breaker const err: any = new Error('Transient failure'); - err.isTransient = true; + err.code = 'TRANSIENT_ERROR'; throw err; } diff --git a/libs/bridge-core/src/error-codes.ts b/libs/bridge-core/src/error-codes.ts new file mode 100644 index 0000000..29e9ed2 --- /dev/null +++ b/libs/bridge-core/src/error-codes.ts @@ -0,0 +1,226 @@ +/** + * Standard backend error codes for bridge operations + * These codes are used consistently across all bridge adapters + */ +export enum BridgeErrorCode { + // Network errors + NETWORK_ERROR = 'NETWORK_ERROR', + RPC_TIMEOUT = 'RPC_TIMEOUT', + RPC_CONNECTION_FAILED = 'RPC_CONNECTION_FAILED', + + // Validation errors + INVALID_CHAIN_PAIR = 'INVALID_CHAIN_PAIR', + INVALID_AMOUNT = 'INVALID_AMOUNT', + INVALID_ADDRESS = 'INVALID_ADDRESS', + INVALID_TOKEN = 'INVALID_TOKEN', + + // Ledger/Account errors + INSUFFICIENT_BALANCE = 'INSUFFICIENT_BALANCE', + ACCOUNT_NOT_FOUND = 'ACCOUNT_NOT_FOUND', + ACCOUNT_SEQUENCE_MISMATCH = 'ACCOUNT_SEQUENCE_MISMATCH', + + // Transaction errors + TRANSACTION_FAILED = 'TRANSACTION_FAILED', + TRANSACTION_REJECTED = 'TRANSACTION_REJECTED', + INSUFFICIENT_GAS = 'INSUFFICIENT_GAS', + DUST_AMOUNT = 'DUST_AMOUNT', + + // Contract errors + CONTRACT_ERROR = 'CONTRACT_ERROR', + CONTRACT_NOT_FOUND = 'CONTRACT_NOT_FOUND', + CONTRACT_INVOCATION_FAILED = 'CONTRACT_INVOCATION_FAILED', + + // Rate limit and quota errors + RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED', + QUOTA_EXCEEDED = 'QUOTA_EXCEEDED', + + // Unknown errors + UNKNOWN_ERROR = 'UNKNOWN_ERROR', +} + +/** + * Detailed error information with standard codes + */ +export interface StandardBridgeError { + code: BridgeErrorCode; + message: string; + details?: Record; + originalError?: unknown; +} + +/** + * Error mapping configuration for a specific provider + */ +export interface ErrorMappingConfig { + errorPatterns: Array<{ + pattern: RegExp | string; + code: BridgeErrorCode; + description: string; + }>; +} + +/** + * Maps Stellar RPC errors to standard backend error codes + */ +export const STELLAR_ERROR_MAPPING: ErrorMappingConfig = { + errorPatterns: [ + // Network errors + { + pattern: /timeout/i, + code: BridgeErrorCode.RPC_TIMEOUT, + description: 'Stellar RPC request timed out', + }, + { + pattern: /ECONNREFUSED|ENOTFOUND|connection.*refused/i, + code: BridgeErrorCode.RPC_CONNECTION_FAILED, + description: 'Unable to connect to Stellar RPC endpoint', + }, + + // Account and sequence errors + { + pattern: /tx_bad_seq|SequenceTooHigh|SequenceTooLow/i, + code: BridgeErrorCode.ACCOUNT_SEQUENCE_MISMATCH, + description: 'Transaction sequence number does not match account state', + }, + { + pattern: /account.*not.*found|Account not found/i, + code: BridgeErrorCode.ACCOUNT_NOT_FOUND, + description: 'Account does not exist on Stellar network', + }, + { + pattern: /insufficient.*balance|Not enough|InsufficientBalance/i, + code: BridgeErrorCode.INSUFFICIENT_BALANCE, + description: 'Account has insufficient funds for transaction', + }, + + // Transaction errors + { + pattern: /tx_failed|TransactionFailed/i, + code: BridgeErrorCode.TRANSACTION_FAILED, + description: 'Transaction failed during execution', + }, + { + pattern: /tx_bad_auth|BadAuth|NotAuthorized/i, + code: BridgeErrorCode.TRANSACTION_REJECTED, + description: 'Transaction was rejected due to authorization failure', + }, + { + pattern: /MissingSignature|tx_missing_operation/i, + code: BridgeErrorCode.TRANSACTION_REJECTED, + description: 'Transaction is missing required signatures', + }, + + // Contract errors + { + pattern: /ContractNotFound|contract.*not.*found/i, + code: BridgeErrorCode.CONTRACT_NOT_FOUND, + description: 'Contract does not exist on network', + }, + { + pattern: /InvokeHostFunctionFailed|contract.*invocation.*failed/i, + code: BridgeErrorCode.CONTRACT_INVOCATION_FAILED, + description: 'Contract function invocation failed', + }, + { + pattern: /ExecutionError|UnknownError|InternalError/i, + code: BridgeErrorCode.CONTRACT_ERROR, + description: 'Contract execution resulted in an error', + }, + + // Validation errors + { + pattern: /invalid.*address|InvalidAddress/i, + code: BridgeErrorCode.INVALID_ADDRESS, + description: 'Provided address is invalid', + }, + { + pattern: /invalid.*amount|InvalidAmount|AmountTooSmall|DustAmount/i, + code: BridgeErrorCode.DUST_AMOUNT, + description: 'Amount is below minimum or invalid', + }, + + // Rate limiting + { + pattern: /rate.*limit|too.*many.*requests|429/i, + code: BridgeErrorCode.RATE_LIMIT_EXCEEDED, + description: 'Rate limit exceeded on RPC endpoint', + }, + ], +}; + +/** + * Error mapper utility for converting provider-specific errors to standard codes + */ +export class ErrorMapper { + private config: ErrorMappingConfig; + + constructor(config: ErrorMappingConfig) { + this.config = config; + } + + /** + * Map an error from a provider to a standard backend error code + */ + mapError(error: unknown): StandardBridgeError { + const errorMessage = this.extractErrorMessage(error); + + // Try to match against configured patterns + for (const pattern of this.config.errorPatterns) { + const regex = + pattern.pattern instanceof RegExp + ? pattern.pattern + : new RegExp(pattern.pattern, 'i'); + + if (regex.test(errorMessage)) { + return { + code: pattern.code, + message: pattern.description, + details: { + originalMessage: errorMessage, + }, + originalError: error, + }; + } + } + + // Default to unknown error + return { + code: BridgeErrorCode.UNKNOWN_ERROR, + message: `An unknown error occurred: ${errorMessage}`, + details: { + originalMessage: errorMessage, + }, + originalError: error, + }; + } + + /** + * Extract error message from various error types + */ + private extractErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + + if (typeof error === 'string') { + return error; + } + + if (error && typeof error === 'object') { + if ('message' in error && typeof error.message === 'string') { + return error.message; + } + if ('response' in error && typeof error.response === 'object') { + const response = error.response as Record; + if ('data' in response && typeof response.data === 'object') { + const data = response.data as Record; + if ('message' in data && typeof data.message === 'string') { + return data.message; + } + } + } + } + + return JSON.stringify(error); + } +} diff --git a/libs/bridge-core/src/fee-estimation.ts b/libs/bridge-core/src/fee-estimation.ts new file mode 100644 index 0000000..604d77f --- /dev/null +++ b/libs/bridge-core/src/fee-estimation.ts @@ -0,0 +1,206 @@ +/** + * Fee calculation and estimation utilities for bridge operations + */ + +export interface FeeEstimate { + /** Network/operation fee (e.g., Stellar base fee) */ + networkFee: bigint; + /** Bridge protocol fee */ + bridgeFee: bigint; + /** Slippage fee */ + slippageFee: bigint; + /** Total fees */ + totalFee: bigint; + /** Fee percentage of input amount */ + feePercentage: number; +} + +export interface LatencyEstimate { + /** Estimated time in seconds */ + estimatedSeconds: number; + /** Confidence level (0-100) */ + confidence: number; + /** Detailed breakdown of latency components */ + breakdown: { + networkLatency: number; + blockTime: number; + bridgeProcessing: number; + confirmationTime: number; + }; +} + +/** + * Stellar-specific fee constants and calculations + */ +export namespace StellarFees { + // Base fee per operation in stroops (1 XLM = 10,000,000 stroops) + export const BASE_OPERATION_FEE = 100n; // stroops + + // Typical transaction size in operations + export const TYPICAL_TX_SIZE = 2n; // 2 operations for a bridge tx + + // Bridge protocol fees (in basis points, 1 bp = 0.01%) + export const STELLAR_TO_EVM_BRIDGE_FEE_BP = 50n; // 0.5% + export const EVM_TO_STELLAR_BRIDGE_FEE_BP = 75n; // 0.75% + + // Minimum amounts to avoid dust issues + export const MIN_STELLAR_AMOUNT = 1n * 10n ** 6n; // 1 XLM in stroops + export const MIN_EVM_AMOUNT = 1n * 10n ** 6n; // 1 USDC/USDT in smallest units + + /** + * Calculate network fee for Stellar transactions + */ + export function calculateNetworkFee(operationCount: bigint = TYPICAL_TX_SIZE): bigint { + return BASE_OPERATION_FEE * operationCount; + } + + /** + * Calculate bridge protocol fee based on direction and amount + */ + export function calculateBridgeFee(amount: bigint, isFromStellar: boolean): bigint { + const feeBp = isFromStellar ? STELLAR_TO_EVM_BRIDGE_FEE_BP : EVM_TO_STELLAR_BRIDGE_FEE_BP; + return (amount * feeBp) / 10000n; + } + + /** + * Calculate slippage fee + */ + export function calculateSlippageFee(amount: bigint, slippagePercentage: number): bigint { + const slippageBp = BigInt(Math.floor(slippagePercentage * 100)); + return (amount * slippageBp) / 10000n; + } + + /** + * Full fee estimation for a bridge transaction + */ + export function estimateFees( + inputAmount: bigint, + isFromStellar: boolean, + slippagePercentage: number = 0.5, + operationCount: bigint = TYPICAL_TX_SIZE + ): FeeEstimate { + const networkFee = calculateNetworkFee(operationCount); + const bridgeFee = calculateBridgeFee(inputAmount, isFromStellar); + const slippageFee = calculateSlippageFee(inputAmount - bridgeFee, slippagePercentage); + const totalFee = networkFee + bridgeFee + slippageFee; + + const feePercentage = inputAmount > 0n + ? Number((totalFee * 10000n) / inputAmount) / 100 + : 0; + + return { + networkFee, + bridgeFee, + slippageFee, + totalFee, + feePercentage: Math.min(100, feePercentage), + }; + } + + /** + * Validate amount is not dust + */ + export function isValidAmount(amount: bigint, isStellarAmount: boolean): boolean { + const minAmount = isStellarAmount ? MIN_STELLAR_AMOUNT : MIN_EVM_AMOUNT; + return amount >= minAmount; + } + + /** + * Calculate minimum output amount with slippage + */ + export function calculateMinAmountOut( + outputAmount: bigint, + slippagePercentage: number + ): bigint { + const slippageBp = BigInt(Math.floor(slippagePercentage * 100)); + const slippageAmount = (outputAmount * slippageBp) / 10000n; + return outputAmount - slippageAmount; + } +} + +/** + * Latency estimation for bridge operations + */ +export namespace LatencyEstimation { + // Baseline latencies in seconds + const STELLAR_NETWORK_LATENCY = 2; // Stellar close time + const EVM_NETWORK_LATENCY_L1 = 12; // Ethereum block time + const EVM_NETWORK_LATENCY_L2 = 2; // Optimistic L2 block time + + // Bridge processing times + const BRIDGE_PROCESSING_BASE = 5; // Base processing time + const CONFIRMATION_TIME_L1 = 60; // 5 blocks for finality + const CONFIRMATION_TIME_L2 = 5; // 1-2 blocks for L2 + + /** + * Get base network latency for a chain + */ + function getNetworkLatency(chain: string): number { + if (chain === 'stellar') return STELLAR_NETWORK_LATENCY; + if (chain === 'ethereum') return EVM_NETWORK_LATENCY_L1; + // Assume L2 for other EVM chains + return EVM_NETWORK_LATENCY_L2; + } + + /** + * Get confirmation time requirement for a chain + */ + function getConfirmationTime(chain: string): number { + if (chain === 'ethereum') return CONFIRMATION_TIME_L1; + return CONFIRMATION_TIME_L2; + } + + /** + * Estimate latency for a bridge route + */ + export function estimateLatency( + sourceChain: string, + targetChain: string, + baseLoad: number = 0.5 // 0-1 scale, network congestion + ): LatencyEstimate { + const sourceLatency = getNetworkLatency(sourceChain); + const targetLatency = getNetworkLatency(targetChain); + const sourceConfirmation = getConfirmationTime(sourceChain); + const targetConfirmation = getConfirmationTime(targetChain); + + // Adjust for network load + const loadFactor = 1 + baseLoad * 0.5; // Up to 50% additional latency under load + + const networkLatency = Math.ceil((sourceLatency + targetLatency) * loadFactor); + const confirmationTime = Math.ceil((sourceConfirmation + targetConfirmation) * loadFactor); + const bridgeProcessing = Math.ceil(BRIDGE_PROCESSING_BASE * loadFactor); + + const estimatedSeconds = networkLatency + confirmationTime + bridgeProcessing; + const confidence = Math.max(40, 95 - Math.floor(baseLoad * 30)); // Confidence decreases with load + + return { + estimatedSeconds, + confidence, + breakdown: { + networkLatency, + blockTime: networkLatency / 2, + bridgeProcessing, + confirmationTime, + }, + }; + } + + /** + * Get human-readable time estimate string + */ + export function formatEstimate(estimate: LatencyEstimate): string { + const { estimatedSeconds, confidence } = estimate; + + if (estimatedSeconds < 60) { + return `${estimatedSeconds}s (${confidence}% confidence)`; + } + + const minutes = Math.ceil(estimatedSeconds / 60); + if (minutes < 60) { + return `~${minutes} min (${confidence}% confidence)`; + } + + const hours = Math.ceil(minutes / 60); + return `~${hours}h (${confidence}% confidence)`; + } +} diff --git a/libs/bridge-core/src/index.ts b/libs/bridge-core/src/index.ts index f1db43c..5619cbb 100644 --- a/libs/bridge-core/src/index.ts +++ b/libs/bridge-core/src/index.ts @@ -6,21 +6,32 @@ * including Stellar/Soroban, LayerZero, and Hop Protocol. */ +import { BridgeAggregator } from './aggregator'; +import type { RouteRequest } from './types'; + // Types export * from './types'; // Adapters -export { BridgeAdapter, BaseBridgeAdapter } from './adapters/base'; +export type { BridgeAdapter } from './adapters/base'; +export { BaseBridgeAdapter } from './adapters/base'; export { HopAdapter } from './adapters/hop'; export { LayerZeroAdapter } from './adapters/layerzero'; export { StellarAdapter } from './adapters/stellar'; +// Fee Estimation +export * from './fee-estimation'; + +// Error Codes and Mapping +export * from './error-codes'; + // Aggregator -export { BridgeAggregator, AggregatorConfig } from './aggregator'; +export { BridgeAggregator } from './aggregator'; +export type { AggregatorConfig } from './aggregator'; // Validator -export { - BridgeValidator, +export { BridgeValidator } from './validator'; +export type { ValidationError, ValidationResult, BridgeExecutionRequest, @@ -47,14 +58,7 @@ export { * ``` */ export async function getBridgeRoutes( - request: { - sourceChain: string; - targetChain: string; - assetAmount: string; - tokenAddress?: string; - slippageTolerance?: number; - recipientAddress?: string; - }, + request: RouteRequest, config?: { providers?: { hop?: boolean; diff --git a/libs/bridge-core/src/opossum.d.ts b/libs/bridge-core/src/opossum.d.ts new file mode 100644 index 0000000..2b5dd59 --- /dev/null +++ b/libs/bridge-core/src/opossum.d.ts @@ -0,0 +1,8 @@ +declare module 'opossum' { + class CircuitBreaker { + constructor(fn: (args: any) => Promise, options?: any); + fire(args: any): Promise; + on(event: string, callback: (result?: any) => void): void; + } + export = CircuitBreaker; +} diff --git a/libs/bridge-core/src/types.ts b/libs/bridge-core/src/types.ts index 4001a4a..35f7eda 100644 --- a/libs/bridge-core/src/types.ts +++ b/libs/bridge-core/src/types.ts @@ -18,6 +18,18 @@ export type ChainId = */ export type BridgeProvider = 'stellar' | 'layerzero' | 'hop'; +/** + * Fee breakdown components + */ +export interface FeeBreakdown { + /** Network fee (in smallest unit) */ + networkFee: string; + /** Bridge protocol fee (in smallest unit) */ + bridgeFee: string; + /** Slippage fee (in smallest unit) */ + slippageFee?: string; +} + /** * Unified bridge route response */ @@ -63,6 +75,8 @@ export interface BridgeRoute { description?: string; /** Risk level (1-5, 1 being safest) */ riskLevel?: number; + /** Fee breakdown */ + feeBreakdown?: FeeBreakdown; /** Bridge-specific data */ [key: string]: unknown; }; @@ -108,3 +122,25 @@ export interface BridgeError { error: string; code?: string; } + +/** + * API request for bridge provider + */ +export interface ApiRequest { + provider: { + name: BridgeProvider; + }; + [key: string]: unknown; +} + +/** + * API response from bridge provider + */ +export interface ApiResponse { + success: boolean; + data?: unknown; + error?: { + code: string; + message: string; + }; +} diff --git a/libs/bridge-core/tsconfig.json b/libs/bridge-core/tsconfig.json index bfef725..71467b8 100644 --- a/libs/bridge-core/tsconfig.json +++ b/libs/bridge-core/tsconfig.json @@ -11,7 +11,10 @@ "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, - "moduleResolution": "node" + "moduleResolution": "node", + "jsx": "react-jsx", + "types": ["node", "jest"], + "isolatedModules": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] diff --git a/package-lock.json b/package-lock.json index efa8f83..d38bc2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,11 @@ "@nestjs/common": "^11.0.1", "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", + "axios": "^1.13.2", + "express": "^5.2.1", + "opossum": "^9.0.0", + "react": "^19.2.3", + "react-dom": "^19.2.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" }, @@ -21,9 +26,11 @@ "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.1", - "@types/express": "^5.0.0", + "@types/express": "^5.0.6", "@types/jest": "^30.0.0", "@types/node": "^22.10.7", + "@types/react": "^19.2.9", + "@types/react-dom": "^19.2.3", "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^8.53.1", "@typescript-eslint/parser": "^8.53.1", @@ -2706,6 +2713,26 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/react": { + "version": "19.2.9", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz", + "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, "node_modules/@types/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", @@ -3740,9 +3767,19 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", @@ -4338,7 +4375,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -4536,6 +4572,13 @@ "node": ">= 8" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -4602,7 +4645,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -4776,7 +4818,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5335,6 +5376,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -5384,7 +5445,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -5401,7 +5461,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -5411,7 +5470,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -5733,7 +5791,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -7544,6 +7601,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opossum": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/opossum/-/opossum-9.0.0.tgz", + "integrity": "sha512-K76U0QkxOfUZamneQuzz+AP0fyfTJcCplZ2oZL93nxeupuJbN4s6uFNbmVCt4eWqqGqRnnowdFuBicJ1fLMVxw==", + "license": "Apache-2.0", + "engines": { + "node": "^24 || ^22 || ^20" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -7942,6 +8008,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -8018,6 +8090,27 @@ "node": ">= 0.10" } }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -8184,6 +8277,12 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, "node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", diff --git a/package.json b/package.json index 760a933..7e4811b 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,11 @@ "@nestjs/common": "^11.0.1", "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", + "axios": "^1.13.2", + "express": "^5.2.1", + "opossum": "^9.0.0", + "react": "^19.2.3", + "react-dom": "^19.2.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" }, @@ -32,9 +37,11 @@ "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.1", - "@types/express": "^5.0.0", + "@types/express": "^5.0.6", "@types/jest": "^30.0.0", "@types/node": "^22.10.7", + "@types/react": "^19.2.9", + "@types/react-dom": "^19.2.3", "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^8.53.1", "@typescript-eslint/parser": "^8.53.1", diff --git a/tsconfig.json b/tsconfig.json index aba29b0..635d3be 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,6 +20,8 @@ "forceConsistentCasingInFileNames": true, "noImplicitAny": false, "strictBindCallApply": false, - "noFallthroughCasesInSwitch": false + "noFallthroughCasesInSwitch": false, + "jsx": "react-jsx", + "types": ["node"] } }