@d20dao/vrf-sdk 0.3.4 → 0.4.0
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 +25 -15
- package/API.md +342 -209
- package/BUILD-MANIFEST.json +18 -16
- package/CHANGELOG.md +44 -0
- package/PROTOCOL-PROVENANCE.json +9 -8
- package/README.md +129 -108
- package/abi/EpochEntropy.json +372 -26
- package/dist/abi.d.ts +292 -24
- package/dist/abi.js +1 -1
- package/dist/epoch.d.ts +42 -78
- package/dist/epoch.js +121 -27
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/sources.js +2 -0
- package/dist/templates.d.ts +22 -0
- package/dist/templates.js +220 -0
- package/examples/DiceConsumer.sol +37 -29
- package/examples/LootDropConsumer.sol +60 -0
- package/examples/RaffleConsumer.sol +71 -0
- package/package.json +2 -1
- package/contracts/examples/MiningRandomnessConsumer.sol +0 -59
package/README.md
CHANGED
|
@@ -1,32 +1,38 @@
|
|
|
1
1
|
# d20dao VRF SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Randomness your users can check.** Your contract asks the coordinator for a random result and pays a fee. A few seconds later the coordinator calls your contract back with a word taken from a VRF proof it verified on chain. Nobody picks the answer, nobody gets a second attempt, and anyone can replay the proof afterwards with this package.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`@d20dao/vrf-sdk` is everything you need on the application side: the Solidity base contract your consumer inherits, the coordinator ABI, an off-chain fee quote for your front end, and the replay code that re-derives a published result from public evidence. It holds no keys and runs no service.
|
|
6
|
+
|
|
7
|
+
The service is live on **Arc Mainnet** (chain 5042); develop against **Arc Testnet** (chain 5042002). Requests are permissionless — no allowlist, no subscription, no upfront deposit — but they must come from a contract, so a wallet or backend pays through its own consumer. Each request pays a fee quoted from the current base fee, a fraction of a USDC at typical gas prices (see [Pricing](#pricing)), and is served within 60 seconds or can be refunded.
|
|
8
|
+
|
|
9
|
+
## Start here
|
|
6
10
|
|
|
7
11
|
```sh
|
|
8
12
|
npm install @d20dao/vrf-sdk
|
|
9
13
|
```
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
## Quick path
|
|
15
|
+
1. **Copy an example.** [`examples/DiceConsumer.sol`](examples/DiceConsumer.sol) rolls a d20, [`examples/RaffleConsumer.sol`](examples/RaffleConsumer.sol) picks one winner from a list, [`examples/LootDropConsumer.sol`](examples/LootDropConsumer.sol) makes a weighted drop. Each is sixty to seventy lines and stands alone.
|
|
16
|
+
2. **Compile it.** Node 22.13+, solc 0.8.28, `evmVersion: cancun`. Hardhat and Foundry settings are in [Compiler setup](#compiler-setup).
|
|
17
|
+
3. **Deploy it** against the coordinator proxy for your chain, from [Deployments](#deployments). There is no default network; configure the address explicitly.
|
|
18
|
+
4. **Request and read.** Quote the fee off-chain, send it through your consumer, take `requestId` from the receipt and poll until `fulfilled` — or until the 60-second deadline passes and you refund. See [Paying for a request](#paying-for-a-request) and [Reading results](#reading-results).
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
2. **Install and compile.** Install the package as above and configure Hardhat or Foundry in [Compiler setup](#compiler-setup).
|
|
19
|
-
3. **Consumer.** Start from [Recommended payment pattern](#recommended-payment-pattern), choose a result type in [Randomness options](#randomness-options) and size the [callback gas limit](#callback-gas-limit).
|
|
20
|
-
4. **Request.** Quote off-chain and send the quoted value through your consumer: [Wallets and backends that pay through a consumer](#wallets-and-backends-that-pay-through-a-consumer).
|
|
21
|
-
5. **Wait and read.** Take `requestId` from the receipt and poll until `fulfilled` or the deadline passes: [Reading results](#reading-results), [Timing](#timing).
|
|
22
|
-
6. **Expiry and recovery.** Refund expired requests and retry failed callbacks with enough gas: [Expiry and refunds](#expiry-and-refunds), [Gas for refund and retry calls](#gas-for-refund-and-retry-calls).
|
|
20
|
+
Then read [Best practices](#best-practices): ten rules that cover most of what goes wrong. [API.md](API.md) lists every coordinator and registry function, event and error with its selector, caller and, for errors, what to do about it. [d20dao/randomizer-demo](https://github.com/d20dao/randomizer-demo) is a complete dapp built this way, and [CHANGELOG.md](CHANGELOG.md) records what changed in this release.
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
For agent-assisted integration, give your agent the installed `AGENTS.md`, `API.md` and `PROTOCOL-PROVENANCE.json`, plus the [integration skills](https://github.com/d20dao/skills). The website guides are on d20dao.org: [guides](https://d20dao.org/docs) including [Getting started](https://d20dao.org/docs/getting-started) with its Copy prompt action, the guide index [d20dao.org/llms.txt](https://d20dao.org/llms.txt), the full text [d20dao.org/llms-full.txt](https://d20dao.org/llms-full.txt) and [d20dao.org/agents.md](https://d20dao.org/agents.md).
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
## Best practices
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
1. **Quote with `quoteFeeAt(callbackGasLimit, block.baseFeePerGas)` plus a buffer, never through `eth_call`.** `eth_call` reports a base fee of 0, so `quoteFee` collapses to the minimum fee and the real transaction reverts with `IncorrectFee`. Inside the requesting transaction `quoteFee(callbackGasLimit)` is exact; off-chain, use `quoteRequestFee` or `quoteFeeAt` with the latest header's base fee and a buffer for the movement until inclusion.
|
|
27
|
+
2. **Requests come from a contract, never an EOA.** A wallet call reverts with `ContractConsumerRequired`. The consumer is what the coordinator calls back, so it has to exist before the request.
|
|
28
|
+
3. **Keep the callback small: store the result, do the work later.** It runs inside `callbackGasLimit`. If it reverts or runs out of gas, its state changes are rolled back while the request stays served and paid, and anyone can redeliver the same accepted word with `retryCallback(requestId, gasLimit)`, with more gas if needed. A delivery that succeeded is never repeated, but still refuse a request id you have already finished: the check costs one read and does not depend on the coordinator.
|
|
29
|
+
4. **Map each request id to your own context, and reject anything else.** Record who asked and what for when you request, then in the callback refuse a request id you never issued and one you have already finished.
|
|
30
|
+
5. **Derive many values from one word instead of making many requests.** One request buys 256 bits. Ask for `diceRoll(sides, count)`, `chooseMany` or `shuffle` and the coordinator maps them for you; for application-specific values, `keccak256(abi.encode(word, i))` gives an independent value per `i`. A second request costs a second fee and a second wait.
|
|
31
|
+
6. **Handle expiry, and choose the refund address deliberately.** If no proof is accepted within 60 seconds the request expires: nothing is fulfilled late, and anyone may call `refundRequest(requestId)`. The refund is pushed to the address fixed at request time, so pick one that can receive a plain native transfer or call `withdrawRefundCredit` — usually the paying user.
|
|
32
|
+
7. **Never re-roll a result you dislike.** The word is final once `fulfilled` is true. Re-requesting after seeing an outcome is the one thing verifiable randomness cannot protect your users from, and the evidence trail makes it visible.
|
|
33
|
+
8. **Never use `blockhash` or `block.timestamp` as randomness.** Both are chosen by whoever builds the block, and `blockhash` is only available for the last 256 blocks. That is the problem this service exists to solve.
|
|
34
|
+
9. **Withdraw the refund credit your fee buffer leaves behind.** Anything above the escrowed quote is credited to the refund address (`FeeOverpaymentCredited`), readable with `refundCredits(address)` and pulled with `withdrawRefundCredit(recipient)`. Returning the change in the requesting transaction, as `DiceConsumer` does, avoids the second transaction entirely.
|
|
35
|
+
10. **Freeze any list before you request an index into it.** `chooseOne`, `chooseMany` and `shuffle` answer with indices. Commit the list — hashing it into `clientSeed` puts the commitment in the request log, as `RaffleConsumer` does.
|
|
30
36
|
|
|
31
37
|
## Networks
|
|
32
38
|
|
|
@@ -49,14 +55,11 @@ Solidity imports require compiler 0.8.28 and your compiler's npm resolver:
|
|
|
49
55
|
- `@d20dao/vrf-sdk/contracts/interfaces/ID20VRF.sol`
|
|
50
56
|
- `@d20dao/vrf-sdk/contracts/libraries/D20VRFRequests.sol`
|
|
51
57
|
- `@d20dao/vrf-sdk/contracts/libraries/RandomnessMapping.sol`
|
|
52
|
-
- `@d20dao/vrf-sdk/contracts/examples/MiningRandomnessConsumer.sol`
|
|
53
58
|
|
|
54
59
|
These sources import only each other; no OpenZeppelin installation is needed for a consumer.
|
|
55
60
|
|
|
56
61
|
`D20VRFConsumer` authenticates the coordinator proxy. Verify the expected request in the callback and store the word with minimal work. Pin the effective coordinator proxy address, initialized configuration and implementation history of both service proxies. A constructor code-length check, SDK installation or permissionless request acceptance does not guarantee service.
|
|
57
62
|
|
|
58
|
-
A complete consumer following the recommended payment pattern is shown in [Recommended payment pattern](#recommended-payment-pattern).
|
|
59
|
-
|
|
60
63
|
### Compiler setup
|
|
61
64
|
|
|
62
65
|
Hardhat resolves `@d20dao/vrf-sdk/...` imports from `node_modules` without remappings:
|
|
@@ -86,10 +89,16 @@ With either tool, `import {D20VRFConsumer} from "@d20dao/vrf-sdk/contracts/D20VR
|
|
|
86
89
|
|
|
87
90
|
### Examples
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
Three complete consumers ship in the package and install as `@d20dao/vrf-sdk/examples/<name>.sol`. Each is sixty to seventy lines, deals with one idea and is meant to be copied and edited rather than imported. All three take the coordinator proxy in their constructor, authenticate the callback through `D20VRFConsumer`, refuse a request id they did not issue or have already finished, and read their outcome from `getMappedResult` instead of recomputing it.
|
|
93
|
+
|
|
94
|
+
| Example | What it shows |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| [`DiceConsumer.sol`](examples/DiceConsumer.sol) | One d20 per player. Reads the exact `quoteFee` inside the requesting transaction, pays it, returns the change and names the player as the refund address. |
|
|
97
|
+
| [`RaffleConsumer.sol`](examples/RaffleConsumer.sol) | One winner from a list. Closes entry before requesting, hashes the frozen list into `clientSeed` as an on-chain commitment, maps the winning index with `ChooseOne` and, through `_onRefund`, lets the draw be sent again only after an expired request was refunded. |
|
|
98
|
+
| [`LootDropConsumer.sol`](examples/LootDropConsumer.sol) | A weighted drop. Asks for a `NumberRange` draw over the total weight instead of reducing the word itself, stores the word in the callback and walks the weights on read. |
|
|
99
|
+
|
|
91
100
|
- [d20dao/randomizer-demo](https://github.com/d20dao/randomizer-demo) is a complete dapp: a consumer with one function per randomness option that stores the latest results onchain, and a single-page UI that requests, waits for and displays them. It runs on Arc Mainnet at https://mainnet-demo.d20dao.org.
|
|
92
|
-
- `skills/d20-consumer/assets/RandomnessConsumer.sol` in [d20dao/skills](https://github.com/d20dao/skills) shows raw, mapped and shuffle requests
|
|
101
|
+
- `skills/d20-consumer/assets/RandomnessConsumer.sol` in [d20dao/skills](https://github.com/d20dao/skills) shows raw, mapped and shuffle requests in one contract, with refund notification and refund-credit withdrawal.
|
|
93
102
|
|
|
94
103
|
### Client seed
|
|
95
104
|
|
|
@@ -117,6 +126,8 @@ Every option is a `RandomnessMapping.Spec` `(operation, lower, upper, count, pop
|
|
|
117
126
|
|
|
118
127
|
TypeScript bounds (`sides`, `min`, `max`) are `bigint`; `count`, `size` and `population` are integer `number` values. Invalid parameters throw in TypeScript and revert the request with `InvalidMapping` onchain. Results are `uint256[]` from `getMappedResult` and the coordinator's `mapRandomness`, and `bigint[]` from the SDK's `mapRandomness`. Sampling rejects the short residue range instead of taking a biased modulo. Choice and shuffle results are zero-based indices into a list the application must fix before requesting. Callbacks always receive the raw `bytes32` word, including for mapped requests.
|
|
119
128
|
|
|
129
|
+
`RandomnessMapping.Spec` is the Solidity struct `(operation, lower, upper, count, population)` stored with a request. The TypeScript `MappingSpec` that `builtins` return has the same fields as an object, with `lower` and `upper` as `bigint`; ethers encodes it for the struct unchanged, and `hashMapping(spec)` equals the request's `mappingHash`. "Mapping" here means a randomness mapping, not a Solidity `mapping`.
|
|
130
|
+
|
|
120
131
|
## Pricing
|
|
121
132
|
|
|
122
133
|
The coordinator prices every request from the base fee of the transaction that creates it:
|
|
@@ -135,13 +146,15 @@ Labelled examples with the initialization values (multiplier 5, overhead 300,000
|
|
|
135
146
|
|
|
136
147
|
`quoteFeeAt(callbackGasLimit, baseFee)` evaluates the formula for a base fee you supply; `quoteFee(callbackGasLimit)` evaluates it for `block.basefee`. Quotes above the `uint96` escrow limit revert with `FeeOverflow` rather than truncating.
|
|
137
148
|
|
|
149
|
+
Three fee names are easy to confuse. `pricing()` and `quoteFee` price *future* requests; `requestFeePaid(requestId)` is what *one* request escrowed and settles from. `refundBps()` is the current refund ratio, copied into each new request, while `requestRefundBps(requestId)` is the ratio that request copied at creation and is refunded at. `keeperFeeBps()` has no per-request copy at all: it is read when a proof is accepted.
|
|
150
|
+
|
|
138
151
|
## Paying for a request
|
|
139
152
|
|
|
140
153
|
`requestRandomness(clientSeed, callbackGasLimit, refundAddress)` and `requestMappedRandomness(..., spec)` accept `msg.value >= fee`, where `fee` is the quote computed inside that transaction. Less reverts with `IncorrectFee(expected, actual)`. Exactly `fee` is escrowed and stored as `requestFeePaid(requestId)`; `RandomnessRequested` emits that charged fee as `feePaid`, not `msg.value`. Anything above it is not revenue: it is credited to the request's `refundAddress` as refund credit (`FeeOverpaymentCredited(requestId, refundAddress, amount)`, readable through `refundCredits(address)`) and is withdrawn by that address calling `withdrawRefundCredit(recipient)`. Choose a refund address that can make that call, or that can receive a plain native transfer for expiry refunds; a contract that can do neither strands its credit.
|
|
141
154
|
|
|
142
155
|
### Contracts that pay in the same transaction
|
|
143
156
|
|
|
144
|
-
`quoteFee(callbackGasLimit)` is exact inside the requesting transaction. `D20VRFRequests` helpers
|
|
157
|
+
`quoteFee(callbackGasLimit)` is exact inside the requesting transaction, so a contract can read the price and pay it in one go. The `D20VRFRequests` helpers do exactly that from the calling contract's balance:
|
|
145
158
|
|
|
146
159
|
```solidity
|
|
147
160
|
uint256 fee = rng.quoteFee(callbackGasLimit);
|
|
@@ -156,67 +169,19 @@ Do not call `quoteFee` through `eth_call`: it prices with `block.basefee`, which
|
|
|
156
169
|
import { quoteRequestFee } from '@d20dao/vrf-sdk';
|
|
157
170
|
// provider: ethers Provider; coordinator: coordinator proxy address; 100_000: callbackGasLimit
|
|
158
171
|
const { fee, value, baseFee } = await quoteRequestFee(provider, coordinator, 100_000, { bufferBps: 3000 });
|
|
159
|
-
await dice.roll(
|
|
172
|
+
await dice.roll({ value }); // DiceConsumer pays the exact quote and returns the rest
|
|
160
173
|
```
|
|
161
174
|
|
|
162
175
|
`fee` is `quoteFeeAt(callbackGasLimit, baseFee)` for the block's actual base fee. `value` is the same quote recomputed at a base fee `bufferBps` higher (default 3000, 30%: an EIP-1559 base fee can rise 12.5% per block), so the request still pays if the base fee rises by up to that much before inclusion. When the minimum fee dominates even at the buffered base fee, `value` equals `fee` and nothing extra is sent. In example A, `value` is 5 × 228.8 gwei × 400,000 = 0.4576 USDC; a request included at 176 gwei escrows 0.352 USDC, and the remaining 0.1056 USDC is either returned by the consumer or credited to the refund address, depending on the payment pattern below. The helper never uses `quoteFee`, needs only `getBlock` and `call`, and throws if the block has no `baseFeePerGas`. If the base fee outruns the buffer or pricing changes in between, the transaction reverts with `IncorrectFee`; quote again and resend.
|
|
163
176
|
|
|
164
|
-
###
|
|
165
|
-
|
|
166
|
-
For a consumer whose users pay per request, pay the exact quote and return the change in the same transaction: read `fee = quoteFee(callbackGasLimit)`, require `msg.value >= fee`, send exactly `fee` to the coordinator and return `msg.value - fee` to the caller. Use the paying user as the refund address when it can receive a native transfer or call `withdrawRefundCredit` (any wallet can), so an expiry refund goes straight back to the payer. The front end sends `value` from `quoteRequestFee`; the unused buffer comes back immediately, nothing accumulates as refund credit and the contract holds no user funds.
|
|
177
|
+
### Choosing a payment pattern
|
|
167
178
|
|
|
168
|
-
|
|
169
|
-
// SPDX-License-Identifier: MIT
|
|
170
|
-
pragma solidity 0.8.28;
|
|
171
|
-
|
|
172
|
-
import {D20VRFConsumer} from "@d20dao/vrf-sdk/contracts/D20VRFConsumer.sol";
|
|
173
|
-
import {ID20VRF} from "@d20dao/vrf-sdk/contracts/interfaces/ID20VRF.sol";
|
|
174
|
-
import {D20VRFRequests} from "@d20dao/vrf-sdk/contracts/libraries/D20VRFRequests.sol";
|
|
175
|
-
|
|
176
|
-
contract D20Game is D20VRFConsumer {
|
|
177
|
-
using D20VRFRequests for ID20VRF;
|
|
178
|
-
|
|
179
|
-
uint32 public constant CALLBACK_GAS = 100_000;
|
|
180
|
-
mapping(uint256 => address) public playerOf;
|
|
181
|
-
mapping(uint256 => bytes32) public wordOf;
|
|
182
|
-
mapping(uint256 => bool) public ready;
|
|
183
|
-
error Underpaid(uint256 fee, uint256 sent);
|
|
184
|
-
error ChangeFailed();
|
|
185
|
-
error UnexpectedCallback();
|
|
186
|
-
|
|
187
|
-
constructor(address coordinator) D20VRFConsumer(coordinator) {}
|
|
188
|
-
|
|
189
|
-
function roll(bytes32 operationId) external payable returns (uint256 requestId) {
|
|
190
|
-
ID20VRF rng = ID20VRF(vrfCoordinator);
|
|
191
|
-
uint256 fee = rng.quoteFee(CALLBACK_GAS); // exact inside this transaction
|
|
192
|
-
if (msg.value < fee) revert Underpaid(fee, msg.value);
|
|
193
|
-
// The helper pays the same quoteFee from this contract's balance, which msg.value just funded.
|
|
194
|
-
// The player is the refund address: an expiry refund goes straight back to them.
|
|
195
|
-
requestId = rng.d20(D20VRFRequests.Options(keccak256(abi.encode(msg.sender, operationId)), CALLBACK_GAS, msg.sender));
|
|
196
|
-
playerOf[requestId] = msg.sender;
|
|
197
|
-
if (msg.value > fee) {
|
|
198
|
-
(bool ok,) = payable(msg.sender).call{value: msg.value - fee}("");
|
|
199
|
-
if (!ok) revert ChangeFailed();
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function _fulfillRandomness(uint256 requestId, bytes32 randomness) internal override {
|
|
204
|
-
if (playerOf[requestId] == address(0) || ready[requestId]) revert UnexpectedCallback();
|
|
205
|
-
wordOf[requestId] = randomness;
|
|
206
|
-
ready[requestId] = true;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/// 1 to 20 once ready.
|
|
210
|
-
function result(uint256 requestId) external view returns (uint256) {
|
|
211
|
-
return ID20VRF(vrfCoordinator).getMappedResult(requestId)[0];
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
```
|
|
179
|
+
Two patterns cover almost every consumer, and the examples ship both.
|
|
215
180
|
|
|
216
|
-
The
|
|
181
|
+
- **Pay the exact quote and return the change** ([`DiceConsumer.sol`](examples/DiceConsumer.sol)). Read `fee = quoteFee(callbackGasLimit)`, require `msg.value >= fee`, pay exactly `fee` and send `msg.value - fee` back to the caller. The front end sends `value` from `quoteRequestFee`, the unused buffer returns immediately, nothing accumulates as refund credit and the contract never holds user funds. Name the paying user as the refund address — any wallet can receive a native transfer or call `withdrawRefundCredit` — so an expiry refund goes back to whoever paid.
|
|
182
|
+
- **Forward `msg.value`** ([`RaffleConsumer.sol`](examples/RaffleConsumer.sol), [`LootDropConsumer.sol`](examples/LootDropConsumer.sol)). Fewer lines: the coordinator escrows its own quote, reverts `IncorrectFee` when that is more than arrived, and credits everything above it to the refund address. With a buffered off-chain quote most requests leave some credit, which that address pulls later with `withdrawRefundCredit`.
|
|
217
183
|
|
|
218
|
-
|
|
219
|
-
- **Pay from the contract balance** (`D20VRFRequests` helpers without returning change, `MiningRandomnessConsumer`). The application funds the contract and charges users under its own rules; it needs its own funding and withdrawal policy, and the refund address decides who receives expiry refunds.
|
|
184
|
+
A contract that funds requests from its own balance uses the first form without the change transfer. It then needs its own funding and withdrawal policy, and its refund address decides who receives expiry refunds.
|
|
220
185
|
|
|
221
186
|
## Reading results
|
|
222
187
|
|
|
@@ -241,14 +206,14 @@ Views on the coordinator (all in `coordinatorAbi`; only `getMappedResult` is par
|
|
|
241
206
|
|
|
242
207
|
[API.md](API.md) documents every view, event and error, including the order of events in a receipt.
|
|
243
208
|
|
|
244
|
-
Polling with ethers 6, after sending the request through a consumer such as `
|
|
209
|
+
Polling with ethers 6, after sending the request through a consumer such as `DiceConsumer`:
|
|
245
210
|
|
|
246
211
|
```js
|
|
247
212
|
import { Contract } from 'ethers';
|
|
248
213
|
import { coordinatorAbi } from '@d20dao/vrf-sdk/abi';
|
|
249
214
|
|
|
250
215
|
const coordinator = new Contract(coordinatorAddress, coordinatorAbi, provider);
|
|
251
|
-
const receipt = await (await
|
|
216
|
+
const receipt = await (await dice.roll({ value })).wait();
|
|
252
217
|
const requestId = receipt.logs
|
|
253
218
|
.filter((log) => log.address.toLowerCase() === coordinatorAddress.toLowerCase())
|
|
254
219
|
.map((log) => coordinator.interface.parseLog(log))
|
|
@@ -270,8 +235,6 @@ for (;;) {
|
|
|
270
235
|
|
|
271
236
|
- The package is ESM only (`"type": "module"`, `import` export conditions) for Node 22.13+ and bundlers. In a browser application, import it through a bundler such as Vite, webpack or esbuild; the test suite bundles the root and `/abi` entries for the browser platform with esbuild. Import `@d20dao/vrf-sdk/abi` alone when only ABIs are needed.
|
|
272
237
|
- `quoteRequestFee(provider, coordinator, callbackGasLimit, options)` expects an ethers v6 provider such as `JsonRpcProvider` or `BrowserProvider`, or any object with ethers-v6-shaped `getBlock(tag)` (with `baseFeePerGas` as `bigint`) and `call(tx)`. With viem or another client, repeat its steps: read the latest block's `baseFeePerGas`, add the buffer and call `quoteFeeAt(callbackGasLimit, bufferedBaseFee)`.
|
|
273
|
-
- Quote from the block header base fee plus a buffer, never with `quoteFee` through `eth_call`, because `eth_call` reports a base fee of 0 (see [Wallets and backends that pay through a consumer](#wallets-and-backends-that-pay-through-a-consumer)).
|
|
274
|
-
- Send the transaction to your consumer contract; the coordinator rejects requests from wallets with `ContractConsumerRequired`.
|
|
275
238
|
- The SDK does not wrap viem or other clients; its ABIs are plain JSON and work with any library.
|
|
276
239
|
- To add Arc to a browser wallet, use `wallet_addEthereumChain` with the values from [Networks](#networks). The native currency uses 18 decimals:
|
|
277
240
|
|
|
@@ -289,20 +252,20 @@ for (;;) {
|
|
|
289
252
|
```
|
|
290
253
|
- Reverts from the coordinator are custom errors, and `coordinatorAbi` includes all of them. Decode revert data with `coordinator.interface.parseError(data)` in ethers, or add the coordinator ABI next to your consumer ABI so the wallet or library can name the error. The ones a consumer meets most often: `IncorrectFee(expected, actual)` (re-quote and resend), `InvalidCallbackGas`, `InvalidMapping` (from `RandomnessMapping`), `ContractConsumerRequired`, `UnknownRequest`, `NotFulfilled`, `RefundNotAvailable` (not yet past the deadline, already served or already refunded), `RequestRefunded` and `InsufficientCallbackGas` (raise the transaction gas limit; see [Gas for refund and retry calls](#gas-for-refund-and-retry-calls)). `OnlyCoordinator` comes from `D20VRFConsumer` and is only in your consumer's ABI. [API.md](API.md) gives every error's selector, the calls that raise it and what to do.
|
|
291
254
|
- ethers v6 returns structs as `Result` objects that are also arrays. A field named like an `Array` or `Result` member, such as `values`, `length` or `map`, is shadowed; read it with `result.getValue('values')`, by position or from `result.toObject()`, or choose another field name.
|
|
292
|
-
-
|
|
293
|
-
-
|
|
255
|
+
- Every public Arc RPC endpoint is on `*.arc.io`, and common browser ad-block filter lists block that domain, so a page that reads the chain directly fails with `net::ERR_BLOCKED_BY_CLIENT` for a share of users. Read through the connected wallet's EIP-1193 provider when there is one (check its chain ID first), or through a read-only relay you serve from your own origin; the [randomizer-demo](https://github.com/d20dao/randomizer-demo) does the latter for read methods only and leaves transactions to the wallet.
|
|
256
|
+
- Some RPC endpoints reject or rate-limit large JSON-RPC batches, and limits differ between providers and plans. ethers `JsonRpcProvider` batches up to 100 calls by default; lower `batchMaxCount` (`new JsonRpcProvider(url, 5042, { staticNetwork: true, batchMaxCount: 1 })`) when you meet one. Poll no faster than you need and cache what cannot change: once `fulfilled` is true, `randomness` and the mapped result are final.
|
|
294
257
|
|
|
295
258
|
## Request lifecycle
|
|
296
259
|
|
|
297
|
-
Epochs last 200 blocks.
|
|
260
|
+
Epochs last 200 blocks. Each epoch uses the catalog in force for it: 1 to 10 ordered sources, each a registered recipe with its signer (see [Recipes](#recipes)). The keeper selects a source using the canonical block hash at epoch start minus one and prepares its first validated API3 snapshot locally. If the selected source yields no valid packet, the next source in catalog order can be committed instead, one source per 20-block window (attempts 1 to count − 1); a saved response is never refreshed or resampled. The registry committer publishes, or a backup committer the owner allowed so that a second keeper can take over. Idle preparation publishes no transaction. An unused local snapshot can be retained for 50 epochs (10,000 blocks), subject to live-demand and unresolved-transaction protection.
|
|
298
261
|
|
|
299
262
|
A request escrows its quoted fee even when its epoch packet is not published yet, and fixes its original block, epoch, client seed, mapping, refund address, `feePaid`, `refundBps` and 60-second deadline. The keeper publishes the saved packet only for live paid demand. The randomness target becomes `max(requestBlock, committedBlock + 1)`, so its hash is unknown at publication; before publication the request has no usable target or VRF seed. Multiple requests share the packet, and timely requests can settle across epoch boundaries without changing their epoch.
|
|
300
263
|
|
|
301
|
-
Timely service is onchain proof acceptance at or before `requestedAt + 60` seconds; a pending transaction is not acceptance. At acceptance the keeper share, `keeperFeeBps` of `feePaid`, is paid to the registry
|
|
264
|
+
Timely service is onchain proof acceptance at or before `requestedAt + 60` seconds; a pending transaction is not acceptance. At acceptance the keeper share, `keeperFeeBps` of `feePaid`, is paid to the submitting wallet when the registry authorizes it as its committer or one of its backup committers, and to the configured committer for any other submitter (a failed transfer becomes keeper credit) and the remainder becomes withdrawable protocol fees. With a 50% share, example A pays 0.176 USDC to the keeper and 0.176 USDC to the treasury. Callback failure still earns the fee; `retryCallback(requestId, gasLimit)` redelivers only the same accepted result and never pays a second share.
|
|
302
265
|
|
|
303
266
|
### Timing
|
|
304
267
|
|
|
305
|
-
Each request's deadline is its block timestamp plus 60 seconds (`RESPONSE_TIMEOUT`). A proof accepted onchain at or before the deadline serves the request; after it the request can only be refunded. On Arc, a single request is normally fulfilled within a few seconds.
|
|
268
|
+
Each request's deadline is its block timestamp plus 60 seconds (`RESPONSE_TIMEOUT`). A proof accepted onchain at or before the deadline serves the request; after it the request can only be refunded. On Arc, a single request is normally fulfilled within a few seconds. Under load, 200 simultaneous requests were all delivered within 36 seconds, with a median of 19 seconds. Measured timings are not an SLA: wait up to the deadline, as in [Reading results](#reading-results), and handle expiry. If the keeper does not publish the request's epoch packet or a proof in time, for any reason, the request simply expires: nothing is fulfilled late, and the fee can be refunded as described below. Your application only needs to treat the request as expired.
|
|
306
269
|
|
|
307
270
|
### Expiry and refunds
|
|
308
271
|
|
|
@@ -326,22 +289,78 @@ The keeper may fulfill up to 16 prepared requests in one transaction with `fulfi
|
|
|
326
289
|
|
|
327
290
|
## Optional refund notification
|
|
328
291
|
|
|
329
|
-
After `refundRequest` has paid the fixed refund address or recorded its refund credit, the coordinator calls `onRefund(requestId)` on the original consumer. Extend `D20VRFConsumer` and override `_onRefund(uint256 requestId)` to update application state; the base authenticates the coordinator. The callback only carries the request ID and does not imply that the consumer itself received money. Application assets and fees remain the application's responsibility.
|
|
292
|
+
After `refundRequest` has paid the fixed refund address or recorded its refund credit, the coordinator calls `onRefund(requestId)` on the original consumer. Extend `D20VRFConsumer` and override `_onRefund(uint256 requestId)` to update application state, as `RaffleConsumer` does to allow a new draw; the base authenticates the coordinator. The callback only carries the request ID and does not imply that the consumer itself received money. Application assets and fees remain the application's responsibility.
|
|
330
293
|
|
|
331
294
|
The first attempt forwards 100,000 gas. A reverting or gas-exhausting hook cannot undo the fee settlement. After failure, `retryRefundCallback(requestId, gasLimit)` retries the notification without another payment; successful delivery is recorded by `refundCallbackDelivered(requestId)`. Refund/retry needs sufficient outer gas (see [Gas for refund and retry calls](#gas-for-refund-and-retry-calls)). Never request new randomness from within either callback; use a separate application transaction.
|
|
332
295
|
|
|
333
296
|
## Replay and verification
|
|
334
297
|
|
|
335
|
-
Use independently trusted successful receipts and state. Decode the registry `EpochCommitted` packet with `decodeEpochEvidencePacket` and verify with `replayEpochCommitment`, using the original source anchor, exact packet, commit block/time,
|
|
298
|
+
Use independently trusted successful receipts and state. Decode the registry `EpochCommitted` packet with `decodeEpochEvidencePacket` and verify with `replayEpochCommitment`, using the original source anchor, exact packet, commit block/time, the epoch's catalog with its recipe definitions and the registry identity. Decode the coordinator `FulfillmentEvidence` packet with `decodeEvidencePacket`, then call `replayCoordinator` with its exported input type (`Parameters<typeof replayCoordinator>[0]`).
|
|
336
299
|
|
|
337
300
|
`RequestContext` binds both `requestBlock` and `targetBlock`. Validate the epoch from the original request block, reconstruct the target from the actual publication block, and compare the event and stored transcript. Proof evidence is 416 bytes; fulfillment calldata is 452 bytes. Neither evidence packet has a version prefix. Choose the decoder from trusted emitter/event context. Decoding and mapping alone are not proof verification; replay does not authenticate RPC or establish receipt inclusion.
|
|
338
301
|
|
|
339
302
|
`EpochProtocolConfiguration` is the initialized configuration: `feeRecipient` from `initialFeeRecipient()`, `initialMinFee` from `initialMinFee()` (the `initialize` fee argument), `catalogHash` from `catalogHash()`. Live `pricing()`, `feeRecipient()` and scheduled catalogs never change `protocolConfigurationHash`.
|
|
340
303
|
|
|
341
|
-
|
|
304
|
+
Catalogs are per epoch. `catalogAt(epochId)` returns the hash, recipe ids and signers an epoch selects and commits with, and `Epoch.catalogHash` records that hash at publication. A registry starts with its initial catalog, recipes 0 to 3 with the four signers given at initialization, whose hash `catalogHash()` is bound into the configuration hash; `catalogHash()` and the initial signer getters never change. The owner replaces the catalog for epochs at least two ahead with `scheduleCatalog(recipes, signers, fromEpoch)`: 1 to 10 distinct registered recipes with one signer each, hashed as `keccak256(abi.encode(RECIPE_DOMAIN, recipes, signers))` (event `CatalogScheduled(fromEpoch, catalogHash, recipes, signers)`). 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 catalog. For replay, build `epoch.catalog` from the epoch's `catalogAt` view with `resolveEpochCatalog(base, view)`, which recognizes the initial catalog by its hash (or from the `CatalogScheduled` history without replaced versions), while `configuration.catalogHash` stays the initial `catalogHash()`; `replayEpochCommitment` binds the supplied catalog to `record.catalogHash`.
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
import { readEpochRecipes, resolveEpochCatalog } from '@d20dao/vrf-sdk/epoch';
|
|
308
|
+
|
|
309
|
+
const [hash, recipes, signers] = await registry.catalogAt(epochId); // registry: ethers Contract with epochEntropyAbi
|
|
310
|
+
const recipeBook = await readEpochRecipes(provider, registryAddress, recipes.map(Number));
|
|
311
|
+
const catalog = resolveEpochCatalog({ registry: registryAddress, chainId, firstEpochStart, recipeBook }, { hash, recipes, signers });
|
|
312
|
+
```
|
|
342
313
|
|
|
343
314
|
At publication a signed attestation may be at most 240 seconds old and never future-dated (`MAX_ATTESTATION_AGE`, exported from `/epoch`); `replayEpochCommitment` enforces the same bound against the commit timestamp.
|
|
344
315
|
|
|
316
|
+
### Recipes
|
|
317
|
+
|
|
318
|
+
Epoch sources are recipes in an owner-managed, append-only registry in `EpochEntropy`. A recipe is its canonical request, whose `keccak256` is the query hash the signer signs; a data template that fixes the exact signed bytes the registry accepts ([Data templates](#data-templates)); and the JSON body keepers post to the provider gateway. `registerRecipe(canonicalRequest, template, body)` appends the next id (0 to 255) and emits `RecipeRegistered` with the full definition. A registered recipe never changes, so a changed listing becomes a new id. `recipeCount()` and `getRecipe(id)` read the registry. Every registry registers six built-in recipes itself, at initialization or in its recipe-registry upgrade:
|
|
319
|
+
|
|
320
|
+
| Recipe | Provider | Query | Exact signed record |
|
|
321
|
+
| --- | --- | --- | --- |
|
|
322
|
+
| 0 | Hyperliquid | `metaAndAssetCtxs`, dex `""`, projection symbol `/0/universe/0/name` and value `/1/0/dayNtlVlm` | `{"symbol":"BTC","value":"<decimal>"}` |
|
|
323
|
+
| 1 | dRPC | `jsonRpc` on Ethereum mainnet: `eth_call` of Multicall3 `getLastBlockHash()` at `latest` | `{"id":null,"jsonrpc":"2.0","result":"0x<64 lowercase hex>"}` |
|
|
324
|
+
| 2 | TickerLayer | `lastTrade`, crypto, BTCUSD | `{"symbol":"BTCUSD","price":<number>,"size":<number>,"timestamp":<1 to 16 digits>}` |
|
|
325
|
+
| 3 | TickerLayer | `lastTrade`, crypto, ETHUSD | as recipe 2 with ETHUSD |
|
|
326
|
+
| 4 | Nodary | `latestFeeds`, name ETH/USD | `{"ETH/USD":{"value":<number>,"timestamp":<13 digits>,"category":"crypto"}}` |
|
|
327
|
+
| 5 | dRPC | as recipe 1 on Base | as recipe 1 |
|
|
328
|
+
|
|
329
|
+
Both networks now draw from a five-source catalog, `[0, 1, 2, 4, 5]`: Hyperliquid BTC day volume, the dRPC Ethereum block hash, TickerLayer BTCUSD, Nodary ETH/USD and the dRPC Base block hash: Arc Testnet from epoch 966 and Arc Mainnet from epoch 848. Read the catalog an epoch actually used from `catalogAt(epochId)` rather than assuming this one.
|
|
330
|
+
|
|
331
|
+
`BUILTIN_EPOCH_RECIPES` (from `@d20dao/vrf-sdk/epoch`) holds these definitions, and replay uses them unless the catalog carries a `recipeBook`. For any other recipe, put its definition in `epoch.catalog.recipeBook`: `readEpochRecipes(provider, registry, ids)` reads `getRecipe` and checks each query hash, or rebuild it from `RecipeRegistered` logs. Replay checks every definition it uses: the committed packet must carry the recipe's canonical request and the signed data must match its template.
|
|
332
|
+
|
|
333
|
+
Registry implementations before variable catalogs hardcoded ANU random numbers as recipe 1. Neither public registry ever committed an epoch from it, so every published Arc epoch replays with the built-in recipes. Older evidence that did use ANU replays when its definition is supplied in `recipeBook` under id 1: canonical request `["randomNumbers",[["length",4],["size",8],["type","hex8"]]]`, body `{"operation":"randomNumbers","parameters":{"type":"hex8","length":4,"size":8}}` and the template described in [Data templates](#data-templates).
|
|
334
|
+
|
|
335
|
+
### Data templates
|
|
336
|
+
|
|
337
|
+
A template is a byte string of segments, each an opcode and its operands:
|
|
338
|
+
|
|
339
|
+
| Opcode | Segment | Operands | Matches |
|
|
340
|
+
| --- | --- | --- | --- |
|
|
341
|
+
| `0x01` | LITERAL | length n (1 to 128), then n bytes | exactly those bytes |
|
|
342
|
+
| `0x02` | HEX | n (1 to 128) | exactly n characters `0-9a-f` |
|
|
343
|
+
| `0x03` | DECIMAL | flags (0 to 3) | `0` or a nonzero digit followed by digits, then an optional fraction `.digits` when flags & 1 and an optional exponent `(e\|E)(+\|-)?digits` when flags & 2 |
|
|
344
|
+
| `0x04` | INTEGER | min, max (1 ≤ min ≤ max ≤ 128) | a nonzero digit followed by digits, min to max digits in total |
|
|
345
|
+
|
|
346
|
+
Variable segments are greedy and never backtrack. Signed data is accepted only when the segments consume it exactly, with no trailing bytes, and it is at most 128 bytes (`MAX_DATA_BYTES`). A well-formed template is at most 256 bytes (`MAX_TEMPLATE_BYTES`), contains at least one variable segment and has a shortest possible match of at most 128 bytes. Records therefore have exactly the bytes a template describes: no whitespace, extra fields, reordered keys, escaped characters, uppercase hex or other lengths.
|
|
347
|
+
|
|
348
|
+
The TypeScript helpers produce and check the same bytes as the contract:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
import { encodeDataTemplate, decodeDataTemplate, matchesDataTemplate } from '@d20dao/vrf-sdk';
|
|
352
|
+
|
|
353
|
+
const template = encodeDataTemplate([
|
|
354
|
+
{ literal: '{"symbol":"BTCUSD","price":' }, { decimal: { fraction: true, exponent: true } },
|
|
355
|
+
{ literal: ',"size":' }, { decimal: { fraction: true, exponent: true } },
|
|
356
|
+
{ literal: ',"timestamp":' }, { integer: { minDigits: 1, maxDigits: 16 } }, { literal: '}' },
|
|
357
|
+
]); // equals BUILTIN_EPOCH_RECIPES[2].template
|
|
358
|
+
matchesDataTemplate(template, '0x' + Buffer.from('{"symbol":"BTCUSD","price":117000.5,"size":0.01,"timestamp":1789503538000}').toString('hex')); // true
|
|
359
|
+
decodeDataTemplate(template)[1]; // { decimal: { fraction: true, exponent: true } }
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
`encodeDataTemplate` throws with the rule a template breaks; `isValidDataTemplate` and `validateDataTemplate` check an encoded template. The ANU record `{"success":true,"type":"hex8","length":"4","data":["<16 hex>","<16 hex>","<16 hex>","<16 hex>"]}` is the literal `{"success":true,"type":"hex8","length":"4","data":["`, then HEX 16 and the literal `","` alternating, and the literal `"]}`.
|
|
363
|
+
|
|
345
364
|
## Security and trust
|
|
346
365
|
|
|
347
366
|
The contracts have not had an external security audit. The coordinator source carries the developer comment "Prototype: not audited or validated on Arc"; it is kept byte-for-byte because the source is part of the deployed bytecode metadata. The service is now live on Arc Mainnet, and the absence of an external audit still applies. Reusing the unmodified Chainlink VRF verifier does not extend any upstream audit to this coordinator (`notices/PROVENANCE.md`).
|
|
@@ -349,14 +368,16 @@ The contracts have not had an external security audit. The coordinator source ca
|
|
|
349
368
|
Trust model:
|
|
350
369
|
|
|
351
370
|
- **Owner.** On Arc Mainnet both service proxies are owned by the DAO treasury Safe `0xB57f656149749eff6b496dF090336491f977E744`, which is also the fee recipient; each manifest records the owner for its network. The owner can upgrade either implementation, which can change any behavior. Ownership moves only through a two-step transfer, and `renounceOwnership` reverts.
|
|
352
|
-
- **Owner settings without an upgrade.** Coordinator: fee recipient, keeper share (0–100%), pricing within the bounds in [Pricing](#pricing), and the refund ratio for future requests (50–100%). Registry: committer and
|
|
371
|
+
- **Owner settings without an upgrade.** Coordinator: fee recipient, keeper share (0–100%), pricing within the bounds in [Pricing](#pricing), and the refund ratio for future requests (50–100%). Registry: committer, up to four backup committers, recipe registration (append-only) and catalogs for epochs at least two ahead. Open requests keep their escrowed fee and refund ratio.
|
|
372
|
+
- **Publishers.** The committer and each backup committer can publish an epoch from any valid signed record of its selected source, under the same rules. A backup committer has no other role, and earns the keeper share of the requests whose proofs it submits itself.
|
|
373
|
+
- **Recipes.** A signature establishes what a provider's gateway signed for a recipe's request, not that the upstream value is unbiased. The owner decides which recipes and signers future epochs use; a lax template accepts more signed records for a publisher to choose from.
|
|
353
374
|
- **Keeper.** The VRF key holder can withhold a proof but cannot substitute a different result for a request's fixed seed. A request that is not served within 60 seconds is refundable at its snapshotted ratio.
|
|
354
375
|
|
|
355
|
-
|
|
376
|
+
Both proxies are atomically initialized ERC1967 endpoints with owner-authorized UUPS upgrades, and the implementations behind them are locked against initialization. No setter rewrites a request, a published epoch or the VRF key, but upgrade authority can change code and is an explicit trust assumption. Stable proxy addresses alone do not identify executed code, so verify the implementation history of **both** proxies at the relevant receipts 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.
|
|
356
377
|
|
|
357
378
|
## Use locally
|
|
358
379
|
|
|
359
|
-
For SDK development, run `npm ci` and `npm test` from this repository. The test builds, checks that `API.md` matches the reference that `scripts/api-reference.mjs` generates from the built ABIs and the curated `scripts/api-descriptions.mjs` (regenerate with `npm run build && npm run api-reference`), packs and installs a real tarball in an isolated consumer, replays the recipe fixtures, type-checks a strict consumer, exercises `quoteRequestFee` against a mock provider and compiles the
|
|
380
|
+
For SDK development, run `npm ci` and `npm test` from this repository. The test builds, checks that `API.md` matches the reference that `scripts/api-reference.mjs` generates from the built ABIs and the curated `scripts/api-descriptions.mjs` (regenerate with `npm run build && npm run api-reference`), packs and installs a real tarball in an isolated consumer, replays the recipe fixtures, type-checks a strict consumer, exercises `quoteRequestFee` against a mock provider, and compiles all three examples from the installed package and checks the outcomes they publish. `npm pack` also produces an installable local artifact.
|
|
360
381
|
|
|
361
382
|
```js
|
|
362
383
|
import { builtins, mapRandomness, replayCoordinator, quoteRequestFee } from '@d20dao/vrf-sdk';
|
|
@@ -365,23 +386,23 @@ const mapping = builtins.d20();
|
|
|
365
386
|
// Use only an independently verified accepted word for real outcomes.
|
|
366
387
|
```
|
|
367
388
|
|
|
368
|
-
The root exports ESM and TypeScript declarations, including `quoteRequestFee`, `DEFAULT_FEE_BUFFER_BPS` and the `FeeQuote`, `FeeQuoteOptions` and `FeeQuoteProvider` types; `/epoch` exports epoch helpers and `MAX_ATTESTATION_AGE
|
|
389
|
+
The root exports ESM and TypeScript declarations, including `quoteRequestFee`, `DEFAULT_FEE_BUFFER_BPS` and the `FeeQuote`, `FeeQuoteOptions` and `FeeQuoteProvider` types; `/epoch` exports epoch helpers, `BUILTIN_EPOCH_RECIPES`, `readEpochRecipes`, `resolveEpochCatalog` and `MAX_ATTESTATION_AGE`, and the root also exports the data-template helpers (`encodeDataTemplate`, `decodeDataTemplate`, `matchesDataTemplate`, `isValidDataTemplate`, `validateDataTemplate`). `/abi` exports `coordinatorAbi` and `epochEntropyAbi`, with JSON forms `D20VRFCoordinator.json` and `EpochEntropy.json`, both described in the installed `API.md` (`@d20dao/vrf-sdk/API.md`). The service implementations have locked empty constructors and explicit initializers. Registry initialization takes `address[4]`; it is not a four-address constructor deployment.
|
|
369
390
|
|
|
370
|
-
##
|
|
391
|
+
## What this package is not
|
|
371
392
|
|
|
372
|
-
This SDK contains no keeper service, API fetching, proof generation, signer secrets or deployment automation.
|
|
393
|
+
This SDK contains no keeper service, API fetching, proof generation, signer secrets or deployment automation. Installing it neither authorizes nor performs anything on chain.
|
|
373
394
|
|
|
374
|
-
|
|
395
|
+
The public protocol source is this repository's [`protocol/`](https://github.com/d20dao/d20-sdk/tree/main/protocol) folder. `PROTOCOL-PROVENANCE.json` names the keeper commit it was copied from and the SHA-256 of every file; that keeper repository is not public, so read the source in `protocol/`. Builds use these reviewed protocol Git blobs and verify every SHA-256 in PROTOCOL-PROVENANCE.json. `src/fees.ts` (the fee-quoting helper) is SDK-owned rather than vendored; BUILD-MANIFEST.json records it under `packageSources` next to the protocol source, dependency-lock and imported OpenZeppelin hashes. The UUPS build uses OpenZeppelin contracts and contracts-upgradeable 5.6.1. Consumer source is copied exactly; service implementations, test fixtures and provers are excluded from the tarball.
|
|
375
396
|
|
|
376
|
-
|
|
397
|
+
Each replay fixture set records how it was produced, so a real API3 capture is never mistaken for a test signature. Fixtures are not included in the package. The browser-target bundle is executed under Node, not in an actual browser; independently trusted chain context is still required for real verification.
|
|
377
398
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
SDK installation provides consumer and verification tooling. Chain availability, provider quotas, upgrade administration and application settlement remain separate concerns. A healthy process alone does not guarantee a particular request's timely fulfillment.
|
|
399
|
+
SDK installation provides consumer and verification tooling. Chain availability, provider quotas, upgrade administration and application settlement remain separate concerns, and none of them guarantees a particular request's timely fulfillment.
|
|
381
400
|
|
|
382
401
|
## Deployments
|
|
383
402
|
|
|
384
|
-
|
|
403
|
+
Both networks run the implementations this package describes, behind the same proxy addresses as before: the recipe registry in `EpochEntropy` and the coordinator that pays the keeper share to the authorized wallet which submitted the accepted proof. The two chains run the same implementation addresses. Epochs published before the upgrade still replay with this SDK's built-in recipes.
|
|
404
|
+
|
|
405
|
+
Obtain proxy addresses, implementation addresses and independently checked code hashes from the public deployment manifests, [arc-mainnet.json](https://d20dao.org/deployments/arc-mainnet.json) and [arc-testnet.json](https://d20dao.org/deployments/arc-testnet.json). The addresses below are copied from them and are only valid together with the manifest revision they came from, because implementations move through owner-authorized upgrades. Before relying on this SDK's interface, check that the implementation at your chain's proxy matches the manifest entry.
|
|
385
406
|
|
|
386
407
|
### Arc Mainnet
|
|
387
408
|
|
|
@@ -392,11 +413,11 @@ Chain ID: **5042**. The live service; use the **coordinator proxy** when constru
|
|
|
392
413
|
| D20VRFCoordinator | Consumer entry point / proxy | [`0xd20da057469C45928912d983F45790C41e290571`](https://explorer.arc.io/address/0xd20da057469C45928912d983F45790C41e290571) |
|
|
393
414
|
| EpochEntropy | Epoch registry / proxy | [`0xd20Da048C1A68fa3Bc0B5f5Bc454D1530062C82D`](https://explorer.arc.io/address/0xd20Da048C1A68fa3Bc0B5f5Bc454D1530062C82D) |
|
|
394
415
|
| D20CostClient | Restricted cost client / proxy | [`0xD20da0048aED2BBb9f0e7078Bc452815D626D29d`](https://explorer.arc.io/address/0xD20da0048aED2BBb9f0e7078Bc452815D626D29d) |
|
|
395
|
-
| D20VRFCoordinator | Implementation | [`
|
|
396
|
-
| EpochEntropy | Implementation | [`
|
|
416
|
+
| D20VRFCoordinator | Implementation | [`0xd20da0DADa4352A1a9722be43a2D85923443458c`](https://explorer.arc.io/address/0xd20da0DADa4352A1a9722be43a2D85923443458c) |
|
|
417
|
+
| EpochEntropy | Implementation | [`0xd20dA048C969e5aDcC703Dfdf8220cc9dCB2f865`](https://explorer.arc.io/address/0xd20dA048C969e5aDcC703Dfdf8220cc9dCB2f865) |
|
|
397
418
|
| D20CostClient | Implementation | [`0xD20DA00A872acfDe3e4721Fc1051BD23CC84B66b`](https://explorer.arc.io/address/0xD20DA00A872acfDe3e4721Fc1051BD23CC84B66b) |
|
|
398
419
|
|
|
399
|
-
Addresses are copied from the [Arc Mainnet deployment manifest](https://d20dao.org/deployments/arc-mainnet.json).
|
|
420
|
+
Addresses are copied from the [Arc Mainnet deployment manifest](https://d20dao.org/deployments/arc-mainnet.json). Both networks run the same coordinator and registry implementations.
|
|
400
421
|
|
|
401
422
|
### Arc Testnet
|
|
402
423
|
|
|
@@ -407,8 +428,8 @@ Chain ID: **5042002**. For development and testing. Use the **coordinator proxy*
|
|
|
407
428
|
| D20VRFCoordinator | Consumer entry point / proxy | [`0xd20DA0FF9087d053f0291524Eac12abA1ADBd945`](https://testnet.arcscan.app/address/0xd20DA0FF9087d053f0291524Eac12abA1ADBd945) |
|
|
408
429
|
| EpochEntropy | Epoch registry / proxy | [`0xD20Da00B47A7cD2211dC4683E306913b05903756`](https://testnet.arcscan.app/address/0xD20Da00B47A7cD2211dC4683E306913b05903756) |
|
|
409
430
|
| D20CostClient | Restricted cost client / proxy | [`0xD20da026090B8472579a2B93030F1fC4c94807F1`](https://testnet.arcscan.app/address/0xD20da026090B8472579a2B93030F1fC4c94807F1) |
|
|
410
|
-
| D20VRFCoordinator | Implementation | [`
|
|
411
|
-
| EpochEntropy | Implementation | [`
|
|
431
|
+
| D20VRFCoordinator | Implementation | [`0xd20da0DADa4352A1a9722be43a2D85923443458c`](https://testnet.arcscan.app/address/0xd20da0DADa4352A1a9722be43a2D85923443458c) |
|
|
432
|
+
| EpochEntropy | Implementation | [`0xd20dA048C969e5aDcC703Dfdf8220cc9dCB2f865`](https://testnet.arcscan.app/address/0xd20dA048C969e5aDcC703Dfdf8220cc9dCB2f865) |
|
|
412
433
|
| D20CostClient | Implementation | [`0xD20DA00A872acfDe3e4721Fc1051BD23CC84B66b`](https://testnet.arcscan.app/address/0xD20DA00A872acfDe3e4721Fc1051BD23CC84B66b) |
|
|
413
434
|
|
|
414
|
-
Addresses are copied from the [Arc Testnet deployment manifest](https://d20dao.org/deployments/arc-testnet.json). Explorer links identify addresses; they do not assert explorer source-code verification.
|
|
435
|
+
Addresses are copied from the [Arc Testnet deployment manifest](https://d20dao.org/deployments/arc-testnet.json). Explorer links identify addresses; they do not assert explorer source-code verification. The cost client is internal tooling, not a shared application entry point.
|