Blockchain / Global / Address
Contract
0x50AAEE5ECE41627F2bc4b64Ad29a4659a5C018cb
- Balance
- 81.489501 SOMI
- Transactions sent
- 1
- Type
- Contract · 8,915 bytes of code
SomniaVRFCoordinator Verified
- Compiler
- solc 0.8.30+commit.73712a01
- Language
- Solidity
- Optimization
- Yes, 200 runs
- EVM version
- shanghai
- License
- —
- Verified
- 2026-10-12
src/SomniaVRFCoordinator.sol
and 6 more files
Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import {ISomniaVRFCoordinator} from "./interfaces/ISomniaVRFCoordinator.sol";
import {IVRFConsumer} from "./interfaces/IVRFConsumer.sol";
import {ISomniaEventHandler} from "./lib/reactivity/ISomniaEventHandler.sol";
import {ISomniaReactivityPrecompile} from "./lib/reactivity/ISomniaReactivityPrecompile.sol";
import {SomniaExtensions} from "./lib/reactivity/SomniaExtensions.sol";
import {
IAgentRequester,
IJsonApiAgent,
Response as AgentResponse,
Request as AgentRequestStruct,
ResponseStatus as AgentResponseStatus
} from "./lib/agents/ISomniaAgents.sol";
/// @title SomniaVRFCoordinator
/// @notice Reactivity-native VRF coordinator for Somnia, with two entropy modes:
///
/// Mode A — blockhash-only. Cheap, fast, testnet-grade.
/// Mode B — drand mix (via Somnia Agents JSON API Request). Trust-minimized,
/// publicly verifiable, ~150x the cost of Mode A.
///
/// The coordinator itself pays every request out of its own STT balance.
/// Consumers only pay their tx gas.
contract SomniaVRFCoordinator is ISomniaVRFCoordinator, ISomniaEventHandler {
// ─── Constants ────────────────────────────────────────────────────────────
uint16 public constant MIN_COMMIT_DELAY_BLOCKS = 16;
uint16 public constant MAX_COMMIT_DELAY_BLOCKS = 200;
uint32 public constant MAX_CALLBACK_GAS_LIMIT = 2_500_000;
uint32 public constant MAX_NUM_WORDS = 500;
address public constant SOMNIA_REACTIVITY_PRECOMPILE = address(0x0100);
// Somnia Agents platform on Shannon testnet + JSON API Request agent id.
// These are fixed per network; a mainnet-target coordinator would set the
// platform address in the constructor instead of a constant.
address public immutable AGENT_PLATFORM;
uint256 public constant JSON_API_AGENT_ID = 13174292974160097713;
uint256 public constant JSON_API_PRICE_PER_AGENT = 0.03 ether;
uint256 public constant AGENT_SUBCOMMITTEE_SIZE = 3;
// Mainnet deployments set this to `false` to reject Mode A (blockhash-only)
// requests at request time. Testnet leaves it enabled so the coin-flip demo
// can show both modes side by side. Immutable, set once at construction.
bool public immutable FAST_MODE_ENABLED;
// drand League of Entropy — default mainnet chain, /public/latest returns
// the freshest round. The selector picks out the `randomness` field, a
// 64-char hex string (32-byte BLS-derived hash). We keccak it with the
// captured blockhash to form the final seed.
string public constant DRAND_URL = "https://api.drand.sh/public/latest";
string public constant DRAND_SELECTOR = "randomness";
// ─── State ────────────────────────────────────────────────────────────────
address public owner;
uint256 public nextRequestId = 1;
mapping(uint256 => Request) internal _requests;
mapping(uint64 => uint256[]) internal _queuedAt;
mapping(uint256 => uint256) internal _drandAgentToVrf; // agent requestId → VRF requestId
// ─── Errors + events (owner) ──────────────────────────────────────────────
error NotOwner();
error TransferFailed();
event OwnershipTransferred(address indexed previous, address indexed next);
event CoordinatorFunded(address indexed from, uint256 amount, uint256 newBalance);
event CoordinatorWithdrawn(address indexed to, uint256 amount);
modifier onlyOwner() {
if (msg.sender != owner) revert NotOwner();
_;
}
// ─── Construction ─────────────────────────────────────────────────────────
constructor(address _owner, address _agentPlatform, bool _fastModeEnabled) payable {
owner = _owner;
AGENT_PLATFORM = _agentPlatform;
FAST_MODE_ENABLED = _fastModeEnabled;
emit OwnershipTransferred(address(0), _owner);
if (msg.value > 0) emit CoordinatorFunded(msg.sender, msg.value, address(this).balance);
}
// ─── Requests ─────────────────────────────────────────────────────────────
function requestRandomWords(RandomWordsRequest calldata req)
external
override
returns (uint256 requestId)
{
if (req.commitDelayBlocks < MIN_COMMIT_DELAY_BLOCKS) {
revert CommitDelayTooShort(req.commitDelayBlocks, MIN_COMMIT_DELAY_BLOCKS);
}
if (req.commitDelayBlocks > MAX_COMMIT_DELAY_BLOCKS) {
revert CommitDelayTooLong(req.commitDelayBlocks, MAX_COMMIT_DELAY_BLOCKS);
}
if (req.callbackGasLimit > MAX_CALLBACK_GAS_LIMIT) {
revert CallbackGasTooHigh(req.callbackGasLimit, MAX_CALLBACK_GAS_LIMIT);
}
if (req.numWords == 0 || req.numWords > MAX_NUM_WORDS) {
revert NumWordsOutOfRange(req.numWords, 1, MAX_NUM_WORDS);
}
if (!req.useVerifiableEntropy && !FAST_MODE_ENABLED) {
revert FastModeDisabled();
}
requestId = nextRequestId++;
uint64 revealBlock = uint64(block.number) + req.commitDelayBlocks;
_requests[requestId] = Request({
consumer: msg.sender,
callbackGasLimit: req.callbackGasLimit,
numWords: req.numWords,
revealBlock: revealBlock,
useVerifiableEntropy: req.useVerifiableEntropy,
fulfilled: false,
capturedBlockhash: bytes32(0)
});
_queuedAt[revealBlock].push(requestId);
if (_queuedAt[revealBlock].length == 1) {
SomniaExtensions.scheduleSubscriptionAtBlock(
address(this),
revealBlock,
SomniaExtensions.SubscriptionOptions({
priorityFeePerGas: SomniaExtensions.DEFAULT_PRIORITY_FEE_PER_GAS,
maxFeePerGas: SomniaExtensions.DEFAULT_MAX_FEE_PER_GAS,
gasLimit: SomniaExtensions.DEFAULT_HANDLER_GAS_LIMIT
})
);
}
emit RandomWordsRequested(
requestId,
msg.sender,
revealBlock,
req.numWords,
req.callbackGasLimit,
req.useVerifiableEntropy
);
}
// ─── Reactivity callback ──────────────────────────────────────────────────
function onEvent(address, /* emitter */ bytes32[] calldata eventTopics, bytes calldata /* data */)
external
override
{
if (msg.sender != SOMNIA_REACTIVITY_PRECOMPILE) revert NotPrecompile(msg.sender);
uint64 blockNumber = uint64(uint256(eventTopics[1]));
uint256[] memory ids = _queuedAt[blockNumber];
delete _queuedAt[blockNumber];
for (uint256 i = 0; i < ids.length; i++) {
_revealOrKickDrand(ids[i], blockNumber);
}
}
function _revealOrKickDrand(uint256 requestId, uint64 blockNumber) internal {
Request storage r = _requests[requestId];
if (r.consumer == address(0)) revert UnknownRequest(requestId);
if (r.fulfilled) return;
bytes32 bh = blockhash(uint256(blockNumber) - 1);
r.capturedBlockhash = bh;
emit BlockhashCaptured(requestId, blockNumber, bh);
if (!r.useVerifiableEntropy) {
_fulfillFromSeed(requestId, keccak256(abi.encode(bh, requestId, r.consumer, blockNumber)));
} else {
_kickDrandRequest(requestId);
}
}
function _kickDrandRequest(uint256 requestId) internal {
bytes memory payload = abi.encodeWithSelector(
IJsonApiAgent.fetchString.selector,
DRAND_URL,
DRAND_SELECTOR
);
IAgentRequester platform = IAgentRequester(AGENT_PLATFORM);
uint256 deposit = platform.getRequestDeposit()
+ JSON_API_PRICE_PER_AGENT * AGENT_SUBCOMMITTEE_SIZE;
// Best-effort; failure emits a fulfilled=false event so consumers can detect it.
try platform.createRequest{value: deposit}(
JSON_API_AGENT_ID,
address(this),
this.handleDrandResponse.selector,
payload
) returns (uint256 agentReqId) {
_drandAgentToVrf[agentReqId] = requestId;
emit DrandRequested(requestId, agentReqId);
} catch {
Request storage r = _requests[requestId];
r.fulfilled = true;
emit RandomWordsFulfilled(requestId, r.consumer, r.revealBlock, false, true);
}
}
// ─── Agents platform callback ─────────────────────────────────────────────
/// @notice Called by the Somnia Agents platform when the JSON API validator
/// subcommittee reaches consensus on a drand fetch.
function handleDrandResponse(
uint256 agentRequestId,
AgentResponse[] memory responses,
AgentResponseStatus status,
AgentRequestStruct memory /* details */
) external {
if (msg.sender != AGENT_PLATFORM) revert NotAgentPlatform(msg.sender);
uint256 vrfRequestId = _drandAgentToVrf[agentRequestId];
if (vrfRequestId == 0) revert UnknownAgentRequest(agentRequestId);
delete _drandAgentToVrf[agentRequestId];
Request storage r = _requests[vrfRequestId];
if (r.fulfilled) revert DrandAlreadyResolved(vrfRequestId);
// Consensus must have succeeded; a failed drand fetch fulfils the VRF
// as failed so consumers can retry rather than block indefinitely.
if (status != AgentResponseStatus.Success || responses.length == 0) {
r.fulfilled = true;
emit RandomWordsFulfilled(vrfRequestId, r.consumer, r.revealBlock, false, true);
return;
}
// Every validator ABI-encoded the same string; take the first entry.
string memory drandHex = abi.decode(responses[0].result, (string));
bytes memory drandBytes = bytes(drandHex);
bytes32 seed = keccak256(
abi.encode(
drandBytes,
r.capturedBlockhash,
vrfRequestId,
r.consumer,
r.revealBlock
)
);
_fulfillFromSeed(vrfRequestId, seed);
}
// ─── Fulfillment ──────────────────────────────────────────────────────────
function _fulfillFromSeed(uint256 requestId, bytes32 seed) internal {
Request storage r = _requests[requestId];
r.fulfilled = true;
uint256 n = r.numWords;
uint256[] memory words = new uint256[](n);
for (uint256 i = 0; i < n; i++) {
words[i] = uint256(keccak256(abi.encode(seed, i)));
}
bool ok;
try IVRFConsumer(r.consumer).rawFulfillRandomWords{gas: r.callbackGasLimit}(requestId, words) {
ok = true;
} catch {
ok = false;
}
emit RandomWordsFulfilled(requestId, r.consumer, r.revealBlock, ok, r.useVerifiableEntropy);
}
// ─── Views ────────────────────────────────────────────────────────────────
function getRequest(uint256 requestId) external view override returns (Request memory) {
return _requests[requestId];
}
function pendingRequestsAt(uint64 blockNumber) external view returns (uint256[] memory) {
return _queuedAt[blockNumber];
}
function vrfRequestOfDrand(uint256 agentRequestId) external view returns (uint256) {
return _drandAgentToVrf[agentRequestId];
}
// ─── Funding and admin ────────────────────────────────────────────────────
receive() external payable {
emit CoordinatorFunded(msg.sender, msg.value, address(this).balance);
}
function withdraw(address payable to, uint256 amount) external onlyOwner {
(bool ok,) = to.call{value: amount}("");
if (!ok) revert TransferFailed();
emit CoordinatorWithdrawn(to, amount);
}
function transferOwnership(address next) external onlyOwner {
emit OwnershipTransferred(owner, next);
owner = next;
}
}
Other files
src/interfaces/ISomniaVRFCoordinator.sol
// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; /// @title ISomniaVRFCoordinator /// @notice Reactivity-native VRF coordinator for Somnia. Consumers request /// random words; the coordinator delivers them via a callback after /// a commit delay. /// /// Two entropy modes: /// /// Mode A — fast (`useVerifiableEntropy = false`) /// Entropy from `blockhash(revealBlock - 1)`. Cheap and low-latency, /// but the block producer for the reveal block can influence it. /// Testnet-grade only. /// /// Mode B — verifiable (`useVerifiableEntropy = true`) /// Entropy mixes drand (League of Entropy publicly-verifiable /// beacon) with the blockhash. The coordinator fires a Somnia /// Agents JSON API Request at reveal time; the resulting seed is /// `keccak256(drand, blockhash, requestId, consumer, revealBlock)`. /// Compromising either drand or the reveal-block producer alone /// does not compromise the result. interface ISomniaVRFCoordinator { struct RandomWordsRequest { uint32 callbackGasLimit; uint16 commitDelayBlocks; uint32 numWords; bool useVerifiableEntropy; } struct Request { address consumer; uint32 callbackGasLimit; uint32 numWords; uint64 revealBlock; bool useVerifiableEntropy; bool fulfilled; bytes32 capturedBlockhash; } event RandomWordsRequested( uint256 indexed requestId, address indexed consumer, uint64 revealBlock, uint32 numWords, uint32 callbackGasLimit, bool useVerifiableEntropy ); event BlockhashCaptured( uint256 indexed requestId, uint64 revealBlock, bytes32 blockhashValue ); event DrandRequested( uint256 indexed requestId, uint256 indexed agentRequestId ); event RandomWordsFulfilled( uint256 indexed requestId, address indexed consumer, uint64 revealBlock, bool callbackSuccess, bool usedVerifiableEntropy ); error CommitDelayTooShort(uint16 provided, uint16 minimum); error CommitDelayTooLong(uint16 provided, uint16 maximum); error CallbackGasTooHigh(uint32 provided, uint32 maximum); error NumWordsOutOfRange(uint32 provided, uint32 minimum, uint32 maximum); error UnknownRequest(uint256 requestId); error UnknownAgentRequest(uint256 agentRequestId); error NotPrecompile(address caller); error NotAgentPlatform(address caller); error DrandAlreadyResolved(uint256 requestId); error DrandResponseFailed(uint8 status); error FastModeDisabled(); /// @notice Request `numWords` uniformly random uint256 values, delivered via /// the caller's `fulfillRandomWords(requestId, uint256[] words)` callback /// after `commitDelayBlocks` blocks (Mode A) or after the drand round /// following that block resolves (Mode B). function requestRandomWords(RandomWordsRequest calldata req) external returns (uint256 requestId); /// @notice Read a pending or historical request. function getRequest(uint256 requestId) external view returns (Request memory); }src/interfaces/IVRFConsumer.sol
// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; /// @title IVRFConsumer /// @notice Callback interface a VRF consumer must implement. /// Signature mirrors Chainlink's VRFConsumerBaseV2Plus for portability. interface IVRFConsumer { function rawFulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) external; }src/lib/agents/ISomniaAgents.sol
// SPDX-License-Identifier: MIT pragma solidity ^0.8.30; /// @notice Types and interfaces for the Somnia Agents platform. /// Vendored from somnia-chain/somnia-agents-docs so the coordinator can /// fire an off-chain agent (here: JSON API Request → drand) alongside /// the Reactivity BlockTick that carries the blockhash side of the seed. enum ConsensusType { Majority, Threshold } enum ResponseStatus { None, Pending, Success, Failed, TimedOut } struct Response { address validator; bytes result; ResponseStatus status; uint256 receipt; uint256 timestamp; uint256 executionCost; } struct Request { uint256 id; address requester; address callbackAddress; bytes4 callbackSelector; address[] subcommittee; Response[] responses; uint256 responseCount; uint256 failureCount; uint256 threshold; uint256 createdAt; uint256 deadline; ResponseStatus status; ConsensusType consensusType; uint256 remainingBudget; uint256 perAgentBudget; } interface IAgentRequester { function createRequest( uint256 agentId, address callbackAddress, bytes4 callbackSelector, bytes calldata payload ) external payable returns (uint256 requestId); function getRequestDeposit() external view returns (uint256); } /// @notice ABI of the JSON API Request base agent (agentId 13174292974160097713). interface IJsonApiAgent { function fetchString(string calldata url, string calldata selector) external returns (string memory); }src/lib/reactivity/ISomniaEventHandler.sol
// SPDX-License-Identifier: MIT // Copyright (c) 2026 Somnia Foundation pragma solidity 0.8.30; /// @notice Somnia event handler interface /// @title ISomniaEventHandler /// @author Somnia Foundation interface ISomniaEventHandler { /** * @notice Default function invoked for a matching reactivity subscription filter * @param emitter Smart contract that emitted the EVM event log(s) * @param eventTopics List of event topics associated with the event (event signature being first param) * @param data Event data for non-indexed event arguments. May be empty if event only has indexed params */ function onEvent( address emitter, bytes32[] calldata eventTopics, bytes calldata data ) external; }src/lib/reactivity/ISomniaReactivityPrecompile.sol
// SPDX-License-Identifier: MIT // Copyright (c) 2026 Somnia Foundation pragma solidity 0.8.30; /// @notice Somnia reactivity precompile interface and related data structures /// @title ISomniaReactivityPrecompile /// @author Somnia Foundation interface ISomniaReactivityPrecompile { /// @notice Data structure representing a subscription struct SubscriptionData { // Topic filter, use 0x0 to indicate a wildcard or unused topic bytes32[4] eventTopics; // Origin (tx.origin) filter, use address(0) to indicate a wildcard address origin; // Reserved for future use, use address(0) address caller; // Contract emitting the event filter, use address(0) to indicate a wildcard address emitter; // The address of the contract that will handle the event address handlerContractAddress; // The function selector in the handler contract, defaults to ISomniaEventHandler.onEvent bytes4 handlerFunctionSelector; // Extra fee per gas paid to validators to prioritize this event handling uint64 priorityFeePerGas; // Maximum fee per gas the subscriber is willing to pay (base fee + priority fee) uint64 maxFeePerGas; // Maximum gas that will be provisioned per subscription callback uint64 gasLimit; // Whether the event handling is guaranteed, i.e. moved to the next block if current is full bool isGuaranteed; // Whether multiple events can be coalesced into a single handling call per block bool isCoalesced; } /// @notice System tick emitted at the end of each block. /// @param blockNumber Current block number. event BlockTick(uint64 indexed blockNumber); /// @notice System tick emitted at epoch boundaries. /// @param epochNumber Current epoch number. /// @param blockNumber Current block number. event EpochTick(uint64 indexed epochNumber, uint64 indexed blockNumber); /// @notice Scheduled system event emitted at the requested timestamp. /// @param timestampMillis Timestamp in milliseconds. event Schedule(uint256 indexed timestampMillis); /// @notice Emitted when a new subscription is created /// @param subscriptionId New subscription identifier. /// @param owner Owner of the created subscription. /// @param subscriptionData Persisted subscription payload. event SubscriptionCreated( uint256 indexed subscriptionId, address indexed owner, SubscriptionData subscriptionData ); /// @notice Emitted when a subscription is removed /// @param subscriptionId Removed subscription identifier. /// @param owner Owner of the removed subscription. event SubscriptionRemoved( uint256 indexed subscriptionId, address indexed owner ); /** * @notice Creates a new Solidity subscription * @dev Cost: SUBSCRIPTION_MANAGEMENT_GAS_COST (~210k GAS) * @dev The caller becomes the owner of the subscription. * @dev The subscription starts being active immediately. * @param subscriptionData Defines the parameters for the subscription incl the handler contract * @return subscriptionId The subscription ID assigned to the new subscription */ function subscribe( SubscriptionData calldata subscriptionData ) external returns (uint256 subscriptionId); /** * @notice Cancels a Solidity event subscription * @dev It can only be called by the owner * @dev Cost: SUBSCRIPTION_MANAGEMENT_GAS_COST (~210K GAS) * @param subscriptionId Unique subscription identifier */ function unsubscribe(uint256 subscriptionId) external; /** * @notice Gets detailed information about a specific subscription * @param subscriptionId The unique subscription identifier * @return subscriptionData The recorded subscription information * @return owner The owner of the subscription that can make changes and pays for invocations */ function getSubscriptionInfo( uint256 subscriptionId ) external view returns (SubscriptionData memory subscriptionData, address owner); }src/lib/reactivity/SomniaExtensions.sol
// SPDX-License-Identifier: MIT // Copyright (c) 2026 Somnia Foundation pragma solidity 0.8.30; import {ISomniaReactivityPrecompile} from "./ISomniaReactivityPrecompile.sol"; import {ISomniaEventHandler} from "./ISomniaEventHandler.sol"; /// @notice The Somnia Reactivity Precompile is a privileged contract at a fixed address /// @title SomniaExtensions /// @author Somnia Foundation library SomniaExtensions { /// @notice Topic/origin/emitter matching criteria for a subscription. struct SubscriptionFilter { bytes32[4] eventTopics; address origin; address emitter; } /// @notice Gas and fee controls for subscription callback execution. struct SubscriptionOptions { uint64 priorityFeePerGas; uint64 maxFeePerGas; uint64 gasLimit; } /// @notice Fixed address for the Somnia Reactivity Precompile. address public constant SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS = address(0x0100); /// @notice Minimum balance required by a subscription owner. uint256 public constant SUBSCRIPTION_OWNER_MINIMUM_BALANCE = 32 ether; /// @notice Minimum protocol base fee per gas used in fee validation. uint256 public constant MINIMUM_BASE_FEE_PER_GAS = 6 gwei; /// @notice Maximum callback gas limit allowed for a single subscription. uint64 public constant MAXIMUM_HANDLER_GAS_LIMIT = 200_000_000; /// @notice Default priority fee per gas used when callers opt into defaults. uint64 public constant DEFAULT_PRIORITY_FEE_PER_GAS = 0; /// @notice Default max fee per gas used when callers opt into defaults. uint64 public constant DEFAULT_MAX_FEE_PER_GAS = 20 gwei; /// @notice Default callback gas limit used when callers opt into defaults. uint64 public constant DEFAULT_HANDLER_GAS_LIMIT = 10_000_000; error HandlerZeroAddress(); error EmptyFilter(); error GasLimitZero(); error GasLimitExceeded(); error InvalidMaxFeePerGas(); error InsufficientBalance(); error TimestampInPast(); error BlockInPast(); error UnsubscribeFailed(); /// @notice Creates a subscription owned by the caller using filter and gas options. /// @param handler Address of the handler contract that receives callbacks. /// @param filter Filter criteria for matching logs. /// @param options Fee and gas options for callback execution. /// @return subscriptionId Newly created subscription identifier. function subscribe( address handler, SubscriptionFilter memory filter, SubscriptionOptions memory options ) internal returns (uint256 subscriptionId) { return _subscribe(handler, filter, options); } /// @notice Creates a schedule subscription for an absolute millisecond timestamp. /// @param handler Address of the handler contract that receives callbacks. /// @param timestampMillis Absolute unix timestamp in milliseconds. /// @param options Fee and gas options for callback execution. /// @return subscriptionId Newly created subscription identifier. function scheduleSubscriptionAtTimestamp( address handler, uint256 timestampMillis, SubscriptionOptions memory options ) internal returns (uint256 subscriptionId) { if (timestampMillis < ((block.timestamp + 1) * 1000) + 1) { revert TimestampInPast(); } SubscriptionFilter memory filter = SubscriptionFilter({ eventTopics: [ ISomniaReactivityPrecompile.Schedule.selector, bytes32(timestampMillis), bytes32(0), bytes32(0) ], origin: address(0), emitter: SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS }); return _subscribe(handler, filter, options); } /// @notice Creates a subscription triggered at a specific future block. /// @param handler Address of the handler contract that receives callbacks. /// @param blockNumber Future block number to trigger on. /// @param options Fee and gas options for callback execution. /// @return subscriptionId Newly created subscription identifier. function scheduleSubscriptionAtBlock( address handler, uint64 blockNumber, SubscriptionOptions memory options ) internal returns (uint256 subscriptionId) { if (blockNumber < block.number + 1) { revert BlockInPast(); } SubscriptionFilter memory filter = SubscriptionFilter({ eventTopics: [ ISomniaReactivityPrecompile.BlockTick.selector, bytes32(uint256(blockNumber)), bytes32(0), bytes32(0) ], origin: address(0), emitter: SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS }); return _subscribe(handler, filter, options); } /// @notice Creates a subscription triggered at a specific epoch. /// @param handler Address of the handler contract that receives callbacks. /// @param epochNumber Epoch number to trigger on. /// @param options Fee and gas options for callback execution. /// @return subscriptionId Newly created subscription identifier. function scheduleSubscriptionAtEpoch( address handler, uint64 epochNumber, SubscriptionOptions memory options ) internal returns (uint256 subscriptionId) { SubscriptionFilter memory filter = SubscriptionFilter({ eventTopics: [ ISomniaReactivityPrecompile.EpochTick.selector, bytes32(uint256(epochNumber)), bytes32(0), bytes32(0) ], origin: address(0), emitter: SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS }); return _subscribe(handler, filter, options); } /// @notice Cancels a subscription. /// @param subscriptionId Existing subscription identifier. function unsubscribe(uint256 subscriptionId) internal { // Low-level call used to bypass false positives from Solidity safety checks // solhint-disable-next-line avoid-low-level-calls (bool success, ) = SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS.call( abi.encodeWithSelector(ISomniaReactivityPrecompile.unsubscribe.selector, subscriptionId) ); if (!success) revert UnsubscribeFailed(); } /// @notice Fetches subscription details and owner for a subscription id. /// @param subscriptionId Existing subscription identifier. /// @return subscriptionData Stored subscription parameters. /// @return owner Address currently owning the subscription. function getSubscriptionInfo( uint256 subscriptionId ) internal view returns ( ISomniaReactivityPrecompile.SubscriptionData memory subscriptionData, address owner ) { return ISomniaReactivityPrecompile(SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS) .getSubscriptionInfo(subscriptionId); } /// @notice Returns the library default fee and gas options for subscriptions. /// @return options Default subscription options struct. function defaultSubscriptionOptions() internal pure returns (SubscriptionOptions memory) { return SubscriptionOptions({ priorityFeePerGas: DEFAULT_PRIORITY_FEE_PER_GAS, maxFeePerGas: DEFAULT_MAX_FEE_PER_GAS, gasLimit: DEFAULT_HANDLER_GAS_LIMIT }); } /// @dev Internal helper used by all public subscription creation functions. function _subscribe( address handler, SubscriptionFilter memory filter, SubscriptionOptions memory options ) private returns (uint256 subscriptionId) { if (handler == address(0)) { revert HandlerZeroAddress(); } if (!_hasAnyFilter(filter)) { revert EmptyFilter(); } if (options.gasLimit == 0) { revert GasLimitZero(); } if (options.gasLimit > MAXIMUM_HANDLER_GAS_LIMIT) { revert GasLimitExceeded(); } if ( options.maxFeePerGas != 0 && options.priorityFeePerGas + MINIMUM_BASE_FEE_PER_GAS > options.maxFeePerGas ) { revert InvalidMaxFeePerGas(); } if (address(this).balance < SUBSCRIPTION_OWNER_MINIMUM_BALANCE) { revert InsufficientBalance(); } return ISomniaReactivityPrecompile(SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS) .subscribe(_buildSubscriptionData(handler, filter, options)); } /// @dev Returns true when at least one filter criterion is non-wildcard. function _hasAnyFilter( SubscriptionFilter memory filter ) private pure returns (bool hasAnyFilter) { return filter.origin != address(0) || filter.emitter != address(0) || filter.eventTopics[0] != bytes32(0) || filter.eventTopics[1] != bytes32(0) || filter.eventTopics[2] != bytes32(0) || filter.eventTopics[3] != bytes32(0); } /// @dev Builds low-level precompile subscription payload from high-level inputs. function _buildSubscriptionData( address handler, SubscriptionFilter memory filter, SubscriptionOptions memory options ) private pure returns ( ISomniaReactivityPrecompile.SubscriptionData memory subscriptionData ) { ISomniaReactivityPrecompile.SubscriptionData memory data = ISomniaReactivityPrecompile.SubscriptionData({ eventTopics: filter.eventTopics, origin: filter.origin, caller: address(0), emitter: filter.emitter, handlerContractAddress: handler, handlerFunctionSelector: ISomniaEventHandler.onEvent.selector, priorityFeePerGas: options.priorityFeePerGas, maxFeePerGas: options.maxFeePerGas, gasLimit: options.gasLimit, isGuaranteed: false, isCoalesced: false }); return data; } }
ABI · 51 entries
json
[
{
"type": "constructor",
"inputs": [
{
"name": "_owner",
"type": "address",
"internalType": "address"
},
{
"name": "_agentPlatform",
"type": "address",
"internalType": "address"
},
{
"name": "_fastModeEnabled",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "payable"
},
{
"name": "BlockInPast",
"type": "error",
"inputs": []
},
{
"name": "CallbackGasTooHigh",
"type": "error",
"inputs": [
{
"name": "provided",
"type": "uint32",
"internalType": "uint32"
},
{
"name": "maximum",
"type": "uint32",
"internalType": "uint32"
}
]
},
{
"name": "CommitDelayTooLong",
"type": "error",
"inputs": [
{
"name": "provided",
"type": "uint16",
"internalType": "uint16"
},
{
"name": "maximum",
"type": "uint16",
"internalType": "uint16"
}
]
},
{
"name": "CommitDelayTooShort",
"type": "error",
"inputs": [
{
"name": "provided",
"type": "uint16",
"internalType": "uint16"
},
{
"name": "minimum",
"type": "uint16",
"internalType": "uint16"
}
]
},
{
"name": "DrandAlreadyResolved",
"type": "error",
"inputs": [
{
"name": "requestId",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "DrandResponseFailed",
"type": "error",
"inputs": [
{
"name": "status",
"type": "uint8",
"internalType": "uint8"
}
]
},
{
"name": "EmptyFilter",
"type": "error",
"inputs": []
},
{
"name": "FastModeDisabled",
"type": "error",
"inputs": []
},
{
"name": "GasLimitExceeded",
"type": "error",
"inputs": []
},
{
"name": "GasLimitZero",
"type": "error",
"inputs": []
},
{
"name": "HandlerZeroAddress",
"type": "error",
"inputs": []
},
{
"name": "InsufficientBalance",
"type": "error",
"inputs": []
},
{
"name": "InvalidMaxFeePerGas",
"type": "error",
"inputs": []
},
{
"name": "NotAgentPlatform",
"type": "error",
"inputs": [
{
"name": "caller",
"type": "address",
"internalType": "address"
}
]
},
{
"name": "NotOwner",
"type": "error",
"inputs": []
},
{
"name": "NotPrecompile",
"type": "error",
"inputs": [
{
"name": "caller",
"type": "address",
"internalType": "address"
}
]
},
{
"name": "NumWordsOutOfRange",
"type": "error",
"inputs": [
{
"name": "provided",
"type": "uint32",
"internalType": "uint32"
},
{
"name": "minimum",
"type": "uint32",
"internalType": "uint32"
},
{
"name": "maximum",
"type": "uint32",
"internalType": "uint32"
}
]
},
{
"name": "TransferFailed",
"type": "error",
"inputs": []
},
{
"name": "UnknownAgentRequest",
"type": "error",
"inputs": [
{
"name": "agentRequestId",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "UnknownRequest",
"type": "error",
"inputs": [
{
"name": "requestId",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "BlockhashCaptured",
"type": "event",
"inputs": [
{
"name": "requestId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
},
{
"name": "revealBlock",
"type": "uint64",
"indexed": false,
"internalType": "uint64"
},
{
"name": "blockhashValue",
"type": "bytes32",
"indexed": false,
"internalType": "bytes32"
}
],
"anonymous": false
},
{
"name": "CoordinatorFunded",
"type": "event",
"inputs": [
{
"name": "from",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "amount",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "newBalance",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "CoordinatorWithdrawn",
"type": "event",
"inputs": [
{
"name": "to",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "amount",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "DrandRequested",
"type": "event",
"inputs": [
{
"name": "requestId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
},
{
"name": "agentRequestId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "OwnershipTransferred",
"type": "event",
"inputs": [
{
"name": "previous",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "next",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"name": "RandomWordsFulfilled",
"type": "event",
"inputs": [
{
"name": "requestId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
},
{
"name": "consumer",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "revealBlock",
"type": "uint64",
"indexed": false,
"internalType": "uint64"
},
{
"name": "callbackSuccess",
"type": "bool",
"indexed": false,
"internalType": "bool"
},
{
"name": "usedVerifiableEntropy",
"type": "bool",
"indexed": false,
"internalType": "bool"
}
],
"anonymous": false
},
{
"name": "RandomWordsRequested",
"type": "event",
"inputs": [
{
"name": "requestId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
},
{
"name": "consumer",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "revealBlock",
"type": "uint64",
"indexed": false,
"internalType": "uint64"
},
{
"name": "numWords",
"type": "uint32",
"indexed": false,
"internalType": "uint32"
},
{
"name": "callbackGasLimit",
"type": "uint32",
"indexed": false,
"internalType": "uint32"
},
{
"name": "useVerifiableEntropy",
"type": "bool",
"indexed": false,
"internalType": "bool"
}
],
"anonymous": false
},
{
"name": "AGENT_PLATFORM",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"name": "AGENT_SUBCOMMITTEE_SIZE",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "DRAND_SELECTOR",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "view"
},
{
"name": "DRAND_URL",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "view"
},
{
"name": "FAST_MODE_ENABLED",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "JSON_API_AGENT_ID",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "JSON_API_PRICE_PER_AGENT",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "MAX_CALLBACK_GAS_LIMIT",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint32",
"internalType": "uint32"
}
],
"stateMutability": "view"
},
{
"name": "MAX_COMMIT_DELAY_BLOCKS",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint16",
"internalType": "uint16"
}
],
"stateMutability": "view"
},
{
"name": "MAX_NUM_WORDS",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint32",
"internalType": "uint32"
}
],
"stateMutability": "view"
},
{
"name": "MIN_COMMIT_DELAY_BLOCKS",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint16",
"internalType": "uint16"
}
],
"stateMutability": "view"
},
{
"name": "SOMNIA_REACTIVITY_PRECOMPILE",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"name": "getRequest",
"type": "function",
"inputs": [
{
"name": "requestId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "tuple",
"components": [
{
"name": "consumer",
"type": "address",
"internalType": "address"
},
{
"name": "callbackGasLimit",
"type": "uint32",
"internalType": "uint32"
},
{
"name": "numWords",
"type": "uint32",
"internalType": "uint32"
},
{
"name": "revealBlock",
"type": "uint64",
"internalType": "uint64"
},
{
"name": "useVerifiableEntropy",
"type": "bool",
"internalType": "bool"
},
{
"name": "fulfilled",
"type": "bool",
"internalType": "bool"
},
{
"name": "capturedBlockhash",
"type": "bytes32",
"internalType": "bytes32"
}
],
"internalType": "struct ISomniaVRFCoordinator.Request"
}
],
"stateMutability": "view"
},
{
"name": "handleDrandResponse",
"type": "function",
"inputs": [
{
"name": "agentRequestId",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "responses",
"type": "tuple[]",
"components": [
{
"name": "validator",
"type": "address",
"internalType": "address"
},
{
"name": "result",
"type": "bytes",
"internalType": "bytes"
},
{
"name": "status",
"type": "uint8",
"internalType": "enum ResponseStatus"
},
{
"name": "receipt",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "timestamp",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "executionCost",
"type": "uint256",
"internalType": "uint256"
}
],
"internalType": "struct Response[]"
},
{
"name": "status",
"type": "uint8",
"internalType": "enum ResponseStatus"
},
{
"name": "",
"type": "tuple",
"components": [
{
"name": "id",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "requester",
"type": "address",
"internalType": "address"
},
{
"name": "callbackAddress",
"type": "address",
"internalType": "address"
},
{
"name": "callbackSelector",
"type": "bytes4",
"internalType": "bytes4"
},
{
"name": "subcommittee",
"type": "address[]",
"internalType": "address[]"
},
{
"name": "responses",
"type": "tuple[]",
"components": [
{
"name": "validator",
"type": "address",
"internalType": "address"
},
{
"name": "result",
"type": "bytes",
"internalType": "bytes"
},
{
"name": "status",
"type": "uint8",
"internalType": "enum ResponseStatus"
},
{
"name": "receipt",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "timestamp",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "executionCost",
"type": "uint256",
"internalType": "uint256"
}
],
"internalType": "struct Response[]"
},
{
"name": "responseCount",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "failureCount",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "threshold",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "createdAt",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "deadline",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "status",
"type": "uint8",
"internalType": "enum ResponseStatus"
},
{
"name": "consensusType",
"type": "uint8",
"internalType": "enum ConsensusType"
},
{
"name": "remainingBudget",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "perAgentBudget",
"type": "uint256",
"internalType": "uint256"
}
],
"internalType": "struct Request"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "nextRequestId",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "onEvent",
"type": "function",
"inputs": [
{
"name": "",
"type": "address",
"internalType": "address"
},
{
"name": "eventTopics",
"type": "bytes32[]",
"internalType": "bytes32[]"
},
{
"name": "",
"type": "bytes",
"internalType": "bytes"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "owner",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"name": "pendingRequestsAt",
"type": "function",
"inputs": [
{
"name": "blockNumber",
"type": "uint64",
"internalType": "uint64"
}
],
"outputs": [
{
"name": "",
"type": "uint256[]",
"internalType": "uint256[]"
}
],
"stateMutability": "view"
},
{
"name": "requestRandomWords",
"type": "function",
"inputs": [
{
"name": "req",
"type": "tuple",
"components": [
{
"name": "callbackGasLimit",
"type": "uint32",
"internalType": "uint32"
},
{
"name": "commitDelayBlocks",
"type": "uint16",
"internalType": "uint16"
},
{
"name": "numWords",
"type": "uint32",
"internalType": "uint32"
},
{
"name": "useVerifiableEntropy",
"type": "bool",
"internalType": "bool"
}
],
"internalType": "struct ISomniaVRFCoordinator.RandomWordsRequest"
}
],
"outputs": [
{
"name": "requestId",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"name": "transferOwnership",
"type": "function",
"inputs": [
{
"name": "next",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "vrfRequestOfDrand",
"type": "function",
"inputs": [
{
"name": "agentRequestId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "withdraw",
"type": "function",
"inputs": [
{
"name": "to",
"type": "address",
"internalType": "address payable"
},
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "receive",
"stateMutability": "payable"
}
]