- 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.
202 lines
6.9 KiB
Solidity
202 lines
6.9 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.19;
|
|
|
|
import "@openzeppelin/contracts/access/AccessControl.sol";
|
|
import "./interfaces/IRailTriggerRegistry.sol";
|
|
import "./libraries/RailTypes.sol";
|
|
|
|
/**
|
|
* @title RailTriggerRegistry
|
|
* @notice Canonical registry of payment rails, message types, and trigger lifecycle
|
|
* @dev Manages trigger state machine and enforces idempotency by instructionId
|
|
*/
|
|
contract RailTriggerRegistry is IRailTriggerRegistry, AccessControl {
|
|
bytes32 public constant RAIL_OPERATOR_ROLE = keccak256("RAIL_OPERATOR_ROLE");
|
|
bytes32 public constant RAIL_ADAPTER_ROLE = keccak256("RAIL_ADAPTER_ROLE");
|
|
|
|
uint256 private _nextTriggerId;
|
|
mapping(uint256 => Trigger) private _triggers;
|
|
mapping(bytes32 => uint256) private _triggerByInstructionId; // instructionId => triggerId
|
|
|
|
/**
|
|
* @notice Initializes the registry with an admin address
|
|
* @param admin Address that will receive DEFAULT_ADMIN_ROLE
|
|
*/
|
|
constructor(address admin) {
|
|
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
|
}
|
|
|
|
/**
|
|
* @notice Creates a new trigger
|
|
* @dev Requires RAIL_OPERATOR_ROLE. Enforces idempotency by instructionId.
|
|
* @param t Trigger struct with all required fields
|
|
* @return id The assigned trigger ID
|
|
*/
|
|
function createTrigger(Trigger calldata t) external override onlyRole(RAIL_OPERATOR_ROLE) returns (uint256 id) {
|
|
require(t.token != address(0), "RailTriggerRegistry: zero token");
|
|
require(t.amount > 0, "RailTriggerRegistry: zero amount");
|
|
require(t.accountRefId != bytes32(0), "RailTriggerRegistry: zero accountRefId");
|
|
require(t.instructionId != bytes32(0), "RailTriggerRegistry: zero instructionId");
|
|
require(t.state == RailTypes.State.CREATED, "RailTriggerRegistry: invalid initial state");
|
|
|
|
// Enforce idempotency: check if instructionId already exists
|
|
require(!instructionIdExists(t.instructionId), "RailTriggerRegistry: duplicate instructionId");
|
|
|
|
id = _nextTriggerId++;
|
|
uint64 timestamp = uint64(block.timestamp);
|
|
|
|
_triggers[id] = Trigger({
|
|
id: id,
|
|
rail: t.rail,
|
|
msgType: t.msgType,
|
|
accountRefId: t.accountRefId,
|
|
walletRefId: t.walletRefId,
|
|
token: t.token,
|
|
amount: t.amount,
|
|
currencyCode: t.currencyCode,
|
|
instructionId: t.instructionId,
|
|
state: RailTypes.State.CREATED,
|
|
createdAt: timestamp,
|
|
updatedAt: timestamp
|
|
});
|
|
|
|
_triggerByInstructionId[t.instructionId] = id;
|
|
|
|
emit TriggerCreated(
|
|
id,
|
|
uint8(t.rail),
|
|
t.msgType,
|
|
t.instructionId,
|
|
t.accountRefId,
|
|
t.token,
|
|
t.amount
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @notice Updates the state of a trigger
|
|
* @dev Requires RAIL_ADAPTER_ROLE. Enforces valid state transitions.
|
|
* @param id The trigger ID
|
|
* @param newState The new state
|
|
* @param reason Optional reason code for the state change
|
|
*/
|
|
function updateState(
|
|
uint256 id,
|
|
RailTypes.State newState,
|
|
bytes32 reason
|
|
) external override onlyRole(RAIL_ADAPTER_ROLE) {
|
|
require(triggerExists(id), "RailTriggerRegistry: trigger not found");
|
|
|
|
Trigger storage trigger = _triggers[id];
|
|
RailTypes.State oldState = trigger.state;
|
|
|
|
// Validate state transition
|
|
require(isValidStateTransition(oldState, newState), "RailTriggerRegistry: invalid state transition");
|
|
|
|
trigger.state = newState;
|
|
trigger.updatedAt = uint64(block.timestamp);
|
|
|
|
emit TriggerStateUpdated(id, uint8(oldState), uint8(newState), reason);
|
|
}
|
|
|
|
/**
|
|
* @notice Returns a trigger by ID
|
|
* @param id The trigger ID
|
|
* @return The trigger struct
|
|
*/
|
|
function getTrigger(uint256 id) external view override returns (Trigger memory) {
|
|
require(triggerExists(id), "RailTriggerRegistry: trigger not found");
|
|
return _triggers[id];
|
|
}
|
|
|
|
/**
|
|
* @notice Returns a trigger by instructionId
|
|
* @param instructionId The instruction ID
|
|
* @return The trigger struct
|
|
*/
|
|
function getTriggerByInstructionId(bytes32 instructionId) external view override returns (Trigger memory) {
|
|
uint256 id = _triggerByInstructionId[instructionId];
|
|
require(id != 0 || _triggers[id].instructionId == instructionId, "RailTriggerRegistry: trigger not found");
|
|
return _triggers[id];
|
|
}
|
|
|
|
/**
|
|
* @notice Checks if a trigger exists
|
|
* @param id The trigger ID
|
|
* @return true if trigger exists
|
|
*/
|
|
function triggerExists(uint256 id) public view override returns (bool) {
|
|
return _triggers[id].id == id && _triggers[id].instructionId != bytes32(0);
|
|
}
|
|
|
|
/**
|
|
* @notice Checks if an instructionId already exists
|
|
* @param instructionId The instruction ID to check
|
|
* @return true if instructionId exists
|
|
*/
|
|
function instructionIdExists(bytes32 instructionId) public view override returns (bool) {
|
|
uint256 id = _triggerByInstructionId[instructionId];
|
|
return id != 0 && _triggers[id].instructionId == instructionId;
|
|
}
|
|
|
|
/**
|
|
* @notice Validates a state transition
|
|
* @param from Current state
|
|
* @param to Target state
|
|
* @return true if transition is valid
|
|
*/
|
|
function isValidStateTransition(
|
|
RailTypes.State from,
|
|
RailTypes.State to
|
|
) internal pure returns (bool) {
|
|
// Cannot transition to CREATED
|
|
if (to == RailTypes.State.CREATED) {
|
|
return false;
|
|
}
|
|
|
|
// Terminal states cannot transition
|
|
if (
|
|
from == RailTypes.State.SETTLED ||
|
|
from == RailTypes.State.REJECTED ||
|
|
from == RailTypes.State.CANCELLED ||
|
|
from == RailTypes.State.RECALLED
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
// Valid transitions
|
|
if (from == RailTypes.State.CREATED) {
|
|
return to == RailTypes.State.VALIDATED || to == RailTypes.State.REJECTED || to == RailTypes.State.CANCELLED;
|
|
}
|
|
|
|
if (from == RailTypes.State.VALIDATED) {
|
|
return (
|
|
to == RailTypes.State.SUBMITTED_TO_RAIL ||
|
|
to == RailTypes.State.REJECTED ||
|
|
to == RailTypes.State.CANCELLED
|
|
);
|
|
}
|
|
|
|
if (from == RailTypes.State.SUBMITTED_TO_RAIL) {
|
|
return (
|
|
to == RailTypes.State.PENDING ||
|
|
to == RailTypes.State.REJECTED ||
|
|
to == RailTypes.State.CANCELLED ||
|
|
to == RailTypes.State.RECALLED
|
|
);
|
|
}
|
|
|
|
if (from == RailTypes.State.PENDING) {
|
|
return (
|
|
to == RailTypes.State.SETTLED ||
|
|
to == RailTypes.State.REJECTED ||
|
|
to == RailTypes.State.CANCELLED ||
|
|
to == RailTypes.State.RECALLED
|
|
);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|