Integration guide
View raw

Integrating with Quiver

This guide shows how to consume Quiver randomness from a smart contract (push and pull flows) and from off-chain TypeScript. For the mechanism behind it, see protocol-design.md.


Choosing a flow

Push (requestWithCallback)Pull (requestreveal)
DeliveryCoordinator calls your quiverCallbackYou call reveal and read the return value
Who fulfillsThe provider's Fletcher keeperYou (or anyone with both secrets)
Your value secret?No (emitted for the keeper)Yes, until you reveal
blockhash mixingNoOptional
Best forGames, mints, most dapps (hands-off UX)Max trust-minimization; you run your own reveal

Most integrations want the push flow. Use the pull flow when you need the requester's contribution to stay secret until reveal and/or want blockhash folded in.


Push flow (recommended)

1. Inherit QuiverConsumer

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {QuiverConsumer} from "quiver/QuiverConsumer.sol";

contract Lottery is QuiverConsumer {
    constructor(address quiver, address provider) QuiverConsumer(quiver, provider) {}

    // Draw randomness. `msg.value` (or the contract's balance) must cover the fee.
    function drawWinner(bytes32 userRandom) external returns (uint64 seq) {
        seq = _requestRandomness(userRandom);
        // record `seq -> round` so the callback can resolve the right draw
    }

    // Called ONLY by the coordinator. Put your logic here.
    function _fulfillRandomness(uint64 seq, address /*provider*/, bytes32 rnd) internal override {
        uint256 winnerIndex = uint256(rnd) % participants.length;
        // settle the round with `winnerIndex`
    }
}

The base contract handles the coordinator-only check on the callback, quoting/paying the fee, and dispatching to _fulfillRandomness. You never write the raw quiverCallback.

2. Fund the fee

_requestRandomness calls requestWithCallback{value: fee} using the consumer contract's balance. Ensure the contract holds enough ETH, or forward msg.value in your request function and keep a buffer. Read the current fee with _randomnessFee().

3. Generate a good userRandomNumber

Pass fresh, unpredictable 32 bytes each request. Off-chain callers should use a CSPRNG (QuiverClient.generateUserRandom()); on-chain, derive from values not known to the provider in advance where possible. Even a weak user value cannot let the provider bias the result, but a predictable one weakens your independent contribution.

Callback rules (important)

  • Keep it lean and non-reverting. If _fulfillRandomness reverts or runs out of gas, the coordinator buffers the randomness and emits CallbackFailed; recover it later via retryCallback(provider, seq). Don't rely on this as normal flow.
  • Be idempotent / defensive. Ignore unknown or already-resolved sequence numbers (see the CoinFlip/DiceGame examples), since retryCallback can re-invoke you.
  • Never trust tx.origin or re-enter assumptions. Only msg.sender == coordinator is guaranteed (enforced for you by the base contract).

Pull flow

Use the coordinator directly (or the SDK). You keep userRandom secret and reveal it yourself.

import {IQuiverCoordinator} from "quiver/interfaces/IQuiverCoordinator.sol";

// 1. Commit
bytes32 userRandom = /* your secret 32 bytes */;
bytes32 commitment = coordinator.constructUserCommitment(userRandom);
uint64 seq = coordinator.request{value: coordinator.getFee(provider)}(provider, commitment, true /*useBlockhash*/);

// 2. Fetch providerRevelation for `seq` from the provider's endpoint (off-chain),
//    then reveal:
bytes32 rnd = coordinator.reveal(provider, seq, userRandom, providerRevelation);

With useBlockhash = true, reveal within 256 blocks of the request (older block hashes are unavailable on-chain and would fold in bytes32(0)).


Off-chain with @quiver/sdk

import {QuiverClient, robinhoodTestnet} from "@quiver/sdk";
import {createPublicClient, createWalletClient, http} from "viem";
import {privateKeyToAccount} from "viem/accounts";

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const publicClient = createPublicClient({chain: robinhoodTestnet, transport: http()});
const walletClient = createWalletClient({account, chain: robinhoodTestnet, transport: http()});

const quiver = new QuiverClient({coordinator, publicClient, walletClient});

// Push flow: request, then await the RandomnessRevealed event
const {sequenceNumber} = await quiver.requestRandomness(provider);
const {randomNumber} = await quiver.waitForFulfillment(provider, sequenceNumber);

// Pull flow: request, then reveal yourself
const userRandom = QuiverClient.generateUserRandom();
const {sequenceNumber: s} = await quiver.request(provider, userRandom, {useBlockhash: true});
const providerRevelation = /* from the provider's endpoint */;
const {randomNumber: rnd} = await quiver.reveal(provider, s, userRandom, providerRevelation);

Hash-chain helpers (commitmentOf, revelationOf, combineRandomValues, userCommitmentOf) are also exported for tooling and tests.


Testing your integration (Foundry)

You don't need a live provider to test — build the chain in-process with the test helper and reveal manually. See test/examples/Examples.t.sol for a complete pattern:

import {HashChainLib} from "quiver-test/helpers/HashChain.sol"; // or copy it in

bytes32 seed = keccak256("test-seed");
uint64 N = 64;
vm.prank(provider);
coordinator.register(0, HashChainLib.commitment(seed, N), "", N, 100, "");

// request via your consumer, then fulfill as the provider would:
coordinator.revealWithCallback(provider, seq, userRandom, HashChainLib.revelation(seed, N, seq));

Common pitfalls

  • Fee changes. Quote getFee immediately before requesting; providers can update fees. The SDK does this for you. Overpayment is refunded by the coordinator.
  • vm.prank + external helper. In tests, computing the commitment via an external call to the coordinator consumes a pending vm.prank. Compute it locally (keccak256(abi.encodePacked(userRandom))) instead.
  • Assuming instant fulfillment. The push flow is asynchronous — design your UX around a pending → resolved lifecycle keyed by the sequence number.
  • Reusing a userRandomNumber. Use a fresh value per request.