import { Address, Cell, Contract, ContractProvider, Sender, Slice } from '@ton/core'; import { PHOEBE_STORAGE_VERSION, PHOEBE_CONFIG_BLOB_VERSION, DEFAULT_GROUP_ID, MAX_FEED_COUNT, OPERATOR_BPS_DENOM, DEFAULT_OPERATOR_BPS } from '../opcodes'; export { PHOEBE_STORAGE_VERSION, PHOEBE_CONFIG_BLOB_VERSION, DEFAULT_GROUP_ID, MAX_FEED_COUNT, OPERATOR_BPS_DENOM, DEFAULT_OPERATOR_BPS, }; export declare const MIN_RESERVE_FLOOR: bigint; export declare const MIN_UPGRADE_DELAY: number; export declare const DEFAULT_MAX_PUSH_DRIFT: number; export declare const MAX_MAX_INACTIVITY: number; export declare const CAUSE_FIRST_SYNC: number; export declare const CAUSE_ACTIVATION_CHANGE: number; export type PhoebeTunables = { pullFee: bigint; minStorageReserve: bigint; maxPushDrift: number; /** Basis points (0..10000) of pullFee credited to the lastSubmitter on each accepted pull. Default 7500 (75%). */ operatorBps: number; /** Per-operator silence window (seconds). When > 0, lastSubmitter must have pushed within the window to receive the operator share — otherwise the reflected share rolls into rewardPool. 0 = disabled (default; lastSubmitter always credited when set). Max MAX_MAX_INACTIVITY (24h). */ maxInactivity: number; }; export declare const PHOEBE_DEFAULTS: PhoebeTunables; export type PhoebeInitConfig = { owner: Address; /** The ForgeTON pool Phoebe trusts for AutomatonSync. Pinned at deploy. */ forgeton: Address; /** The Atlas contract Phoebe trusts for GroupKeySync. Pinned at deploy. */ atlas: Address; /** Optional tunable overrides — any omitted field uses PHOEBE_DEFAULTS. */ tunables?: Partial; }; /** * Build the storage init cell. Field order MUST match `PhoebeStorage` in * contracts/phoebe-storage.tolk: fixed-width scalars first (cheap `lazy` * skip), then addresses, then refs. * * MIRROR-OF: contracts/phoebe-storage.tolk:PhoebeStorage * * schemaVersion(8) | paused(1) | * owner | forgeton | atlas | * configRef | groups(map) | operators(map) */ export declare function phoebeConfigToCell(cfg: PhoebeInitConfig): Cell; export type OwnershipReply = { owner: Address; forgeton: Address; atlas: Address; }; export type PhoebeConfigReply = { pullFee: bigint; minStorageReserve: bigint; maxPushDrift: number; rewardPool: bigint; operatorBps: number; /** Owner-set per-operator silence window (seconds). 0 = disabled. */ maxInactivity: number; }; export type SchemaVersionsReply = { storage: number; config: number; }; export type OperatorReply = { isActive: boolean; /** Unix seconds of operator's most recent successful PushSnapshot. 0 = never pushed. */ lastPushAt: number; }; export type GroupKeyReply = { groupId: number; groupEpoch: number; threshold: number; memberCount: number; /** 48-byte G1 point, big-endian raw bytes. */ groupPk: Buffer; cachedAt: number; }; export type SnapshotReply = { /** Unix seconds — 0 if no snapshot has ever been pushed. */ lastSnapshotTime: number; /** Merkle root of latest signed snapshot — 0n if no snapshot has been pushed. */ lastRoot: bigint; }; export type PendingUpgradeReply = { /** 32-byte hash of the proposed new code; 0n when no upgrade pending. */ newCodeHash: bigint; /** Unix seconds at which execution becomes legal; 0 when no upgrade pending. */ eta: number; }; export type PriceLeaf = { feedId: number; /** Signed; covers BTC ($100k × 10^8 fits) with headroom. */ mantissa: bigint; /** Signed exponent: actual_price = mantissa × 10^expo. */ expo: number; /** Confidence interval in basis points of price. */ confBps: number; /** Unix seconds — when off-chain aggregator produced this leaf. */ pubTime: number; }; export declare function priceLeafToCell(leaf: PriceLeaf): Cell; export declare function priceLeafFromSlice(s: Slice): PriceLeaf; /** * Decode a `FulfillPrice` callback body (opcode `0x72`) that your * consumer receives after a successful `sendRequestPrice`. * * **Off-chain (TS consumer / indexer):** * ```typescript * import { parseFulfillPrice, OP } from '@titon-network/phoebe-sdk'; * * // Inside your tx-handling code, find the inbound to your consumer: * for (const tx of pulledTxs) { * const body = tx.inMessage!.body; * if (body.beginParse().preloadUint(32) !== OP.FulfillPrice) continue; * const { queryId, leaf } = parseFulfillPrice(body); * console.log(`Pull #${queryId}: feed ${leaf.feedId} = ${leaf.mantissa}e${leaf.expo}`); * } * ``` * * **On-chain (Tolk consumer)** — declare a matching struct + receiver; * see `templates/consumer.tolk` for the canonical shape. Byte layout is * frozen across all Phoebe versions. * * MIRROR-OF: contracts/messages.tolk:FulfillPrice (0x72) * opcode(32) | queryId(64) | leaf: PriceLeaf (inline, 200 bits) * * @throws Error — if `body`'s opcode prefix is not `0x72`. */ export declare function parseFulfillPrice(body: Cell): { queryId: bigint; leaf: PriceLeaf; }; /** * Decoded payload of `EvtRewardClaimed` (opcode 0x8C). Emitted by * `handleClaimReward` on every successful operator claim. Indexers use * this to track operator revenue (consumer apps can correlate against * `EvtSnapshotPushed.submitter` for share-of-revenue analytics). */ export type EvtRewardClaimedPayload = { /** Address of the operator that called `ClaimReward`. */ claimant: Address; /** Payout destination (may differ from `claimant`). */ to: Address; /** Amount paid out in nanoTON. */ amount: bigint; }; /** * Decode an `EvtRewardClaimed` external-log body (opcode 0x8C). * * MIRROR-OF: contracts/messages.tolk:EvtRewardClaimed (0x8C) * opcode(32) | claimant: address | to: address | amount: coins */ export declare function parseEvtRewardClaimed(body: Cell): EvtRewardClaimedPayload; /** * Build the on-wire `Cell` from a 68-byte signed-data buffer * + 96-byte BLS-G2 aggregate signature. Mirror of the Tolk struct: * * struct FreshUpdate { * signedDataRef: Cell // 68 bytes inline (phoebeHash + ts + root) * aggSigRef: Cell // 96 bytes inline * } * * Used by `sendRequestPrice({ ..., freshUpdate })` — exposed for advanced * callers building raw bodies (CLI, debug tooling). */ export declare function buildFreshUpdateCell(opts: { signedData: Buffer; sig: Buffer; }): Cell; export type SendDeployOpts = { value: bigint; }; export type SendPauseOpts = { value: bigint; }; export type SendUnpauseOpts = { value: bigint; }; export type SendUpdateConfigOpts = { value: bigint; /** Omitted fields preserve the on-chain value. */ pullFee?: bigint; minStorageReserve?: bigint; maxPushDrift?: number; /** Operator share of pullFee in basis points (0..10000). Default 7500 = 75%. */ operatorBps?: number; /** Per-operator silence window (seconds). 0 = disabled; max MAX_MAX_INACTIVITY (24h). When > 0, enables the pull-side filter (silent lastSubmitter → owner share). */ maxInactivity?: number; }; export type SendWithdrawFeesOpts = { value: bigint; to: Address; amount: bigint; }; export type SendSyncAtlasOpts = { value: bigint; }; export type SendProposeCodeUpgradeOpts = { value: bigint; newCode: Cell; eta?: number; delaySeconds?: number; }; export type SendExecuteCodeUpgradeOpts = { value: bigint; }; export type SendCancelCodeUpgradeOpts = { value: bigint; }; export type SendClaimRewardOpts = { value: bigint; /** Payout recipient. Free to differ from the sender (route to treasury / cold wallet). */ to: Address; /** 0n = claim entire unclaimed balance; non-zero claims exactly that amount (reverts if > balance). */ amount: bigint; }; export type SendPushSnapshotOpts = { value: bigint; /** Unix seconds — must be > storage.lastSnapshotTime AND within ±maxPushDrift of `now`. */ timestamp: number; /** 32-byte Merkle root over all 256 feed leaves. */ root: bigint; /** 96-byte G2 aggregated signature (BLS12-381 min-pk) over `computeSnapshotHash(phoebeAddress, timestamp, root)` — the 68-byte buffer `phoebeHash || be32(timestamp) || be256(root)`. TVM `BLS_VERIFY` hashes-to-curve via SSWU_RO with DST `BLS_DST_G2_POP`. */ aggSig: Buffer; }; /** * Optional inline snapshot upgrade for `RequestPrice`. Attach a freshly * signed snapshot (from any operator's signed-data feed) and Phoebe will * BLS-verify it, advance the on-chain cache, and use it as the active root * for THIS pull's Merkle walk. Permissionless caller — the BLS aggregate * under Atlas's groupPk is the auth. * * Costs ~60k extra gas (the BLS_VERIFY) on top of the cached fast-path's * ~13k. Use when 30s heartbeat freshness isn't enough (lending * liquidations, perps, etc.). Subsequent pulls in the same block read * the advanced cache for free. * * Fresh snapshot must be strictly newer than `getSnapshot().lastSnapshotTime`, * else the tx reverts with `E_FRESH_NOT_FRESHER (187)` and the consumer * eats the gas. Query first if your operator endpoint might be lagging. * * @example * const { lastSnapshotTime } = await phoebe.getSnapshot(); * const fresh = await operatorClient.getSignedSnapshot(); // { timestamp, root, sig } * if (fresh.timestamp > lastSnapshotTime) { * await phoebe.sendRequestPrice(consumer, { ..., freshUpdate: { * signedData: computeSnapshotHash(phoebe.address, fresh.timestamp, fresh.root), * sig: fresh.sig, * }}); * } */ export type FreshUpdateOpts = { /** 68-byte buffer `phoebeHash || be32(timestamp) || be256(root)` — the canonical BLS hash domain. Use `computeSnapshotHash(phoebeAddress, timestamp, root)` to construct. */ signedData: Buffer; /** 96-byte G2 aggregate signature over `signedData` under Atlas's groupPk (BLS12-381 min-pk, DST `BLS_DST_G2_POP`). */ sig: Buffer; }; export type SendRequestPriceOpts = { value: bigint; queryId: bigint; feedId: number; /** Pruned-branch cell tree whose representation hash equals the active root (cached `lastRoot`, or `freshUpdate`'s root if attached). */ proof: Cell; /** Claimed leaf at position feedId — must match the leaf the proof walks to. */ leaf: PriceLeaf; /** 0 = use whatever's on-chain; non-zero = reject if active root older than this many seconds. */ maxStaleness: number; /** Optional fresh-update path — see FreshUpdateOpts. Omit for cached fast-path (~13k gas); attach for sub-heartbeat freshness (~73k gas). */ freshUpdate?: FreshUpdateOpts; }; export type SendAutomatonSyncOpts = { value: bigint; automaton: Address; isActive: boolean; }; export type SendGroupKeySyncOpts = { value: bigint; groupId: number; /** 48-byte G1 public key. */ groupPk: Buffer; /** * 96-byte G2 sibling of `groupPk`. Carried as a `Cell` ref on * the wire so the parent struct stays under the 1023-bit cell budget. * Phoebe doesn't cache or use this field — Phoebe verifies BLS via * `BLS_VERIFY(G1 groupPk, msg, G2 sig)` (min-pk semantics) so the G2 * pubkey is unused. Atlas subgroup + identity-checks the G2 point at * its border before fan-out, so Phoebe inherits the validity * guarantee. Carried here purely to match the byte-identical wire * layout. */ groupPkG2: Buffer; groupEpoch: number; threshold: number; memberCount: number; }; /** * On-chain wrapper for the Phoebe price oracle. Exposes every receiver + * getter as a typed method. * * **Quick start:** * * ```typescript * import { TonClient } from '@ton/ton'; * import { Address } from '@ton/core'; * import { Phoebe } from '@titon-network/phoebe-sdk'; * * const client = new TonClient({ endpoint: 'https://toncenter.com/api/v2/jsonRPC' }); * const phoebe = client.open(Phoebe.createFromAddress(Address.parse('EQ...'))); * * // Read the latest snapshot * const snap = await phoebe.getSnapshot(); * console.log(`root @ ${snap.lastSnapshotTime}: 0x${snap.lastRoot.toString(16)}`); * ``` * * **Three call surfaces:** * * | Surface | Methods | * |---------------|------------------------------------------------------------| * | Operator-side | `sendPushSnapshot`, `sendClaimReward` | * | Consumer-side | `sendRequestPrice` (mode A cached / mode B fresh-update) | * | Owner-side | `sendUpdateConfig`, `sendWithdrawFees`, `sendPause` / `sendUnpause`, `sendSyncAtlas`, `sendProposeCodeUpgrade` / `sendExecute…` / `sendCancel…` | * | Cross-contract (sandbox helpers) | `sendAutomatonSync`, `sendGroupKeySync` (production sender is ForgeTON / Atlas) | * | Read-only getters | `getOwnership`, `getPaused`, `getConfig`, `getLastSubmitter`, `getUnclaimedReward`, `getSchemaVersions`, `getOperator`, `getGroupKey`, `getSnapshot`, `getPendingUpgrade` | * * **Deploy:** * * ```typescript * import { Phoebe, loadPhoebeCode } from '@titon-network/phoebe-sdk'; * * const code = loadPhoebeCode(); * const phoebe = client.open(Phoebe.createFromConfig( * { * owner: ownerAddr, * forgeton: forgetonAddr, * atlas: atlasAddr, * tunables: { pullFee: toNano('0.01'), maxInactivity: 300 }, // optional overrides * }, * code, * )); * await phoebe.sendDeploy(ownerSender, toNano('1')); // 1 TON seed * ``` * * **Errors** — every revert throws a numeric `exitCode`. Use `explainError(code)` * from the SDK to get a structured explanation: * * ```typescript * import { explainError, ERR } from '@titon-network/phoebe-sdk'; * * try { await phoebe.sendPushSnapshot(...); } * catch (e: any) { * const err = explainError(e.exitCode); * console.error(`${err.name}: ${err.message}\n${err.hint ?? ''}`); * } * ``` * * **Sandbox testing** — use the bundled fixture for one-call setup: * * ```typescript * import { Blockchain } from '@ton/sandbox'; * import { deployPhoebeFixture } from '@titon-network/phoebe-sdk/testing'; * * const blockchain = await Blockchain.create(); * const fx = await deployPhoebeFixture(blockchain); * await fx.pushSnapshot({ leaves: new Map([[42, leaf]]) }); * await fx.requestPrice({ feedId: 42, leaf, queryId: 1n }); * ``` * * Full recipe collection: see `RECIPES.md`. AI-agent surface: `AGENTS.md`. * * @see {@link parseFulfillPrice} — decode the `FulfillPrice` callback your consumer receives. * @see {@link decodeEvent} — decode any external-out event Phoebe emits. * @see {@link explainError} — turn any exit code into prose. * @see {@link estimatePullValue}, {@link estimatePushValue}, {@link estimateClaimValue} — pre-compute the `value` to send. * @see {@link PhoebeMerkleTree} — off-chain Merkle tree builder + proof generator. */ export declare class Phoebe implements Contract { readonly address: Address; readonly init?: { code: Cell; data: Cell; } | undefined; constructor(address: Address, init?: { code: Cell; data: Cell; } | undefined); static createFromAddress(address: Address): Phoebe; static createFromConfig(cfg: PhoebeInitConfig, code: Cell, workchain?: number): Phoebe; sendDeploy(provider: ContractProvider, via: Sender, value: bigint): Promise; /** * Owner pauses all push/pull work. Cross-contract syncs, claims, owner * admin, and the code-upgrade path still process during pause — * see GUARANTEES.md for the full pause-skip table. * * @example * ```typescript * await phoebe.sendPause(ownerSender, { value: toNano('0.05') }); * console.log('paused:', await phoebe.getPaused()); // true * ``` * * @throws E_NOT_OWNER (100) — sender ≠ storage.owner. * @throws E_ALREADY_PAUSED (103) — already paused. * @throws E_INSUFFICIENT_VALUE (240) — `value < MIN_XC_GAS` (0.01 TON). */ sendPause(provider: ContractProvider, via: Sender, opts: SendPauseOpts): Promise; /** * Owner unpauses. Mirror of {@link sendPause}. * * @example * ```typescript * await phoebe.sendUnpause(ownerSender, { value: toNano('0.05') }); * ``` * * @throws E_NOT_OWNER (100) — sender ≠ storage.owner. * @throws E_NOT_PAUSED (102) — already unpaused. * @throws E_INSUFFICIENT_VALUE (240). */ sendUnpause(provider: ContractProvider, via: Sender, opts: SendUnpauseOpts): Promise; /** * Owner updates runtime tunables. **Partial update**: omitted fields * preserve their on-chain value; non-null fields replace. Pause-skip: * works during emergency pause. * * @example * ```typescript * // Tighten the pull-side liveness filter and bump operator share. * await phoebe.sendUpdateConfig(ownerSender, { * value: toNano('0.05'), * maxInactivity: 300, // 5min silence window * operatorBps: 8000, // 80% to operator (was 7500) * }); * * // Confirm: * const cfg = await phoebe.getConfig(); * assert(cfg.maxInactivity === 300 && cfg.operatorBps === 8000); * ``` * * @param opts.pullFee — Per-pull fee in nanoTON. Default at deploy: 0.01 TON. * `0` is allowed (fee-free reads). * @param opts.minStorageReserve — Reserve floor. Range: [`MIN_RESERVE_FLOOR` * = 0.05 TON, `MAX_MIN_STORAGE_RESERVE` = 10 TON]. Out-of-range reverts * `E_INVALID_MIN_STORAGE_RESERVE (192)` or `E_RESERVE_INVARIANT (241)`. * @param opts.maxPushDrift — Bidirectional drift bound (seconds). * Range: [1, 86400]. `0` would brick PushSnapshot (asserts become * contradictory); >24h is rejected. Default 300 s. * @param opts.operatorBps — Operator share in basis points. Range: * [0, 10000]. Default 7500 (75% operator / 25% owner). * @param opts.maxInactivity — Per-operator silence window (seconds). * Range: [0, 86400]. `0` disables the filter (lastSubmitter always * credited when set). When > 0, silent operators (haven't pushed * within window) get their share rolled into rewardPool. * * @throws E_NOT_OWNER (100). * @throws E_INVALID_OPERATOR_BPS (189) — operatorBps > 10000. * @throws E_INVALID_MAX_INACTIVITY (190) — maxInactivity > 24h. * @throws E_INVALID_MAX_PUSH_DRIFT (191) — maxPushDrift == 0 or > 24h. * @throws E_INVALID_MIN_STORAGE_RESERVE (192) — minStorageReserve > 10 TON. * @throws E_RESERVE_INVARIANT (241) — minStorageReserve < MIN_RESERVE_FLOOR (0.05 TON). */ sendUpdateConfig(provider: ContractProvider, via: Sender, opts: SendUpdateConfigOpts): Promise; /** * Owner drains accumulated pull-fee share from `rewardPool` to `to`. * Operators' accrued rewards live in a separate map and are NOT * drainable by this method — owner can only withdraw their own share. * * **Outbound mode**: `CARRY_REMAINING_VALUE` (refunds owner's leftover * gas alongside `amount`). NO `IGNORE_ERRORS` — bad `msg.to` aborts * the tx and rolls back the rewardPool decrement. * * @example * ```typescript * const { rewardPool } = await phoebe.getConfig(); * await phoebe.sendWithdrawFees(ownerSender, { * value: toNano('0.05'), * to: treasuryAddr, * amount: rewardPool, // drain everything * }); * ``` * * @throws E_NOT_OWNER (100). * @throws E_BAD_WITHDRAW_TARGET (243) — `to == contract.getAddress()`. * @throws E_AMOUNT_EXCEEDS_ACCUMULATED (242) — `amount > rewardPool`. * @throws E_RESERVE_INVARIANT (241) — would breach minStorageReserve floor. */ sendWithdrawFees(provider: ContractProvider, via: Sender, opts: SendWithdrawFeesOpts): Promise; /** * Owner-triggered Atlas re-sync. Phoebe sends `SyncRequest { groupId: 0 }` * to Atlas; Atlas re-pushes the current `GroupKeySync` back to Phoebe. * Use when Phoebe's bootstrap missed Atlas's initial fan-out OR after * a same-epoch ForceSync recovery on the Atlas side. * * Pause-skip — works during emergency pause. * * @example * ```typescript * // Bootstrap recovery: deployed Phoebe AFTER Atlas had published. * console.log(await phoebe.getGroupKey()); // null * await phoebe.sendSyncAtlas(ownerSender, { value: toNano('0.1') }); // round-trip * console.log(await phoebe.getGroupKey()); // populated * ``` * * @throws E_NOT_OWNER (100). * @throws E_INSUFFICIENT_VALUE (240) — `value < MIN_GAS_FOR_PUSH` (0.05 TON; * covers Phoebe-side compute + Atlas-side compute via PAY_FEES_SEPARATELY). */ sendSyncAtlas(provider: ContractProvider, via: Sender, opts: SendSyncAtlasOpts): Promise; /** * Step 1/3 of the timelocked code upgrade. Stores `(newCode, hash, eta)` * in `cfg.upgradeRef`. Real execution waits at least 24h * (`MIN_UPGRADE_DELAY`) — see {@link sendExecuteCodeUpgrade}. * * @example * ```typescript * import { compile } from '@ton/blueprint'; * * // 1. Compile the new bytecode (or load it from a vetted source). * const newCode = await compile('PhoebeV2'); * * // 2. Propose with the canonical 24h delay. * await phoebe.sendProposeCodeUpgrade(ownerSender, { * value: toNano('0.05'), * newCode, * delaySeconds: 86400 + 60, // 24h + safety margin * }); * * // 3. Confirm via getter. * const up = await phoebe.getPendingUpgrade(); * console.log(`Upgrade hash 0x${up.newCodeHash.toString(16)} ready at ts=${up.eta}`); * ``` * * @param opts.newCode — Compiled bytecode cell. Must be non-empty * (`E_EMPTY_CODE_CELL` (229) otherwise). * @param opts.eta — Absolute unix-seconds ETA. Either this OR `delaySeconds`, * not both. Must satisfy `eta >= now + 86400`. * @param opts.delaySeconds — Relative delay from "now" (computed at submit time). * Convenience over `eta`. Must be `>= 86400`. * * @throws E_NOT_OWNER (100). * @throws E_UPGRADE_ETA_TOO_SOON (225) — eta < now + 24h. * @throws E_UPGRADE_ALREADY_PENDING (227) — another proposal in flight. * Cancel first via {@link sendCancelCodeUpgrade}. * @throws E_EMPTY_CODE_CELL (229) — newCode has no bits AND no refs. */ sendProposeCodeUpgrade(provider: ContractProvider, via: Sender, opts: SendProposeCodeUpgradeOpts): Promise; /** * Step 2/3 of the timelocked code upgrade. Swaps the contract's code * cell via `setCodePostponed` (deferred to next inbound). Must wait * until `now >= up.eta` (24h after Propose). * * @example * ```typescript * const up = await phoebe.getPendingUpgrade(); * if (Math.floor(Date.now()/1000) < up.eta) { * console.log(`Wait ${up.eta - Math.floor(Date.now()/1000)}s more.`); * return; * } * await phoebe.sendExecuteCodeUpgrade(ownerSender, { value: toNano('0.05') }); * ``` * * @throws E_NOT_OWNER (100). * @throws E_NO_PENDING_UPGRADE (228). * @throws E_UPGRADE_NOT_READY (226) — `now < up.eta`. */ sendExecuteCodeUpgrade(provider: ContractProvider, via: Sender, opts: SendExecuteCodeUpgradeOpts): Promise; /** * Step 3/3 (optional) of the timelocked code upgrade. Aborts the * pending proposal, returning to the no-upgrade-pending state. Owner * can re-propose immediately. * * @example * ```typescript * await phoebe.sendCancelCodeUpgrade(ownerSender, { value: toNano('0.05') }); * console.log((await phoebe.getPendingUpgrade()).eta); // 0 * ``` * * @throws E_NOT_OWNER (100). * @throws E_NO_PENDING_UPGRADE (228). */ sendCancelCodeUpgrade(provider: ContractProvider, via: Sender, opts: SendCancelCodeUpgradeOpts): Promise; /** * Operator claims accrued pull-fee share. Sender (`via`) MUST be the * operator address as registered in `storage.operators` (mirrored from * ForgeTON's `AutomatonSync` stream). `isActive` is NOT required — * rewards accrued while active stay claimable after deactivation * ("earned is earned"). * * **Outbound mode**: `PAY_FEES_SEPARATELY | CARRY_REMAINING_VALUE`. * Recipient (`msg.to`) gets `accrued + leftover_inbound_gas`; forward * fee from contract balance keeps `accrued` arriving exact. NO * `IGNORE_ERRORS` — bad `msg.to` aborts the tx and rolls back the * `operatorRewards` decrement so the operator can retry. * * **Pause-skip** — claims work during emergency pause (settlement of * already-earned revenue, not new work). * * @example * ```typescript * // 1. Check accrued balance. * const accrued = await phoebe.getUnclaimedReward(operator.address); * if (accrued === 0n) return; * * // 2. Drain everything (amount = 0n) to the operator's own wallet. * await phoebe.sendClaimReward(operatorSender, { * value: toNano('0.05'), * to: operator.address, * amount: 0n, // 0n = claim all * }); * * // Or: route to a treasury, claiming a specific slice. * await phoebe.sendClaimReward(operatorSender, { * value: toNano('0.05'), * to: treasuryAddr, * amount: toNano('1.0'), // claim exactly 1 TON * }); * ``` * * @param opts.value — Attached TON. Must cover MIN_GAS_FOR_PULL (0.03 TON). * Excess is refunded alongside `amount` to `msg.to` (mode 64). * @param opts.to — Payout destination. Free to differ from sender so * operators can route to a treasury / cold wallet without rotating * their submitter key. Reverts `E_BAD_WITHDRAW_TARGET (243)` if equal * to `contract.getAddress()`. * @param opts.amount — `0n` = drain entire unclaimed balance. Non-zero * = claim exactly that many nanoTON; reverts if > balance. * * @throws E_INSUFFICIENT_VALUE (240) — `value < MIN_GAS_FOR_PULL` (0.03 TON). * @throws E_BAD_WITHDRAW_TARGET (243) — `to == contract.getAddress()`. * @throws E_OPERATOR_NOT_FOUND (140) — sender not in operators map. * @throws E_NO_UNCLAIMED_REWARD (188) — operator has no entry (never * accrued OR already fully claimed). Map entries are deleted on * full claim. * @throws E_AMOUNT_EXCEEDS_ACCUMULATED (242) — `amount > unclaimed balance`. * @throws E_RESERVE_INVARIANT (241) — claim would breach storage reserve * (shouldn't fire under normal accounting; defensive grep-bait). */ sendClaimReward(provider: ContractProvider, via: Sender, opts: SendClaimRewardOpts): Promise; /** * **Owner-only.** Prune a deactivated operator's entry from * `storage.operators`. Bounds long-term map growth from operator * churn at ForgeTON (audit F-001). * * Refuses to prune an operator that is (a) still `isActive` per the * ForgeTON mirror, OR (b) carrying any unclaimed reward balance. * The "earned-is-earned" invariant survives gc — an operator with * accrued rewards must claim before owner can evict. * * @example * ```typescript * // Owner evicts a long-since-deactivated operator that has no unclaimed rewards. * await phoebe.sendGcOperator(ownerSender, { * value: toNano('0.02'), * automaton: deactivatedOperatorAddr, * }); * ``` * * @param opts.value — Attached TON. Must cover `MIN_XC_GAS` (0.01 TON) + * forward fee. `0.02 TON` is a comfortable default. * @param opts.automaton — The operator address whose map entry to prune. * * @throws E_NOT_OWNER (100) — sender != storage.owner. * @throws E_INSUFFICIENT_VALUE (240) — `value < MIN_XC_GAS`. * @throws E_OPERATOR_NOT_FOUND (140) — operator not in `storage.operators`. * @throws E_OPERATOR_STILL_ACTIVE (193) — operator's `isActive == true`; * wait for ForgeTON to deactivate first. * @throws E_OPERATOR_HAS_UNCLAIMED (194) — operator has accrued * unclaimed rewards; operator must `sendClaimReward` first. */ sendGcOperator(provider: ContractProvider, via: Sender, opts: { value: bigint; automaton: Address; }): Promise; /** * Push a new BLS-attested Merkle snapshot to Phoebe. * * The signed payload `(phoebeHash, timestamp, root)` (68 bytes, * byte-aligned) lives in a nested ref so the on-chain BLS verify walks * the slice's bits directly — no `cell.hash()` round-trip. The * `phoebeHash` prefix binds the signed payload to THIS contract * (cross-deployment replay defense); auto-injected here from * `this.address.hash`. * * **Caller must be a registered + active operator.** Phoebe authenticates * via the ForgeTON-mirrored `operators` map; a non-operator wallet will * revert `E_OPERATOR_NOT_FOUND (140)`. * * **Gas budget:** `value >= MIN_GAS_FOR_PUSH (0.05 TON)` covers the * BLS_VERIFY (~60k gas) + state mutations + refund of unused gas back * to the operator. Use {@link estimatePushValue} for a precise floor. * * @example * ```typescript * import { * Phoebe, PhoebeMerkleTree, signMessage, computeSnapshotHash * } from '@titon-network/phoebe-sdk'; * import { toNano } from '@ton/core'; * * // 1. Build the 256-leaf Merkle tree (sparse map → placeholder pad). * const tree = PhoebeMerkleTree.fromSparseLeaves(new Map([ * [42, { feedId: 42, mantissa: 6_543_210n * 100_000_000n, expo: -8, * confBps: 50, pubTime: Math.floor(Date.now() / 1000) }], * ])); * const root = tree.rootAsBigint(); * * // 2. Threshold-BLS-sign (phoebeHash, timestamp, root) under groupPk. * const timestamp = Math.floor(Date.now() / 1000); * const aggSig = signMessage(operatorSecret, * computeSnapshotHash(phoebe.address, timestamp, root)); * * // 3. Submit. Operator's wallet pays — leftover gas refunded. * await phoebe.sendPushSnapshot(operatorSender, { * value: toNano('0.05'), * timestamp, * root, * aggSig, // 96 bytes G2 * }); * ``` * * @param opts.value — Attached TON. Must cover MIN_GAS_FOR_PUSH (0.05 TON). * Excess is refunded via SEND_MODE_CARRY_REMAINING_VALUE | IGNORE_ERRORS. * @param opts.timestamp — Unix seconds. Must satisfy: * `timestamp > storage.lastSnapshotTime` (strict monotonicity) * AND `|nowTs - timestamp| < storage.maxPushDrift` (default ±300 s). * @param opts.root — 32-byte Merkle root. Use `tree.rootAsBigint()`. * @param opts.aggSig — 96-byte BLS-G2 aggregate signature over * `computeSnapshotHash(this.address, timestamp, root)`. Must verify * under Atlas's cached `groupPk`. * * @throws E_OPERATION_PAUSED (101) — contract paused. * @throws E_INSUFFICIENT_VALUE (240) — `value < MIN_GAS_FOR_PUSH`. * @throws E_OPERATOR_NOT_FOUND (140) — sender not in the operators map. * @throws E_OPERATOR_NOT_ACTIVE (141) — sender is mirrored but inactive. * @throws E_GROUP_KEY_NOT_SET (180) — Atlas hasn't published yet. * @throws E_STALE_SNAPSHOT (181) — `timestamp <= lastSnapshotTime`. * @throws E_BAD_TIMESTAMP_DRIFT (182) — `timestamp` outside ±maxPushDrift. * @throws E_WRONG_WORKCHAIN (333) — Phoebe is workchain-0-only. * @throws E_WRONG_DEPLOYMENT_BINDING (144) — `phoebeHash` mismatch (bug * in the SDK shouldn't happen; happens if you signed with wrong dest). * @throws E_INVALID_BLS_SIGNATURE (161) — sig fails BLS_VERIFY against * cached groupPk. Most common cause: wrong DST (use `BLS_DST_G2_POP` * from the SDK), or stale pkShares from a pre-rotation epoch. */ sendPushSnapshot(provider: ContractProvider, via: Sender, opts: SendPushSnapshotOpts): Promise; /** * Pull a single feed's price by Merkle proof. Permissionless caller — * `via` IS the consumer that receives the `FulfillPrice` callback at * opcode `0x72`. Spam-prevention is the `pullFee` gate (default 0.01 * TON, owner-tunable via `sendUpdateConfig`). * * **Two operating modes:** * * | Mode | When | Cost | * |------|------|------| * | **A** Cached | `freshUpdate` omitted | ~16k gas + pullFee | * | **B** Fresh-update | `freshUpdate` attached | ~73k gas + pullFee (BLS verify inline) | * * Mode A reads `cfg.lastRoot` (last operator-pushed snapshot). Mode B * accepts a freshly-signed snapshot inline, BLS-verifies it under * Atlas's cached `groupPk`, advances the on-chain cache (positive * externality — subsequent same-block pulls read the fresh root for * free), and walks the proof against the fresh root. Use mode B for * sub-heartbeat freshness (lending liquidations, perps); mode A for * routine reads. * * **Mode A example (cached fast-path):** * ```typescript * import { Phoebe, PhoebeMerkleTree } from '@titon-network/phoebe-sdk'; * import { toNano } from '@ton/core'; * * // Off-chain: rebuild the tree from indexer data + grab leaf at slot 42. * const tree = PhoebeMerkleTree.fromSparseLeaves(/* …leaves from indexer… *\/); * const leaf = tree.leaves[42]; // PriceLeaf * * await phoebe.sendRequestPrice(consumerSender, { * value: toNano('0.05'), // pullFee + gas * queryId: 1n, * feedId: 42, * proof: tree.proof(42), // pruned merkle_proof * leaf, * maxStaleness: 0, // accept any cached root * }); * ``` * * **Mode B example (Pyth-style update + read):** * ```typescript * import { computeSnapshotHash } from '@titon-network/phoebe-sdk'; * * // Query Phoebe first to avoid wasting gas on a stale freshUpdate. * const { lastSnapshotTime } = await phoebe.getSnapshot(); * const fresh = await operatorClient.getSignedSnapshot(); // {timestamp, root, sig} * if (fresh.timestamp <= lastSnapshotTime) return; // cache already fresh * * await phoebe.sendRequestPrice(consumerSender, { * value: toNano('0.1'), // larger gas budget for BLS * queryId: 2n, * feedId: 42, * proof: freshTree.proof(42), // proof against fresh.root * leaf: freshLeaf, * maxStaleness: 30, // require ≤ 30s freshness * freshUpdate: { * signedData: computeSnapshotHash(phoebe.address, fresh.timestamp, fresh.root), * sig: fresh.sig, // 96-byte G2 aggregate * }, * }); * ``` * * **Receiving the callback** — your consumer contract implements an * inbound at opcode `0x72`: * ```tolk * struct (0x00000072) FulfillPrice { * queryId: uint64 * leaf: PriceLeaf * } * ``` * See `templates/consumer.tolk` and `examples/price-aware-vault/`. * * @param opts.value — Attached TON. Must cover `pullFee + MIN_GAS_FOR_PULL` (0.03 TON); * use {@link estimatePullValue} for a precise floor including BLS gas in mode B. * @param opts.queryId — Echoed back in the `FulfillPrice` callback so consumers * can correlate request → response. Use a monotonic counter; see {@link QueryIdStream}. * @param opts.feedId — 0..255 (256 dense slots). Out-of-range reverts * `E_FEED_ID_OUT_OF_RANGE (186)`. * @param opts.proof — Merkle proof. Use `tree.proof(feedId)` for the recommended * pruned-branch shape (~417 B); `tree.fullTreeProof(feedId)` for the legacy * ordinary-cell shape (~8.5 KB). * @param opts.leaf — Claimed `PriceLeaf` at position `feedId`. Must hash to the * leaf the proof commits to, and `leaf.feedId === feedId` (else * `E_BAD_MERKLE_PROOF (183)`). * @param opts.maxStaleness — `0` accepts any cached root; non-zero reverts * `E_STALE_SNAPSHOT (181)` if `now - activeSnapshotTime > maxStaleness`. * Applies to both modes A and B. * @param opts.freshUpdate — Optional. See {@link FreshUpdateOpts}. * * @throws E_OPERATION_PAUSED (101) — contract paused. * @throws E_INSUFFICIENT_VALUE (240) — `value < pullFee + MIN_GAS_FOR_PULL`. * @throws E_BAD_MERKLE_PROOF (183) — proof.hash() != activeRoot, OR walked-leaf * hash != claimed-leaf hash, OR leaf.feedId != msg.feedId. * @throws E_FEED_ID_OUT_OF_RANGE (186) — `feedId >= 256`. * @throws E_STALE_SNAPSHOT (181) — `maxStaleness` exceeded. * @throws E_FRESH_NOT_FRESHER (187) — Mode B only: attached `freshUpdate.timestamp <= lastSnapshotTime`. * @throws E_INVALID_BLS_SIGNATURE (161) — Mode B only: fresh sig fails verify. */ sendRequestPrice(provider: ContractProvider, via: Sender, opts: SendRequestPriceOpts): Promise; sendAutomatonSync(provider: ContractProvider, via: Sender, opts: SendAutomatonSyncOpts): Promise; sendGroupKeySync(provider: ContractProvider, via: Sender, opts: SendGroupKeySyncOpts): Promise; /** * Returns the three pinned addresses: contract owner, ForgeTON pool, * Atlas. `forgeton` and `atlas` are immutable post-deploy (rotate via * timelocked code upgrade); `owner` is mutable via direct storage * surgery in a code upgrade if needed. * * @example * ```typescript * const { owner, forgeton, atlas } = await phoebe.getOwnership(); * console.log(`Phoebe owner: ${owner.toString()}`); * console.log(`ForgeTON: ${forgeton.toString()}`); * console.log(`Atlas: ${atlas.toString()}`); * ``` */ getOwnership(provider: ContractProvider): Promise; /** * Returns the contract's pause state. When `true`, `sendPushSnapshot` * and `sendRequestPrice` revert with `E_OPERATION_PAUSED (101)`. All * other paths (cross-contract syncs, owner admin, claims, code upgrade) * still work — see GUARANTEES.md for the full pause-skip table. * * @example * ```typescript * if (await phoebe.getPaused()) { * console.warn('Phoebe is paused — push/pull will revert.'); * } * ``` */ getPaused(provider: ContractProvider): Promise; /** * Returns the full owner-tunable config + handler-managed reward pool. * Use to display tunables in dashboards or to estimate `value` for * `sendRequestPrice` (`pullFee + MIN_GAS_FOR_PULL` = 0.03 TON minimum). * * @example * ```typescript * const cfg = await phoebe.getConfig(); * console.log(`pullFee: ${cfg.pullFee} nanoTON`); * console.log(`operatorBps: ${cfg.operatorBps}`); // 7500 = 75% to lastSubmitter * console.log(`rewardPool: ${cfg.rewardPool} nanoTON`); // owner-withdrawable * console.log(`maxInactivity: ${cfg.maxInactivity}s`); // 0 = filter disabled * ``` */ getConfig(provider: ContractProvider): Promise; /** * Returns the address of the most recent successful PushSnapshot * submitter, or null pre-first-push. Off-chain monitoring uses this to * surface daemon submission patterns + estimate per-operator earnings * rate. */ getLastSubmitter(provider: ContractProvider): Promise
; /** * Returns the unclaimed pull-fee balance accrued by `operator`. Returns * 0n (not null) when the operator has never accrued or has fully * claimed — consistent surface for tooling that just wants a number. */ getUnclaimedReward(provider: ContractProvider, operator: Address): Promise; /** * Returns the on-chain storage + config-blob schema versions. Compare * against the SDK's `PHOEBE_STORAGE_VERSION` / `PHOEBE_CONFIG_BLOB_VERSION` * exports — drift means the deployed contract has been upgraded to a * schema the SDK doesn't know about; abort and bump the SDK first. * * @example * ```typescript * import { * PHOEBE_STORAGE_VERSION, PHOEBE_CONFIG_BLOB_VERSION, * } from '@titon-network/phoebe-sdk'; * * const v = await phoebe.getSchemaVersions(); * if (v.storage !== PHOEBE_STORAGE_VERSION || v.config !== PHOEBE_CONFIG_BLOB_VERSION) { * throw new Error(`SDK ↔ contract version drift; upgrade SDK to read this Phoebe.`); * } * ``` */ getSchemaVersions(provider: ContractProvider): Promise; /** * Returns the per-automaton mirror entry from ForgeTON, or `null` if * the automaton was never mirrored. `lastPushAt` powers the pull-side * liveness filter (when `cfg.maxInactivity > 0`) AND off-chain * monitoring (detect silent operators). * * @example * ```typescript * const op = await phoebe.getOperator(operatorAddr); * if (op === null) { * console.log('Not mirrored — operator must stake at ForgeTON first.'); * } else if (!op.isActive) { * console.log(`Operator deactivated; lastPushAt=${op.lastPushAt}`); * } else if (op.lastPushAt === 0) { * console.log('Active but never pushed.'); * } else { * console.log(`Active; last push ${Math.floor(Date.now()/1000) - op.lastPushAt}s ago.`); * } * ``` * * @returns `OperatorReply` or `null` if `automaton` not in the map. */ getOperator(provider: ContractProvider, automaton: Address): Promise; /** * Returns the cached BLS group public key + threshold config, or * `null` if Atlas hasn't published yet. `PushSnapshot` will revert * `E_GROUP_KEY_NOT_SET (180)` until this returns non-null. * * Use {@link sendSyncAtlas} (owner-gated) to re-trigger Atlas's fan-out * if the initial bootstrap was missed. * * @example * ```typescript * const gk = await phoebe.getGroupKey(); * if (gk === null) { * console.warn('Atlas bootstrap pending — push will revert.'); * } else { * console.log(`groupEpoch ${gk.groupEpoch}; t=${gk.threshold}/n=${gk.memberCount}`); * console.log(`groupPk: ${gk.groupPk.toString('hex')}`); * } * ``` * * @returns `GroupKeyReply` or `null` if no GroupKeySync has landed yet. */ getGroupKey(provider: ContractProvider): Promise; /** * Returns the latest pushed snapshot — `(timestamp, root)`. Both are * `0` pre-first-push. Consumers query this to decide between mode A * (cached read) and mode B (attach a fresher snapshot in * `sendRequestPrice`). * * @example * ```typescript * const { lastSnapshotTime, lastRoot } = await phoebe.getSnapshot(); * if (lastSnapshotTime === 0) { * console.warn('No snapshot yet — operators must push first.'); * } else { * const ageSeconds = Math.floor(Date.now() / 1000) - lastSnapshotTime; * console.log(`Cached root 0x${lastRoot.toString(16)} (${ageSeconds}s old)`); * } * ``` */ getSnapshot(provider: ContractProvider): Promise; /** * Returns the pending code-upgrade slot — `(newCodeHash, eta)`. Both * are `0n` / `0` when no proposal is pending. Use to confirm a * `sendProposeCodeUpgrade` landed + display the ready-time in admin UIs. * * @example * ```typescript * const up = await phoebe.getPendingUpgrade(); * if (up.eta === 0) { * console.log('No upgrade pending.'); * } else { * const wait = up.eta - Math.floor(Date.now() / 1000); * console.log(`Upgrade to 0x${up.newCodeHash.toString(16)} ready in ${wait}s.`); * } * ``` */ getPendingUpgrade(provider: ContractProvider): Promise; }