Add Oracle Aggregator and CCIP Integration
- 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.
This commit is contained in:
200
scripts/offchain/state-anchor-service.js
Executable file
200
scripts/offchain/state-anchor-service.js
Executable file
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* State Anchor Service
|
||||
*
|
||||
* Off-chain service to collect Chain-138 state proofs and anchor them
|
||||
* to MainnetTether contract on Ethereum Mainnet.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/offchain/state-anchor-service.js
|
||||
*
|
||||
* Environment Variables:
|
||||
* - CHAIN_138_RPC: RPC endpoint for Chain-138
|
||||
* - ETHEREUM_MAINNET_RPC: RPC endpoint for Ethereum Mainnet
|
||||
* - MAINNET_TETHER_ADDRESS: MainnetTether contract address
|
||||
* - PRIVATE_KEY: Private key for signing transactions
|
||||
* - ANCHOR_INTERVAL: Block interval for anchoring (default: 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 || '',
|
||||
MAINNET_TETHER_ADDRESS: process.env.MAINNET_TETHER_ADDRESS || '',
|
||||
PRIVATE_KEY: process.env.PRIVATE_KEY || '',
|
||||
ANCHOR_INTERVAL: parseInt(process.env.ANCHOR_INTERVAL || '100'),
|
||||
START_BLOCK: process.env.START_BLOCK ? parseInt(process.env.START_BLOCK) : null,
|
||||
STATE_FILE: path.join(__dirname, '../../data/state-anchor-state.json'),
|
||||
};
|
||||
|
||||
// MainnetTether ABI (simplified)
|
||||
const MAINNET_TETHER_ABI = [
|
||||
"function anchorStateProof(uint256 blockNumber, bytes32 blockHash, bytes32 stateRoot, bytes32 previousBlockHash, uint256 timestamp, bytes calldata signatures, uint256 validatorCount) external",
|
||||
"function isAnchored(uint256 blockNumber) external view returns (bool)",
|
||||
"function paused() external view returns (bool)",
|
||||
"event StateProofAnchored(uint256 indexed blockNumber, bytes32 indexed blockHash, bytes32 indexed stateRoot, uint256 timestamp, uint256 validatorCount)"
|
||||
];
|
||||
|
||||
class StateAnchorService {
|
||||
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.tetherContract = new ethers.Contract(
|
||||
CONFIG.MAINNET_TETHER_ADDRESS,
|
||||
MAINNET_TETHER_ABI,
|
||||
this.wallet
|
||||
);
|
||||
this.lastAnchoredBlock = this.loadState();
|
||||
}
|
||||
|
||||
loadState() {
|
||||
try {
|
||||
if (fs.existsSync(CONFIG.STATE_FILE)) {
|
||||
const data = JSON.parse(fs.readFileSync(CONFIG.STATE_FILE, 'utf8'));
|
||||
return data.lastAnchoredBlock || (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({
|
||||
lastAnchoredBlock: blockNumber,
|
||||
lastUpdate: new Date().toISOString()
|
||||
}, null, 2));
|
||||
} catch (error) {
|
||||
console.error('Error saving state:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async getChain138Block(blockNumber) {
|
||||
try {
|
||||
const block = await this.chain138Provider.getBlock(blockNumber, true);
|
||||
return {
|
||||
number: block.number,
|
||||
hash: block.hash,
|
||||
stateRoot: block.stateRoot || ethers.ZeroHash, // Fallback if not available
|
||||
parentHash: block.parentHash,
|
||||
timestamp: block.timestamp,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error fetching Chain-138 block ${blockNumber}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async collectSignatures(blockNumber) {
|
||||
// Placeholder: In production, collect validator signatures
|
||||
// This would involve querying Chain-138 validators
|
||||
return ethers.toUtf8Bytes('signatures-placeholder');
|
||||
}
|
||||
|
||||
async anchorStateProof(blockData) {
|
||||
try {
|
||||
// Check if contract is paused
|
||||
const paused = await this.tetherContract.paused();
|
||||
if (paused) {
|
||||
console.warn('MainnetTether is paused, skipping anchor');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if already anchored
|
||||
const isAnchored = await this.tetherContract.isAnchored(blockData.number);
|
||||
if (isAnchored) {
|
||||
console.log(`Block ${blockData.number} already anchored`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Collect signatures (placeholder)
|
||||
const signatures = await this.collectSignatures(blockData.number);
|
||||
const validatorCount = 1; // Placeholder
|
||||
|
||||
// Anchor state proof
|
||||
console.log(`Anchoring block ${blockData.number}...`);
|
||||
const tx = await this.tetherContract.anchorStateProof(
|
||||
blockData.number,
|
||||
blockData.hash,
|
||||
blockData.stateRoot,
|
||||
blockData.parentHash,
|
||||
blockData.timestamp,
|
||||
signatures,
|
||||
validatorCount,
|
||||
{ gasLimit: 500000 }
|
||||
);
|
||||
|
||||
console.log(`Transaction sent: ${tx.hash}`);
|
||||
const receipt = await tx.wait();
|
||||
console.log(`Block ${blockData.number} anchored in transaction ${receipt.hash}`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Error anchoring block ${blockData.number}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async run() {
|
||||
console.log('State Anchor Service starting...');
|
||||
console.log(`MainnetTether: ${CONFIG.MAINNET_TETHER_ADDRESS}`);
|
||||
console.log(`Last anchored block: ${this.lastAnchoredBlock}`);
|
||||
console.log(`Anchor interval: ${CONFIG.ANCHOR_INTERVAL} blocks`);
|
||||
console.log('');
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
// Get latest Chain-138 block
|
||||
const latestBlock = await this.chain138Provider.getBlockNumber();
|
||||
const targetBlock = this.lastAnchoredBlock + CONFIG.ANCHOR_INTERVAL;
|
||||
|
||||
if (targetBlock <= latestBlock) {
|
||||
console.log(`Processing block ${targetBlock} (latest: ${latestBlock})`);
|
||||
const blockData = await this.getChain138Block(targetBlock);
|
||||
|
||||
if (blockData) {
|
||||
const success = await this.anchorStateProof(blockData);
|
||||
if (success) {
|
||||
this.lastAnchoredBlock = targetBlock;
|
||||
this.saveState(targetBlock);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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.MAINNET_TETHER_ADDRESS || !CONFIG.PRIVATE_KEY) {
|
||||
console.error('Missing required environment variables:');
|
||||
console.error(' - ETHEREUM_MAINNET_RPC');
|
||||
console.error(' - MAINNET_TETHER_ADDRESS');
|
||||
console.error(' - PRIVATE_KEY');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const service = new StateAnchorService();
|
||||
service.run().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = StateAnchorService;
|
||||
|
||||
290
scripts/offchain/transaction-mirror-service.js
Executable file
290
scripts/offchain/transaction-mirror-service.js
Executable file
@@ -0,0 +1,290 @@
|
||||
#!/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;
|
||||
|
||||
Reference in New Issue
Block a user