evm-randomness
Version:
Minimal, typed SDK skeleton for HyperEVM VRF
327 lines (252 loc) • 11.4 kB
Markdown
# 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:
```ts
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)
```ts
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
```bash
pnpm add hyperevm-vrf-sdk
# or
npm i hyperevm-vrf-sdk
# or
yarn add hyperevm-vrf-sdk
```
### Quickstart
```ts
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:
```ts
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
```ts
// 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:
```ts
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):
```dotenv
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
```ts
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
```ts
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:
```ts
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:
```ts
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:
```ts
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