// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "../ccip/IRouterClient.sol"; /** * @title CCIPTxReporter * @notice Sends Chain-138 transaction reports to Ethereum Mainnet via Chainlink CCIP * @dev Encodes batch data for CCIPLogger.ccipReceive */ contract CCIPTxReporter { IRouterClient public immutable router; uint64 public destChainSelector; address public destReceiver; address public owner; event SingleTxReported(bytes32 indexed txHash, address from, address to, uint256 value); event BatchReported(bytes32 indexed batchId, uint256 count); modifier onlyOwner() { require(msg.sender == owner, "CCIPTxReporter: only owner"); _; } constructor( address _router, uint64 _destChainSelector, address _destReceiver ) { require(_router != address(0), "CCIPTxReporter: zero router"); require(_destReceiver != address(0), "CCIPTxReporter: zero receiver"); router = IRouterClient(payable(_router)); destChainSelector = _destChainSelector; destReceiver = _destReceiver; owner = msg.sender; } /** * @notice Report a single transaction to the CCIPLogger on destination chain */ function reportTx( bytes32 txHash, address from, address to, uint256 value, bytes calldata extraData ) external payable returns (bytes32 messageId) { bytes32[] memory txHashes = new bytes32[](1); address[] memory froms = new address[](1); address[] memory tos = new address[](1); uint256[] memory values = new uint256[](1); txHashes[0] = txHash; froms[0] = from; tos[0] = to; values[0] = value; messageId = _reportBatch(txHash, txHashes, froms, tos, values, extraData); emit SingleTxReported(txHash, from, to, value); } /** * @notice Report a batch of transactions to the CCIPLogger on destination chain */ function reportBatch( bytes32 batchId, bytes32[] calldata txHashes, address[] calldata froms, address[] calldata tos, uint256[] calldata values, bytes calldata extraData ) external payable returns (bytes32 messageId) { messageId = _reportBatch(batchId, txHashes, froms, tos, values, extraData); emit BatchReported(batchId, txHashes.length); } function _reportBatch( bytes32 batchId, bytes32[] memory txHashes, address[] memory froms, address[] memory tos, uint256[] memory values, bytes memory extraData ) internal returns (bytes32 messageId) { bytes memory data = abi.encode( batchId, txHashes, froms, tos, values, extraData ); IRouterClient.EVM2AnyMessage memory message = IRouterClient.EVM2AnyMessage({ receiver: abi.encode(destReceiver), data: data, tokenAmounts: new IRouterClient.TokenAmount[](0), feeToken: address(0), extraArgs: "" }); (messageId, ) = router.ccipSend{value: msg.value}(destChainSelector, message); } /** * @notice Estimate fee for sending a batch */ function estimateFee( bytes32[] calldata txHashes, address[] calldata froms, address[] calldata tos, uint256[] calldata values ) external view returns (uint256 fee) { // Use block.difficulty for London EVM compatibility (prevrandao is Paris+) bytes32 batchId = keccak256(abi.encodePacked(block.timestamp, block.difficulty, txHashes.length)); bytes memory data = abi.encode( batchId, txHashes, froms, tos, values, bytes("") ); IRouterClient.EVM2AnyMessage memory message = IRouterClient.EVM2AnyMessage({ receiver: abi.encode(destReceiver), data: data, tokenAmounts: new IRouterClient.TokenAmount[](0), feeToken: address(0), extraArgs: "" }); return router.getFee(destChainSelector, message); } function setDestReceiver(address _destReceiver) external onlyOwner { require(_destReceiver != address(0), "CCIPTxReporter: zero address"); destReceiver = _destReceiver; } function transferOwnership(address newOwner) external onlyOwner { require(newOwner != address(0), "CCIPTxReporter: zero address"); owner = newOwner; } }