feat: bridges, PMM, flash workflow, token-aggregation, and deployment docs

- CCIP/trustless bridge contracts, GRU tokens, DEX/PMM tests, reserve vault.
- Token-aggregation service routes, planner, chain config, relay env templates.
- Config snapshots and multi-chain deployment markdown updates.
- gitignore services/btc-intake/dist/ (tsc output); do not track dist.

Run forge build && forge test before deploy (large solc graph).

Made-with: Cursor
This commit is contained in:
defiQUG
2026-04-07 23:40:52 -07:00
parent 0fb7bba07b
commit 76aa419320
289 changed files with 28367 additions and 824 deletions

View File

@@ -31,6 +31,215 @@ export class RelayService {
this.messageSentInterface = new ethers.Interface(MessageSentABI);
/** @type {number} throttle for shedding warning logs */
this._lastSheddingLogTs = 0;
this.startedAt = new Date().toISOString();
this.startEpochMs = Date.now();
this.relayerAddress = '';
this.lastSourcePoll = null;
this.lastSeenMessage = null;
this.lastRelayAttempt = null;
this.lastRelaySuccess = null;
this.lastError = null;
}
normalizeAddress(value) {
if (!value) return '';
try {
return ethers.getAddress(value);
} catch (_) {
return '';
}
}
getConfiguredSourceBridge() {
return this.normalizeAddress(this.config.sourceChain.bridgeAddress);
}
getDestinationBridgeAllowlist() {
return (this.config.destinationChain.relayBridgeAllowlist || [])
.map((value) => String(value || '').trim().toLowerCase())
.filter(Boolean);
}
resolveTargetBridge(receiver) {
let targetBridge = this.normalizeAddress(this.config.destinationChain.relayBridgeAddress);
try {
if (receiver) {
const decoded = ethers.AbiCoder.defaultAbiCoder().decode(['address'], receiver);
if (decoded && decoded[0]) {
targetBridge = this.normalizeAddress(decoded[0]) || targetBridge;
}
}
} catch (_) {
// Keep the configured fallback bridge when receiver decode fails.
}
return targetBridge;
}
evaluateMessageScope(messageData) {
const sourceBridge = this.getConfiguredSourceBridge();
const sender = this.normalizeAddress(messageData.sender);
const targetBridge = this.resolveTargetBridge(messageData.receiver);
const allowlist = this.getDestinationBridgeAllowlist();
if (sourceBridge && sender && sender.toLowerCase() !== sourceBridge.toLowerCase()) {
return {
inScope: false,
reason: `sender ${sender} does not match worker source bridge ${sourceBridge}`,
sourceBridge,
sender,
targetBridge
};
}
if (!targetBridge) {
return {
inScope: false,
reason: 'destination bridge could not be resolved from receiver or config',
sourceBridge,
sender,
targetBridge: ''
};
}
if (allowlist.length > 0 && !allowlist.includes(targetBridge.toLowerCase())) {
return {
inScope: false,
reason: `destination bridge ${targetBridge} not in worker allowlist`,
sourceBridge,
sender,
targetBridge
};
}
return {
inScope: true,
reason: '',
sourceBridge,
sender,
targetBridge
};
}
static summarizeError(error) {
if (!error) return '';
if (typeof error === 'string') return error;
return String(error.shortMessage || error.message || error);
}
recordError(scope, error, extra = {}) {
this.lastError = {
at: new Date().toISOString(),
scope,
message: RelayService.summarizeError(error),
...extra
};
}
getHealthStatus() {
if (!this.isRunning) {
return 'stopped';
}
if (isRelayShedding()) {
return 'paused';
}
if (this.lastSourcePoll && this.lastSourcePoll.ok === false) {
return 'degraded';
}
if (!this.lastSourcePoll || !this.lastSourcePoll.at) {
return 'starting';
}
const lastPollMs = Date.parse(this.lastSourcePoll.at);
const staleAfterMs = Math.max(this.getSourcePollIntervalMs() * 3, 15000);
if (Number.isFinite(lastPollMs) && Date.now() - lastPollMs > staleAfterMs) {
return 'stale';
}
const lastErrorMs = this.lastError && this.lastError.at ? Date.parse(this.lastError.at) : 0;
const lastSuccessMs = this.lastRelaySuccess && this.lastRelaySuccess.at ? Date.parse(this.lastRelaySuccess.at) : 0;
if (
this.lastError &&
this.lastError.scope === 'relay_message' &&
Number.isFinite(lastErrorMs) &&
lastErrorMs > 0 &&
lastErrorMs >= lastSuccessMs
) {
return 'degraded';
}
return 'operational';
}
getHealthSnapshot() {
const queueStats = this.messageQueue.getStats();
const status = this.getHealthStatus();
const sourceBridge = this.getConfiguredSourceBridge();
const allowlist = this.getDestinationBridgeAllowlist();
const defaultTargetBridge = this.normalizeAddress(this.config.destinationChain.relayBridgeAddress);
return {
ok: status === 'operational' || status === 'paused' || status === 'starting',
status,
service: {
name: 'ccip-relay',
running: this.isRunning,
pid: process.pid,
profile: process.env.RELAY_PROFILE || 'default',
started_at: this.startedAt,
uptime_sec: Math.max(0, Math.floor((Date.now() - this.startEpochMs) / 1000)),
relayer_address: this.relayerAddress || this.config.relayer.address || ''
},
source: {
chain_name: this.config.sourceChain.name,
chain_id: this.config.sourceChain.chainId,
chain_selector: this.config.sourceChainSelector.toString(),
router_address: this.config.sourceChain.routerAddress,
bridge_address: this.config.sourceChain.bridgeAddress,
bridge_filter: sourceBridge || ''
},
destination: {
chain_name: this.config.destinationChain.name,
chain_id: this.config.destinationChain.chainId,
chain_selector: this.config.destinationChain.chainSelector.toString(),
relay_router: this.config.destinationChain.relayRouterAddress,
relay_bridge: this.config.destinationChain.relayBridgeAddress,
relay_bridge_default: defaultTargetBridge || '',
relay_bridge_allowlist: allowlist,
delivery_mode: this.config.destinationChain.deliveryMode
},
scope: {
source_bridge: sourceBridge || '',
destination_bridge_default: defaultTargetBridge || '',
destination_bridge_allowlist: allowlist
},
monitoring: {
start_block: String(this.config.monitoring.startBlock),
poll_interval_ms: this.config.monitoring.pollInterval,
effective_source_poll_interval_ms: this.getSourcePollIntervalMs(),
confirmation_blocks: this.config.monitoring.confirmationBlocks,
finality_delay_blocks: this.config.monitoring.finalityDelayBlocks || 0,
replay_window_blocks: this.config.monitoring.replayWindowBlocks || 0,
shedding: isRelayShedding(),
delivery_enabled: !isRelayShedding()
},
queue: {
size: queueStats.queueSize,
processed: queueStats.processed,
failed: queueStats.failed
},
last_source_poll: this.lastSourcePoll,
last_seen_message: this.lastSeenMessage,
last_relay_attempt: this.lastRelayAttempt,
last_relay_success: this.lastRelaySuccess,
last_error: this.lastError
};
}
shouldSkipMessageId(messageId) {
const normalized = String(messageId || '').toLowerCase();
return normalized && this.config.skipMessageIds && this.config.skipMessageIds.has(normalized);
}
/** Polling interval for source router logs (longer while shedding to cut RPC churn). */
@@ -40,6 +249,41 @@ export class RelayService {
}
return this.config.monitoring.pollInterval;
}
async getDestinationTxOptions() {
const txOptions = {
gasLimit: BigInt(process.env.RELAY_DEST_GAS_LIMIT || '1000000')
};
if (process.env.RELAY_DEST_LEGACY_TX !== '1') {
return txOptions;
}
let gasPrice = process.env.RELAY_DEST_GAS_PRICE_WEI ? BigInt(process.env.RELAY_DEST_GAS_PRICE_WEI) : null;
if (gasPrice === null) {
try {
const feeData = await this.destinationProvider.getFeeData();
if (feeData.gasPrice) {
gasPrice = feeData.gasPrice;
}
} catch (_) {
// Fall through to raw RPC fallback below.
}
}
if (gasPrice === null) {
const rawGasPrice = await this.destinationProvider.send('eth_gasPrice', []);
gasPrice = BigInt(rawGasPrice);
}
const bumpPct = BigInt(process.env.RELAY_DEST_GAS_PRICE_BUMP_PCT || '20');
const bumpWei = BigInt(process.env.RELAY_DEST_GAS_PRICE_BUMP_WEI || '1000000');
gasPrice = gasPrice + (gasPrice * bumpPct) / 100n + bumpWei;
txOptions.type = 0;
txOptions.gasPrice = gasPrice;
return txOptions;
}
async start() {
this.logger.info('Initializing relay service...');
@@ -55,8 +299,9 @@ export class RelayService {
}
this.sourceSigner = new ethers.Wallet(this.config.relayer.privateKey, this.sourceProvider);
this.destinationSigner = new ethers.Wallet(this.config.relayer.privateKey, this.destinationProvider);
this.relayerAddress = String(this.destinationSigner.address);
this.logger.info('Relayer address: %s', String(this.destinationSigner.address));
this.logger.info('Relayer address: %s', this.relayerAddress);
// Validate relay router address (bridge can be dynamic from message receiver)
if (!this.config.destinationChain.relayRouterAddress ||
@@ -213,16 +458,28 @@ export class RelayService {
try {
const destSelector = destinationChainSelector.toString();
const expectedSelector = this.config.destinationChain.chainSelector.toString();
if (this.shouldSkipMessageId(messageId)) {
this.logger.warn(`Skipping MessageSent ${messageId}; message id is in RELAY_SKIP_MESSAGE_IDS`);
return;
}
if (destSelector !== expectedSelector) {
this.logger.debug(`Ignoring message for different chain: ${destSelector}`);
return;
}
const scope = this.evaluateMessageScope({ messageId, sender, receiver });
if (!scope.inScope) {
this.logger.debug(`Ignoring message ${messageId}; ${scope.reason}`);
return;
}
this.logger.info('MessageSent event detected:', {
messageId: messageId,
destinationChainSelector: destinationChainSelector.toString(),
sender: sender,
targetBridge: scope.targetBridge,
blockNumber: event.blockNumber,
transactionHash: event.transactionHash
});
@@ -245,6 +502,7 @@ export class RelayService {
destinationChainSelector,
sender,
receiver,
targetBridge: scope.targetBridge,
data,
tokenAmounts: formattedTokenAmounts,
feeToken,
@@ -275,6 +533,16 @@ export class RelayService {
this.logger.info(`Polling events from block ${startBlock} to ${toBlock}`);
const logs = await this.fetchSourceLogsChunked(startBlock, toBlock);
this.lastSourcePoll = {
at: new Date().toISOString(),
ok: true,
from_block: startBlock,
to_block: toBlock,
logs_fetched: logs.length
};
if (this.lastError && this.lastError.scope === 'source_poll') {
this.lastError = null;
}
this.logger.info(`Fetched ${logs.length} MessageSent log(s) from source router`);
@@ -292,23 +560,44 @@ export class RelayService {
const destSelector = destinationChainSelector.toString();
const expectedSelector = this.config.destinationChain.chainSelector.toString();
if (this.shouldSkipMessageId(messageId)) {
this.logger.warn(`Skipping historical MessageSent ${messageId}; message id is in RELAY_SKIP_MESSAGE_IDS`);
continue;
}
if (destSelector !== expectedSelector) {
continue;
}
const scope = this.evaluateMessageScope({ messageId, sender, receiver });
if (!scope.inScope) {
this.logger.debug(`Ignoring historical message ${messageId}; ${scope.reason}`);
continue;
}
this.logger.info('Historical MessageSent detected:', {
messageId,
destinationChainSelector: destSelector,
sender,
targetBridge: scope.targetBridge,
blockNumber: log.blockNumber,
transactionHash: log.transactionHash
});
this.lastSeenMessage = {
at: new Date().toISOString(),
message_id: messageId,
destination_chain_selector: destSelector,
sender,
block_number: log.blockNumber,
transaction_hash: log.transactionHash
};
await this.messageQueue.add({
messageId,
destinationChainSelector,
sender,
receiver,
targetBridge: scope.targetBridge,
data,
tokenAmounts: tokenAmounts.map((ta) => ({
token: ta.token,
@@ -330,6 +619,15 @@ export class RelayService {
} catch (error) {
this.logger.error('Error polling historical events:', error);
this.lastSourcePoll = {
at: new Date().toISOString(),
ok: false,
from_block: startBlock,
error: RelayService.summarizeError(error)
};
this.recordError('source_poll', error, {
from_block: startBlock
});
await new Promise(resolve => setTimeout(resolve, this.getSourcePollIntervalMs()));
}
}
@@ -373,6 +671,19 @@ export class RelayService {
async relayMessage(messageData) {
const { messageId, destinationChainSelector, sender, receiver, data, tokenAmounts } = messageData;
if (this.shouldSkipMessageId(messageId)) {
this.logger.warn(`Skipping queued message ${messageId}; message id is in RELAY_SKIP_MESSAGE_IDS`);
await this.messageQueue.markProcessed(messageId);
return null;
}
const scope = this.evaluateMessageScope(messageData);
if (!scope.inScope) {
this.logger.info(`Skipping queued message ${messageId}; ${scope.reason}`);
await this.messageQueue.markProcessed(messageId);
return null;
}
this.logger.info(`Relaying message ${messageId} to destination chain...`);
@@ -395,23 +706,26 @@ export class RelayService {
}
// Route to bridge encoded in MessageSent.receiver (bytes). Fallback to static env bridge.
let targetBridge = this.config.destinationChain.relayBridgeAddress;
try {
if (receiver) {
const decoded = ethers.AbiCoder.defaultAbiCoder().decode(['address'], receiver);
if (decoded && decoded[0]) targetBridge = ethers.getAddress(decoded[0]);
}
} catch (_) {
// keep fallback targetBridge
}
const targetBridge = scope.targetBridge || this.resolveTargetBridge(receiver);
if (!targetBridge) {
throw new Error(`No destination bridge for message ${messageId}: receiver decode failed and DEST_RELAY_BRIDGE not set`);
}
this.lastRelayAttempt = {
at: new Date().toISOString(),
message_id: messageId,
destination_chain_selector: destinationChainSelector ? destinationChainSelector.toString() : '',
target_bridge: targetBridge,
token_count: tokenAmounts.length
};
// Optional allowlist hardening.
const allowlist = this.config.destinationChain.relayBridgeAllowlist || [];
const allowlist = this.getDestinationBridgeAllowlist();
if (allowlist.length > 0 && !allowlist.includes(String(targetBridge).toLowerCase())) {
throw new Error(`Bridge ${targetBridge} not in DEST_RELAY_BRIDGE_ALLOWLIST`);
this.logger.info(
`Skipping message ${messageId} for bridge ${targetBridge}; not in this worker's DEST_RELAY_BRIDGE_ALLOWLIST`
);
await this.messageQueue.markProcessed(messageId);
return null;
}
let targetBridgeContract = this.destinationBridgeContracts.get(targetBridge.toLowerCase());
@@ -494,6 +808,7 @@ export class RelayService {
tokenAmountsCount: any2EVMMessage.tokenAmounts.length
});
const txOptions = await this.getDestinationTxOptions();
let tx;
if (this.config.destinationChain.deliveryMode === 'direct') {
this.logger.info(`Direct-delivery mode: calling bridge ${targetBridge} without relay router`);
@@ -502,13 +817,13 @@ export class RelayService {
RelayBridgeABI,
this.destinationSigner
);
tx = await directBridge.ccipReceive(any2EVMMessage, { gasLimit: 1000000 });
tx = await directBridge.ccipReceive(any2EVMMessage, txOptions);
} else {
// Call relay router with properly formatted struct
tx = await this.destinationRelayRouter.relayMessage(
targetBridge,
any2EVMMessage,
{ gasLimit: 1000000 }
txOptions
);
}
@@ -518,6 +833,16 @@ export class RelayService {
const receipt = await tx.wait();
this.logger.info(`Message ${messageId} relayed successfully. Transaction: ${receipt.hash}`);
this.lastRelaySuccess = {
at: new Date().toISOString(),
message_id: messageId,
destination_chain_selector: destinationChainSelector ? destinationChainSelector.toString() : '',
target_bridge: targetBridge,
tx_hash: receipt.hash
};
if (this.lastError && this.lastError.scope === 'relay_message') {
this.lastError = null;
}
// Mark message as processed
await this.messageQueue.markProcessed(messageId);
@@ -526,6 +851,9 @@ export class RelayService {
} catch (error) {
this.logger.error(`Error relaying message ${messageId}:`, error);
this.recordError('relay_message', error, {
message_id: messageId
});
// Retry logic
const retryCount = await this.messageQueue.getRetryCount(messageId);