Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m3s
CI/CD Pipeline / Security Scanning (push) Successful in 2m18s
CI/CD Pipeline / Lint and Format (push) Failing after 34s
CI/CD Pipeline / Terraform Validation (push) Failing after 20s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 22s
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 40s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 49s
OMNL reconcile anchor / Run omnl:reconcile and upload artifacts (push) Failing after 21s
Validation / validate-genesis (push) Successful in 25s
Validation / validate-terraform (push) Failing after 21s
Validation / validate-kubernetes (push) Failing after 8s
Validation / validate-smart-contracts (push) Failing after 8s
Validation / validate-security (push) Failing after 1m11s
Validation / validate-documentation (push) Failing after 14s
Verify Deployment / Verify Deployment (push) Failing after 45s
Ship AddressActivityRegistry V1/V2, ISO20022IntakeGateway, Chain138ParticipantSurface, checkpoint hub contracts, checkpoint-core package, aggregator/indexer/sdk services, relay profile guards, M00 diamond bridge facet, and OMNL compliance contracts. Co-authored-by: Cursor <cursoragent@cursor.com>
245 lines
8.4 KiB
JavaScript
245 lines
8.4 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Minimal tests for CCIP Relay Service.
|
|
* Run with: npm test
|
|
* Does not start the relay or connect to RPC.
|
|
* For full tests, set PRIVATE_KEY (or RELAYER_PRIVATE_KEY) and run; config load will then validate.
|
|
*/
|
|
|
|
import { existsSync } from 'fs';
|
|
import { mkdtemp, rm } from 'fs/promises';
|
|
import { join, dirname } from 'path';
|
|
import { tmpdir } from 'os';
|
|
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));
|
|
|
|
function assert(condition, message) {
|
|
if (!condition) {
|
|
console.error('FAIL:', message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
console.log('Relay service structure tests...');
|
|
|
|
assert(existsSync(join(__dirname, 'src', 'config.js')), 'src/config.js should exist');
|
|
assert(existsSync(join(__dirname, 'src', 'RelayService.js')), 'src/RelayService.js should exist');
|
|
assert(existsSync(join(__dirname, 'src', 'healthServer.js')), 'src/healthServer.js should exist');
|
|
assert(existsSync(join(__dirname, 'index.js')), 'index.js should exist');
|
|
|
|
const logger = {
|
|
info() {},
|
|
warn() {},
|
|
error() {},
|
|
debug() {},
|
|
};
|
|
|
|
const relay = new RelayService({
|
|
sourceChain: {
|
|
name: 'Chain 138',
|
|
chainId: 138,
|
|
rpcUrl: 'http://example.invalid',
|
|
routerAddress: '0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817',
|
|
bridgeAddress: '0x152ed3e9912161b76bdfd368d0c84b7c31c10de7',
|
|
},
|
|
destinationChain: {
|
|
name: 'Ethereum Mainnet',
|
|
chainId: 1,
|
|
rpcUrl: 'http://example.invalid',
|
|
relayRouterAddress: '0x416564Ab73Ad5710855E98dC7bC7Bff7387285BA',
|
|
relayBridgeAddress: '0x2bF74583206A49Be07E0E8A94197C12987AbD7B5',
|
|
relayBridgeAllowlist: ['0x2bF74583206A49Be07E0E8A94197C12987AbD7B5'],
|
|
chainSelector: 5009297550715157269n,
|
|
deliveryMode: 'router',
|
|
},
|
|
tokenMapping: {},
|
|
sourceChainSelector: 138n,
|
|
relayer: { privateKey: '', address: '' },
|
|
monitoring: {
|
|
startBlock: 'latest',
|
|
pollInterval: 5000,
|
|
confirmationBlocks: 1,
|
|
finalityDelayBlocks: 2,
|
|
replayWindowBlocks: 32,
|
|
},
|
|
retry: { maxRetries: 3, retryDelay: 5000 },
|
|
skipMessageIds: new Set(),
|
|
}, logger);
|
|
|
|
const cwReceiver = ethers.AbiCoder.defaultAbiCoder().encode(
|
|
['address'],
|
|
['0x2bF74583206A49Be07E0E8A94197C12987AbD7B5']
|
|
);
|
|
const scoped = relay.evaluateMessageScope({
|
|
sender: '0x152ed3e9912161b76bdfd368d0c84b7c31c10de7',
|
|
receiver: cwReceiver,
|
|
});
|
|
assert(scoped.inScope === true, 'cW message should match the configured worker scope');
|
|
assert(
|
|
scoped.targetBridge === '0x2bF74583206A49Be07E0E8A94197C12987AbD7B5',
|
|
'cW target bridge should decode from receiver bytes'
|
|
);
|
|
|
|
const wethReceiver = ethers.AbiCoder.defaultAbiCoder().encode(
|
|
['address'],
|
|
['0xF9A32F37099c582D28b4dE7Fca6eaC1e5259f939']
|
|
);
|
|
const rejected = relay.evaluateMessageScope({
|
|
sender: '0xcacfd227A040002e49e2e01626363071324f820a',
|
|
receiver: wethReceiver,
|
|
});
|
|
assert(rejected.inScope === false, 'WETH message should be rejected by the cW worker scope');
|
|
|
|
const tempDir = await mkdtemp(join(tmpdir(), 'relay-queue-test-'));
|
|
const queueStatePath = join(tempDir, 'queue-state.json');
|
|
const queue = new MessageQueue(logger, { persistencePath: queueStatePath });
|
|
await queue.init();
|
|
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');
|
|
await queue.resetRetryCount(queuedMessage.messageId);
|
|
await queue.retry(queuedMessage.messageId, { increment: false });
|
|
assert((await queue.getRetryCount(queuedMessage.messageId)) === 0, 'deferred requeue should not consume retry budget');
|
|
|
|
await queue.markProcessed(queuedMessage.messageId);
|
|
const persistentQueue = new MessageQueue(logger, { persistencePath: queueStatePath });
|
|
await persistentQueue.init();
|
|
assert(
|
|
persistentQueue.getStats().queueSize === 0,
|
|
'persisted queue snapshot should not reload processed messages'
|
|
);
|
|
assert(
|
|
persistentQueue.getStats().processed === 1,
|
|
'persisted processedIds should reload after restart'
|
|
);
|
|
await persistentQueue.add(queuedMessage);
|
|
assert(
|
|
persistentQueue.getStats().queueSize === 0,
|
|
'add should skip message ids in persisted processedIds'
|
|
);
|
|
|
|
const probeRelay = new RelayService({
|
|
sourceChain: {
|
|
name: 'Chain 138',
|
|
chainId: 138,
|
|
rpcUrl: 'http://example.invalid',
|
|
routerAddress: '0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817',
|
|
bridgeAddress: '0x152ed3e9912161b76bdfd368d0c84b7c31c10de7',
|
|
},
|
|
destinationChain: {
|
|
name: 'Ethereum Mainnet',
|
|
chainId: 1,
|
|
rpcUrl: 'http://example.invalid',
|
|
relayRouterAddress: '0x416564Ab73Ad5710855E98dC7bC7Bff7387285BA',
|
|
relayBridgeAddress: '0x2bF74583206A49Be07E0E8A94197C12987AbD7B5',
|
|
relayBridgeAllowlist: ['0x2bF74583206A49Be07E0E8A94197C12987AbD7B5'],
|
|
chainSelector: 5009297550715157269n,
|
|
deliveryMode: 'router',
|
|
},
|
|
tokenMapping: {},
|
|
sourceChainSelector: 138n,
|
|
relayer: { privateKey: '0x' + '11'.repeat(32), address: '' },
|
|
monitoring: {
|
|
startBlock: 'latest',
|
|
pollInterval: 5000,
|
|
confirmationBlocks: 1,
|
|
finalityDelayBlocks: 2,
|
|
replayWindowBlocks: 32,
|
|
},
|
|
retry: { maxRetries: 3, retryDelay: 1 },
|
|
queuePersistence: { path: join(tempDir, 'probe-queue-state.json') },
|
|
skipMessageIds: new Set(),
|
|
}, logger);
|
|
await probeRelay.messageQueue.init();
|
|
await probeRelay.messageQueue.add({
|
|
messageId: '0xprobe-fail',
|
|
destinationChainSelector: 5009297550715157269n,
|
|
sender: '0x152ed3e9912161b76bdfd368d0c84b7c31c10de7',
|
|
receiver: cwReceiver,
|
|
data: '0x',
|
|
tokenAmounts: [{ token: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', amount: 1n, amountType: 0 }],
|
|
});
|
|
probeRelay.destinationRelayRouter = { paused: async () => false };
|
|
probeRelay.destinationProvider = {};
|
|
probeRelay.getDestinationTxOptions = async () => ({ gasLimit: 1000000n });
|
|
probeRelay.ensureTargetBridgeInventory = async () => {
|
|
throw new Error('temporary eth_call failure');
|
|
};
|
|
const deferredMessage = await probeRelay.messageQueue.getNext();
|
|
await probeRelay.relayMessage(deferredMessage);
|
|
assert(
|
|
probeRelay.lastError?.scope === 'bridge_inventory_probe',
|
|
'inventory probe failures should be tracked under bridge_inventory_probe'
|
|
);
|
|
assert(
|
|
(await probeRelay.messageQueue.getRetryCount('0xprobe-fail')) === 0,
|
|
'inventory probe deferrals should not consume retry budget'
|
|
);
|
|
assert(
|
|
probeRelay.messageQueue.getStats().queueSize === 1,
|
|
'inventory probe deferrals should requeue the original message'
|
|
);
|
|
|
|
await rm(tempDir, { recursive: true, force: true });
|
|
|
|
// Chain-id guard (prevents mainnet router + Avalanche RPC mismatch)
|
|
const guardRelay = new RelayService({
|
|
sourceChain: {
|
|
name: 'Chain 138',
|
|
chainId: 138,
|
|
rpcUrl: 'http://example.invalid',
|
|
routerAddress: '0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817',
|
|
bridgeAddress: '0x152ed3e9912161b76bdfd368d0c84b7c31c10de7',
|
|
},
|
|
destinationChain: {
|
|
name: 'Ethereum Mainnet',
|
|
chainId: 1,
|
|
rpcUrl: 'http://example.invalid',
|
|
relayRouterAddress: '0x416564Ab73Ad5710855E98dC7bC7Bff7387285BA',
|
|
relayBridgeAddress: '0x2bF74583206A49Be07E0E8A94197C12987AbD7B5',
|
|
relayBridgeAllowlist: [],
|
|
chainSelector: 5009297550715157269n,
|
|
deliveryMode: 'router',
|
|
},
|
|
tokenMapping: {},
|
|
sourceChainSelector: 138n,
|
|
relayer: { privateKey: '', address: '' },
|
|
monitoring: { startBlock: 'latest', pollInterval: 5000, confirmationBlocks: 1 },
|
|
retry: { maxRetries: 3, retryDelay: 5000 },
|
|
skipMessageIds: new Set(),
|
|
}, logger);
|
|
|
|
const okProvider = { getNetwork: async () => ({ chainId: 1n }) };
|
|
await guardRelay.assertNetworkAlignment('destination', okProvider, 1);
|
|
|
|
let mismatchThrown = false;
|
|
try {
|
|
const badProvider = { getNetwork: async () => ({ chainId: 43114n }) };
|
|
await guardRelay.assertNetworkAlignment('destination', badProvider, 1);
|
|
} catch (error) {
|
|
mismatchThrown = true;
|
|
assert(
|
|
String(error.message || error).includes('chain-id mismatch'),
|
|
'wrong-chain RPC should throw chain-id mismatch'
|
|
);
|
|
}
|
|
assert(mismatchThrown, 'assertNetworkAlignment should reject Avalanche RPC when DEST_CHAIN_ID=1');
|
|
|
|
console.log('OK: relay service structure valid');
|
|
process.exit(0);
|