// The OracleHub surface (Oracle v2, DESIGN §8e — EARMARK-AT-CREATION). There is // ONE governance-approved adapter, the OracleHub: it deduplicates question // scheduling content-addressed (same template definition → same question id, // charged only marginal cost), EARMARKS the resolution reserve per market at // onBind (attached with the market-creation value, LOCKED so resolution funds // are always present, never withdrawable while the market is live), debits the // EXACT metered resolve cost against that earmark at resolution, credits any // surplus (`reserve − charged`) to the operator's WITHDRAWABLE credit, and // delivers answers as payout vectors. // // USER-SIDE QUOTE RULE (§8e): the create-market native value is now // `getSchedulingCost(def) + resolveReserve()` // — the reserve is ATTACHED to the create (forwarded to onBind and locked); there // is NO standing prepaid balance / deposit pre-fund anymore. {@link // quoteCreateMarketValue} returns exactly that sum (excess is refunded in-tx). // The operator's owner can later withdraw only the accrued surplus credit // ({@link withdrawableOf}) via credit-only `withdraw`. // // Reads are standalone (no signer: subject, hub, client); writes live on the // {@link OracleHubAdmin} built via `client.createOracleHubAdmin(config)` — // same signer doctrine as the other machinery admins (makeMachineryWriter). import { decodeEventLog, type Address, type Hex, type PublicClient, type TransactionReceipt } from "viem"; import type { ClientConfig } from "./config.js"; import * as IndexerRead from "./indexerRead.js"; import { graphql } from "./gql/gql.js"; import { NotConfiguredError, RpcError } from "./errors.js"; import * as MachineryAbi from "./machineryAbi.js"; import type { MachineryAdminConfig } from "./machineryWriter.js"; import * as MachineryWriter from "./machineryWriter.js"; import type { TxResult } from "./trade.js"; export type { MachineryAdminConfig as OracleHubAdminConfig }; // ---- QuestionDefinition (mirror of src/types/OracleTypes.sol) --------------- /** * `QuestionSourceType` enum values (OracleTypes.sol). Only all-JSON * definitions participate in the hub's content-addressed dedup. * * @category oracles */ export const QUESTION_SOURCE_TYPE = { /** Answer scraped from a website URL. */ Website: 0, /** Answer fetched from a JSON endpoint — the only source type eligible for dedup. */ JSON: 1, /** Answer read from an on-chain contract. */ Contract: 2, } as const; /** * `AnswerType` enum values (OracleTypes.sol). * * @category oracles */ export const ANSWER_TYPE = { /** Resolves to a number, bucketed by `numericIntervals`. */ Numeric: 0, /** Resolves to one of the `discreteOutcomes` strings. */ Discrete: 1, } as const; /** * One answer source (website URL / JSON URL / on-chain contract) with its * encoded params. * * @category oracles */ export interface QuestionSourceInput { /** {@link QUESTION_SOURCE_TYPE} value (uint8 on the wire). */ sourceType: number; /** Source-type-specific encoded params. */ params: Hex; } /** * Inclusive numeric interval (`low <= value <= high` matches the bucket). * * @category oracles */ export interface QuestionIntervalInput { /** Lower bound (inclusive), scaled by `numericDecimals`. */ low: bigint; /** Upper bound (inclusive), scaled by `numericDecimals`. */ high: bigint; } /** * The set of valid answers a question may resolve to. * * @category oracles */ export interface ValidAnswersInput { /** {@link ANSWER_TYPE} value (uint8 on the wire). */ answerType: number; /** Outcome labels for a Discrete question; empty for Numeric. */ discreteOutcomes: string[]; /** Value buckets for a Numeric question (one outcome slot each); empty for Discrete. */ numericIntervals: QuestionIntervalInput[]; /** Fixed-point decimals the numeric answer and interval bounds are scaled by. */ numericDecimals: bigint; } /** * Full question definition — the input to `scheduleQuestion` / * `getSchedulingCost` / `questionKeyOf`. Mirrors the on-chain * `QuestionDefinition` struct field-for-field. * * @category oracles */ export interface QuestionDefinitionInput { /** * Free-form question text. EXCLUDED from the dedup key for template * (all-JSON-source) definitions — the sources determine the answer. */ questionText: string; /** Answer sources the oracle fetches; all-JSON definitions dedup content-addressed. */ sources: QuestionSourceInput[]; /** The answer space the question resolves within. */ validAnswers: ValidAnswersInput; /** Unix-seconds the oracle answers at. */ resolutionTime: bigint; /** Minimum count of sources that must agree for the oracle to resolve (else void). */ minAgreement: bigint; /** Oracle subcommittee nodes assigned to answer the question. */ subcommitteeSize: bigint; /** Subcommittee nodes that must concur on the answer. */ subcommitteeThreshold: bigint; } /** Shape viem expects for the definition tuple (named components). */ function toDefTuple(def: QuestionDefinitionInput) { return { questionText: def.questionText, sources: def.sources.map((s) => ({ sourceType: s.sourceType, params: s.params })), validAnswers: { answerType: def.validAnswers.answerType, discreteOutcomes: def.validAnswers.discreteOutcomes, numericIntervals: def.validAnswers.numericIntervals.map((i) => ({ low: i.low, high: i.high })), numericDecimals: def.validAnswers.numericDecimals, }, resolutionTime: def.resolutionTime, minAgreement: def.minAgreement, subcommitteeSize: def.subcommitteeSize, subcommitteeThreshold: def.subcommitteeThreshold, } as const; } // ---- Standalone reads (no signer; args are subject, hub, client) ----- /** * The MARGINAL scheduling cost for `def` (§8c dedup pricing): 0 when the * definition's canonical key is already scheduled below the bind cap (the * call would dedup onto the existing question), the full oracle submission * cost otherwise (new question or cap-split). Pure chain read. */ export async function getSchedulingCost( def: QuestionDefinitionInput, hub: Address, client: PublicClient, ): Promise { return client.readContract({ address: hub, abi: MachineryAbi.oracleHubAbi, functionName: "getSchedulingCost", args: [toDefTuple(def)], }); } /** * Native LOCKED for an operator's outstanding (bound-but-unresolved) markets * (§8e). Grows by `resolveReserve()` per market at onBind, released at * resolution. NEVER withdrawable (`earmarked == outstandingOf × resolveReserve`). * Pure chain read. */ export async function earmarkedOf(operatorId: number, hub: Address, client: PublicClient): Promise { return client.readContract({ address: hub, abi: MachineryAbi.oracleHubAbi, functionName: "earmarkedOf", args: [operatorId], }); } /** * An operator's accrued WITHDRAWABLE surplus credit on the hub (§8e): the * Σ `reserve − charged` released when resolved markets metered under their * earmark. The only balance `withdraw` may draw from. Pure chain read. */ export async function creditOf(operatorId: number, hub: Address, client: PublicClient): Promise { return client.readContract({ address: hub, abi: MachineryAbi.oracleHubAbi, functionName: "creditOf", args: [operatorId], }); } /** * Count of an operator's bound-but-unresolved markets (§8e). Pure chain read. * `earmarkedOf == outstandingOf × resolveReserve` at every quiescent point. */ export async function outstandingOf(operatorId: number, hub: Address, client: PublicClient): Promise { return client.readContract({ address: hub, abi: MachineryAbi.oracleHubAbi, functionName: "outstandingOf", args: [operatorId], }); } /** * Wei an operator's owner may withdraw right now (§8e) — equal to `creditOf`; * the earmark backing live markets is never withdrawable. Pure chain read. */ export async function withdrawableOf(operatorId: number, hub: Address, client: PublicClient): Promise { return client.readContract({ address: hub, abi: MachineryAbi.oracleHubAbi, functionName: "withdrawableOf", args: [operatorId], }); } /** * A1: the withdrawable surplus credited to a reserve-PAYER (`origin.creator` — * an open-venue user, or the autonomous MarketCreator on its rolls) rather than * the operator. That account draws it itself via `withdrawMyCredit`. Pure chain * read. */ export async function payerCreditOf(payer: Address, hub: Address, client: PublicClient): Promise { return client.readContract({ address: hub, abi: MachineryAbi.oracleHubAbi, functionName: "payerCreditOf", args: [payer], }); } /** * A1: the account recorded as the reserve-payer for a market at onBind (the * surplus recipient); zero-address once the market has settled + been swept. * Pure chain read. */ export async function payerOf(marketId: Hex, hub: Address, client: PublicClient): Promise
{ return client.readContract({ address: hub, abi: MachineryAbi.oracleHubAbi, functionName: "payerOf", args: [marketId], }); } /** * The per-market resolution reserve (§8e): the exact wei ATTACHED to each * `onBind` (the market-creation value) and LOCKED per-market — one worst-case * resolve (`perMarketResolveGas × maxFeePerGas`). Pure chain read. */ export async function resolveReserve(hub: Address, client: PublicClient): Promise { return client.readContract({ address: hub, abi: MachineryAbi.oracleHubAbi, functionName: "resolveReserve", }); } /** * THE §8e user-side create-value rule: * `quoteCreateMarketValue(def) = getSchedulingCost(def) + resolveReserve()`. * * This is the exact native value to attach to * `BinaryMarketsModule.scheduleAndCreateMarket` (schedule + bind in one tx): * the marginal scheduling cost PLUS the resolution reserve, which is ATTACHED to * the create and LOCKED per-market at onBind (earmark-at-creation — there is NO * standing prepaid pre-fund anymore). For `createMarket` against an * ALREADY-scheduled question the scheduling cost is 0, so the value is just the * reserve. Quote fresh per create — the scheduling cost is marginal (drops to 0 * once the question exists). Excess is refunded in-tx. */ export async function quoteCreateMarketValue( def: QuestionDefinitionInput, hub: Address, client: PublicClient, ): Promise { const [cost, reserve] = await Promise.all([getSchedulingCost(def, hub, client), resolveReserve(hub, client)]); return cost + reserve; } // ---- Admin surface ----------------------------------------------------------- /** * One question's live dedup state on the hub. * * @category oracles */ export interface HubQuestionState { /** * Canonical dedup key for the definition (zero for non-template * definitions, which bypass dedup). */ questionKey: Hex; /** The ACTIVE question id the key resolves to (0n = never scheduled). */ oracleQuestionId: bigint; /** Lifetime bind count for that question (drives the cap-split). */ bindCount: number; /** Every marketId bound to the question (the fan-out list the drain walks). */ markets: Hex[]; } /** * Live on-chain status of the hub (the point read a panel confirms). * * @category oracles */ export interface HubStatus { /** Hub owner — the account gated into `withdraw` / gas + drain param writes. */ owner: Address; /** * Total native balance (wei) the hub holds (Σ earmarked + accrued credit + * reactivity bond float). */ balanceWei: bigint; /** * Whether the module has the hub approved (`approvedAdapters(hub)`) — the * wired/live gate. */ approved: boolean; /** Reactivity subscription id; 0n until `enableReactivity` succeeds. */ subscriptionId: bigint; /** Reactivity-callback tip (wei per gas). */ priorityFeePerGas: bigint; /** Reactivity-callback fee ceiling (wei per gas); one factor of `resolveReserve`. */ maxFeePerGas: bigint; /** Gas limit each reactivity callback runs with. */ gasLimit: bigint; /** Per-market resolve-slice gas constant (sizes `resolveReserve`). */ perMarketResolveGas: bigint; /** Explicitly-attributed callback overhead gas constant (metering). */ callbackBaseGas: bigint; /** Belt-and-suspenders cap on markets resolved per callback. */ maxResolvesPerCallback: bigint; /** Gas reserve that breaks the drain loop to the next block. */ resolveGasReserve: bigint; /** Per-market resolution reserve attached+locked at onBind (`resolveReserve()`, wei). */ resolveReserveWei: bigint; /** Markets still queued for resolution across all pending qids. */ pendingResolves: bigint; } /** * Parameters for {@link OracleHubAdmin.scheduleQuestion}. * * @category oracles */ export interface ScheduleQuestionParams { /** The question to schedule (deduped content-addressed for template definitions). */ def: QuestionDefinitionInput; /** * Native value to attach. Defaults to the live `getSchedulingCost(def)` * quote (the marginal price — 0 for a dedup reuse). */ valueWei?: bigint; /** Gas-limit override for this tx (defaults to the admin config's `gas`). */ gas?: bigint; } /** * Result of {@link OracleHubAdmin.scheduleQuestion}. * * **Details** * * Scheduling is idempotent by question DEFINITION: an identical definition * deduplicates onto the existing question instead of creating a second one. * * **Gotchas** * * Because of that deduplication, check `reused` before assuming this call is * what created the id. * * @category oracles */ export interface ScheduleQuestionResult extends TxResult { /** Oracle-assigned (or deduplicated) question id. */ oracleQuestionId: bigint; /** * True when the definition deduplicated onto an EXISTING question * (`QuestionReused` — the caller was charged nothing). */ reused: boolean; } /** * Parameters for {@link OracleHubAdmin.withdraw}. * * @category oracles */ export interface WithdrawParams { /** Operator whose WITHDRAWABLE credit to draw down. OWNER-gated on-chain. */ operatorId: number; /** * Wei to withdraw from the operator's accrued surplus credit (must be ≤ * `withdrawableOf(operatorId)`, else the hub reverts `InsufficientCredit`). */ amountWei: bigint; /** Recipient of the native transfer. */ to: Address; /** Gas-limit override for this tx (defaults to the admin config's `gas`). */ gas?: bigint; } /** * Parameters for {@link OracleHubAdmin.withdrawMyCredit}. * * @category oracles */ export interface WithdrawMyCreditParams { /** * Wei to withdraw from the CALLER's own accrued A1 payer credit (must be ≤ * `payerCreditOf(caller)`, else the hub reverts). msg.sender-gated — the * connected signer draws only its own credit. */ amountWei: bigint; /** Recipient of the native transfer. */ to: Address; /** Gas-limit override for this tx (defaults to the admin config's `gas`). */ gas?: bigint; } /** * Parameters for {@link OracleHubAdmin.fundHub}. * * @category oracles */ export interface FundHubParams { /** * Native amount (wei) sent to the hub's `receive()` — tops up the * reactivity bond the precompile debits callbacks from. NOT credited to any * operator (resolution funding is attached per-market at create/onBind). */ amountWei: bigint; /** Gas-limit override for this tx (defaults to the admin config's `gas`). */ gas?: bigint; } /** * Parameters for {@link OracleHubAdmin.setGasParams} (OWNER-only). * * @category oracles */ export interface SetHubGasParams { /** New reactivity-callback tip (wei per gas). */ priorityFeePerGas: bigint; /** * New reactivity-callback fee ceiling (wei per gas) — also reprices * `resolveReserve()` immediately. */ maxFeePerGas: bigint; /** New gas limit each reactivity callback runs with. */ gasLimit: bigint; /** Gas-limit override for this tx (defaults to the admin config's `gas`). */ gas?: bigint; } /** * Parameters for {@link OracleHubAdmin.setDrainParams} (OWNER-only). * * @category oracles */ export interface SetHubDrainParams { /** * Gas budgeted per market resolve — sizes `resolveReserve()` * (`perMarketResolveGas × maxFeePerGas`). */ perMarketResolveGas: bigint; /** * Callback overhead gas explicitly attributed across the markets resolved * in it (the metering overhead term). */ callbackBaseGas: bigint; /** Belt-and-suspenders cap on markets resolved per callback. */ maxResolvesPerCallback: bigint; /** Remaining-gas floor at which the drain loop breaks to the next block. */ resolveGasReserve: bigint; /** Gas-limit override for this tx (defaults to the admin config's `gas`). */ gas?: bigint; } /** * Parameters for {@link OracleHubAdmin.enableReactivity} / * {@link OracleHubAdmin.migrateSubscription}. * * @category oracles */ export interface EnableHubReactivityParams { /** Gas-limit override for this tx (defaults to the admin config's `gas`). */ gas?: bigint; } /** * Admin handle for the OracleHub — the protocol's ONE governance-approved oracle * adapter (Oracle v2 §8e, earmark-at-creation). Point reads (quotes, the * earmark/credit accounts, dedup state, live status) plus the signed writes * (schedule, credit-only withdrawals, funding, owner-gated gas/drain params, * reactivity wiring). Built via `client.createOracleHubAdmin(config)`; needs * `config.addresses.oracleHub` (and `binaryModule` for the approval reads). * * Every write throws `ContractRevertError` when the chain rejects it — at * simulation, at send, or as a mined receipt with `status: "reverted"` (the * reason is recovered by replaying the call at that block while the node still * has the state) — and `RpcError` when the send or the receipt read does not * complete. A reverted write never resolves as if it had been confirmed. * * @category oracles */ export interface OracleHubAdmin { // ---- reads (quote + earmark/credit surface; no signer required by the chain) ---- /** Marginal scheduling cost for `def` (0 = would dedup). */ getSchedulingCost(def: QuestionDefinitionInput): Promise; /** Native LOCKED for an operator's outstanding markets (wei; never withdrawable). */ earmarkedOf(operatorId: number): Promise; /** An operator's accrued WITHDRAWABLE surplus credit on the hub (wei). */ creditOf(operatorId: number): Promise; /** Count of an operator's bound-but-unresolved markets. */ outstandingOf(operatorId: number): Promise; /** Wei an operator's owner may withdraw right now (== `creditOf`). */ withdrawableOf(operatorId: number): Promise; /** * A1: withdrawable surplus credited to a reserve-PAYER (open-venue creator or * the autonomous MarketCreator), drawn by that account itself. */ payerCreditOf(payer: Address): Promise; /** * A1: the reserve-payer recorded for a market at onBind (surplus recipient); * zero-address once settled + swept. */ payerOf(marketId: Hex): Promise
; /** The per-market resolution reserve attached+locked at onBind (wei). */ resolveReserve(): Promise; /** * THE §8e create-market value rule: `getSchedulingCost(def) + resolveReserve()` * (the reserve is attached to the create; attach exactly this to * `scheduleAndCreateMarket`; excess refunds). */ quoteCreateMarketValue(def: QuestionDefinitionInput): Promise; /** * One definition's full dedup state: canonical key, the active question id * it resolves to (0n = none), bind count, and the bound-market fan-out list. */ getQuestionState(def: QuestionDefinitionInput): Promise; /** The ACTIVE question id for a canonical key (0n = never scheduled). */ getQuestionIdByKey(questionKey: Hex): Promise; /** Lifetime bind count for a question id. */ getBindCount(oracleQuestionId: bigint): Promise; /** Every marketId bound to a question id (the fan-out list). */ getMarketsForQuestion(oracleQuestionId: bigint): Promise; /** * Whether the module has the hub approved (`approvedAdapters(hub)`) — the * Oracle v2 equivalent of the old per-adapter `isAdapterApproved`. Needs * `config.addresses.binaryModule`. */ isHubApproved(): Promise; /** * The hub's full live status (owner / balance / approval / subscription / * gas + drain params / resolveReserve + pending drain). */ getHubStatus(): Promise; // ---- writes ---- /** * Schedule a question through the hub (content-addressed: an identical * template definition returns the EXISTING id and refunds the value). * Resolves with the question id decoded from `QuestionScheduled` / * `QuestionReused` and whether it deduplicated. */ scheduleQuestion(p: ScheduleQuestionParams): Promise; /** * Withdraw an operator's accrued WITHDRAWABLE surplus credit (credit-only; * OWNER-gated on-chain: only the operator's owner, read from MarketsCore, may * draw it, and only up to `withdrawableOf` — else `InsufficientCredit`). The * earmark backing live markets is untouchable. */ withdraw(p: WithdrawParams): Promise; /** * A1: withdraw the CALLER's own accrued payer credit (msg.sender-gated — the * connected signer draws only its own surplus, up to `payerCreditOf(caller)`). * This is how an open-venue creator claims their refund, and how the * autonomous MarketCreator self-reclaims (via its own on-chain call). */ withdrawMyCredit(p: WithdrawMyCreditParams): Promise; /** * Send native to the hub's `receive()` — funds the reactivity bond (hub * float; NOT credited to any operator — resolution funding is per-market). */ fundHub(p: FundHubParams): Promise; /** * Update the reactivity gas params (OWNER-only). `maxFeePerGas` also * reprices `resolveReserve()` immediately. */ setGasParams(p: SetHubGasParams): Promise; /** * Update the bounded-drain metering params (OWNER-only): `perMarketResolveGas` * sizes `resolveReserve`, `callbackBaseGas` is the attributed overhead term, * `maxResolvesPerCallback` + `resolveGasReserve` bound one callback's work. */ setDrainParams(p: SetHubDrainParams): Promise; /** * Register the Somnia reactivity subscription (OWNER-only, one-shot). NOTE: * calls the reactivity precompile at 0x0100, which does NOT exist on local * anvil — testnet/mainnet only. */ enableReactivity(p?: EnableHubReactivityParams): Promise; /** * Unsubscribe + re-subscribe with the current topic/gas params (OWNER-only; * for upstream event-signature changes). Precompile — testnet/mainnet only. */ migrateSubscription(p?: EnableHubReactivityParams): Promise; } /** * Dependencies supplied by the owning client to its OracleHub capability. * * @internal */ export interface OracleHubAdminDeps { getConfig: () => ClientConfig; getClient: () => PublicClient; } export function createOracleHubAdminWithDeps(config: MachineryAdminConfig, deps: OracleHubAdminDeps): OracleHubAdmin { const hub = deps.getConfig().addresses?.oracleHub; const binaryModule = deps.getConfig().addresses?.binaryModule; const writer = MachineryWriter.makeMachineryWriter(config, deps, "createOracleHubAdmin"); const pc = () => deps.getClient(); function requireHub(): Address { if (!hub) { throw new NotConfiguredError("config.addresses.oracleHub", "createOracleHubAdmin"); } return hub; } function requireModule(): Address { if (!binaryModule) { throw new NotConfiguredError("config.addresses.binaryModule", "hub-approval reads"); } return binaryModule; } function decodeScheduled(receipt: TransactionReceipt): { oracleQuestionId: bigint; reused: boolean } { for (const log of receipt.logs) { try { const decoded = decodeEventLog({ abi: MachineryAbi.oracleHubEventsAbi, data: log.data, topics: log.topics, }) as { eventName: string; args: Record; }; if (decoded.eventName === "QuestionScheduled") { return { oracleQuestionId: decoded.args.oracleQuestionId as bigint, reused: false }; } if (decoded.eventName === "QuestionReused") { return { oracleQuestionId: decoded.args.oracleQuestionId as bigint, reused: true }; } } catch { continue; } } throw new RpcError("scheduleQuestion", "the receipt carried no QuestionScheduled/QuestionReused event"); } async function isHubApproved(): Promise { return await pc().readContract({ address: requireModule(), abi: MachineryAbi.moduleGovernanceAbi, functionName: "approvedAdapters", args: [requireHub()], }); } return { getSchedulingCost: (def) => getSchedulingCost(def, requireHub(), pc()), earmarkedOf: (operatorId) => earmarkedOf(operatorId, requireHub(), pc()), creditOf: (operatorId) => creditOf(operatorId, requireHub(), pc()), outstandingOf: (operatorId) => outstandingOf(operatorId, requireHub(), pc()), withdrawableOf: (operatorId) => withdrawableOf(operatorId, requireHub(), pc()), payerCreditOf: (payer) => payerCreditOf(payer, requireHub(), pc()), payerOf: (marketId) => payerOf(marketId, requireHub(), pc()), resolveReserve: () => resolveReserve(requireHub(), pc()), quoteCreateMarketValue: (def) => quoteCreateMarketValue(def, requireHub(), pc()), async getQuestionState(def): Promise { const a = { address: requireHub(), abi: MachineryAbi.oracleHubAbi } as const; const questionKey = await pc().readContract({ ...a, functionName: "questionKeyOf", args: [toDefTuple(def)], }); const oracleQuestionId = await pc().readContract({ ...a, functionName: "questionIdByKey", args: [questionKey], }); if (oracleQuestionId === 0n) { return { questionKey, oracleQuestionId, bindCount: 0, markets: [] }; } const [bindCount, markets] = await Promise.all([ pc().readContract({ ...a, functionName: "bindCount", args: [oracleQuestionId] }), pc().readContract({ ...a, functionName: "marketsForQuestion", args: [oracleQuestionId] }), ]); return { questionKey, oracleQuestionId, bindCount: Number(bindCount), markets: [...markets] }; }, async getQuestionIdByKey(questionKey): Promise { return pc().readContract({ address: requireHub(), abi: MachineryAbi.oracleHubAbi, functionName: "questionIdByKey", args: [questionKey], }); }, async getBindCount(oracleQuestionId): Promise { const count = await pc().readContract({ address: requireHub(), abi: MachineryAbi.oracleHubAbi, functionName: "bindCount", args: [oracleQuestionId], }); return Number(count); }, async getMarketsForQuestion(oracleQuestionId): Promise { const markets = await pc().readContract({ address: requireHub(), abi: MachineryAbi.oracleHubAbi, functionName: "marketsForQuestion", args: [oracleQuestionId], }); return [...markets]; }, isHubApproved, async getHubStatus(): Promise { const address = requireHub(); const a = { address, abi: MachineryAbi.oracleHubAbi } as const; const [ owner, balanceWei, approved, subscriptionId, priorityFeePerGas, maxFeePerGas, gasLimit, perMarketResolveGas, callbackBaseGas, maxResolvesPerCallback, resolveGasReserve, resolveReserveWei, pendingResolves, ] = await Promise.all([ pc().readContract({ ...a, functionName: "owner" }), pc().getBalance({ address }), isHubApproved(), pc().readContract({ ...a, functionName: "subscriptionId" }), pc().readContract({ ...a, functionName: "priorityFeePerGas" }), pc().readContract({ ...a, functionName: "maxFeePerGas" }), pc().readContract({ ...a, functionName: "gasLimit" }), pc().readContract({ ...a, functionName: "perMarketResolveGas" }), pc().readContract({ ...a, functionName: "callbackBaseGas" }), pc().readContract({ ...a, functionName: "maxResolvesPerCallback" }), pc().readContract({ ...a, functionName: "resolveGasReserve" }), pc().readContract({ ...a, functionName: "resolveReserve" }), pc().readContract({ ...a, functionName: "pendingResolves" }), ]); return { owner, balanceWei, approved, subscriptionId, priorityFeePerGas: BigInt(priorityFeePerGas), maxFeePerGas: BigInt(maxFeePerGas), gasLimit: BigInt(gasLimit), perMarketResolveGas: BigInt(perMarketResolveGas), callbackBaseGas: BigInt(callbackBaseGas), maxResolvesPerCallback: BigInt(maxResolvesPerCallback), resolveGasReserve: BigInt(resolveGasReserve), resolveReserveWei, pendingResolves, }; }, async scheduleQuestion(p): Promise { const address = requireHub(); const value = p.valueWei ?? (await getSchedulingCost(p.def, address, pc())); const result = await writer.execute({ to: address, abi: MachineryAbi.oracleHubAbi, functionName: "scheduleQuestion", args: [toDefTuple(p.def)], value, gas: p.gas, }); return { ...result, ...decodeScheduled(result.receipt) }; }, async withdraw(p): Promise { return writer.execute({ to: requireHub(), abi: MachineryAbi.oracleHubAbi, functionName: "withdraw", args: [p.operatorId, p.amountWei, p.to], gas: p.gas, }); }, async withdrawMyCredit(p): Promise { return writer.execute({ to: requireHub(), abi: MachineryAbi.oracleHubAbi, functionName: "withdrawMyCredit", args: [p.amountWei, p.to], gas: p.gas, }); }, async fundHub(p): Promise { return writer.execute({ to: requireHub(), value: p.amountWei, gas: p.gas }); }, async setGasParams(p): Promise { return writer.execute({ to: requireHub(), abi: MachineryAbi.oracleHubAbi, functionName: "setGasParams", args: [p.priorityFeePerGas, p.maxFeePerGas, p.gasLimit], gas: p.gas, }); }, async setDrainParams(p): Promise { return writer.execute({ to: requireHub(), abi: MachineryAbi.oracleHubAbi, functionName: "setDrainParams", args: [p.perMarketResolveGas, p.callbackBaseGas, p.maxResolvesPerCallback, p.resolveGasReserve], gas: p.gas, }); }, async enableReactivity(p?): Promise { return writer.execute({ to: requireHub(), abi: MachineryAbi.oracleHubAbi, functionName: "enableReactivity", args: [], gas: p?.gas, }); }, async migrateSubscription(p?): Promise { return writer.execute({ to: requireHub(), abi: MachineryAbi.oracleHubAbi, functionName: "migrateSubscription", args: [], gas: p?.gas, }); }, }; } // ---- Oracle v2 hub entities (OracleQuestion / OperatorHubAccount / OracleBind / // OracleCallback) ------------------------------------------------------ // Field names mirror the indexer schema EXACTLY (indexer/schema.graphql, Oracle // v2 §8e earmark-at-creation). The scalar columns map 1:1 onto the hub events // (QuestionScheduled/QuestionReused, ReserveEarmarked/SurplusCredited/ // CreditWithdrawn, MarketBound/MarketResolveCharged, CallbackAccounted). /** * A hub-scheduled oracle question (mirror of the indexer `OracleQuestion` * entity; id = oracleQuestionId as a decimal string). Tracks the * content-addressed dedup state: the canonical key, who paid the oracle * submission, and how many markets bound to it. * * @category oracles */ export type OracleQuestionRecord = { /** oracleQuestionId (decimal string == entity id). */ id: string; /** * Canonical dedup key (bytes32 hex); zero/null for non-template definitions * that bypassed dedup. */ questionKey: string | null; /** Caller that paid the oracle submission cost (lowercased). */ scheduler: string; /** Native wei forwarded to the oracle for this submission. */ oracleCost: string; /** Lifetime bind count (from `MarketBound.bindCount`, monotonic). */ bindCount: number; /** * Times an identical definition deduplicated onto this question * (`QuestionReused` count). */ reuseCount: number; /** Block the question was scheduled in (decimal string). */ createdAtBlock: string; /** Timestamp (unix seconds) the question was scheduled. */ createdAtTimestamp: string; }; /** * An operator's hub account (mirror of the indexer `OperatorHubAccount` entity; * id = operatorId decimal string) — the earmark-at-creation surface. Running * totals derived from the hub events: `earmarked` (LOCKED, never withdrawable; * == `outstanding × resolveReserve`), `credit` (WITHDRAWABLE surplus), and * `outstanding` (bound-but-unresolved market count). Live/authoritative figures * are the chain reads `earmarkedOf`/`creditOf`/`outstandingOf`. * * @category oracles */ export type OperatorHubAccountRecord = { /** operatorId (decimal string == entity id). */ id: string; /** The operator the account belongs to (numeric form of `id`). */ operatorId: number; /** Native LOCKED for outstanding markets (wei); Σ ReserveEarmarked − Σ released. */ earmarked: string; /** Withdrawable surplus (wei): Σ SurplusCredited − Σ CreditWithdrawn. */ credit: string; /** Bound-but-unresolved market count (binds − resolutions). */ outstanding: string; /** Block the account first appeared in (decimal string). */ createdAtBlock: string; /** Timestamp (unix seconds) the account first appeared. */ createdAtTimestamp: string; /** Block of the last update to the running totals (decimal string). */ updatedAtBlock: string; /** Timestamp (unix seconds) of the last update to the running totals. */ updatedAtTimestamp: string; }; /** * One market's bind on a question (mirror of the indexer `OracleBind` entity, * Oracle v2 §8e). Opens with `MarketBound` (operator attribution; the paired * `ReserveEarmarked` locks the reserve), stamped at resolution with * `MarketResolveCharged` (exact metered charge + subsidy; a surplus is credited * via `SurplusCredited`). Per-market conservation: `charged + subsidy == cost`. * * @category oracles */ export type OracleBindRecord = { /** Bind id (`${oracleQuestionId}_${bindIndex}`). */ id: string; /** Question the bind belongs to (decimal string). */ oracleQuestionId: string; /** 1-based lifetime bind sequence on the question. */ bindIndex: number; /** Operator whose earmark funds this market's resolve. */ operatorId: number; /** gasleft()-wrapped measured gas of the resolve slice; null until resolved. */ measuredGas: string | null; /** Pro-rata share of the callback overhead gas; null until resolved. */ overheadShare: string | null; /** Exact wei the resolve cost worked out to; null until resolved. */ cost: string | null; /** * Wei actually charged against this market's earmark; null until resolved * (== min(cost, reserve)). */ charged: string | null; /** * cost − charged: the reserve-capped shortfall the hub subsidised; null * until resolved. */ subsidy: string | null; /** * Timestamp the resolve cost was charged (`MarketResolveCharged`); null * until resolved — the resolved flag is simply `resolvedAt != null`. */ resolvedAt: string | null; /** Block the bind landed in (decimal string). */ boundAtBlock: string; /** Timestamp (unix seconds) of the bind. */ boundAtTimestamp: string; /** Tx hash of the bind (the market-creation tx). */ txHash: string; }; /** * One resolution callback's conservation record (mirror of the indexer * `OracleCallback` entity ← the hub's `CallbackAccounted` event). NOT * per-question (a callback drains across many qids). Invariants: * `totalCost == (measuredGas + overheadGasAttributed) * gasPrice`; Σ of the * callback's `OracleBindRecord.charged == totalCharged`; `totalCharged + * subsidy == totalCost` (the subsidy is the hub's explicit reserve-capped * shortfall). * * @category oracles */ export type OracleCallbackRecord = { /** Callback id (`${blockNumber}_${logIndex}`). */ id: string; /** Number of markets resolved in this callback. */ marketsResolved: string; /** `tx.gasprice` of the callback (wei). */ gasPrice: string; /** Σ `gasleft()`-wrapped measured gas across the resolved markets. */ measuredGas: string; /** The governance-calibrated `callbackBaseGas` term attributed on top. */ overheadGasAttributed: string; /** Exact wei distributed: `(measuredGas + overhead) * gasPrice`. */ totalCost: string; /** Σ wei actually charged against market earmarks. */ totalCharged: string; /** `totalCost − totalCharged`: the hub's explicit reserve-capped subsidy. */ subsidy: string; /** Markets still queued after this callback (0 = drain complete). */ pendingRemaining: string; /** Block the callback landed in (decimal string). */ blockNumber: string; /** Timestamp (unix seconds) of the callback. */ timestamp: string; /** Tx hash of the callback. */ txHash: string; }; // prettier-ignore const OracleQuestionFields = graphql(` fragment OracleQuestionFields on OracleQuestion { id questionKey scheduler oracleCost bindCount reuseCount createdAtBlock createdAtTimestamp } `); // prettier-ignore const OperatorHubAccountFields = graphql(` fragment OperatorHubAccountFields on OperatorHubAccount { id operatorId earmarked credit outstanding createdAtBlock createdAtTimestamp updatedAtBlock updatedAtTimestamp } `); // prettier-ignore const OracleBindFields = graphql(` fragment OracleBindFields on OracleBind { id oracleQuestionId bindIndex operatorId measuredGas overheadShare cost charged subsidy resolvedAt boundAtBlock boundAtTimestamp txHash } `); // prettier-ignore const OracleCallbackFields = graphql(` fragment OracleCallbackFields on OracleCallback { id marketsResolved gasPrice measuredGas overheadGasAttributed totalCost totalCharged subsidy pendingRemaining blockNumber timestamp txHash } `); /** One hub-scheduled question by its oracleQuestionId, or null. Indexer read. */ export async function getOracleQuestion( oracleQuestionId: string, indexerUrl: string, ): Promise { const data = await IndexerRead.gqlRequest(OracleQuestionQuery, { id: String(oracleQuestionId) }, indexerUrl); return data.OracleQuestion_by_pk; } /** * Hub-scheduled questions, newest first — filter by `scheduler` and/or * `questionKey`, paginate. Indexer read. */ export async function listOracleQuestions( opts: { scheduler?: string; questionKey?: string; limit?: number; offset?: number } = {}, indexerUrl: string, ): Promise { const where: Record = {}; if (opts.scheduler != null) where.scheduler = { _eq: opts.scheduler.toLowerCase() }; if (opts.questionKey != null) where.questionKey = { _eq: opts.questionKey.toLowerCase() }; const data = await IndexerRead.gqlRequest( OracleQuestionsQuery, { where, limit: opts.limit ?? 50, offset: opts.offset ?? 0 }, indexerUrl, ); return data.OracleQuestion; } /** * One operator's hub account (earmarked / credit / outstanding) by operatorId, * or null. Indexer read. */ export async function getOperatorHubAccount( operatorId: string | number, indexerUrl: string, ): Promise { const data = await IndexerRead.gqlRequest(OperatorHubAccountQuery, { id: String(operatorId) }, indexerUrl); return data.OperatorHubAccount_by_pk; } /** * Operator hub-account records, most-recently-updated first, paginated. * Indexer read. */ export async function listOperatorHubAccounts( opts: { limit?: number; offset?: number } = {}, indexerUrl: string, ): Promise { const data = await IndexerRead.gqlRequest( OperatorHubAccountsQuery, { limit: opts.limit ?? 50, offset: opts.offset ?? 0 }, indexerUrl, ); return data.OperatorHubAccount; } /** * Bind records, newest first — filter by `operatorId` (an operator's bound * markets + their exact resolve charges) and/or `oracleQuestionId`, optionally * scope to `resolved` binds (`resolvedAt != null`), paginate. Oracle v2 §8e: * no escrow — resolved rows carry the exact metered charge + subsidy. Indexer * read. */ export async function listOracleBinds( opts: { operatorId?: number; oracleQuestionId?: string; resolved?: boolean; limit?: number; offset?: number } = {}, indexerUrl: string, ): Promise { const where: Record = {}; if (opts.operatorId != null) where.operatorId = { _eq: opts.operatorId }; if (opts.oracleQuestionId != null) where.oracleQuestionId = { _eq: String(opts.oracleQuestionId) }; if (opts.resolved != null) where.resolvedAt = opts.resolved ? { _is_null: false } : { _is_null: true }; const data = await IndexerRead.gqlRequest( OracleBindsQuery, { where, limit: opts.limit ?? 50, offset: opts.offset ?? 0 }, indexerUrl, ); return data.OracleBind; } /** * Resolution-callback conservation records, newest first, paginated. The audit * trail an off-chain verifier reconciles the hub's exact-metering invariants * against (a callback drains across many questions, so there is no * per-question filter). Indexer read. */ export async function listOracleCallbacks( opts: { limit?: number; offset?: number } = {}, indexerUrl: string, ): Promise { const data = await IndexerRead.gqlRequest( OracleCallbacksQuery, { limit: opts.limit ?? 50, offset: opts.offset ?? 0 }, indexerUrl, ); return data.OracleCallback; } // prettier-ignore const OracleQuestionQuery = graphql(` query OracleQuestion($id: String!) { OracleQuestion_by_pk(id: $id) { ...OracleQuestionFields } } `); // prettier-ignore const OracleQuestionsQuery = graphql(` query OracleQuestions($where: OracleQuestion_bool_exp!, $limit: Int, $offset: Int) { OracleQuestion(where: $where, order_by: {createdAtTimestamp: desc}, limit: $limit, offset: $offset) { ...OracleQuestionFields } } `); // prettier-ignore const OperatorHubAccountQuery = graphql(` query OperatorHubAccount($id: String!) { OperatorHubAccount_by_pk(id: $id) { ...OperatorHubAccountFields } } `); // prettier-ignore const OperatorHubAccountsQuery = graphql(` query OperatorHubAccounts($limit: Int, $offset: Int) { OperatorHubAccount(order_by: {updatedAtTimestamp: desc}, limit: $limit, offset: $offset) { ...OperatorHubAccountFields } } `); // prettier-ignore const OracleBindsQuery = graphql(` query OracleBinds($where: OracleBind_bool_exp!, $limit: Int, $offset: Int) { OracleBind(where: $where, order_by: {boundAtTimestamp: desc}, limit: $limit, offset: $offset) { ...OracleBindFields } } `); // prettier-ignore const OracleCallbacksQuery = graphql(` query OracleCallbacks($limit: Int, $offset: Int) { OracleCallback(order_by: {timestamp: desc}, limit: $limit, offset: $offset) { ...OracleCallbackFields } } `);