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 (request → reveal) | |
|---|---|---|
| Delivery | Coordinator calls your quiverCallback | You call reveal and read the return value |
| Who fulfills | The provider's Fletcher keeper | You (or anyone with both secrets) |
| Your value secret? | No (emitted for the keeper) | Yes, until you reveal |
blockhash mixing | No | Optional |
| Best for | Games, 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
_fulfillRandomnessreverts or runs out of gas, the coordinator buffers the randomness and emitsCallbackFailed; recover it later viaretryCallback(provider, seq). Don't rely on this as normal flow. - Be idempotent / defensive. Ignore unknown or already-resolved sequence numbers
(see the
CoinFlip/DiceGameexamples), sinceretryCallbackcan re-invoke you. - Never trust
tx.originor re-enter assumptions. Onlymsg.sender == coordinatoris 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
getFeeimmediately 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 pendingvm.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.