evm-randomness
v0.4.0
Published
Minimal, typed SDK skeleton for HyperEVM VRF
Maintainers
Readme
HyperEVM VRF SDK
TypeScript SDK to request and fulfill on-chain VRF using DRAND beacons.
TL;DR (Quick Start)
- 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);No private key input (generate ephemeral wallet)
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);Why this SDK
- 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
Installation
pnpm add hyperevm-vrf-sdk
# or
npm i hyperevm-vrf-sdk
# or
yarn add hyperevm-vrf-sdkQuickstart
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
fulfillRandomnesson-chain - Return fulfillment details including transaction hash
Configuration (Schema)
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.
Address/Chain Resolution
- 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.
Policy Enforcement
The SDK enforces VRF request policies to ensure randomness quality and security:
strictmode: Only allows fulfillment when the target round is exactly the latest published roundwindowmode: 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.
Boundary Case Handling
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.
Gas Configuration
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.
Environment
- 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.
API (Surface)
class
HyperEVMVRFconstructor(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
Error Handling
The SDK provides comprehensive typed error handling with specific error classes for different failure scenarios:
Error Classes
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
Error Properties
All errors include:
message: Human-readable error descriptioncode: Error category identifierdetails: Additional context informationname: Error class name for type checking
Example Error Handling
import { HyperEVMVRF, ConfigurationError, VrfRequestAlreadyFulfilledError } from "hyperevm-vrf-sdk";
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}`);
}
}Error Codes
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_ERRORReturn Types
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
}Usage Examples
- Minimal request + fulfill:
import "dotenv/config";
import { HyperEVMVRF } from "hyperevm-vrf-sdk";
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 },
});How it works (high level)
- 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
Scripts
pnpm build– build library with typespnpm dev– watch buildpnpm lint– eslint checkpnpm test– run unit tests (vitest)
Scope / Notes
- This SDK performs DRAND round selection (
max(minRound, roundFromDeadline)) and signature retrieval. - Default policy is permissive (
window=10000). Setpolicy: undefinedto disable orstrict/windowto 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*.
License
MIT
