Flash unwinder contracts and scripts, relay lane tuning, trustless bridge and token-aggregation updates.
Made-with: Cursor
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# Copy to .env.avax and adjust if you need a different AVAX RPC.
|
||||
# start-relay.sh avax loads this profile before .env.local / .env.
|
||||
RPC_URL_138=http://192.168.11.211:8545
|
||||
# Use the public 138 RPC for relay polling so a Core deploy-RPC restart does not strand this lane.
|
||||
RPC_URL_138=https://rpc-http-pub.d-bis.org
|
||||
CCIP_ROUTER_CHAIN138=0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817
|
||||
CCIPWETH9_BRIDGE_CHAIN138=0xcacfd227A040002e49e2e01626363071324f820a
|
||||
SOURCE_CHAIN_SELECTOR=138
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Forward relay profile for non-prefunded AVAX cW minting.
|
||||
RPC_URL_138=http://192.168.11.211:8545
|
||||
# Use the public 138 RPC for relay polling so a Core deploy-RPC restart does not strand this lane.
|
||||
RPC_URL_138=https://rpc-http-pub.d-bis.org
|
||||
CCIP_ROUTER_CHAIN138=0x42dab7b888dd382bd5adcf9e038dbf1fd03b4817
|
||||
SOURCE_BRIDGE_ADDRESS=0x152ed3e9912161b76bdfd368d0c84b7c31c10de7
|
||||
SOURCE_CHAIN_SELECTOR=138
|
||||
|
||||
@@ -5,6 +5,7 @@ SOURCE_CHAIN_SELECTOR=6433500567565415381
|
||||
SOURCE_RPC_URL=https://api.avax.network/ext/bc/C/rpc
|
||||
SOURCE_ROUTER_ADDRESS=0x1773125b280d296354f4f4b958a7cfc4e5975b60
|
||||
SOURCE_BRIDGE_ADDRESS=0x635002c5fb227160cd2eac926d1baa61847f3c75
|
||||
SOURCE_LOGS_MAX_BLOCK_RANGE=2048
|
||||
|
||||
DEST_CHAIN_NAME=Chain 138
|
||||
DEST_CHAIN_ID=138
|
||||
|
||||
@@ -24,7 +24,9 @@ MAX_RETRIES=3
|
||||
RETRY_DELAY=5000
|
||||
|
||||
# Keep the WETH lane observably alive but safe until the Mainnet release bridge is funded again.
|
||||
RELAY_SHEDDING=1
|
||||
RELAY_SHEDDING=0
|
||||
RELAY_DELIVERY_ENABLED=1
|
||||
RELAY_ENFORCE_BRIDGE_TOKEN_BALANCE=1
|
||||
|
||||
# Park the known oversized WETH release message until the Mainnet bridge inventory is restored.
|
||||
# 2026-04-05 purge: drop the historical WETH backlog that reloaded into the paused queue so
|
||||
|
||||
@@ -9,6 +9,8 @@ export class MessageQueue {
|
||||
this.processed = new Set();
|
||||
this.failed = new Set();
|
||||
this.retryCounts = new Map();
|
||||
this.messageStore = new Map();
|
||||
this.inFlight = new Map();
|
||||
}
|
||||
|
||||
async add(messageData) {
|
||||
@@ -27,6 +29,7 @@ export class MessageQueue {
|
||||
return;
|
||||
}
|
||||
|
||||
this.messageStore.set(messageId, messageData);
|
||||
// Add to queue
|
||||
this.queue.push(messageData);
|
||||
this.logger.info(`Added message ${messageId} to queue. Queue size: ${this.queue.length}`);
|
||||
@@ -37,17 +40,25 @@ export class MessageQueue {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.queue.shift();
|
||||
const messageData = this.queue.shift();
|
||||
if (messageData && messageData.messageId) {
|
||||
this.inFlight.set(messageData.messageId, messageData);
|
||||
}
|
||||
return messageData;
|
||||
}
|
||||
|
||||
async markProcessed(messageId) {
|
||||
this.processed.add(messageId);
|
||||
this.retryCounts.delete(messageId);
|
||||
this.inFlight.delete(messageId);
|
||||
this.messageStore.delete(messageId);
|
||||
this.logger.info(`Message ${messageId} marked as processed`);
|
||||
}
|
||||
|
||||
async markFailed(messageId) {
|
||||
this.failed.add(messageId);
|
||||
this.retryCounts.delete(messageId);
|
||||
this.inFlight.delete(messageId);
|
||||
this.logger.error(`Message ${messageId} marked as failed`);
|
||||
}
|
||||
|
||||
@@ -58,10 +69,22 @@ export class MessageQueue {
|
||||
async retry(messageId) {
|
||||
const count = this.retryCounts.get(messageId) || 0;
|
||||
this.retryCounts.set(messageId, count + 1);
|
||||
|
||||
// Find message in queue or re-add it
|
||||
// In a production system, you'd store the original message data
|
||||
this.logger.info(`Message ${messageId} retry count: ${count + 1}`);
|
||||
|
||||
const existingIndex = this.queue.findIndex(m => m.messageId === messageId);
|
||||
if (existingIndex >= 0) {
|
||||
this.logger.info(`Message ${messageId} retry count: ${count + 1}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const messageData = this.inFlight.get(messageId) || this.messageStore.get(messageId);
|
||||
if (!messageData) {
|
||||
this.logger.warn(`Cannot requeue ${messageId}; original message payload is unavailable`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.inFlight.delete(messageId);
|
||||
this.queue.push(messageData);
|
||||
this.logger.info(`Message ${messageId} requeued. Retry count: ${count + 1}. Queue size: ${this.queue.length}`);
|
||||
}
|
||||
|
||||
getStats() {
|
||||
@@ -72,4 +95,3 @@ export class MessageQueue {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { ethers } from 'ethers';
|
||||
import { MessageSentABI, RelayRouterABI, RelayBridgeABI } from './abis.js';
|
||||
import { MessageSentABI, RelayRouterABI, RelayBridgeABI, ERC20ABI } from './abis.js';
|
||||
import { MessageQueue } from './MessageQueue.js';
|
||||
import {
|
||||
isRelayShedding,
|
||||
@@ -161,7 +161,7 @@ export class RelayService {
|
||||
const lastSuccessMs = this.lastRelaySuccess && this.lastRelaySuccess.at ? Date.parse(this.lastRelaySuccess.at) : 0;
|
||||
if (
|
||||
this.lastError &&
|
||||
this.lastError.scope === 'relay_message' &&
|
||||
(this.lastError.scope === 'relay_message' || this.lastError.scope === 'bridge_inventory') &&
|
||||
Number.isFinite(lastErrorMs) &&
|
||||
lastErrorMs > 0 &&
|
||||
lastErrorMs >= lastSuccessMs
|
||||
@@ -172,6 +172,35 @@ export class RelayService {
|
||||
return 'operational';
|
||||
}
|
||||
|
||||
async ensureTargetBridgeInventory(messageId, targetBridge, tokenAmounts) {
|
||||
if (process.env.RELAY_ENFORCE_BRIDGE_TOKEN_BALANCE !== '1') {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
for (const tokenAmount of tokenAmounts) {
|
||||
const tokenAddress = ethers.getAddress(tokenAmount.token);
|
||||
const requiredAmount = typeof tokenAmount.amount === 'bigint'
|
||||
? tokenAmount.amount
|
||||
: BigInt(tokenAmount.amount.toString());
|
||||
|
||||
const tokenContract = new ethers.Contract(tokenAddress, ERC20ABI, this.destinationProvider);
|
||||
const availableAmount = await tokenContract.balanceOf(targetBridge);
|
||||
if (availableAmount < requiredAmount) {
|
||||
const shortfall = requiredAmount - availableAmount;
|
||||
return {
|
||||
ok: false,
|
||||
token: tokenAddress,
|
||||
requiredAmount,
|
||||
availableAmount,
|
||||
shortfall,
|
||||
message: `Insufficient bridge inventory for ${messageId}: ${tokenAddress} available=${availableAmount.toString()} required=${requiredAmount.toString()} shortfall=${shortfall.toString()}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
getHealthSnapshot() {
|
||||
const queueStats = this.messageQueue.getStats();
|
||||
const status = this.getHealthStatus();
|
||||
@@ -359,7 +388,13 @@ export class RelayService {
|
||||
return (
|
||||
msg.includes('maximum RPC range') ||
|
||||
msg.includes('exceeds maximum') ||
|
||||
(msg.includes('-32000') && msg.includes('range'))
|
||||
msg.includes('requested too many blocks') ||
|
||||
msg.includes('maximum is set to') ||
|
||||
(msg.includes('-32000') && (
|
||||
msg.includes('range') ||
|
||||
msg.includes('too many blocks') ||
|
||||
msg.includes('maximum is set to')
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -771,6 +806,27 @@ export class RelayService {
|
||||
amountType: Number(ta.amountType) // Ensure it's a number (uint8)
|
||||
};
|
||||
});
|
||||
|
||||
const inventoryCheck = await this.ensureTargetBridgeInventory(
|
||||
messageId,
|
||||
targetBridge,
|
||||
mappedTokenAmounts
|
||||
);
|
||||
if (!inventoryCheck.ok) {
|
||||
const inventoryError = new Error(inventoryCheck.message);
|
||||
this.logger.warn(inventoryCheck.message);
|
||||
this.recordError('bridge_inventory', inventoryError, {
|
||||
message_id: messageId,
|
||||
target_bridge: targetBridge,
|
||||
token: inventoryCheck.token,
|
||||
available_amount: inventoryCheck.availableAmount.toString(),
|
||||
required_amount: inventoryCheck.requiredAmount.toString(),
|
||||
shortfall: inventoryCheck.shortfall.toString()
|
||||
});
|
||||
await this.messageQueue.retry(messageId);
|
||||
await new Promise(resolve => setTimeout(resolve, this.config.retry.retryDelay));
|
||||
return null;
|
||||
}
|
||||
|
||||
// Optional normalization for legacy bridges that decode 4-field payloads:
|
||||
// (recipient, amount, sender, nonce). TwoWayTokenBridgeL1/L2 decode 2-field payloads
|
||||
@@ -840,7 +896,7 @@ export class RelayService {
|
||||
target_bridge: targetBridge,
|
||||
tx_hash: receipt.hash
|
||||
};
|
||||
if (this.lastError && this.lastError.scope === 'relay_message') {
|
||||
if (this.lastError && (this.lastError.scope === 'relay_message' || this.lastError.scope === 'bridge_inventory')) {
|
||||
this.lastError = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,3 +19,7 @@ export const RelayBridgeABI = [
|
||||
"function processed(bytes32) view returns (bool)",
|
||||
"function processedTransfers(bytes32) view returns (bool)"
|
||||
];
|
||||
|
||||
export const ERC20ABI = [
|
||||
"function balanceOf(address account) view returns (uint256)"
|
||||
];
|
||||
|
||||
@@ -11,6 +11,7 @@ import { existsSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { ethers } from 'ethers';
|
||||
import { MessageQueue } from './src/MessageQueue.js';
|
||||
import { RelayService } from './src/RelayService.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
@@ -92,5 +93,20 @@ const rejected = relay.evaluateMessageScope({
|
||||
});
|
||||
assert(rejected.inScope === false, 'WETH message should be rejected by the cW worker scope');
|
||||
|
||||
const queue = new MessageQueue(logger);
|
||||
const queuedMessage = {
|
||||
messageId: '0xtest-message',
|
||||
sender: '0x152ed3e9912161b76bdfd368d0c84b7c31c10de7',
|
||||
receiver: cwReceiver,
|
||||
data: '0x',
|
||||
tokenAmounts: []
|
||||
};
|
||||
await queue.add(queuedMessage);
|
||||
const inFlightMessage = await queue.getNext();
|
||||
assert(inFlightMessage?.messageId === queuedMessage.messageId, 'getNext should return the queued message');
|
||||
await queue.retry(queuedMessage.messageId);
|
||||
const retriedMessage = await queue.getNext();
|
||||
assert(retriedMessage?.messageId === queuedMessage.messageId, 'retry should requeue the original message payload');
|
||||
|
||||
console.log('OK: relay service structure valid');
|
||||
process.exit(0);
|
||||
|
||||
Reference in New Issue
Block a user