- Introduced Aggregator.sol for Chainlink-compatible oracle functionality, including round-based updates and access control. - Added OracleWithCCIP.sol to extend Aggregator with CCIP cross-chain messaging capabilities. - Created .gitmodules to include OpenZeppelin contracts as a submodule. - Developed a comprehensive deployment guide in NEXT_STEPS_COMPLETE_GUIDE.md for Phase 2 and smart contract deployment. - Implemented Vite configuration for the orchestration portal, supporting both Vue and React frameworks. - Added server-side logic for the Multi-Cloud Orchestration Portal, including API endpoints for environment management and monitoring. - Created scripts for resource import and usage validation across non-US regions. - Added tests for CCIP error handling and integration to ensure robust functionality. - Included various new files and directories for the orchestration portal and deployment scripts.
291 lines
12 KiB
JavaScript
Executable File
291 lines
12 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* Transaction Mirror Service
|
|
*
|
|
* Off-chain service to mirror Chain-138 transactions to TransactionMirror
|
|
* contract on Ethereum Mainnet for Etherscan visibility.
|
|
*
|
|
* Usage:
|
|
* node scripts/offchain/transaction-mirror-service.js
|
|
*
|
|
* Environment Variables:
|
|
* - CHAIN_138_RPC: RPC endpoint for Chain-138
|
|
* - ETHEREUM_MAINNET_RPC: RPC endpoint for Ethereum Mainnet
|
|
* - TRANSACTION_MIRROR_ADDRESS: TransactionMirror contract address
|
|
* - PRIVATE_KEY: Private key for signing transactions
|
|
* - MIRROR_INTERVAL: Block interval for mirroring (default: 10)
|
|
* - BATCH_SIZE: Number of transactions per batch (default: 50, max: 100)
|
|
* - START_BLOCK: Starting block number (default: latest)
|
|
*/
|
|
|
|
const { ethers } = require('ethers');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Configuration
|
|
const CONFIG = {
|
|
CHAIN_138_RPC: process.env.CHAIN_138_RPC || 'http://localhost:8545',
|
|
ETHEREUM_MAINNET_RPC: process.env.ETHEREUM_MAINNET_RPC || '',
|
|
TRANSACTION_MIRROR_ADDRESS: process.env.TRANSACTION_MIRROR_ADDRESS || '',
|
|
PRIVATE_KEY: process.env.PRIVATE_KEY || '',
|
|
MIRROR_INTERVAL: parseInt(process.env.MIRROR_INTERVAL || '10'),
|
|
BATCH_SIZE: Math.min(parseInt(process.env.BATCH_SIZE || '50'), 100),
|
|
START_BLOCK: process.env.START_BLOCK ? parseInt(process.env.START_BLOCK) : null,
|
|
STATE_FILE: path.join(__dirname, '../../data/transaction-mirror-state.json'),
|
|
};
|
|
|
|
// TransactionMirror ABI (simplified)
|
|
const TRANSACTION_MIRROR_ABI = [
|
|
"function mirrorTransaction(bytes32 txHash, address from, address to, uint256 value, uint256 blockNumber, uint256 blockTimestamp, uint256 gasUsed, bool success, bytes calldata data) external",
|
|
"function mirrorBatchTransactions(bytes32[] calldata txHashes, address[] calldata froms, address[] calldata tos, uint256[] calldata values, uint256[] calldata blockNumbers, uint256[] calldata blockTimestamps, uint256[] calldata gasUseds, bool[] calldata successes, bytes[] calldata datas) external",
|
|
"function isMirrored(bytes32 txHash) external view returns (bool)",
|
|
"function paused() external view returns (bool)",
|
|
"event TransactionMirrored(bytes32 indexed txHash, address indexed from, address indexed to, uint256 value, uint256 blockNumber, uint256 blockTimestamp, uint256 gasUsed, bool success)"
|
|
];
|
|
|
|
class TransactionMirrorService {
|
|
constructor() {
|
|
this.chain138Provider = new ethers.JsonRpcProvider(CONFIG.CHAIN_138_RPC);
|
|
this.mainnetProvider = new ethers.JsonRpcProvider(CONFIG.ETHEREUM_MAINNET_RPC);
|
|
this.wallet = new ethers.Wallet(CONFIG.PRIVATE_KEY, this.mainnetProvider);
|
|
this.mirrorContract = new ethers.Contract(
|
|
CONFIG.TRANSACTION_MIRROR_ADDRESS,
|
|
TRANSACTION_MIRROR_ABI,
|
|
this.wallet
|
|
);
|
|
this.lastMirroredBlock = this.loadState();
|
|
this.pendingTransactions = [];
|
|
}
|
|
|
|
loadState() {
|
|
try {
|
|
if (fs.existsSync(CONFIG.STATE_FILE)) {
|
|
const data = JSON.parse(fs.readFileSync(CONFIG.STATE_FILE, 'utf8'));
|
|
return data.lastMirroredBlock || (CONFIG.START_BLOCK || 0);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading state:', error);
|
|
}
|
|
return CONFIG.START_BLOCK || 0;
|
|
}
|
|
|
|
saveState(blockNumber) {
|
|
try {
|
|
const dir = path.dirname(CONFIG.STATE_FILE);
|
|
if (!fs.existsSync(dir)) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
fs.writeFileSync(CONFIG.STATE_FILE, JSON.stringify({
|
|
lastMirroredBlock: blockNumber,
|
|
lastUpdate: new Date().toISOString()
|
|
}, null, 2));
|
|
} catch (error) {
|
|
console.error('Error saving state:', error);
|
|
}
|
|
}
|
|
|
|
async getChain138Transactions(blockNumber) {
|
|
try {
|
|
const block = await this.chain138Provider.getBlock(blockNumber, true);
|
|
if (!block || !block.transactions) {
|
|
return [];
|
|
}
|
|
|
|
const transactions = [];
|
|
for (const txHash of block.transactions) {
|
|
try {
|
|
const tx = await this.chain138Provider.getTransaction(txHash);
|
|
const receipt = await this.chain138Provider.getTransactionReceipt(txHash);
|
|
|
|
if (tx && receipt) {
|
|
transactions.push({
|
|
hash: txHash,
|
|
from: tx.from,
|
|
to: tx.to || ethers.ZeroAddress,
|
|
value: tx.value.toString(),
|
|
blockNumber: block.number,
|
|
blockTimestamp: block.timestamp,
|
|
gasUsed: receipt.gasUsed.toString(),
|
|
success: receipt.status === 1,
|
|
data: tx.data || '0x',
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.warn(`Error fetching transaction ${txHash}:`, error.message);
|
|
}
|
|
}
|
|
|
|
return transactions;
|
|
} catch (error) {
|
|
console.error(`Error fetching Chain-138 block ${blockNumber}:`, error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async mirrorTransaction(tx) {
|
|
try {
|
|
// Check if contract is paused
|
|
const paused = await this.mirrorContract.paused();
|
|
if (paused) {
|
|
console.warn('TransactionMirror is paused, skipping mirror');
|
|
return false;
|
|
}
|
|
|
|
// Check if already mirrored
|
|
const isMirrored = await this.mirrorContract.isMirrored(tx.hash);
|
|
if (isMirrored) {
|
|
return true; // Already mirrored
|
|
}
|
|
|
|
// Mirror transaction
|
|
console.log(`Mirroring transaction ${tx.hash}...`);
|
|
const txResponse = await this.mirrorContract.mirrorTransaction(
|
|
tx.hash,
|
|
tx.from,
|
|
tx.to,
|
|
tx.value,
|
|
tx.blockNumber,
|
|
tx.blockTimestamp,
|
|
tx.gasUsed,
|
|
tx.success,
|
|
tx.data,
|
|
{ gasLimit: 200000 }
|
|
);
|
|
|
|
console.log(`Transaction sent: ${txResponse.hash}`);
|
|
const receipt = await txResponse.wait();
|
|
console.log(`Transaction ${tx.hash} mirrored in transaction ${receipt.hash}`);
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error(`Error mirroring transaction ${tx.hash}:`, error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async mirrorBatch(transactions) {
|
|
try {
|
|
// Check if contract is paused
|
|
const paused = await this.mirrorContract.paused();
|
|
if (paused) {
|
|
console.warn('TransactionMirror is paused, skipping batch mirror');
|
|
return false;
|
|
}
|
|
|
|
// Filter out already mirrored transactions
|
|
const toMirror = [];
|
|
for (const tx of transactions) {
|
|
const isMirrored = await this.mirrorContract.isMirrored(tx.hash);
|
|
if (!isMirrored) {
|
|
toMirror.push(tx);
|
|
}
|
|
}
|
|
|
|
if (toMirror.length === 0) {
|
|
console.log('All transactions already mirrored');
|
|
return true;
|
|
}
|
|
|
|
// Prepare batch data
|
|
const txHashes = toMirror.map(tx => tx.hash);
|
|
const froms = toMirror.map(tx => tx.from);
|
|
const tos = toMirror.map(tx => tx.to);
|
|
const values = toMirror.map(tx => tx.value);
|
|
const blockNumbers = toMirror.map(tx => tx.blockNumber);
|
|
const blockTimestamps = toMirror.map(tx => tx.blockTimestamp);
|
|
const gasUseds = toMirror.map(tx => tx.gasUsed);
|
|
const successes = toMirror.map(tx => tx.success);
|
|
const datas = toMirror.map(tx => tx.data);
|
|
|
|
console.log(`Mirroring batch of ${toMirror.length} transactions...`);
|
|
const txResponse = await this.mirrorContract.mirrorBatchTransactions(
|
|
txHashes,
|
|
froms,
|
|
tos,
|
|
values,
|
|
blockNumbers,
|
|
blockTimestamps,
|
|
gasUseds,
|
|
successes,
|
|
datas,
|
|
{ gasLimit: 2000000 } // Higher gas limit for batch
|
|
);
|
|
|
|
console.log(`Batch transaction sent: ${txResponse.hash}`);
|
|
const receipt = await txResponse.wait();
|
|
console.log(`Batch mirrored in transaction ${receipt.hash}`);
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Error mirroring batch:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async run() {
|
|
console.log('Transaction Mirror Service starting...');
|
|
console.log(`TransactionMirror: ${CONFIG.TRANSACTION_MIRROR_ADDRESS}`);
|
|
console.log(`Last mirrored block: ${this.lastMirroredBlock}`);
|
|
console.log(`Mirror interval: ${CONFIG.MIRROR_INTERVAL} blocks`);
|
|
console.log(`Batch size: ${CONFIG.BATCH_SIZE} transactions`);
|
|
console.log('');
|
|
|
|
while (true) {
|
|
try {
|
|
// Get latest Chain-138 block
|
|
const latestBlock = await this.chain138Provider.getBlockNumber();
|
|
const targetBlock = this.lastMirroredBlock + CONFIG.MIRROR_INTERVAL;
|
|
|
|
if (targetBlock <= latestBlock) {
|
|
console.log(`Processing block ${targetBlock} (latest: ${latestBlock})`);
|
|
const transactions = await this.getChain138Transactions(targetBlock);
|
|
|
|
if (transactions.length > 0) {
|
|
// Add to pending batch
|
|
this.pendingTransactions.push(...transactions);
|
|
|
|
// Mirror batch if we have enough transactions
|
|
if (this.pendingTransactions.length >= CONFIG.BATCH_SIZE) {
|
|
const batch = this.pendingTransactions.splice(0, CONFIG.BATCH_SIZE);
|
|
await this.mirrorBatch(batch);
|
|
}
|
|
}
|
|
|
|
this.lastMirroredBlock = targetBlock;
|
|
this.saveState(targetBlock);
|
|
} else {
|
|
// Mirror any pending transactions if we've been waiting
|
|
if (this.pendingTransactions.length > 0 &&
|
|
(latestBlock - targetBlock) > CONFIG.MIRROR_INTERVAL) {
|
|
const batch = this.pendingTransactions.splice(0, CONFIG.BATCH_SIZE);
|
|
await this.mirrorBatch(batch);
|
|
}
|
|
|
|
console.log(`Waiting for block ${targetBlock} (current: ${latestBlock})`);
|
|
await new Promise(resolve => setTimeout(resolve, 30000)); // Wait 30 seconds
|
|
}
|
|
} catch (error) {
|
|
console.error('Error in main loop:', error);
|
|
await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10 seconds on error
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run service
|
|
if (require.main === module) {
|
|
if (!CONFIG.ETHEREUM_MAINNET_RPC || !CONFIG.TRANSACTION_MIRROR_ADDRESS || !CONFIG.PRIVATE_KEY) {
|
|
console.error('Missing required environment variables:');
|
|
console.error(' - ETHEREUM_MAINNET_RPC');
|
|
console.error(' - TRANSACTION_MIRROR_ADDRESS');
|
|
console.error(' - PRIVATE_KEY');
|
|
process.exit(1);
|
|
}
|
|
|
|
const service = new TransactionMirrorService();
|
|
service.run().catch(console.error);
|
|
}
|
|
|
|
module.exports = TransactionMirrorService;
|
|
|