TypeScript SDK to request and fulfill on-chain VRF using DRAND beacons.
- Install:
pnpm add evm-randomness
- Minimal usage:
import { HyperEVMVRF } from "evm-randomness";
const vrf = new HyperEVMVRF({ account: { privateKey: process.env.PRIVATE_KEY! }, chainId: 999 });
const { requestId } = await vrf.requestRandomness({ deadline: BigInt(Math.floor(Date.now()/1000)+120) });
await vrf.fulfillWithWait(requestId);
import { createEphemeralWallet } from "evm-randomness";
const { vrf, address } = await createEphemeralWallet({
chainId: 999,
minBalanceWei: 1_000_000_000_000_000n, // 0.001 HYPE
});
console.log("Send gas to:", address);
const deadline = BigInt(Math.floor(Date.now()/1000)+120);
const { requestId } = await vrf.requestRandomness({ deadline });
await vrf.fulfillWithWait(requestId);
- Request + Fulfill DRAND-powered VRF on HyperEVM
- Typed API, ESM/CJS builds
- Chain-aware defaults (rpc, VRF address, DRAND beacon), configurable
- Policy control (strict/window/none) and wait-until-published helpers
pnpm add evm-randomness
# or
npm i evm-randomness
# or
yarn add evm-randomness
import { HyperEVMVRF } from "evm-randomness";
const vrf = new HyperEVMVRF({
account: { privateKey: process.env.WALLET_PRIVATE_KEY! },
// optional overrides shown below
});
const result = await vrf.fulfill(1234n);
console.log(`Fulfilled request ${result.requestId} with round ${result.round}`);
console.log(`Transaction hash: ${result.txHash}`);
This will:
- Read request metadata from the VRF contract
- Compute the required drand round from the request deadline and minRound
- Wait until the round is available (if needed) and fetch its signature
- Submit
fulfillRandomness
on-chain - Return fulfillment details including transaction hash
new HyperEVMVRF(config)
accepts:
interface HyperevmVrfConfig {
rpcUrl?: string; // default resolved from chain (or HyperEVM)
vrfAddress?: string; // default resolved from chain (or HyperEVM)
chainId?: number; // default: 999 (HyperEVM)
account: { privateKey: string }; // required
policy?: { mode: "strict" | "window"; window?: number } | undefined; // default: { mode: "window", window: 10000 }
drand?: { baseUrl?: string; fetchTimeoutMs?: number; beacon?: string }; // defaults: api.drand.sh/v2, 8000ms, evmnet
gas?: { maxFeePerGasGwei?: number; maxPriorityFeePerGasGwei?: number };
}
Defaults are exported from defaultConfig
and defaultVRFABI
. Chain info available via CHAINS
.
- If you pass
chainId
, the SDK will resolve reasonable defaults (rpcUrl, drand beacon, and optionally a knownvrfAddress
). - You can override any field explicitly in config.
The SDK enforces VRF request policies to ensure randomness quality and security:
strict
mode: Only allows fulfillment when the target round is exactly the latest published roundwindow
mode: Allows fulfillment when the target round is within a specified window of the latest round- No policy: Explicitly disable policy enforcement by setting
policy: undefined
Default Behavior: When no policy is specified, the SDK uses a very generous window of 10000 rounds to ensure requests can be fulfilled even if they've been waiting for a long time. This provides maximum usability while still having some reasonable upper bound.
Note: With DRAND's 30-second round interval, a window of 10000 rounds allows requests that are up to ~83 hours (3.5 days) old to be fulfilled. This ensures excellent user experience for most scenarios.
The SDK includes comprehensive boundary case handling for robust operation:
- Deadline == Genesis: Handles cases where request deadline exactly matches or precedes genesis time
- Divisible Deltas: Correctly processes time deltas that are exactly divisible by DRAND period
- Window Boundaries: Enforces policy limits at exact window boundaries (0, 1, 2, etc.)
- Future Rounds: Rejects attempts to fulfill with rounds that haven't been published yet
// Strict policy - only fulfill with latest round
const vrf = new HyperEVMVRF({
account: { privateKey: process.env.PRIVATE_KEY! },
policy: { mode: "strict" }
});
// Window policy - allow up to 3 rounds behind latest
const vrf = new HyperEVMVRF({
account: { privateKey: process.env.PRIVATE_KEY! },
policy: { mode: "window", window: 3 }
});
// No policy enforcement - allow any round difference
const vrf = new HyperEVMVRF({
account: { privateKey: process.env.PRIVATE_KEY! },
policy: undefined
});
// Default policy (very generous window=10000) when no policy specified
const vrf = new HyperEVMVRF({
account: { privateKey: process.env.PRIVATE_KEY! }
// Uses default: { mode: "window", window: 10000 }
});
Policy violations throw VrfPolicyViolationError
with detailed context about the violation.
The SDK supports custom gas settings for VRF fulfillment transactions:
const vrf = new HyperEVMVRF({
account: { privateKey: process.env.PRIVATE_KEY! },
gas: {
maxFeePerGasGwei: 50, // Maximum fee per gas in Gwei
maxPriorityFeePerGasGwei: 2 // Maximum priority fee per gas in Gwei
}
});
Gas Settings:
maxFeePerGasGwei
: Maximum total fee per gas (base fee + priority fee) in GweimaxPriorityFeePerGasGwei
: Maximum priority fee per gas in Gwei (tip for miners/validators)
Note: Values are specified in Gwei for convenience and automatically converted to Wei for transaction submission.
- Node.js >= 18
- Set
WALLET_PRIVATE_KEY
(or pass directly) for the signer
Example .env
(never commit private keys):
WALLET_PRIVATE_KEY=0xabc123...
Load it in scripts/tests with dotenv
if needed.
-
class
HyperEVMVRF
constructor(config: HyperevmVrfConfig)
requestRandomness({ deadline, consumer?, salt? }): Promise<{ requestId, txHash }>
fulfill(requestId: bigint): Promise<FulfillResult>
fulfillWithWait(requestId: bigint, opts?): Promise<FulfillResult>
requestAndFulfill({ deadline, consumer?, salt?, wait? }): Promise<{ requestId, round, signature, requestTxHash, fulfillTxHash }>
-
helper
createEphemeralWallet(options): Promise<{ vrf, address }>
– in-memory account + optional funding wait
The SDK provides comprehensive typed error handling with specific error classes for different failure scenarios:
HyperEVMVrfError
- Base error class for all SDK errorsConfigurationError
- Invalid configuration parametersVrfRequestError
- Base class for VRF request-related errorsVrfRequestAlreadyFulfilledError
- Request has already been fulfilledVrfTargetRoundNotPublishedError
- Target DRAND round not yet availableVrfPolicyViolationError
- Policy enforcement violations
DrandError
- DRAND network or signature errorsDrandRoundMismatchError
- Round mismatch between expected and receivedDrandSignatureError
- Invalid signature format
NetworkError
- Network communication errorsHttpError
- HTTP status code errorsJsonParseError
- JSON parsing failures
ContractError
- Smart contract interaction errorsTransactionError
- Transaction mining failures
All errors include:
message
: Human-readable error descriptioncode
: Error category identifierdetails
: Additional context informationname
: Error class name for type checking
import { HyperEVMVRF, ConfigurationError, VrfRequestAlreadyFulfilledError } from "evm-randomness";
try {
const vrf = new HyperEVMVRF({
account: { privateKey: "invalid_key" }
});
} catch (error) {
if (error instanceof ConfigurationError) {
console.log(`Configuration error in field: ${error.field}`);
console.log(`Details:`, error.details);
}
}
try {
await vrf.fulfill(requestId);
} catch (error) {
if (error instanceof VrfRequestAlreadyFulfilledError) {
console.log(`Request ${error.requestId} already fulfilled`);
} else if (error instanceof VrfTargetRoundNotPublishedError) {
console.log(`Waiting ${error.secondsLeft}s for round ${error.targetRound}`);
} else if (error instanceof VrfPolicyViolationError) {
console.log(`Policy violation: ${error.policyMode} mode requires round difference <= ${error.policyWindow}`);
console.log(`Current: ${error.currentRound}, Target: ${error.targetRound}, Difference: ${error.roundDifference}`);
}
}
import { ERROR_CODES } from "evm-randomness";
// Available error codes:
// ERROR_CODES.VRF_REQUEST_ERROR
// ERROR_CODES.DRAND_ERROR
// ERROR_CODES.NETWORK_ERROR
// ERROR_CODES.CONFIGURATION_ERROR
// ERROR_CODES.CONTRACT_ERROR
// ERROR_CODES.TRANSACTION_ERROR
The fulfill
method returns a FulfillResult
object:
interface FulfillResult {
requestId: bigint; // The fulfilled request ID
round: bigint; // The DRAND round used
signature: [bigint, bigint]; // BLS signature components
txHash: `0x${string}`; // Transaction hash
}
- Minimal request + fulfill:
import "dotenv/config";
import { HyperEVMVRF } from "evm-randomness";
async function main() {
const vrf = new HyperEVMVRF({ account: { privateKey: process.env.PRIVATE_KEY! }, chainId: 999, policy: undefined });
const deadline = BigInt(Math.floor(Date.now()/1000)+120);
const { requestId } = await vrf.requestRandomness({ deadline });
const res = await vrf.fulfillWithWait(requestId);
console.log(res);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
- Custom endpoints and gas:
const vrf = new HyperEVMVRF({
rpcUrl: "https://rpc.hyperliquid.xyz/evm",
vrfAddress: "0xCcf1703933D957c10CCD9062689AC376Df33e8E1",
chainId: 999,
account: { privateKey: process.env.WALLET_PRIVATE_KEY! },
drand: { baseUrl: "https://api.drand.sh/v2", fetchTimeoutMs: 8000, beacon: "evmnet" },
gas: { maxFeePerGasGwei: 50, maxPriorityFeePerGasGwei: 2 },
});
- Reads the VRF request from the contract
- Queries DRAND beacon for info to map deadline -> round
- Ensures the target round is published, fetches its BLS signature
- Calls
fulfillRandomness(id, round, signature)
on the VRF contract
pnpm build
– build library with typespnpm dev
– watch buildpnpm lint
– eslint checkpnpm test
– run unit tests (vitest)
- This SDK performs DRAND round selection (
max(minRound, roundFromDeadline)
) and signature retrieval. - Default policy is permissive (
window=10000
). Setpolicy: undefined
to disable orstrict/window
to enforce. - For consumer contracts like your Lottery V2, you typically don’t need
requestRandomness()
because the consumer requests it during its flow; you only needfulfill*
.
MIT