Skip to content

Latest commit

 

History

23 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages