Bridge Adapters
How MNMX integrates with cross-chain bridges and how to add new ones.
Supported Bridges
| Bridge | Chains | Avg. Time | Security Model |
|---|---|---|---|
| Wormhole | 25+ chains including Ethereum, Solana, Arbitrum, Base | 2-15 min | Guardian network (19 validators) |
| deBridge | 15+ chains including Ethereum, Solana, BNB Chain | 1-5 min | Validator network + DLN |
| LayerZero | 30+ chains including Ethereum, Arbitrum, Optimism | 2-10 min | Ultra Light Node + DVN |
| Allbridge | 10+ chains including Ethereum, Solana, BNB Chain | 2-10 min | Multi-sig + liquidity pools |
Adapter Interface
Every bridge adapter implements the BridgeAdapter interface. This abstraction normalizes the vastly different mechanics of each bridge into a uniform contract that the routing engine can consume without knowing bridge-specific details:
1interface BridgeAdapter {2 // Identity3 name: string;4 supportedChains: Chain[];5
6 // Quote a transfer7 getQuote(params: QuoteParams): Promise<BridgeQuote>;8
9 // Execute a quoted transfer10 execute(quote: BridgeQuote, signer: Signer): Promise<BridgeResult>;11
12 // Monitor transfer status13 getStatus(txHash: string): Promise<BridgeStatus>;14
15 // Health check16 getHealth(): Promise<BridgeHealth>;17
18 // Liquidity query19 getLiquidity(chain: string, token: string): Promise<LiquidityInfo>;20
21 // Supported chain pairs22 getSupportedPairs(): Promise<ChainPair[]>;23
24 // Historical performance metrics25 getMetrics(): Promise<BridgeMetrics>;26}27
28interface QuoteParams {29 fromChain: string;30 toChain: string;31 token: string;32 amount: string;33 destinationAddress?: string;34 slippageTolerance?: number;35}36
37interface BridgeQuote {38 bridge: string;39 inputAmount: string;40 outputAmount: string;41 fee: string;42 estimatedTime: number;43 liquidityDepth: string;44 expiresAt: number;45 metadata: Record<string, unknown>; // Bridge-specific data46}47
48interface BridgeHealth {49 online: boolean;50 congestion: 'low' | 'medium' | 'high';51 recentSuccessRate: number;52 medianConfirmTime: number;53 pendingTransfers: number;54 guardianStatus?: string; // Wormhole-specific55 validatorSetHealth?: string; // deBridge-specific56}57
58interface BridgeMetrics {59 totalTransfers24h: number;60 totalVolume24h: string;61 avgConfirmTime: number;62 p95ConfirmTime: number;63 successRate: number;64 failureReasons: { reason: string; count: number }[];65}Wormhole Adapter
Wormhole uses a guardian network of 19 validators to attest cross-chain messages. When a transfer is initiated on the source chain, the guardians observe the transaction, reach consensus, and produce a Verified Action Approval (VAA) — a signed attestation that the message is valid. This VAA is then submitted to the destination chain to release the bridged assets.
Architecture
1Wormhole Transfer Flow:2
3 Source Chain Guardian Network Destination Chain4 ┌───────────┐ ┌──────────────┐ ┌───────────────┐5 │ User calls│ │ 19 Guardians │ │ Token Bridge │6 │ transferTo│──→ Emit Log ──→ │ observe log │ │ Contract │7 │ on Token │ │ sign message │ │ │8 │ Bridge │ │ 13/19 quorum │ │ │9 └───────────┘ └──────┬───────┘ └───────┬───────┘10 │ │11 │ VAA (signed attestation) │12 └──────────────────────────────→│13 │14 Parse VAA, verify15 13/19 signatures,16 mint/release tokens17 │18 ▼19 Tokens in user walletImplementation Details
1class WormholeAdapter implements BridgeAdapter {2 name = 'wormhole';3 supportedChains = [4 'ethereum', 'solana', 'arbitrum', 'base', 'polygon',5 'optimism', 'avalanche', 'bsc', 'fantom', 'celo',6 'moonbeam', 'sui', 'aptos', 'sei', 'injective',7 'terra', 'osmosis', 'near', 'algorand', 'karura',8 'acala', 'klaytn', 'aurora', 'pythnet', 'xpla',9 ];10
11 private wormholeRpc: string;12 private guardianRpc: string;13 private tokenBridgeAddresses: Map<string, string>;14
15 constructor(config: WormholeConfig) {16 this.wormholeRpc = config.wormholeRpc || 'https://wormhole-v2-mainnet-api.certus.one';17 this.guardianRpc = config.guardianRpc || 'https://wormhole-v2-mainnet-api.certus.one';18 this.tokenBridgeAddresses = new Map([19 ['ethereum', '0x3ee18B2214AFF97000D974cf647E7C347E8fa585'],20 ['solana', 'wormDTUJ6AWPNvk59vGQbDvhLYxSc5ogGJ6yR3NB5g'],21 ['arbitrum', '0x0b2402144Bb366A632145ea0e25cF712AE39503C'],22 ['base', '0x8d2de8d2f73F1F4cAB472AC9A881C9b123C79627'],23 // ... additional chains24 ]);25 }26
27 async getQuote(params: QuoteParams): Promise<BridgeQuote> {28 // 1. Look up token attestation on destination chain29 const wrappedAsset = await this.getWrappedAsset(30 params.fromChain, params.toChain, params.token31 );32
33 // 2. Calculate bridge fee (Wormhole charges ~$0.05 relayer fee on most chains)34 const relayerFee = await this.getRelayerFee(params.fromChain, params.toChain);35
36 // 3. Estimate confirmation time based on source chain finality37 const confirmTime = this.getEstimatedConfirmTime(params.fromChain);38
39 // 4. Query current guardian set availability40 const guardianHealth = await this.getGuardianSetStatus();41
42 return {43 bridge: this.name,44 inputAmount: params.amount,45 outputAmount: this.calculateOutput(params.amount, relayerFee),46 fee: relayerFee.toString(),47 estimatedTime: confirmTime,48 liquidityDepth: await this.getLiquidityDepth(params.toChain, wrappedAsset),49 expiresAt: Date.now() + 30_000,50 metadata: {51 wrappedAsset,52 guardianQuorum: guardianHealth.activeGuardians,53 vaaVersion: 1,54 consistencyLevel: this.getConsistencyLevel(params.fromChain),55 },56 };57 }58
59 async execute(quote: BridgeQuote, signer: Signer): Promise<BridgeResult> {60 // Step 1: Initiate transfer on source chain61 const transferTx = await this.initiateTransfer(quote, signer);62
63 // Step 2: Wait for VAA (guardian attestation)64 const vaa = await this.waitForVAA(transferTx.hash, {65 timeout: quote.estimatedTime * 3 * 1000,66 pollInterval: 2_000,67 });68
69 // Step 3: Redeem on destination chain70 const redeemTx = await this.redeemOnDestination(vaa, quote, signer);71
72 return {73 bridge: this.name,74 sourceTxHash: transferTx.hash,75 destinationTxHash: redeemTx.hash,76 actualOutput: redeemTx.amount,77 actualTime: Date.now() - transferTx.timestamp,78 };79 }80
81 private async waitForVAA(82 txHash: string,83 opts: { timeout: number; pollInterval: number }84 ): Promise<VAA> {85 const deadline = Date.now() + opts.timeout;86
87 while (Date.now() < deadline) {88 try {89 const response = await fetch(90 `${this.guardianRpc}/v1/signed_vaa/${txHash}`91 );92 if (response.ok) {93 const data = await response.json();94 return this.parseVAA(data.vaaBytes);95 }96 } catch {97 // Guardian hasn't observed the transaction yet98 }99 await sleep(opts.pollInterval);100 }101
102 throw new BridgeTimeoutError('wormhole', txHash, opts.timeout);103 }104
105 private getEstimatedConfirmTime(chain: string): number {106 // Confirmation time depends on source chain finality107 const confirmTimes: Record<string, number> = {108 ethereum: 780, // ~13 min (32 slots for finality)109 solana: 15, // ~15 seconds (single slot)110 arbitrum: 480, // ~8 min (L2 finality + L1 posting)111 base: 480, // ~8 min (same as Arbitrum)112 polygon: 300, // ~5 min (256 block confirmations)113 avalanche: 10, // ~10 seconds (single round)114 bsc: 45, // ~45 seconds (15 block confirmations)115 };116 return confirmTimes[chain] || 600; // Default 10 minutes117 }118
119 async getHealth(): Promise<BridgeHealth> {120 const guardians = await this.getGuardianSetStatus();121 const recentTransfers = await this.getRecentTransfers(100);122
123 return {124 online: guardians.activeGuardians >= 13, // 13/19 quorum required125 congestion: this.estimateCongestion(recentTransfers),126 recentSuccessRate: recentTransfers.filter(t => t.completed).length / 100,127 medianConfirmTime: this.calculateMedian(128 recentTransfers.map(t => t.confirmTime)129 ),130 pendingTransfers: recentTransfers.filter(t => !t.completed).length,131 guardianStatus: `${guardians.activeGuardians}/19 active`,132 };133 }134}deBridge Adapter
deBridge uses a validator network combined with the DLN (DeBridge Liquidity Network) for fast cross-chain transfers. DLN uses a maker-taker model where professional market makers fill orders on the destination chain before the bridge message is finalized, enabling 1-5 minute transfer times.
Architecture
1deBridge DLN Transfer Flow:2
3 Source Chain DLN Network Destination Chain4 ┌───────────┐ ┌──────────────┐ ┌───────────────┐5 │ User locks│ │ DLN Makers │ │ Maker fills │6 │ tokens in │──→ Order ───→ │ observe order│──→ Fill ──→ │ order from │7 │ DLN Source│ created │ compete to │ │ own liquidity │8 │ Contract │ │ fill fastest │ │ │9 └───────────┘ └──────────────┘ └───────┬───────┘10 │11 ┌────────────────────────────────────────────────────────────────┘12 │ After bridge message confirms, maker claims13 │ locked tokens from source chain14 ▼15 Maker receives locked tokens as compensation16
17 Timeline: User gets tokens in 1-5 min (maker pre-fills)18 Maker claims locked tokens in 5-30 min (bridge finality)Implementation Details
1class DeBridgeAdapter implements BridgeAdapter {2 name = 'debridge';3 supportedChains = [4 'ethereum', 'solana', 'arbitrum', 'bsc', 'polygon',5 'avalanche', 'optimism', 'base', 'fantom', 'linea',6 'gnosis', 'metis', 'neon', 'bitrock', 'core',7 ];8
9 private dlnApiUrl: string;10 private dlnSourceAddresses: Map<string, string>;11 private dlnDestinationAddresses: Map<string, string>;12
13 constructor(config: DeBridgeConfig) {14 this.dlnApiUrl = config.dlnApiUrl || 'https://dln.debridge.finance';15 this.dlnSourceAddresses = new Map([16 ['ethereum', '0xeF4fB24aD0916217251F553c0596F8Edc630EB66'],17 ['solana', 'src5qyZHqTqecJV4aY6Cb6zDZLMDzrDKKezs22MPHr4'],18 ['arbitrum', '0xeF4fB24aD0916217251F553c0596F8Edc630EB66'],19 ['bsc', '0xeF4fB24aD0916217251F553c0596F8Edc630EB66'],20 // ... additional chains21 ]);22 }23
24 async getQuote(params: QuoteParams): Promise<BridgeQuote> {25 // 1. Query DLN for current maker offers26 const dlnOrder = await this.createDlnOrder(params);27
28 // 2. Get best maker offer (competitive market)29 const bestOffer = dlnOrder.bestMakerOffer;30
31 // 3. Calculate effective fee (spread between input and output)32 const fee = BigInt(params.amount) - BigInt(bestOffer.outputAmount);33
34 return {35 bridge: this.name,36 inputAmount: params.amount,37 outputAmount: bestOffer.outputAmount,38 fee: fee.toString(),39 estimatedTime: this.getEstimatedFillTime(params.fromChain, params.toChain),40 liquidityDepth: bestOffer.makerLiquidity,41 expiresAt: Date.now() + 15_000, // DLN quotes expire faster42 metadata: {43 orderId: dlnOrder.orderId,44 makerAddress: bestOffer.maker,45 nonce: dlnOrder.nonce,46 orderType: 'DLN_FILL',47 affiliateFee: dlnOrder.affiliateFee || '0',48 },49 };50 }51
52 async execute(quote: BridgeQuote, signer: Signer): Promise<BridgeResult> {53 const orderId = quote.metadata.orderId as string;54
55 // Step 1: Place DLN order on source chain56 const placeTx = await this.placeDlnOrder(quote, signer);57
58 // Step 2: Wait for maker fill on destination chain59 // DLN makers typically fill within 30-120 seconds60 const fillResult = await this.waitForFill(orderId, {61 timeout: quote.estimatedTime * 3 * 1000,62 pollInterval: 3_000,63 });64
65 return {66 bridge: this.name,67 sourceTxHash: placeTx.hash,68 destinationTxHash: fillResult.fillTxHash,69 actualOutput: fillResult.outputAmount,70 actualTime: Date.now() - placeTx.timestamp,71 };72 }73
74 private async waitForFill(75 orderId: string,76 opts: { timeout: number; pollInterval: number }77 ): Promise<DlnFillResult> {78 const deadline = Date.now() + opts.timeout;79
80 while (Date.now() < deadline) {81 const status = await fetch(82 `${this.dlnApiUrl}/v1.0/dln/order/${orderId}/status`83 ).then(r => r.json());84
85 if (status.state === 'ClaimedUnlock' || status.state === 'Fulfilled') {86 return {87 fillTxHash: status.fulfillTxHash,88 outputAmount: status.fulfillAmount,89 filledBy: status.takerAddress,90 };91 }92
93 if (status.state === 'Cancelled') {94 throw new BridgeExecutionError('debridge', `Order ${orderId} cancelled`);95 }96
97 await sleep(opts.pollInterval);98 }99
100 throw new BridgeTimeoutError('debridge', orderId, opts.timeout);101 }102
103 private getEstimatedFillTime(fromChain: string, toChain: string): number {104 // DLN fill times depend on maker activity and chain pair popularity105 const fastPairs = ['ethereum-solana', 'ethereum-arbitrum', 'ethereum-bsc'];106 const pair = `${fromChain}-${toChain}`;107 if (fastPairs.includes(pair)) return 60; // ~1 min for popular pairs108 return 180; // ~3 min for less popular pairs109 }110
111 async getHealth(): Promise<BridgeHealth> {112 const stats = await fetch(`${this.dlnApiUrl}/v1.0/stats/24h`).then(r => r.json());113
114 return {115 online: stats.activeMakers > 5,116 congestion: stats.pendingOrders > 50 ? 'high' :117 stats.pendingOrders > 20 ? 'medium' : 'low',118 recentSuccessRate: stats.fillRate,119 medianConfirmTime: stats.medianFillTime,120 pendingTransfers: stats.pendingOrders,121 validatorSetHealth: `${stats.activeMakers} active makers`,122 };123 }124}LayerZero Adapter
LayerZero uses an Ultra Light Node (ULN) architecture with configurable Decentralized Verifier Networks (DVNs). Instead of running full nodes on every chain, LayerZero relies on lightweight on-chain endpoints that verify messages using oracle and relayer proofs. The DVN system allows applications to choose their own security configuration.
Architecture
1LayerZero Message Flow:2
3 Source Chain Destination Chain4 ┌───────────┐ ┌───────────────┐5 │ OApp sends│ │ LZ Endpoint │6 │ message to│──→ Emit event ──→ ┌───────────────┐ │ receives msg │7 │ LZ Endpt │ │ DVN Layer │ │ │8 └───────────┘ │ ┌─────────┐ │ └───────┬───────┘9 │ │ DVN #1 │──┤ │10 │ │(Google) │ │ Verify DVN signatures11 │ └─────────┘ │ against configured12 │ ┌─────────┐ │ threshold (e.g., 2/3)13 │ │ DVN #2 │──┼──────────→│14 │ │(Polyhed)│ │ │15 │ └─────────┘ │ Execute message on16 │ ┌─────────┐ │ destination OApp17 │ │ DVN #3 │──┤ │18 │ │(Animoca)│ │ ▼19 │ └─────────┘ │ Tokens transferred20 └───────────────┘Implementation Details
1class LayerZeroAdapter implements BridgeAdapter {2 name = 'layerzero';3 supportedChains = [4 'ethereum', 'arbitrum', 'optimism', 'base', 'polygon',5 'avalanche', 'bsc', 'fantom', 'celo', 'moonbeam',6 'gnosis', 'metis', 'linea', 'scroll', 'mantle',7 'zksync', 'kava', 'aurora', 'harmony', 'coredao',8 'tenet', 'canto', 'fuse', 'meter', 'telos',9 'klaytn', 'dfk', 'dexalot', 'astar', 'moonriver',10 ];11
12 private lzEndpoints: Map<string, string>;13 private oftAddresses: Map<string, Map<string, string>>; // chain → token → OFT address14 private dvnConfig: DvnConfig;15
16 constructor(config: LayerZeroConfig) {17 this.lzEndpoints = new Map([18 ['ethereum', '0x1a44076050125825900e736c501f859c50fE728c'],19 ['arbitrum', '0x1a44076050125825900e736c501f859c50fE728c'],20 ['base', '0x1a44076050125825900e736c501f859c50fE728c'],21 // V2 uses same endpoint address across EVM chains22 ]);23
24 this.dvnConfig = config.dvnConfig || {25 required: ['google-cloud', 'polyhedra'],26 optional: ['animoca', 'bware', 'horizen', 'nethermind'],27 threshold: 2, // Require 2 of the optional DVNs28 };29 }30
31 async getQuote(params: QuoteParams): Promise<BridgeQuote> {32 // 1. Get the OFT (Omnichain Fungible Token) address for this token33 const oftAddress = this.getOftAddress(params.fromChain, params.token);34
35 // 2. Estimate messaging fee (paid in native gas token)36 const messagingFee = await this.estimateMessagingFee(37 params.fromChain,38 params.toChain,39 params.amount,40 oftAddress41 );42
43 // 3. Query DVN availability for this chain pair44 const dvnStatus = await this.getDvnStatus(params.fromChain, params.toChain);45
46 // 4. Calculate confirmation time based on DVN response times47 const confirmTime = this.estimateConfirmTime(dvnStatus);48
49 return {50 bridge: this.name,51 inputAmount: params.amount,52 outputAmount: this.calculateOutput(params.amount, messagingFee),53 fee: messagingFee.nativeFee.toString(),54 estimatedTime: confirmTime,55 liquidityDepth: 'unlimited', // OFT model: mint/burn, no liquidity pools56 expiresAt: Date.now() + 30_000,57 metadata: {58 oftAddress,59 lzFee: messagingFee,60 dvnConfig: this.dvnConfig,61 srcEid: this.getEndpointId(params.fromChain),62 dstEid: this.getEndpointId(params.toChain),63 },64 };65 }66
67 async execute(quote: BridgeQuote, signer: Signer): Promise<BridgeResult> {68 const oftAddress = quote.metadata.oftAddress as string;69 const lzFee = quote.metadata.lzFee as MessagingFee;70
71 // Step 1: Call send() on the OFT contract72 const sendTx = await this.sendOFT(73 oftAddress,74 quote,75 lzFee,76 signer77 );78
79 // Step 2: Wait for DVN verification on destination chain80 const verifiedMsg = await this.waitForVerification(sendTx.hash, {81 srcEid: quote.metadata.srcEid as number,82 dstEid: quote.metadata.dstEid as number,83 timeout: quote.estimatedTime * 3 * 1000,84 pollInterval: 5_000,85 });86
87 return {88 bridge: this.name,89 sourceTxHash: sendTx.hash,90 destinationTxHash: verifiedMsg.executionTxHash,91 actualOutput: verifiedMsg.amount,92 actualTime: Date.now() - sendTx.timestamp,93 };94 }95
96 private estimateConfirmTime(dvnStatus: DvnStatus): number {97 // Confirmation requires the configured threshold of DVNs98 // Time is dominated by the slowest required DVN99 const requiredTimes = dvnStatus.dvns100 .filter(d => this.dvnConfig.required.includes(d.name))101 .map(d => d.avgResponseTime);102
103 const optionalTimes = dvnStatus.dvns104 .filter(d => this.dvnConfig.optional.includes(d.name))105 .sort((a, b) => a.avgResponseTime - b.avgResponseTime)106 .slice(0, this.dvnConfig.threshold)107 .map(d => d.avgResponseTime);108
109 // Max of required DVNs + max of fastest N optional DVNs110 return Math.max(...requiredTimes, ...optionalTimes);111 }112
113 async getHealth(): Promise<BridgeHealth> {114 const dvnStatus = await this.getDvnAvailability();115 const recentMessages = await this.getRecentMessages(100);116
117 const requiredOnline = this.dvnConfig.required.every(118 name => dvnStatus.find(d => d.name === name)?.online119 );120 const optionalOnline = dvnStatus121 .filter(d => this.dvnConfig.optional.includes(d.name) && d.online)122 .length;123
124 return {125 online: requiredOnline && optionalOnline >= this.dvnConfig.threshold,126 congestion: this.estimateCongestion(recentMessages),127 recentSuccessRate: recentMessages.filter(m => m.delivered).length / 100,128 medianConfirmTime: this.calculateMedian(129 recentMessages.map(m => m.confirmTime)130 ),131 pendingTransfers: recentMessages.filter(m => !m.delivered).length,132 };133 }134}Allbridge Adapter
Allbridge uses a liquidity pool model with a unique pool-to-pool design. Each chain has its own stablecoin pool, and transfers move value between pools using a consensus-based messaging layer. The pool model means Allbridge is most efficient for stablecoin transfers and has natural liquidity depth limits.
Architecture
1Allbridge Pool-to-Pool Transfer:2
3 Source Chain Pool Destination Chain Pool4 ┌──────────────────┐ ┌──────────────────┐5 │ USDC Pool │ │ USDC Pool │6 │ Balance: $2.4M │ │ Balance: $1.8M │7 │ │ Messaging Layer │ │8 │ User deposits │──→ ┌──────────────┐ ──→ │ Pool releases │9 │ $5,000 USDC │ │ Allbridge │ │ $4,995 USDC │10 │ │ │ Validators │ │ to user │11 │ Pool: $2.405M │ │ (consensus) │ │ │12 │ │ └──────────────┘ │ Pool: $1.795M │13 └──────────────────┘ └──────────────────┘14
15 Fee: 0.1% ($5.00)16 Slippage: Pool imbalance affects rate (virtual price model)17 Capacity: Limited by destination pool balanceImplementation Details
1class AllbridgeAdapter implements BridgeAdapter {2 name = 'allbridge';3 supportedChains = [4 'ethereum', 'solana', 'bsc', 'polygon', 'avalanche',5 'celo', 'tron', 'stellar', 'arbitrum', 'optimism',6 ];7
8 private poolAddresses: Map<string, Map<string, string>>;9 private messengerContract: string;10
11 constructor(config: AllbridgeConfig) {12 this.poolAddresses = new Map([13 ['ethereum', new Map([14 ['USDC', '0xa7062bbA94c91d565Ae33B893Ab5dFAF1Fc57C4d'],15 ['USDT', '0x7DBF07Ad92Ed4e26D5511b4F285508eBF174135D'],16 ['DAI', '0x3a7e0eDdC225E172e4BF050b0d446A6C30f9bF9E'],17 ])],18 ['solana', new Map([19 ['USDC', 'Hv7JBs5NAt1XFwokREYHuSqMrdkE9SbsMWDd3UT5DEgR'],20 ['USDT', '3pMPwxAsK6eQwTrNg5K6rEprgc3S4j9y5GaLFBaVTkJ3'],21 ])],22 // ... additional chains23 ]);24 }25
26 async getQuote(params: QuoteParams): Promise<BridgeQuote> {27 // 1. Get pool state on both chains28 const srcPool = await this.getPoolState(params.fromChain, params.token);29 const dstPool = await this.getPoolState(params.toChain, params.token);30
31 // 2. Calculate virtual price based on pool imbalance32 const virtualPrice = this.calculateVirtualPrice(srcPool, dstPool);33
34 // 3. Calculate output accounting for pool fee (0.1%) and virtual price35 const fee = BigInt(params.amount) * 10n / 10000n; // 0.1%36 const netAmount = BigInt(params.amount) - fee;37 const outputAmount = this.applyVirtualPrice(netAmount, virtualPrice);38
39 // 4. Check destination pool has sufficient balance40 if (BigInt(dstPool.balance) < outputAmount) {41 throw new InsufficientLiquidityError(42 'allbridge',43 params.toChain,44 dstPool.balance,45 outputAmount.toString()46 );47 }48
49 return {50 bridge: this.name,51 inputAmount: params.amount,52 outputAmount: outputAmount.toString(),53 fee: fee.toString(),54 estimatedTime: this.getEstimatedTime(params.fromChain, params.toChain),55 liquidityDepth: dstPool.balance,56 expiresAt: Date.now() + 20_000,57 metadata: {58 srcPool: srcPool.address,59 dstPool: dstPool.address,60 virtualPrice: virtualPrice.toString(),61 messenger: this.getMessenger(params.fromChain, params.toChain),62 },63 };64 }65
66 async execute(quote: BridgeQuote, signer: Signer): Promise<BridgeResult> {67 // Step 1: Approve and deposit into source pool68 const approveTx = await this.approvePool(69 quote.metadata.srcPool as string,70 quote.inputAmount,71 signer72 );73
74 // Step 2: Initiate cross-chain transfer via messenger75 const bridgeTx = await this.initiateTransfer(quote, signer);76
77 // Step 3: Wait for messenger confirmation78 const confirmation = await this.waitForConfirmation(bridgeTx.hash, {79 timeout: quote.estimatedTime * 3 * 1000,80 pollInterval: 10_000,81 });82
83 return {84 bridge: this.name,85 sourceTxHash: bridgeTx.hash,86 destinationTxHash: confirmation.claimTxHash,87 actualOutput: confirmation.receivedAmount,88 actualTime: Date.now() - bridgeTx.timestamp,89 };90 }91
92 private calculateVirtualPrice(93 srcPool: PoolState,94 dstPool: PoolState95 ): bigint {96 // Virtual price adjusts for pool imbalance97 // If destination pool is depleted, price increases (user gets less)98 // If destination pool is over-balanced, price decreases (user gets more)99 const dstUtilization = (BigInt(dstPool.tokenBalance) * 10000n) /100 BigInt(dstPool.virtualBalance);101
102 if (dstUtilization > 10000n) return 10000n; // 1:1 or better103 return dstUtilization; // Less than 1:1 proportional to depletion104 }105
106 async getLiquidity(chain: string, token: string): Promise<LiquidityInfo> {107 const pool = await this.getPoolState(chain, token);108 return {109 available: pool.tokenBalance,110 total: pool.virtualBalance,111 utilization: Number(BigInt(pool.tokenBalance) * 10000n /112 BigInt(pool.virtualBalance)) / 100,113 };114 }115
116 async getHealth(): Promise<BridgeHealth> {117 const pools = await this.getAllPoolStates();118 const recentTransfers = await this.getRecentTransfers(100);119
120 // Check for severely imbalanced pools121 const imbalancedPools = pools.filter(p => {122 const utilization = Number(BigInt(p.tokenBalance) * 100n /123 BigInt(p.virtualBalance));124 return utilization < 20; // Less than 20% of virtual balance125 });126
127 return {128 online: imbalancedPools.length === 0 || pools.length - imbalancedPools.length > 3,129 congestion: imbalancedPools.length > 2 ? 'high' :130 imbalancedPools.length > 0 ? 'medium' : 'low',131 recentSuccessRate: recentTransfers.filter(t => t.completed).length / 100,132 medianConfirmTime: this.calculateMedian(133 recentTransfers.map(t => t.confirmTime)134 ),135 pendingTransfers: recentTransfers.filter(t => !t.completed).length,136 };137 }138}Adding a New Bridge
To add a new bridge adapter, implement the BridgeAdapterinterface and register it with the router:
1import { BridgeAdapter, QuoteParams, BridgeQuote } from '@mnmx/core';2
3class MyBridgeAdapter implements BridgeAdapter {4 name = 'my-bridge';5 supportedChains = ['ethereum', 'solana', 'arbitrum'];6
7 async getQuote(params: QuoteParams): Promise<BridgeQuote> {8 // 1. Query your bridge for a quote9 // 2. Normalize the response to BridgeQuote format10 // 3. Include bridge-specific metadata11 return { /* ... */ };12 }13
14 async execute(quote: BridgeQuote, signer: Signer): Promise<BridgeResult> {15 // 1. Initiate bridge transfer on source chain16 // 2. Wait for cross-chain confirmation17 // 3. Claim/redeem on destination chain (if needed)18 return { /* ... */ };19 }20
21 async getStatus(txHash: string): Promise<BridgeStatus> {22 // Return current status of a pending transfer23 return { /* ... */ };24 }25
26 async getHealth(): Promise<BridgeHealth> {27 // Return current bridge health metrics28 return { /* ... */ };29 }30
31 async getLiquidity(chain: string, token: string): Promise<LiquidityInfo> {32 // Return available liquidity for this token on this chain33 return { /* ... */ };34 }35
36 async getSupportedPairs(): Promise<ChainPair[]> {37 // Return all supported source-destination chain pairs38 return [ /* ... */ ];39 }40
41 async getMetrics(): Promise<BridgeMetrics> {42 // Return 24h performance metrics43 return { /* ... */ };44 }45}46
47// Register the adapter48const router = new MnmxRouter({49 customBridges: [new MyBridgeAdapter()],50});Once registered, the routing engine automatically includes the new bridge in path discovery. No changes to the routing logic needed. The minimax evaluator will incorporate the new bridge's health, latency, and fee characteristics into its scoring.
Health Monitoring
The routing engine continuously monitors bridge health using a background polling loop. Unhealthy bridges are deprioritized or excluded from routing:
| Condition | Detection | Action |
|---|---|---|
| Bridge offline | Health endpoint returns error or timeout | Exclude from all routes |
| High congestion | Pending transfers > 2x normal or confirm time > 2x median | Increase worst-case time estimate by 3x |
| Success rate < 95% | Rolling 100-transfer window below threshold | Increase bridge risk penalty in scoring |
| Confirm time > 2x median | Recent transfers taking 2x longer than historical median | Flag as degraded, deprioritize |
| Low liquidity | Pool balance below 10% of virtual balance | Reduce max transfer size for this bridge |
| Guardian/DVN issues | Less than quorum of validators online | Exclude from routes (message cannot be verified) |
1class BridgeHealthMonitor {2 private healthCache: Map<string, BridgeHealth>;3 private pollInterval: number;4
5 constructor(adapters: BridgeAdapter[], pollInterval = 30_000) {6 this.pollInterval = pollInterval;7 this.startPolling(adapters);8 }9
10 private startPolling(adapters: BridgeAdapter[]): void {11 setInterval(async () => {12 const healthChecks = await Promise.allSettled(13 adapters.map(async adapter => ({14 bridge: adapter.name,15 health: await adapter.getHealth(),16 }))17 );18
19 for (const result of healthChecks) {20 if (result.status === 'fulfilled') {21 this.healthCache.set(result.value.bridge, result.value.health);22 } else {23 // Health check itself failed — mark bridge as potentially offline24 const bridge = this.getBridgeFromError(result.reason);25 this.healthCache.set(bridge, {26 online: false,27 congestion: 'high',28 recentSuccessRate: 0,29 medianConfirmTime: Infinity,30 pendingTransfers: -1,31 });32 }33 }34 }, this.pollInterval);35 }36
37 isHealthy(bridge: string): boolean {38 const health = this.healthCache.get(bridge);39 if (!health) return false;40 return health.online && health.recentSuccessRate >= 0.95;41 }42
43 getHealthPenalty(bridge: string): number {44 const health = this.healthCache.get(bridge);45 if (!health || !health.online) return 1.0; // Maximum penalty46 if (health.recentSuccessRate < 0.95) return 0.1;47 if (health.congestion === 'high') return 0.05;48 if (health.congestion === 'medium') return 0.02;49 return 0; // Healthy50 }51}How the Engine Selects Bridges
The minimax engine doesn't pick a "best bridge." It evaluates complete paths that may use different bridges for different hops. A single route might use Wormhole for the first hop and deBridge for the second if that combination produces the best minimax score.
Bridge selection factors:
- Liquidity — Higher liquidity means lower slippage. Allbridge pools have hard limits; Wormhole and LayerZero do not.
- Speed — deBridge DLN fills in 1-3 minutes. Wormhole takes 2-15 minutes. Speed weighting determines preference.
- Cost — LayerZero messaging fees are typically lowest. Wormhole relayer fees are moderate. deBridge DLN spread is variable.
- Reliability — Historical success rate affects risk scoring. A bridge with 99.8% success rate gets a lower risk penalty than one at 97%.
- Chain coverage — Some bridges are the only option for certain chain pairs. LayerZero covers 30+ chains. Allbridge covers fewer but includes Tron and Stellar.
- Security model — Guardian networks (Wormhole) vs DVN networks (LayerZero) vs validator sets (deBridge) vs multi-sig (Allbridge) have different trust assumptions.
Bridge Comparison Matrix
| Feature | Wormhole | deBridge | LayerZero | Allbridge |
|---|---|---|---|---|
| Model | Lock/Mint (VAA) | Lock/Fill (DLN) | Burn/Mint (OFT) | Pool-to-Pool |
| Chains | 25+ | 15+ | 30+ | 10+ |
| Speed | 2-15 min | 1-5 min | 2-10 min | 2-10 min |
| Fee model | Flat relayer fee | Maker spread | Messaging fee | 0.1% pool fee |
| Liquidity limit | Wrapped supply | Maker liquidity | Unlimited (OFT) | Pool balance |
| Non-EVM support | Solana, Sui, Aptos | Solana | EVM only (V2) | Solana, Tron, Stellar |
| Security | 19 guardians (13/19) | Validator network | Configurable DVNs | Multi-sig |