// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/AccessControl.sol"; import "./interfaces/IISO20022Router.sol"; import "./interfaces/IRailTriggerRegistry.sol"; import "./libraries/RailTypes.sol"; import "./libraries/ISO20022Types.sol"; /** * @title ISO20022Router * @notice Normalizes ISO-20022 messages into canonical on-chain format * @dev Creates triggers in RailTriggerRegistry for both inbound and outbound messages */ contract ISO20022Router is IISO20022Router, AccessControl { bytes32 public constant RAIL_OPERATOR_ROLE = keccak256("RAIL_OPERATOR_ROLE"); IRailTriggerRegistry public immutable triggerRegistry; mapping(bytes32 => uint256) private _triggerIdByInstructionId; // instructionId => triggerId /** * @notice Initializes the router with registry address * @param admin Address that will receive DEFAULT_ADMIN_ROLE * @param triggerRegistry_ Address of RailTriggerRegistry contract */ constructor(address admin, address triggerRegistry_) { _grantRole(DEFAULT_ADMIN_ROLE, admin); require(triggerRegistry_ != address(0), "ISO20022Router: zero triggerRegistry"); triggerRegistry = IRailTriggerRegistry(triggerRegistry_); } /** * @notice Submits an inbound ISO-20022 message (rail confirmation/notification) * @dev Requires RAIL_OPERATOR_ROLE. Creates a trigger in CREATED state. * @param m Canonical message struct * @return triggerId The created trigger ID */ function submitInbound( CanonicalMessage calldata m ) external override onlyRole(RAIL_OPERATOR_ROLE) returns (uint256 triggerId) { require(m.msgType != bytes32(0), "ISO20022Router: zero msgType"); require(m.instructionId != bytes32(0), "ISO20022Router: zero instructionId"); require(m.accountRefId != bytes32(0), "ISO20022Router: zero accountRefId"); require(m.token != address(0), "ISO20022Router: zero token"); require(m.amount > 0, "ISO20022Router: zero amount"); // Determine rail from message type (simplified - in production, this would be more sophisticated) RailTypes.Rail rail = _determineRailFromMessageType(m.msgType); // Create trigger IRailTriggerRegistry.Trigger memory trigger = IRailTriggerRegistry.Trigger({ id: 0, // Will be assigned by registry rail: rail, msgType: m.msgType, accountRefId: m.accountRefId, walletRefId: bytes32(0), // Will be resolved by orchestrator if needed token: m.token, amount: m.amount, currencyCode: m.currencyCode, instructionId: m.instructionId, state: RailTypes.State.CREATED, createdAt: 0, // Will be set by registry updatedAt: 0 // Will be set by registry }); triggerId = triggerRegistry.createTrigger(trigger); _triggerIdByInstructionId[m.instructionId] = triggerId; emit InboundSubmitted(triggerId, m.msgType, m.instructionId, m.accountRefId); } /** * @notice Submits an outbound ISO-20022 message (rail initiation) * @dev Requires RAIL_OPERATOR_ROLE. Creates a trigger in CREATED state. * @param m Canonical message struct * @return triggerId The created trigger ID */ function submitOutbound( CanonicalMessage calldata m ) external override onlyRole(RAIL_OPERATOR_ROLE) returns (uint256 triggerId) { require(m.msgType != bytes32(0), "ISO20022Router: zero msgType"); require(m.instructionId != bytes32(0), "ISO20022Router: zero instructionId"); require(m.accountRefId != bytes32(0), "ISO20022Router: zero accountRefId"); require(m.token != address(0), "ISO20022Router: zero token"); require(m.amount > 0, "ISO20022Router: zero amount"); // Determine rail from message type RailTypes.Rail rail = _determineRailFromMessageType(m.msgType); // Create trigger IRailTriggerRegistry.Trigger memory trigger = IRailTriggerRegistry.Trigger({ id: 0, // Will be assigned by registry rail: rail, msgType: m.msgType, accountRefId: m.accountRefId, walletRefId: bytes32(0), // Will be resolved by orchestrator if needed token: m.token, amount: m.amount, currencyCode: m.currencyCode, instructionId: m.instructionId, state: RailTypes.State.CREATED, createdAt: 0, // Will be set by registry updatedAt: 0 // Will be set by registry }); triggerId = triggerRegistry.createTrigger(trigger); _triggerIdByInstructionId[m.instructionId] = triggerId; emit OutboundSubmitted(triggerId, m.msgType, m.instructionId, m.accountRefId); } /** * @notice Returns the trigger ID for a given instruction ID * @param instructionId The instruction ID * @return The trigger ID (0 if not found) */ function getTriggerIdByInstructionId(bytes32 instructionId) external view override returns (uint256) { return _triggerIdByInstructionId[instructionId]; } /** * @notice Determines the rail type from an ISO-20022 message type * @dev Simplified implementation - in production, this would use a mapping table * @param msgType The message type * @return The rail type */ function _determineRailFromMessageType(bytes32 msgType) internal pure returns (RailTypes.Rail) { // This is a simplified implementation // In production, you would have a mapping table or more sophisticated logic // For now, we'll use a default based on message family if (msgType == ISO20022Types.PAIN_001 || msgType == ISO20022Types.PACS_008) { // These are commonly used across rails, default to SWIFT return RailTypes.Rail.SWIFT; } // Default to SWIFT for unknown types return RailTypes.Rail.SWIFT; } }