@d20dao/vrf-sdk 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # d20dao consumer-agent guide
2
2
 
3
- Use this guide when integrating @d20dao/vrf-sdk into an application or interpreting its public evidence. Install with `npm install @d20dao/vrf-sdk` (0.3.3 or newer). The package provides a general randomness interface; dice and mining contracts are examples. Read installed declarations for exact types and match the packaged PROTOCOL-PROVENANCE.json to the deployment being used.
3
+ Use this guide when integrating @d20dao/vrf-sdk into an application or interpreting its public evidence. Install with `npm install @d20dao/vrf-sdk` (0.3.4 or newer). The package provides a general randomness interface; dice and mining contracts are examples. Read installed declarations for exact types and match the packaged PROTOCOL-PROVENANCE.json to the deployment being used. The installed API.md (`@d20dao/vrf-sdk/API.md`, generated from the packaged ABIs) lists every D20VRFCoordinator and EpochEntropy function, event and error with its selector, caller, the calls that raise each error and what to do about it.
4
+
5
+ Integration order: network parameters and coordinator proxy; install and compiler setup; consumer contract (recommended payment pattern, callback gas); off-chain quote and request through the consumer; wait for and read the result; expiry, refund and retry gas. https://github.com/d20dao/randomizer-demo is a complete dapp built this way (consumer with one function per randomness option, single-page UI, read-only RPC relay), running at https://mainnet-demo.d20dao.org.
4
6
 
5
7
  ## Networks and toolchain
6
8
 
7
- Arc Mainnet: chain 5042, live service, RPC https://rpc.mainnet.arc.io, explorer https://explorer.arc.io, D20DAO explorer https://arc.d20dao.org, manifest https://d20dao.org/deployments/arc-mainnet.json. Arc Testnet: chain 5042002, development, RPC https://rpc.testnet.arc.io, explorer https://testnet.arcscan.app, D20DAO explorer https://arc-testnet.d20dao.org, manifest https://d20dao.org/deployments/arc-testnet.json. Native USDC (18 decimals) pays gas and request fees; test USDC comes from the faucet linked at https://docs.arc.io/arc/references/connect-to-arc. Use Node 22.13+, solc 0.8.28 and evmVersion cancun. Hardhat resolves `@d20dao/vrf-sdk/contracts/...` from node_modules; Foundry needs the remapping `@d20dao/vrf-sdk/=node_modules/@d20dao/vrf-sdk/`. Consumer sources need no OpenZeppelin. Website guides: https://d20dao.org/docs, index https://d20dao.org/llms.txt. Public protocol source: the SDK repository's protocol/ folder (https://github.com/d20dao/d20-sdk/tree/main/protocol); the keeper repository named in PROTOCOL-PROVENANCE.json is not public.
9
+ Arc Mainnet: chain 5042, live service, RPC https://rpc.mainnet.arc.io, explorer https://explorer.arc.io, D20DAO explorer https://arc.d20dao.org, manifest https://d20dao.org/deployments/arc-mainnet.json. Arc Testnet: chain 5042002, development, RPC https://rpc.testnet.arc.io, explorer https://testnet.arcscan.app, D20DAO explorer https://arc-testnet.d20dao.org, manifest https://d20dao.org/deployments/arc-testnet.json. Native USDC (18 decimals) pays gas and request fees; test USDC comes from the faucet linked at https://docs.arc.io/arc/references/connect-to-arc. Use Node 22.13+, solc 0.8.28 and evmVersion cancun. Hardhat resolves `@d20dao/vrf-sdk/contracts/...` from node_modules; Foundry needs the remapping `@d20dao/vrf-sdk/=node_modules/@d20dao/vrf-sdk/`. Consumer sources need no OpenZeppelin. To add a network to a browser wallet, call wallet_addEthereumChain with chainId '0x13b2' (5042, chainName 'Arc Mainnet') or '0x4cef52' (5042002, 'Arc Testnet'), nativeCurrency { name: 'USDC', symbol: 'USDC', decimals: 18 }, and the rpcUrls and blockExplorerUrls above. Website guides: https://d20dao.org/docs, index https://d20dao.org/llms.txt. Public protocol source: the SDK repository's protocol/ folder (https://github.com/d20dao/d20-sdk/tree/main/protocol); the keeper repository named in PROTOCOL-PROVENANCE.json is not public.
8
10
 
9
11
  ## Public interfaces
10
12
 
@@ -12,6 +14,10 @@ Import builtins, mapRandomness, decodeEvidencePacket, replayCoordinator and quot
12
14
 
13
15
  The package is ESM only, for Node and bundlers; bundle it (Vite, webpack, esbuild) for browsers. quoteRequestFee expects an ethers v6 provider (or getBlock/call with ethers-v6 shapes); with other clients read the latest header baseFeePerGas, add a buffer and call quoteFeeAt. Never quote with quoteFee through eth_call, which reports a base fee of 0.
14
16
 
17
+ Names that are easy to confuse: refundBps() is the current refund ratio copied into each new request, requestRefundBps(requestId) the ratio one request copied and is refunded at; pricing() and quoteFee price future requests, requestFeePaid(requestId) is what one request escrowed; keeperFeeBps() has no per-request copy and is read at proof acceptance. Solidity RandomnessMapping.Spec and the TypeScript MappingSpec returned by builtins carry the same fields (operation, lower, upper, count, population; lower and upper are bigint in TypeScript), ethers encodes a MappingSpec for the struct unchanged and hashMapping(spec) equals the request's mappingHash.
18
+
19
+ Frontends: coordinator reverts are custom errors, all in coordinatorAbi, and pass through a consumer's ordinary Solidity call unchanged, so decode revert data with coordinator.interface.parseError(data) (ethers) or decodeErrorResult({ abi: coordinatorAbi, data }) (viem), or add the coordinator's error entries to the consumer ABI. Common responses: IncorrectFee re-quote and resend; InvalidCallbackGas use 30,000–1,000,000; ContractConsumerRequired send through a deployed consumer, not an EOA or constructor; NotFulfilled keep waiting or, after the deadline, refund; RefundNotAvailable not yet past the deadline, already fulfilled or already refunded; InsufficientCallbackGas raise the transaction gas limit. OnlyCoordinator and InvalidCoordinator are in the consumer ABI only. ethers v6 returns structs as Result arrays: a field named like an Array or Result member (values, length, map, filter, keys) is shadowed; use result.getValue(name), position or result.toObject(). Observed 2026-09-17, not guaranteed: all public Arc RPC endpoints are on *.arc.io, which common browser ad-block lists block (net::ERR_BLOCKED_BY_CLIENT), so read through the connected wallet's EIP-1193 provider after checking its chain ID, or through a same-origin read-only JSON-RPC relay such as the randomizer-demo worker/index.js; rpc.mainnet.arc.io rate-limited batched calls from shared Cloudflare egress addresses while rpc.blockdaemon.mainnet.arc.io accepted them, and the rpc.drpc.*.arc.io free plan rejected batches above 3 calls. ethers JsonRpcProvider batches up to 100 calls by default; set batchMaxCount (for example new JsonRpcProvider(url, 5042, { staticNetwork: true, batchMaxCount: 1 })). Cache final values: once fulfilled, randomness and the mapped result never change.
20
+
15
21
  RequestContext binds chainId, effective coordinator proxy, keyHash, requestId, consumer, clientSeed, mapping, requestBlock, targetBlock, blockHash, epochId and epochHash. Consult Parameters<typeof replayCoordinator>[0] for the complete trusted replay input. configuration.feeRecipient uses the initialized initialFeeRecipient and configuration.initialMinFee the initialize fee argument (getter initialMinFee), not the live payout address or pricing.
16
22
 
17
23
  ## Randomness options
@@ -46,11 +52,11 @@ Epochs last 200 blocks. The keeper prepares the first validated API3 snapshot lo
46
52
 
47
53
  A request escrows its quoted fee even if its epoch is unpublished and fixes its request block, epoch, client seed, mapping, refund address, feePaid, refundBps and 60-second deadline. Live paid demand triggers publication of the saved packet. The target becomes max(requestBlock, committedBlock+1); no usable VRF seed exists until that future hash is known. Older-epoch demand can settle across a boundary without changing its packet.
48
54
 
49
- D20VRFConsumer authenticates the coordinator proxy; verify the expected request and store the raw callback word with minimal work. callbackGasLimit is 30,000–1,000,000 (InvalidCallbackGas otherwise) and is forwarded exactly to rawFulfillRandomness. Mapped requests still callback with bytes32; use getMappedResult or canonical mapping. Keep application actions and payments separate from the callback.
55
+ D20VRFConsumer authenticates the coordinator proxy; verify the expected request and store the raw callback word with minimal work. callbackGasLimit is 30,000–1,000,000 (InvalidCallbackGas otherwise) and is forwarded exactly to rawFulfillRandomness; the fee grows with it. A new storage slot costs about 22,100 gas; a callback storing the word, fulfillment block and time and two index entries measured about 51,000 gas once its slots were in use and about 105,000 gas for its first result. Measure with eth_estimateGas or a local test and add a margin; the examples use 100,000. Mapped requests still callback with bytes32; use getMappedResult or canonical mapping. Keep application actions and payments separate from the callback.
50
56
 
51
- Valid onchain acceptance at or before requestedAt+60 seconds is timely. A pending transaction is not acceptance. Single requests are normally fulfilled within a few seconds on Arc; in a stress test 200 simultaneous requests were delivered within 36 seconds (median 19 seconds). Timings are not an SLA. At acceptance keeperFeeBps of feePaid goes to the configured registry committer, not the proof submitter (a failed transfer becomes keeper credit), and the remainder becomes protocol fees. Callback failure still earns the fee; retryCallback redelivers only the same accepted result and cannot pay a second share.
57
+ Valid onchain acceptance at or before requestedAt+60 seconds is timely. A pending transaction is not acceptance. Single requests are normally fulfilled within a few seconds on Arc; in a stress test 200 simultaneous requests were delivered within 36 seconds (median 19 seconds). Timings are not an SLA. If the keeper publishes no epoch packet or proof by the deadline, for any reason, the request expires: nothing is fulfilled late, the application treats it as expired and anyone can refund it. At acceptance keeperFeeBps of feePaid goes to the configured registry committer, not the proof submitter (a failed transfer becomes keeper credit), and the remainder becomes protocol fees. Callback failure still earns the fee; retryCallback redelivers only the same accepted result and cannot pay a second share.
52
58
 
53
- Read results from coordinator events (RandomnessRequested carries requestId; RandomnessFulfilled, CallbackAttempted, RequestRefundedTo, RefundCallbackAttempted) or poll getRequest(requestId): fields consumer, callbackGasLimit, requestBlock, targetBlock, deadline, refundAddress, clientSeed, mappingHash, blockHash, randomness, proofHash, transcriptHash, fulfilled, delivered, refunded, epochId, epochHash. getMapping(requestId) returns the stored Spec; getMappedResult(requestId) returns uint256[] and reverts NotFulfilled before acceptance; mapRandomness(randomness, spec) is a pure mapping of any word. Only getMappedResult is in ID20VRF; the rest are in coordinatorAbi. A request unfulfilled after its deadline can only be refunded.
59
+ Read results from coordinator events (RandomnessRequested carries requestId; RandomnessFulfilled, CallbackAttempted, RequestRefundedTo, RefundCallbackAttempted) or poll getRequest(requestId): fields consumer, callbackGasLimit, requestBlock, targetBlock (0 until publication), deadline, refundAddress, clientSeed, mappingHash, blockHash, randomness (zero until fulfilled), proofHash, transcriptHash, fulfilled, delivered, refunded, epochId (fixed at request time), epochHash (zero until publication). Take requestId from the coordinator's RandomnessRequested log in the request receipt (FeeOverpaymentCredited precedes it when there is excess). When polling, read the latest block first and then getRequest; fulfilled means final, and not fulfilled with block timestamp > deadline means expired. getMapping(requestId) returns the stored Spec; getMappedResult(requestId) returns uint256[] and reverts NotFulfilled before acceptance; mapRandomness(randomness, spec) is a pure mapping of any word. Only getMappedResult is in ID20VRF; the rest are in coordinatorAbi. A request unfulfilled after its deadline can only be refunded.
54
60
 
55
61
  After the deadline, anyone may call refundRequest(requestId). It pays feePaid × requestRefundBps / 10000 using the ratio snapshotted at request time (default 100%; the owner may lower it to no less than 50% for future requests only, event RefundBpsChanged) to the fixed refund address, or records it as that address's refund credit if the 30,000-gas transfer fails; the remainder is retained as protocol fees. Gas and application payments are separate.
56
62
 
@@ -62,11 +68,11 @@ Keepers may fulfill up to 16 requests in one fulfillRandomnessBatch transaction.
62
68
 
63
69
  The four ordered recipe slots are Hyperliquid BTC volume, ANU, TickerLayer BTCUSD and TickerLayer ETHUSD; the latter two share a provider signer. The epoch anchor selects one slot; fallback attempt n (1–3) is the slot n positions later, valid only from n × 20 blocks into the epoch (getEpochFallbackSelection, fallbackOpensAt, commitEpochFallback). replayEpochCommitment derives the attempt from the committed source and rejects a commit block before its window. Preserve the entire exact signed response, limited to 128 bytes. Signatures establish wrapper provenance, not unbiased upstream data. At publication an attestation may be at most 240 seconds old (MAX_ATTESTATION_AGE) and never future-dated.
64
70
 
65
- Signer catalogs are per epoch. The registry owner can schedule a replacement with scheduleCatalog(signers, fromEpoch) at least two epochs ahead (event CatalogScheduled); the current and next epoch, prepared snapshots and open requests keep their signers. Replay must use signersAt(epochId) or the CatalogScheduled history as epoch.catalog.signers, which replayEpochCommitment binds to the record's catalogHash, while configuration.catalogHash stays the initial catalogHash() bound into protocolConfigurationHash.
71
+ Signer catalogs are per epoch. The registry owner can schedule a replacement with scheduleCatalog(signers, fromEpoch) at least two epochs ahead (event CatalogScheduled). A new schedule replaces a version that has not taken effect yet, which can return the next epoch to the previous catalog; the current epoch, prepared snapshots and open requests keep their signers. Replay must use signersAt(epochId) or the CatalogScheduled history without replaced versions as epoch.catalog.signers, which replayEpochCommitment binds to the record's catalogHash, while configuration.catalogHash stays the initial catalogHash() bound into protocolConfigurationHash.
66
72
 
67
73
  Decode epoch evidence using its trusted registry/event context and decodeEvidencePacket for FulfillmentEvidence. Proof evidence is 416 bytes; a single fulfillRandomness call is 452 calldata bytes. Supply independently trusted successful receipts, proxy implementation history, source/publication/request/target blocks and timestamps, initialized key/configuration and original signed packets. Compare both event and stored transcript commitments. Decoding and mapping alone do not verify origin; replay does not authenticate RPC or establish inclusion.
68
74
 
69
- Both service contracts use atomically initialized D20Proxy endpoints with owner-authorized UUPS upgrades and two-step ownership; renounceOwnership reverts on both. Implementations are locked against initialization. The owner can rotate committer and fee recipient, adjust the keeper share, tune bounded pricing, lower the refund ratio for future requests and schedule future catalogs; no setter rewrites a request, a published epoch or the VRF key. Upgrade authority is trusted. Verify the implementation history of BOTH coordinator and registry; stable proxy addresses alone do not identify executed code. Operator pins stop processing on unreviewed changes while preserving recovery data.
75
+ Both service contracts use atomically initialized D20Proxy endpoints with owner-authorized UUPS upgrades and two-step ownership; renounceOwnership reverts on both. Implementations are locked against initialization. The owner can rotate committer and fee recipient, adjust the keeper share, tune bounded pricing, lower the refund ratio for future requests and schedule future catalogs; no setter rewrites a request, a published epoch or the VRF key. Upgrade authority is trusted. Verify the implementation history of BOTH coordinator and registry; stable proxy addresses alone do not identify executed code. Check it when you integrate and again whenever the deployment manifest records an upgrade or a proxy emits Upgraded(implementation); a mismatch with the manifest means stop and review before sending more requests. Operator pins stop processing on unreviewed changes while preserving recovery data.
70
76
 
71
77
  The contracts have not had an external security audit; the coordinator's "Prototype: not audited or validated on Arc" source comment is unchanged because the source is part of deployed bytecode metadata, and the service is live on Arc Mainnet. On Arc Mainnet the owner of both proxies is the DAO treasury Safe 0xB57f656149749eff6b496dF090336491f977E744. The VRF key holder can withhold a proof, which leads to a refund, but cannot substitute another result for a fixed seed.
72
78
 
@@ -78,7 +84,7 @@ This SDK holds no signer or bot keys, runs no keeper/prover and exposes no opera
78
84
 
79
85
  ## Examples
80
86
 
81
- examples/DiceConsumer.sol (installed at @d20dao/vrf-sdk/examples/DiceConsumer.sol): player-paid d20 request forwarding msg.value, player as refund address, refund hook. contracts/examples/MiningRandomnessConsumer.sol (source protocol/contracts/examples/): abstract claim consumer paying quoteFee from its balance, one request per claim, candidate seeds from the stored word. The README's Recommended payment pattern shows D20Game, which pays the exact quote and returns change.
87
+ examples/DiceConsumer.sol (installed at @d20dao/vrf-sdk/examples/DiceConsumer.sol): player-paid d20 request forwarding msg.value, player as refund address, refund hook. contracts/examples/MiningRandomnessConsumer.sol (source protocol/contracts/examples/): abstract claim consumer paying quoteFee from its balance, one request per claim, candidate seeds from the stored word. The README's Recommended payment pattern shows D20Game, which pays the exact quote and returns change. https://github.com/d20dao/randomizer-demo: complete dapp with a consumer that stores the latest results onchain and a UI that requests, waits for and displays them.
82
88
 
83
89
  ## Optional refund notification
84
90