Docs
Docs/Bridge Adapters

Bridge Adapters

How MNMX integrates with cross-chain bridges and how to add new ones.

Supported Bridges

BridgeChainsAvg. TimeSecurity Model
Wormhole25+ chains including Ethereum, Solana, Arbitrum, Base2-15 minGuardian network (19 validators)
deBridge15+ chains including Ethereum, Solana, BNB Chain1-5 minValidator network + DLN
LayerZero30+ chains including Ethereum, Arbitrum, Optimism2-10 minUltra Light Node + DVN
Allbridge10+ chains including Ethereum, Solana, BNB Chain2-10 minMulti-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:

typescript
1interface BridgeAdapter {
2 // Identity
3 name: string;
4 supportedChains: Chain[];
5
6 // Quote a transfer
7 getQuote(params: QuoteParams): Promise<BridgeQuote>;
8
9 // Execute a quoted transfer
10 execute(quote: BridgeQuote, signer: Signer): Promise<BridgeResult>;
11
12 // Monitor transfer status
13 getStatus(txHash: string): Promise<BridgeStatus>;
14
15 // Health check
16 getHealth(): Promise<BridgeHealth>;
17
18 // Liquidity query
19 getLiquidity(chain: string, token: string): Promise<LiquidityInfo>;
20
21 // Supported chain pairs
22 getSupportedPairs(): Promise<ChainPair[]>;
23
24 // Historical performance metrics
25 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 data
46}
47
48interface BridgeHealth {
49 online: boolean;
50 congestion: 'low' | 'medium' | 'high';
51 recentSuccessRate: number;
52 medianConfirmTime: number;
53 pendingTransfers: number;
54 guardianStatus?: string; // Wormhole-specific
55 validatorSetHealth?: string; // deBridge-specific
56}
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

text
1Wormhole Transfer Flow:
2
3 Source Chain Guardian Network Destination Chain
4 ┌───────────┐ ┌──────────────┐ ┌───────────────┐
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, verify
15 13/19 signatures,
16 mint/release tokens
17
18
19 Tokens in user wallet

Implementation Details

typescript
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 chains
24 ]);
25 }
26
27 async getQuote(params: QuoteParams): Promise<BridgeQuote> {
28 // 1. Look up token attestation on destination chain
29 const wrappedAsset = await this.getWrappedAsset(
30 params.fromChain, params.toChain, params.token
31 );
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 finality
37 const confirmTime = this.getEstimatedConfirmTime(params.fromChain);
38
39 // 4. Query current guardian set availability
40 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 chain
61 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 chain
70 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 yet
98 }
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 finality
107 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 minutes
117 }
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 required
125 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}
VAA expiration: Wormhole VAAs do not expire. Once a guardian set produces a signed VAA, it can be redeemed on the destination chain at any time. This means bridge stalls never result in lost funds — the VAA can be redeemed even days later.

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

text
1deBridge DLN Transfer Flow:
2
3 Source Chain DLN Network Destination Chain
4 ┌───────────┐ ┌──────────────┐ ┌───────────────┐
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 claims
13 │ locked tokens from source chain
14
15 Maker receives locked tokens as compensation
16
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

typescript
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 chains
21 ]);
22 }
23
24 async getQuote(params: QuoteParams): Promise<BridgeQuote> {
25 // 1. Query DLN for current maker offers
26 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 faster
42 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 chain
56 const placeTx = await this.placeDlnOrder(quote, signer);
57
58 // Step 2: Wait for maker fill on destination chain
59 // DLN makers typically fill within 30-120 seconds
60 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 popularity
105 const fastPairs = ['ethereum-solana', 'ethereum-arbitrum', 'ethereum-bsc'];
106 const pair = `${fromChain}-${toChain}`;
107 if (fastPairs.includes(pair)) return 60; // ~1 min for popular pairs
108 return 180; // ~3 min for less popular pairs
109 }
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}
DLN speed advantage: Unlike traditional bridges that wait for cross-chain message finality, DLN makers pre-fill orders from their own liquidity. This means the user receives tokens in 1-3 minutes while the maker waits 5-30 minutes for the source chain message to finalize. The maker accepts this latency risk in exchange for the spread they earn on each fill.

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

text
1LayerZero Message Flow:
2
3 Source Chain Destination Chain
4 ┌───────────┐ ┌───────────────┐
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 signatures
11 │ └─────────┘ │ against configured
12 │ ┌─────────┐ │ threshold (e.g., 2/3)
13 │ │ DVN #2 │──┼──────────→│
14 │ │(Polyhed)│ │ │
15 │ └─────────┘ │ Execute message on
16 │ ┌─────────┐ │ destination OApp
17 │ │ DVN #3 │──┤ │
18 │ │(Animoca)│ │ ▼
19 │ └─────────┘ │ Tokens transferred
20 └───────────────┘

Implementation Details

typescript
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 address
14 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 chains
22 ]);
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 DVNs
28 };
29 }
30
31 async getQuote(params: QuoteParams): Promise<BridgeQuote> {
32 // 1. Get the OFT (Omnichain Fungible Token) address for this token
33 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 oftAddress
41 );
42
43 // 3. Query DVN availability for this chain pair
44 const dvnStatus = await this.getDvnStatus(params.fromChain, params.toChain);
45
46 // 4. Calculate confirmation time based on DVN response times
47 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 pools
56 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 contract
72 const sendTx = await this.sendOFT(
73 oftAddress,
74 quote,
75 lzFee,
76 signer
77 );
78
79 // Step 2: Wait for DVN verification on destination chain
80 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 DVNs
98 // Time is dominated by the slowest required DVN
99 const requiredTimes = dvnStatus.dvns
100 .filter(d => this.dvnConfig.required.includes(d.name))
101 .map(d => d.avgResponseTime);
102
103 const optionalTimes = dvnStatus.dvns
104 .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 DVNs
110 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)?.online
119 );
120 const optionalOnline = dvnStatus
121 .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}
OFT vs liquidity pools: LayerZero's OFT (Omnichain Fungible Token) model uses mint-and-burn instead of liquidity pools. This means there is no liquidity depth limit — any amount can be bridged. However, only tokens that have deployed OFT contracts on both chains can use this model. For non-OFT tokens, MNMX routes through a swap to an OFT-enabled token before bridging.

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

text
1Allbridge Pool-to-Pool Transfer:
2
3 Source Chain Pool Destination Chain Pool
4 ┌──────────────────┐ ┌──────────────────┐
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 balance

Implementation Details

typescript
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 chains
23 ]);
24 }
25
26 async getQuote(params: QuoteParams): Promise<BridgeQuote> {
27 // 1. Get pool state on both chains
28 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 imbalance
32 const virtualPrice = this.calculateVirtualPrice(srcPool, dstPool);
33
34 // 3. Calculate output accounting for pool fee (0.1%) and virtual price
35 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 balance
40 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 pool
68 const approveTx = await this.approvePool(
69 quote.metadata.srcPool as string,
70 quote.inputAmount,
71 signer
72 );
73
74 // Step 2: Initiate cross-chain transfer via messenger
75 const bridgeTx = await this.initiateTransfer(quote, signer);
76
77 // Step 3: Wait for messenger confirmation
78 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: PoolState
95 ): bigint {
96 // Virtual price adjusts for pool imbalance
97 // 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 better
103 return dstUtilization; // Less than 1:1 proportional to depletion
104 }
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 pools
121 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 balance
125 });
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}
Pool liquidity limits: Unlike Wormhole (lock/mint) or LayerZero (OFT mint/burn), Allbridge's pool model has hard liquidity limits. If the destination pool has $1.8M USDC, you cannot bridge more than $1.8M in a single transfer. The routing engine accounts for this by checking pool balances during state collection and excluding Allbridge from routes that exceed available liquidity.

Adding a New Bridge

To add a new bridge adapter, implement the BridgeAdapterinterface and register it with the router:

typescript
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 quote
9 // 2. Normalize the response to BridgeQuote format
10 // 3. Include bridge-specific metadata
11 return { /* ... */ };
12 }
13
14 async execute(quote: BridgeQuote, signer: Signer): Promise<BridgeResult> {
15 // 1. Initiate bridge transfer on source chain
16 // 2. Wait for cross-chain confirmation
17 // 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 transfer
23 return { /* ... */ };
24 }
25
26 async getHealth(): Promise<BridgeHealth> {
27 // Return current bridge health metrics
28 return { /* ... */ };
29 }
30
31 async getLiquidity(chain: string, token: string): Promise<LiquidityInfo> {
32 // Return available liquidity for this token on this chain
33 return { /* ... */ };
34 }
35
36 async getSupportedPairs(): Promise<ChainPair[]> {
37 // Return all supported source-destination chain pairs
38 return [ /* ... */ ];
39 }
40
41 async getMetrics(): Promise<BridgeMetrics> {
42 // Return 24h performance metrics
43 return { /* ... */ };
44 }
45}
46
47// Register the adapter
48const 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:

ConditionDetectionAction
Bridge offlineHealth endpoint returns error or timeoutExclude from all routes
High congestionPending transfers > 2x normal or confirm time > 2x medianIncrease worst-case time estimate by 3x
Success rate < 95%Rolling 100-transfer window below thresholdIncrease bridge risk penalty in scoring
Confirm time > 2x medianRecent transfers taking 2x longer than historical medianFlag as degraded, deprioritize
Low liquidityPool balance below 10% of virtual balanceReduce max transfer size for this bridge
Guardian/DVN issuesLess than quorum of validators onlineExclude from routes (message cannot be verified)
typescript
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 offline
24 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 penalty
46 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; // Healthy
50 }
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

FeatureWormholedeBridgeLayerZeroAllbridge
ModelLock/Mint (VAA)Lock/Fill (DLN)Burn/Mint (OFT)Pool-to-Pool
Chains25+15+30+10+
Speed2-15 min1-5 min2-10 min2-10 min
Fee modelFlat relayer feeMaker spreadMessaging fee0.1% pool fee
Liquidity limitWrapped supplyMaker liquidityUnlimited (OFT)Pool balance
Non-EVM supportSolana, Sui, AptosSolanaEVM only (V2)Solana, Tron, Stellar
Security19 guardians (13/19)Validator networkConfigurable DVNsMulti-sig
More bridges = better routing. Each new bridge adapter adds more candidate paths to the search space, increasing the probability of finding a superior minimax-optimal route. The marginal value of each additional bridge depends on its chain coverage overlap with existing bridges — bridges that cover unique chain pairs add more value than those duplicating existing coverage.