// SPDX-License-Identifier: UNLICENSED // Proprietary - (c) 2026 Durian (durianfun). All rights reserved. Not open source; for Durianfun + authorized integrations only. See /LICENSE. pragma solidity 0.8.24; import "./IEntropyConsumer.sol"; import "./DurianEntropyV2.sol"; /// @title EntropyConsumer /// @notice Inherit this and implement ONE function (`_onRandomness`) to wire your app to /// DurianEntropyV2. It handles the protocol fee, callback auth, double-delivery protection, /// a `settle()` recovery path, and a `_tryReveal` helper for self-cranking (no keeper). /// /// @dev The no-keeper pattern: settlement is permissionless, so the party who WANTS the outcome /// settles it. For a sign-once feel in an active app, "self-crank" — when a new user acts, /// settle the previous pending request in the same transaction (`_tryReveal(prevId)`), so the /// previous user's callback fires without them sending a second transaction. The trailing /// request settles when the next user arrives, the requester reveals it themselves, or it /// expires (your contract must then forfeit any escrow — never refund-and-re-roll). abstract contract EntropyConsumer is IEntropyConsumer { DurianEntropyV2 public immutable entropy; /// @dev Guards against the same id being delivered twice (callback AND settle()). mapping(uint256 => bool) public delivered; /// @dev Callback PROVENANCE: ids this consumer actually created via _roll(). Without this, anyone /// could call entropy.requestCallback(seed, , gas), pay the fee, and trigger /// _onRandomness for an id we never originated (spoofed callback). _deliver requires it. mapping(uint256 => bool) public known; constructor(address e) { entropy = DurianEntropyV2(e); } /// @notice Request randomness, paying the current protocol fee. The user signs only the tx that /// calls this; settlement is done later by any party (see self-crank above). /// @dev Your contract must hold `entropy.fee(cbGas)` (collect it from the user). Size your own /// fee/edge to cover both the entropy fee and the eventual reveal gas. function _roll(bytes32 seed, uint32 cbGas) internal returns (uint256 id) { id = entropy.requestCallback{value: entropy.fee(cbGas)}(seed, address(this), cbGas); known[id] = true; // mark as our own so a spoofed callback for a foreign id is rejected } /// @notice Self-crank helper: settle a previously-pending request if it is ready, ignoring any /// failure (not yet ready / already settled / expired). Triggers that request's callback. function _tryReveal(uint256 id) internal { try entropy.reveal(id) {} catch {} } /// @notice Cheap capture helper: lock in the blockhash within the 256-block window so the result /// can be revealed later with no deadline. Ignores failures (not ready / already sealed). function _trySeal(uint256 id) internal { try entropy.seal(id) {} catch {} } /// @inheritdoc IEntropyConsumer /// @dev Only DurianEntropyV2 may call this. Reverting here is safe: entropy catches it and you can /// recover via settle(). function onRandomness(uint256 id, uint256 rng) external override { require(msg.sender == address(entropy), "only entropy"); _deliver(id, rng); } /// @notice Recovery: if the on-reveal callback failed, anyone can poke this once the request is /// FULFILLED to deliver the stored result exactly once. function settle(uint256 id) external { require(entropy.status(id) == DurianEntropyV2.Status.FULFILLED, "not fulfilled"); _deliver(id, entropy.results(id)); } function _deliver(uint256 id, uint256 rng) private { require(known[id], "unknown request"); // only ids WE created via _roll (anti-spoof) require(!delivered[id], "already delivered"); delivered[id] = true; _onRandomness(id, rng); } /// @dev Implement your settlement logic here. Called exactly once per id. function _onRandomness(uint256 id, uint256 rng) internal virtual; }