// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; /** * @title TransactionMirror * @notice Mirrors Chain-138 transactions to Ethereum Mainnet for Etherscan visibility * @dev Logs all Chain-138 transactions as events on Mainnet, making them searchable * and viewable on Etherscan. This provides transparency and auditability. */ contract TransactionMirror { address public admin; bool public paused; // Chain-138 chain ID uint64 public constant CHAIN_138 = 138; // Maximum batch size to prevent gas limit issues uint256 public constant MAX_BATCH_SIZE = 100; // Transaction structure struct MirroredTransaction { bytes32 txHash; // Chain-138 transaction hash address from; // Sender address address to; // Recipient address (or contract) uint256 value; // Value transferred uint256 blockNumber; // Chain-138 block number uint256 blockTimestamp; // Block timestamp uint256 gasUsed; // Gas used bool success; // Transaction success status bytes data; // Transaction data (if any) bytes32 indexedHash; // Indexed hash for searchability } // Mapping: txHash => MirroredTransaction mapping(bytes32 => MirroredTransaction) public transactions; // Array of all mirrored transaction hashes bytes32[] public mirroredTxHashes; // Mapping: txHash => bool (replay protection) mapping(bytes32 => bool) public processed; // Events (indexed for Etherscan searchability) event TransactionMirrored( bytes32 indexed txHash, address indexed from, address indexed to, uint256 value, uint256 blockNumber, uint256 blockTimestamp, uint256 gasUsed, bool success ); event BatchTransactionsMirrored( uint256 count, uint256 startBlock, uint256 endBlock ); event AdminChanged(address indexed newAdmin); event Paused(); event Unpaused(); modifier onlyAdmin() { require(msg.sender == admin, "only admin"); _; } modifier whenNotPaused() { require(!paused, "paused"); _; } constructor(address _admin) { require(_admin != address(0), "zero admin"); admin = _admin; } /** * @notice Mirror a single Chain-138 transaction to Mainnet * @param txHash Chain-138 transaction hash * @param from Sender address * @param to Recipient address * @param value Value transferred * @param blockNumber Chain-138 block number * @param blockTimestamp Block timestamp * @param gasUsed Gas used * @param success Transaction success status * @param data Transaction data (optional) */ function mirrorTransaction( bytes32 txHash, address from, address to, uint256 value, uint256 blockNumber, uint256 blockTimestamp, uint256 gasUsed, bool success, bytes calldata data ) external onlyAdmin whenNotPaused { require(txHash != bytes32(0), "invalid hash"); require(!processed[txHash], "already mirrored"); bytes32 indexedHash = keccak256(abi.encodePacked(CHAIN_138, txHash)); transactions[txHash] = MirroredTransaction({ txHash: txHash, from: from, to: to, value: value, blockNumber: blockNumber, blockTimestamp: blockTimestamp, gasUsed: gasUsed, success: success, data: data, indexedHash: indexedHash }); mirroredTxHashes.push(txHash); processed[txHash] = true; emit TransactionMirrored( txHash, from, to, value, blockNumber, blockTimestamp, gasUsed, success ); } /** * @notice Mirror multiple Chain-138 transactions in a batch * @param txHashes Array of transaction hashes * @param froms Array of sender addresses * @param tos Array of recipient addresses * @param values Array of values * @param blockNumbers Array of block numbers * @param blockTimestamps Array of timestamps * @param gasUseds Array of gas used * @param successes Array of success statuses * @param datas Array of transaction data */ 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 onlyAdmin whenNotPaused { uint256 count = txHashes.length; require(count > 0, "empty batch"); require(count <= MAX_BATCH_SIZE, "batch too large"); require( count == froms.length && count == tos.length && count == values.length && count == blockNumbers.length && count == blockTimestamps.length && count == gasUseds.length && count == successes.length && count == datas.length, "array length mismatch" ); uint256 startBlock = blockNumbers[0]; uint256 endBlock = blockNumbers[count - 1]; // Process transactions in batches to avoid stack too deep for (uint256 i = 0; i < count; i++) { bytes32 txHash = txHashes[i]; require(txHash != bytes32(0), "invalid hash"); require(!processed[txHash], "already mirrored"); bytes32 indexedHash = keccak256(abi.encodePacked(CHAIN_138, txHash)); transactions[txHash] = MirroredTransaction({ txHash: txHash, from: froms[i], to: tos[i], value: values[i], blockNumber: blockNumbers[i], blockTimestamp: blockTimestamps[i], gasUsed: gasUseds[i], success: successes[i], data: datas[i], indexedHash: indexedHash }); mirroredTxHashes.push(txHash); processed[txHash] = true; emit TransactionMirrored( txHash, froms[i], tos[i], values[i], blockNumbers[i], blockTimestamps[i], gasUseds[i], successes[i] ); } emit BatchTransactionsMirrored(count, startBlock, endBlock); } /** * @notice Get mirrored transaction details * @param txHash Chain-138 transaction hash * @return mirroredTx Mirrored transaction structure */ function getTransaction(bytes32 txHash) external view returns (MirroredTransaction memory mirroredTx) { require(processed[txHash], "not mirrored"); return transactions[txHash]; } /** * @notice Check if a transaction is mirrored * @param txHash Chain-138 transaction hash * @return true if mirrored */ function isMirrored(bytes32 txHash) external view returns (bool) { return processed[txHash]; } /** * @notice Get total number of mirrored transactions * @return count Number of mirrored transactions */ function getMirroredTransactionCount() external view returns (uint256) { return mirroredTxHashes.length; } /** * @notice Get mirrored transaction hash at index * @param index Index in mirroredTxHashes array * @return txHash Transaction hash */ function getMirroredTransaction(uint256 index) external view returns (bytes32) { require(index < mirroredTxHashes.length, "out of bounds"); return mirroredTxHashes[index]; } /** * @notice Admin functions */ function setAdmin(address newAdmin) external onlyAdmin { require(newAdmin != address(0), "zero admin"); admin = newAdmin; emit AdminChanged(newAdmin); } function pause() external onlyAdmin { paused = true; emit Paused(); } function unpause() external onlyAdmin { paused = false; emit Unpaused(); } }