Skip to content

hyperpoolz/hyperevm-vrf-sdk

Repository files navigation

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 evm-randomness
# or
npm i evm-randomness
# or
yarn add evm-randomness

Quickstart

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

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 known vrfAddress).
  • You can override any field explicitly in config.

Policy Enforcement

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 round
  • window 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.

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 Gwei
  • maxPriorityFeePerGasGwei: 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 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

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 errors
  • ConfigurationError - Invalid configuration parameters
  • VrfRequestError - Base class for VRF request-related errors
    • VrfRequestAlreadyFulfilledError - Request has already been fulfilled
    • VrfTargetRoundNotPublishedError - Target DRAND round not yet available
    • VrfPolicyViolationError - Policy enforcement violations
  • DrandError - DRAND network or signature errors
    • DrandRoundMismatchError - Round mismatch between expected and received
    • DrandSignatureError - Invalid signature format
  • NetworkError - Network communication errors
    • HttpError - HTTP status code errors
    • JsonParseError - JSON parsing failures
  • ContractError - Smart contract interaction errors
  • TransactionError - Transaction mining failures

Error Properties

All errors include:

  • message: Human-readable error description
  • code: Error category identifier
  • details: Additional context information
  • name: Error class name for type checking

Example Error Handling

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}`);
  }
}

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_ERROR

Return 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 "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 },
});

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 types
  • pnpm dev – watch build
  • pnpm lint – eslint check
  • pnpm 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). Set policy: undefined to disable or strict/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 need fulfill*.

License

MIT

About

A VRF SDK for HyperEVM

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •