import { ReindexResponse, SubgraphAggregateParams, SubgraphAggregateResponse, SubgraphDetail, SubgraphGapsResponse, SubgraphQueryParams, SubgraphSummary } from "@secondlayer/shared/schemas"; import { DeploySubgraphRequest, DeploySubgraphResponse } from "@secondlayer/shared/schemas/subgraphs"; import { SubgraphAgentSchema, SubgraphSpecOptions } from "@secondlayer/shared/subgraphs/spec"; import { InferSubgraphClient } from "@secondlayer/subgraphs"; type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise; interface SecondLayerOptions { /** Base URL of the instance API (trailing slashes are stripped). */ baseUrl: string; /** Bearer token for authenticated instance requests (`INSTANCE_TOKEN`). */ apiKey?: string; /** Hosted account key (`sk-sl_*`). Env `SECONDLAYER_API_KEY`. Used by * `sl.archive` quote/fetch/credits, never by instance `/v1` or `/api`. */ accountKey?: string; /** Fetch implementation. Tests and edge runtimes can provide their own. */ fetchImpl?: FetchLike; /** Public base URL for Streams bulk parquet dumps (the cold backfill plane). * Required for `streams.dumps.*`; without it the dumps client falls back to * its built-in default. */ dumpsBaseUrl?: string; /** Public base URL for the signed canonical archive tree. Defaults to * `https://archive.secondlayer.tools`. Reaches `sl.archive`. */ archiveBaseUrl?: string; /** Credits and fetch-gate API. Defaults to `https://api.secondlayer.tools`, * not `baseUrl`. Reaches `sl.archive` quote/fetch/credits. */ archiveOpsUrl?: string; /** Deploy origin label sent as `x-sl-origin` (telemetry). Defaults to `cli`. */ origin?: "cli" | "mcp" | "session"; /** Check the ed25519 signature on every Streams read. Omit for lenient * (verify when signed, pass through unsigned self-host responses), `true` * or `{ publicKey }` for strict, `false` for off. Reaches `sl.streams`. */ verify?: boolean | { publicKey: string; }; /** Check the dumps manifest signature before trusting any file hash * (default on). Reaches `sl.streams.dumps`. */ verifyDumpsManifest?: boolean; /** How long one request may take, headers and body, before it fails with a * retryable `ApiError` (default 30 000 ms). A hung socket then trips the * retry policy instead of stalling a walk or consume loop forever. `0` * disables the timeout. */ requestTimeoutMs?: number; } /** Default per-request budget. Long enough for a 1000-row filtered page on a * cold cache, short enough that a half-open connection surfaces as a retry. */ declare const DEFAULT_REQUEST_TIMEOUT_MS = 3e4; /** Per-call options every `request*` method accepts. */ type RequestOptions = { /** Cancels the in-flight request (and its body read). The rejection is the * signal's reason, an `AbortError` by default, never an `ApiError`. */ signal?: AbortSignal; }; /** Product default: the local one-box API. Override with `baseUrl` or * `SECONDLAYER_API_URL`. */ declare const LOCAL_API_URL = "http://127.0.0.1:3800"; /** Instance credential. `secondlayer init` writes this. */ declare const INSTANCE_TOKEN_ENV = "INSTANCE_TOKEN"; /** Hosted account key (`sk-sl_*`) for api.secondlayer.tools. */ declare const ACCOUNT_KEY_ENV = "SECONDLAYER_API_KEY"; declare function resolveBaseUrl(explicit?: string): string; /** Resolve the instance credential. Precedence: explicit `apiKey` (including * `""` for keyless) → `INSTANCE_TOKEN`. Does not read the hosted account key * env vars. Guarded for browsers and edge runtimes. */ declare function resolveApiKey(apiKey?: string): string | undefined; /** Resolve the hosted account key for archive quote/fetch/credits. * Precedence: explicit `accountKey` → `SECONDLAYER_API_KEY` → `SL_API_KEY` / * `SL_ARCHIVE_API_KEY` (one-release warn-fallbacks). Does not read * `INSTANCE_TOKEN`. */ declare function resolveAccountKey(accountKey?: string): string | undefined; declare abstract class BaseClient { protected baseUrl: string; protected apiKey?: string; protected origin: "cli" | "mcp" | "session"; protected fetchImpl: FetchLike; protected requestTimeoutMs: number; constructor(options?: Partial); static authHeaders(apiKey?: string): Record; protected request(method: string, path: string, body?: unknown, opts?: RequestOptions): Promise; /** Like `request`, but maps a 404 to `null` instead of throwing — the one * place that owns the "absent resource" rule for `get*` accessors. */ protected requestOrNull(method: string, path: string, body?: unknown, opts?: RequestOptions): Promise; protected requestText(method: string, path: string, body?: unknown, opts?: RequestOptions): Promise; /** Run one request under the caller's signal plus the per-request timeout. * One combined signal covers the fetch and the body read, so a socket that * stalls mid-body times out the same way one that never answers does. A * timeout rejects with a retryable `ApiError`; a caller abort rejects with * the signal's reason so loops can tell "stop" from "try again". */ private withRequestBudget; /** Issue the HTTP request and map non-2xx statuses onto the error family. * `signal` is the combined caller-plus-timeout signal from `request*`; * subclasses that stream a body call this directly and read it themselves. */ protected fetchResponse(method: string, path: string, body?: unknown, signal?: AbortSignal): Promise; } interface SubgraphSource { name: string; version: string; sourceCode: string | null; readOnly: boolean; reason?: string; updatedAt: string; } /** Status of a tracked reindex/backfill operation (poll until terminal). */ interface SubgraphOperationStatus { id: string; subgraphName: string; kind: "reindex" | "backfill"; status: "queued" | "running" | "completed" | "failed" | "cancelled"; fromBlock: number | null; toBlock: number | null; processedBlocks: number | null; /** 0–1 fraction; null when no denominator is known yet. 1 when completed. */ progress: number | null; error: string | null; startedAt: string | null; finishedAt: string | null; createdAt: string; updatedAt: string; } /** /v1 cursor envelope for subgraph table reads. */ interface SubgraphRowsEnvelope { rows: T[]; next_cursor: string | null; tip: { block_height: number; subgraph_height: number; blocks_behind: number; }; } interface BundleSubgraphResponse { ok: true; name: string; version: string | null; description: string | null; sources: Record>; schema: Record; handlerCode: string; sourceCode: string; bundleSize: number; } declare class Subgraphs extends BaseClient { list(): Promise<{ data: SubgraphSummary[]; }>; /** * The subgraph's current state — health, sync position, tables. The verify * step after a deploy, reindex, or backfill: poll it until `sync` catches * the tip. */ status(name: string): Promise; openapi(name: string, options?: SubgraphSpecOptions): Promise>; schema(name: string, options?: SubgraphSpecOptions): Promise; markdown(name: string, options?: SubgraphSpecOptions): Promise; /** * Reindex always drops and rebuilds the whole subgraph, so it takes no * block range; the API rejects one with `REINDEX_RANGE_NOT_SUPPORTED`. * Use {@link backfill} to process a specific range. While a reindex or * backfill is already running the API answers 409 with code * `OPERATION_IN_PROGRESS`; poll {@link operations} until it finishes. */ reindex(name: string): Promise; stop(name: string): Promise<{ message: string; operationId?: string; status?: string; }>; /** Process one block range. 409 `OPERATION_IN_PROGRESS` while another * reindex or backfill runs on this subgraph. */ backfill(name: string, options: { fromBlock: number; toBlock: number; }): Promise; gaps(name: string, opts?: { limit?: number; offset?: number; resolved?: boolean; }): Promise; delete(name: string, options?: { force?: boolean; }): Promise<{ message: string; }>; /** * Open /v1 read: cursor-paginated rows. Anon works on an open instance; * pass an apiKey on the client where reads are closed. Resume with the * returned `next_cursor`. */ rows(name: string, table: string, params?: Omit & { cursor?: string; }): Promise>; /** Recent reindex/backfill operations for a subgraph, newest first. */ operations(name: string): Promise<{ operations: SubgraphOperationStatus[]; }>; /** Status of a single operation (poll the `operationId` returned by * reindex/backfill/stop until `status` is terminal). */ getOperation(name: string, operationId: string): Promise; /** Create or update a subgraph. A deploy that needs a rebuild queues one; * if a reindex or backfill is already running the API answers 409 with * code `OPERATION_IN_PROGRESS` (an `ApiError`, not a dedicated class). */ deploy(data: DeploySubgraphRequest): Promise; getSource(name: string): Promise; /** * Bundle a TypeScript subgraph source on the server. Used by the web chat * authoring loop so Vercel's serverless runtime doesn't have to run esbuild. */ bundle(data: { code: string; }): Promise; queryTable(name: string, table: string, params?: SubgraphQueryParams): Promise; queryTableCount(name: string, table: string, params?: SubgraphQueryParams): Promise<{ count: number; }>; queryTableAggregate(name: string, table: string, params?: SubgraphAggregateParams): Promise; /** * Returns a typed client for a subgraph defined with `defineSubgraph()`. * Row types are inferred from the subgraph's schema literal types. * * @example * ```ts * import mySubgraph from './subgraphs/my-token-subgraph' * const client = sl.subgraphs.typed(mySubgraph) * const rows = await client.transfers.findMany({ where: { sender: 'SP...' } }) * // rows: InferTableRow[] * ``` */ typed; }>(def: T): InferSubgraphClient; /** * `columns` is the table's declared column set from `defineSubgraph()`. * Filters and orderBy always accept the canonical system names * `_id` / `_blockHeight` / `_txId` / `_createdAt`; the unprefixed * shorthands (`id`, `blockHeight`, ...) mean the system column only when * the table declares no column of that name. */ private createTableClient; } import { InferSubgraphClient as InferSubgraphClient2 } from "@secondlayer/subgraphs"; import { InstanceDiagnosis as InstanceDiagnosis2 } from "@secondlayer/shared/archive/instance-diagnosis"; import { SubgraphSummary as SubgraphSummary2 } from "@secondlayer/shared/schemas"; import { InstanceDiagnosis, PublicStatus } from "@secondlayer/shared/archive/instance-diagnosis"; /** Structural `/public/status` payload. Extra fields are ignored by diagnosis. */ type InstanceStatus = PublicStatus; type InstanceClient = { status(): Promise; diagnose(): Promise; }; type ArchiveVerifyInput = { against: string; target?: string; fromBlock?: number; toBlock?: number; insecure?: boolean; publicKeyPem?: string; }; type ArchiveVerifyRangeStatus = "match" | "digest-mismatch" | "count-mismatch" | "missing"; type ArchiveVerifyResult = { status: "clean" | "diverged" | "unanchored"; target: string; against: string; signature: { verified: boolean; reason?: string; }; coverage?: { from_block: number; to_block: number; }; ranges: Array<{ dataset: string; from_block: number; to_block: number; status: ArchiveVerifyRangeStatus; expected_digest: string | null; actual_digest: string | null; }>; reason?: string; }; import { RangeDigest } from "@secondlayer/shared/archive/range-digest"; import { ArchiveStatus } from "@secondlayer/shared/archive/status"; type ArchiveDataset = "blocks" | "transactions" | "events"; type ArchiveFlow = "bootstrap" | "repair"; type ArchivePartition = { dataset: string; from_block: number; to_block: number; path: string; row_count: number; byte_size: number; sha256: string; }; type ArchiveManifest = { network?: string; coverage?: { from_block: number; to_block: number; }; partition_size_blocks?: number; range_digests?: RangeDigest[]; partitions?: ArchivePartition[]; signature?: string; key_id?: string; [key: string]: unknown; }; type LoadedArchive = { manifest: ArchiveManifest; origin: string; root: string; isRemote: boolean; signature: { verified: boolean; reason?: string; }; }; type ArchiveQuote = { partitions: number; bundles: number; usdMicros: number; usd: string; freeAllowanceAppliedMicros: number; allowanceRemainingBundles: number; balanceUsdMicros: number; sufficient: boolean; }; type ArchiveFetchItem = { path: string; url: string; expiresAt: string; chargedUsdMicros: number; }; type ArchiveFetchResult = { urls: ArchiveFetchItem[]; chargedTotalUsdMicros: number; balanceAfterUsdMicros: number; }; type ArchiveCreditsBalance = { creditsUsdMicros: string; refill: { belowUsd: number | null; packUsd: number | null; lastAt: string | null; }; }; type ArchiveLoadOptions = { insecure?: boolean; }; type ArchiveClient = { latest(against?: string, opts?: ArchiveLoadOptions): Promise; load(against: string, opts?: ArchiveLoadOptions): Promise; status(): Promise; partitions(ref: LoadedArchive, filter?: { dataset?: ArchiveDataset; fromBlock?: number; toBlock?: number; }): ArchivePartition[]; quote(input: { paths: string[]; flow: ArchiveFlow; }): Promise; fetch(input: { paths: string[]; flow: ArchiveFlow; }): Promise; download(partition: ArchivePartition, opts?: { url?: string; }): Promise; credits: { balance(): Promise; checkout(input: { email: string; pack: 10 | 25 | 50 | 100; }): Promise<{ url: string; }>; refill(input: { belowUsd: number; packUsd: 10 | 25 | 50 | 100; } | { off: true; }): Promise<{ belowUsd: number | null; packUsd: number | null; }>; }; }; /** Hosted archive client plus instance `verify`. `status()` is still the public archive tree. */ type SecondLayerArchive = ArchiveClient & { verify(input: ArchiveVerifyInput): Promise; }; /** * Typed client for the contract-discovery API (`GET /v1/contracts`). * * "Find all contracts conforming to a trait" — backed by the contract registry: * `declared` traits parsed from Clarity source, `inferred` standards from static * ABI shape-matching. Anonymous public read. `trait` is required; the ABI blob is * omitted unless `include: "abi"` is passed. */ /** Whether a trait match must be declared in source, inferred from ABI, or either. */ type ContractConformance = "declared" | "inferred" | "any"; interface ContractsListParams { /** Required. Trait identifier to match (e.g. "sip-010", or a fully-qualified trait). */ trait: string; /** Match source. Defaults to "any" server-side. */ conformance?: ContractConformance; /** Set to "abi" to include the full ABI blob in each row. */ include?: "abi"; /** Page size, 1–500 (default 100 server-side). */ limit?: number; /** Opaque cursor from a prior response's `next_cursor`. */ cursor?: string; } interface ContractSummary { contract_id: string; deployer: string; block_height: number; declared_traits: string[] | null; inferred_standards: string[] | null; abi_status: string; /** Present only when `include: "abi"` was requested. */ abi?: unknown; } interface ContractsEnvelope { contracts: ContractSummary[]; next_cursor: string | null; } declare class Contracts extends BaseClient { constructor(options?: Partial); /** Find contracts conforming to `trait`. `trait` is required (server 400s without it). */ list(params: ContractsListParams): Promise; /** * Fetch a single contract from the registry by id (the prod-safe ABI source). * Pass `{ includeAbi: true }` for the full ABI blob. Resolves null on 404. */ get(contractId: string, opts?: { includeAbi?: boolean; }): Promise; } import { Pox5EventTopic } from "@secondlayer/stacks/pox5"; import { SbtcEventTopic } from "@secondlayer/stacks/sbtc"; import { InferredTopicSchema } from "@secondlayer/subgraphs"; import { RewardSet } from "@secondlayer/shared/node/consensus"; import { MerkleProofStep } from "@secondlayer/shared/node/nakamoto"; /** * Trustless transaction-inclusion proof verification. * * Given a proof from `GET /v1/index/transactions/:txid/proof`, the consumer * re-derives everything itself — it does NOT trust any value Secondlayer * computed. Anchored level: (1) recompute the txid from the raw tx bytes, (2) * fold it up the merkle path to the header's `tx_merkle_root`, (3) recompute the * header's `block_hash` and `index_block_hash` from the raw header — "this tx is * included in a header any node can corroborate". Consensus level (when the proof * carries a `consensus` field, or a `rewardSet` is passed): additionally recover * the header's signer signatures and confirm ≥70% of reward-set signer weight * signed the block. * * Note: uses Node's crypto via `@secondlayer/shared` (same as the Streams * signature verify); intended for Node/server verification. */ interface TransactionProof { txid: string; index_block_hash: string; block_height: number; tx_index: number; /** Raw consensus-serialized transaction bytes (hex). */ raw_tx: string; /** Raw Nakamoto block-header bytes (hex) — parsed + re-hashed by the verifier. */ raw_header: string; /** Authentication path from the tx leaf to `tx_merkle_root`. */ tx_merkle_path: MerkleProofStep[]; /** Present when consensus-level verification is available: the reward cycle and * its signer set, against which the header's signer signatures are checked. */ consensus?: { reward_cycle: number; reward_set: RewardSet; }; } interface TransactionProofVerifyResult { /** Highest level actually verified. "consensus" requires the proof's * `consensus` field and a met signer-weight threshold. */ level: "anchored" | "consensus"; /** Recomputed txid === proof.txid. */ txidMatches: boolean; /** Merkle path folds the txid to the header's tx_merkle_root. */ includedInHeader: boolean; /** Recomputed block_hash + index_block_hash match the header / proof. */ headerSelfConsistent: boolean; /** Basis points (0–10000) of reward-set signer weight that signed the block. * Only set when the proof carries a `consensus` field. */ signerWeightBps?: number; /** ≥70% of signer weight signed. Only set with a `consensus` field. */ thresholdMet?: boolean; /** Which reward set the signer check used: "provided" (caller-resolved → * fully trustless) or "embedded" (the one Secondlayer put in the proof). */ rewardSetSource?: "provided" | "embedded"; /** All applicable checks passed (incl. the threshold when consensus is present). */ ok: boolean; errors: string[]; } /** * Resolve a reward set directly from a stacks-node (`/v3/stacker_set/{cycle}`), * so a caller can verify the consensus layer against a node IT trusts rather than * the reward set Secondlayer embedded in the proof. Pass the result as * `verifyTransactionProof(proof, { rewardSet })`. */ declare function fetchRewardSet(opts: { nodeUrl: string; cycle: number; fetchImpl?: typeof fetch; }): Promise; /** * Verify a transaction-inclusion proof. Every check is recomputed client-side, * so a `true` result does not rely on trusting Secondlayer. Pass * `{ rewardSet }` (resolved via {@link fetchRewardSet} from your own node) to * verify the consensus layer against a reward set you trust rather than the one * embedded in the proof. */ declare function verifyTransactionProof(proof: TransactionProof, opts?: { rewardSet?: RewardSet; }): TransactionProofVerifyResult; /** * A consumer sink: the destination adapter a consume loop writes through. * * Without a sink, the user owns the three hard parts of a durable indexer — * checkpoint persistence, rows+cursor atomicity, and reorg rollback — and * the reasoning that makes them safe lives in doc comments. A sink owns all * three: the loop loads its committed cursor, hands the handler a * transaction, commits rows AND cursor atomically, and rolls back reorgs * without any user code. Crucially, a sink also closes the silent-skip * hazard where omitting `onReorg` meant reorgs were ignored forever: with a * sink attached, rollback is unconditional. * * The interface is dependency-free on purpose: implementations with real * database drivers live behind subpath exports (`@secondlayer/sdk/sinks/*`) * so the root entry ships no DB dependency. * * ## The contract * * Binding on every implementation; the conformance kit * (`@secondlayer/sdk/sinks/testing`) probes each one. Violations are SILENT * in production — they surface as gaps or duplicates weeks later, never as * errors at the violation site — which is why they are spelled out here and * mechanically tested. * * What the loop guarantees the sink: * * 1. **Init-before-first-page.** `loadCursor` is called exactly once, before * the first fetch — even when the caller passed an explicit `fromCursor`. * It is the sink's init hook: create checkpoint storage, validate * rollback preconditions. * 2. **Deterministic replay.** Re-reading from the same cursor yields the * same rows in the same order. `cursor` is therefore a valid idempotency * key: an append-only sink can dedup on it with no contract change. * 3. **At-least-once delivery.** A batch whose commit outcome was lost (crash * after commit, before the loop observed it) is re-committed on restart * with the SAME cursor and the same rows. * * What the sink must guarantee — `commitBatch`: * * 4. **Rows+cursor atomicity.** The handler's writes and `cursor` commit in * ONE transaction. Committing them separately is the classic torn-batch * bug: a crash between the two either re-delivers (duplicates) or skips * (gap) depending on the order. * 5. **Abort on throw.** A throw from `write` aborts the WHOLE transaction — * neither rows nor cursor land — so a crashed batch is simply re-read. * 6. **The lent transaction is the real one.** `write(tx)` receives the same * live transaction the cursor commits in; every handler write must go * through it. Writes outside `tx` escape atomicity AND rollback. * 7. **Replay safety.** Committing the same cursor twice must not corrupt * state (per #3 the rows are identical; upsert/dedup make it exact). * * What the sink must guarantee — `rollback`: * * 8. **Delete+rewind atomicity.** Undoing rows at/above the fork and * committing `rewindCursor` happen in ONE transaction. A crash between * them resumes above the fork and the deleted range is never re-read — * the silent-gap bug. * 9. **Inclusive fork point.** Undo AT OR ABOVE `forkPointHeight` (`>=`) — * the new canonical chain re-supplies the fork block itself. * 10. **Idempotent.** The loop may re-apply the same rollback after a crash * (reorgs are deduped in memory only); a second application must be a * harmless no-op. * 11. **Scope by height, not cursor.** On a page reporting several forks the * loop calls `rollback` once per fork: each call carries its own * `forkPointHeight` but ALL carry the same `rewindCursor` (the lowest * fork point). Derive the undo range from `forkPointHeight` only; a * sink that derives it from `rewindCursor` under-deletes silently. * * Error semantics and concurrency: * * 12. **Throw, don't swallow; no internal retry.** The loop owns retry * policy. A swallowed commit error advances the loop past unwritten * data; an internal retry can double-apply around a partial failure. * 13. **Single writer per checkpoint id.** The loop assumes one live * consumer per checkpoint identity. A sink that can detect a second * writer (e.g. a lock) must fail loudly — never block or interleave. */ interface ConsumerSink { /** Phantom marker carrying `Tx` in a directly-inferable position, so the * consume loops' `TTx` type parameter resolves from the `sink` option * (never set at runtime). */ readonly _tx?: Tx; /** Static capabilities, read by the loop before the first fetch to fail * fast on impossible pairings. */ readonly capabilities?: { /** This sink cannot undo committed rows (append-only store, ClickHouse, * parquet, …), so `rollback` is unimplementable and following the * unfinalized tip would corrupt it on the first fork. The loop throws * loudly unless consuming with `finalizedOnly: true` — in that mode * reorgs never reach the sink and `rollback` may simply throw. */ finalizedOnly?: boolean; }; /** The committed checkpoint, or `null` on first run. Called once, before * the first page. Implementations may create their checkpoint storage * here and SHOULD validate their rollback preconditions (e.g. that every * declared table carries the height column). */ loadCursor(): Promise; /** * Apply one batch atomically: open a transaction, run `write(tx)` (the * user's inserts), and commit the rows AND `cursor` together. A throw * from `write` must abort the whole transaction — leaving neither rows * nor cursor, so a crashed batch is simply re-read on restart. */ commitBatch(cursor: string, write: (tx: Tx) => Promise | void): Promise; /** * Roll the projection back to the fork: delete everything AT OR ABOVE * `forkPointHeight` (inclusive `>=` — the new chain re-supplies the fork * block) and commit `rewindCursor` in the SAME transaction. Deleting * without the rewound cursor is the classic silent-gap bug: a crash * between the two writes resumes above the fork and the deleted range is * never re-read. * * On a multi-fork page this is called once PER fork, every call with the * same `rewindCursor` (the lowest fork point) but its own * `forkPointHeight` — scope the undo by `forkPointHeight` only (contract * invariant #11), and expect re-application after a crash (#10). * * `rewindCursor` is `null` only for a fork at genesis: clear the * checkpoint so the next run starts from the first event. */ rollback(forkPointHeight: number, rewindCursor: string | null): Promise; } /** The transaction type a sink hands to `onBatch` (`ctx.tx`). */ type SinkTx = S extends ConsumerSink ? Tx : never; /** `ctx` gains `tx` exactly when a sink is attached; without one the shape * is unchanged (an intersection with `unknown` is an identity, so * contextual typing of `onBatch` callbacks never degrades to a union). */ type WithSinkTx = [TTx] extends [never] ? unknown : { tx: TTx; }; import { StreamsEvent, StreamsEventType } from "@secondlayer/shared/streams-rows"; import { StreamsEventPayload } from "@secondlayer/shared/streams-rows"; type StreamsTip = { block_height: number; block_hash: string; burn_block_height: number; /** * Highest Stacks block past the burn-confirmation finality boundary. * Optional for back-compat; the API always sets it. */ finalized_height?: number; lag_seconds: number; /** * Oldest height still seekable on the live API for the caller's tier * (`tip - retention`). `null` = unlimited retention. Older reads must use the * cold dumps lane. Optional for back-compat. */ oldest_seekable_height?: number | null; /** Oldest seekable cursor (`:0`); `null` = unlimited. */ oldest_cursor?: string | null; }; type StreamsCanonicalBlock = { block_height: number; block_hash: string; burn_block_height: number; burn_block_hash: string | null; is_canonical: true; }; type StreamsReorg = { detected_at: string; fork_point_height: number; orphaned_range: { from: string; to: string; }; /** * First position of the new canonical chain at the fork, `fork:0` * (INCLUSIVE). Not an exclusive resume token — resuming directly from it * skips `fork:0`. The consumer rewinds to the foot of `fork_point_height` * (`Cursor.atHeight`) to re-read the new run from `fork:0` inclusive. */ new_canonical_tip: string; }; type StreamsEventsEnvelope = { events: TEvent[]; next_cursor: string | null; tip: StreamsTip; reorgs: StreamsReorg[]; }; type StreamsEventsListEnvelope = Omit; /** The StreamsEvent union narrowed to a `types` selection — what a * const-generic `types: ["ft_transfer"]` buys at the type level. */ type StreamsEventOfTypes = Extract; type StreamsReorgsListParams = { since: string; limit?: number; }; type StreamsReorgsListEnvelope = { reorgs: StreamsReorg[]; next_since: string | null; }; /** A filter that matches a single value or any value in a list. */ type StreamsFilterValue = string | readonly string[]; /** * One labelled filter group. Fields inside a group AND together; groups in a * map OR together server-side. */ type StreamsLabelledFilter = { types?: readonly StreamsEventType[]; contractId?: StreamsFilterValue; sender?: StreamsFilterValue; recipient?: StreamsFilterValue; assetIdentifier?: string; }; /** A map of label → filter group. Labels are yours; the server echoes them. */ type StreamsFilterMap = Record; /** * The events one label can yield. A group that declares `types` narrows to * exactly those variants, so a per-label handler needs no `event_type` guard; * a group without `types` keeps the full union. */ type StreamsEventForFilter< F, D extends boolean = false > = F extends { types: readonly (infer T extends StreamsEventType)[]; } ? D extends true ? Extract : Extract : D extends true ? IndexEvent : StreamsEvent; type StreamsEventsListParams = { cursor?: string | null; fromHeight?: number; toHeight?: number; types?: readonly StreamsEventType[]; /** Event types to exclude (applied after `types`). */ notTypes?: readonly StreamsEventType[]; contractId?: StreamsFilterValue; sender?: StreamsFilterValue; recipient?: StreamsFilterValue; assetIdentifier?: string; /** * Labelled filter groups. The groups OR together in one scan and each * returned event carries the labels it matched (`event.matched`) — two * unrelated concerns share one page, one cursor, one checkpoint. */ filters?: StreamsFilterMap; limit?: number; }; type StreamsEventsStreamParams = { fromCursor?: string | null; types?: readonly StreamsEventType[]; notTypes?: readonly StreamsEventType[]; contractId?: StreamsFilterValue; sender?: StreamsFilterValue; recipient?: StreamsFilterValue; assetIdentifier?: string; /** Labelled filter groups; see {@link StreamsEventsListParams.filters}. */ filters?: StreamsFilterMap; batchSize?: number; emptyBackoffMs?: number; maxPages?: number; maxEmptyPolls?: number; signal?: AbortSignal; }; type StreamsEventsSubscribeParams = { /** Resume strictly after this cursor; omit to live-tail from the tip. */ fromCursor?: string | null; types?: readonly StreamsEventType[]; notTypes?: readonly StreamsEventType[]; contractId?: StreamsFilterValue; sender?: StreamsFilterValue; recipient?: StreamsFilterValue; assetIdentifier?: string; /** Labelled filter groups; see {@link StreamsEventsListParams.filters}. */ filters?: StreamsFilterMap; /** Abort to unsubscribe (the returned function does the same). */ signal?: AbortSignal; /** * Called for each pushed event, in order. The resume cursor advances only * after this resolves, so a handler that throws sees the same event again * on reconnect (at-least-once). Key durable writes by `cursor`. */ onEvent: (event: StreamsEvent) => void | Promise; /** * Called on every failure. Transport errors (dropped socket, 5xx, 429, a * stale connection, a throwing handler) reconnect from the last handled * cursor with exponential backoff. Errors that a retry cannot fix (401, * 4xx, a bad signature) end the subscription and reject its `done`. */ onError?: (err: unknown) => void; /** * First reconnect pause in ms (default 1000). Doubles per consecutive * failure up to 30 s with jitter, resets once a frame arrives, and never * undercuts a `Retry-After`. */ reconnectDelayMs?: number; /** * Reconnect when no frame (pings included) arrives for this long (default * 60000, three server heartbeats). Catches a half-open socket that never * errors; set it above your instance's heartbeat interval. */ staleAfterMs?: number; }; /** * Handle for a live subscription: call it to unsubscribe. `done` resolves * after unsubscribe (or the signal aborting) and rejects with the error that * ended the loop when a retry could not fix it, so a worker can `await` * it instead of polling for silence. */ type StreamsSubscription = { (): void; done: Promise; }; /** * The checkpoint the SDK computes for a batch. Persist `cursor` inside the same * transaction as your projection writes, then resume from it via `fromCursor`. * It is the position to advance to: `next_cursor` normally, or the last * finalized event when `finalizedOnly` is set. */ /** * What the loop knows at the end of a page, handed to `onBatch`. * * `cursor` is the position to commit. The rest answers "how far along am I, * and am I still moving" — the question every deployed consumer has to expose * for a health check. * * Progress is reported here rather than left to callers because only the loop * can get it right: `height` has to survive empty pages (normal at the tip) * and has to roll back to `fork_point_height - 1` after a reorg rewind. * Deriving it from `cursor` is wrong — `Cursor.atHeight` encodes the foot of a * block as `${height - 1}:`, so a rewound cursor parses to a * position that was never reached. */ type ConsumerBatchContext< TTip = unknown, TReorg = unknown > = { /** Checkpoint to commit alongside your rows. */ cursor: string | null; /** Highest canonical block a row was DELIVERED from. `null` before the * first row of a fresh consume. With a sparse filter this parks at the * last matching event — it measures event recency, not progress. */ height: number | null; /** Highest canonical block the sweep has VERIFIED through: an empty page * is the server confirming nothing matches between the cursor and the * tip, so a caught-up tail scans to the tip even when `height` is far * below it. Rolls back with reorg rewinds. */ scannedHeight: number | null; /** Chain tip as of this page's read. */ tipHeight: number; /** `tipHeight - scannedHeight`, floored at 0 — the consumer's actual * backlog. `null` until a position is known. A caught-up consumer on a * quiet contract reads ~0 here while `tipHeight - height` grows — that * difference is event age, not lag. */ blocksBehind: number | null; /** * The full tip for this page — `StreamsTip` or `IndexTip` depending on the * surface. Exposed here so a handler never needs the `envelope` parameter, * which is the only way to reach it today and is also how the worst * `finalizedOnly` mistake becomes reachable (`return envelope.next_cursor` * jumps the cursor past events that were filtered out and never delivered). */ tip: TTip; /** * Reorgs reported alongside this page; empty when none. Rollback still * belongs in `onReorg` — this is for observability inside a batch. */ reorgs: readonly TReorg[]; }; /** @deprecated Use {@link ConsumerBatchContext} — the Index and Streams loops * share one shape now. Kept as an alias so existing annotations compile. */ type StreamsBatchContext = ConsumerBatchContext; /** * The checkpoint for a reorg rollback. Persist `cursor` (the rewind position) * inside the same transaction as your rollback so the two commit atomically. */ /** `cursor` is the rewind position the loop resumes from after `onReorg`: * the foot of the fork point, or `null` for a fork at genesis. Persist it * in the same transaction as the rollback. */ type StreamsReorgContext = { cursor: string | null; }; type StreamsEventsConsumeParams< TTx = never, D extends boolean = false, F extends StreamsFilterMap = Record > = { fromCursor?: string | null; /** * Deliver events pre-decoded as the flat, `event_type`-discriminated rows * Index serves (`IndexEvent`), so Streams consumption reads identically * to Index consumption — no guard+decode pairs in your handler. */ decoded?: D; /** * Destination adapter that owns the checkpoint + rollback transaction * (e.g. `kyselySink` from `@secondlayer/sdk/sinks/kysely`). With a sink: * the loop resumes from the sink's committed cursor, `onBatch` receives * `ctx.tx` and must write ONLY through it (rows and cursor commit in one * transaction), reorg rollback is automatic, and `onBatch`'s return value * is ignored. Folds (balances) invert in the sink's `onRollback`, not in * `onReorg`. */ sink?: ConsumerSink; /** Fires once per page, before `onBatch` and before any early return. */ onProgress?: (ctx: ConsumerBatchContext) => void; mode?: "tail" | "bounded"; /** * Emit only finalized (immutable) events and never surface reorgs. The SDK * checkpoints at the last finalized event and re-reads the unfinalized tail * until it settles. Trades finality lag for zero reorg handling; `onReorg` is * ignored. */ finalizedOnly?: boolean; types?: readonly StreamsEventType[]; notTypes?: readonly StreamsEventType[]; contractId?: StreamsFilterValue; sender?: StreamsFilterValue; recipient?: StreamsFilterValue; assetIdentifier?: string; /** * Labelled filter groups. The groups OR together in ONE server-side scan, * so two unrelated concerns share one page, one cursor, and one checkpoint * instead of two consume loops. Pair with `on` to handle each label. */ filters?: F; /** * One handler per label in `filters`. Every label must be handled, so * adding a label is a compile error until its handler exists. * * A label's declared `types` narrows its handler's events, so `payload` is * typed without an `event_type` guard. Labels are dispatched in declaration * order: a label's own events stay in cursor order, but events of different * labels do not interleave within a page — add `onBatch` when you need * strict global order across labels. */ on?: { [K in keyof F] : (events: StreamsEventForFilter[], ctx: StreamsBatchContext & WithSinkTx) => void | Promise }; batchSize?: number; /** * Apply a page of canonical events. Persist `ctx.cursor` in the same * transaction as your writes. Returning a cursor overrides `ctx.cursor` as * the resume point (advanced manual control); returning nothing uses it. * * Optional only when `on` handles the page instead; a consume call with * neither throws. */ onBatch?: (events: D extends true ? IndexEvent[] : StreamsEvent[], envelope: StreamsEventsEnvelope, ctx: StreamsBatchContext & WithSinkTx) => void | string | null | undefined | Promise | Promise; /** * Roll your projection back to `reorg.fork_point_height`, persisting * `ctx.cursor` in the same transaction. Called once per *new* reorg at or * below the checkpoint (deduped in-memory, fork-ascending) before the SDK * rewinds and re-reads the now-canonical events. A reorg whose fork sits * above the checkpoint is not a rollback (nothing past it was written) and * is skipped. Omit it to ignore reorgs (events stay canonical, but stale * rows from an orphaned fork are left in place). * * With `onReorg` or a `sink` attached, every empty page also polls * `reorgs.list` (one extra request per idle poll) so a fork that lands * while the consumer idles at the tip is rolled back too. */ onReorg?: (reorg: StreamsReorg, ctx: StreamsReorgContext) => Promise | void; /** Deepest rewind one reorg may make below the checkpoint before the loop * refuses with `ValidationError`. Default 1000 blocks. The fork point is * server supplied and drives a delete on every declared table, so raise * this only for a source you trust. */ maxRollbackDepth?: number; /** Page-fetch retries after the first failure (429/5xx/network only — * 4xx and handler throws always propagate). Default 3; `0` disables. */ retryCount?: number; /** Base retry delay in ms; the n-th retry waits `retryDelay * n`. A server * `Retry-After` overrides it. Default 1000. */ retryDelay?: number; /** Void observer, called before each retry sleep (metrics/logging). */ onError?: (err: unknown, ctx: { attempt: number; retriesLeft: number; delayMs: number; }) => void; emptyBackoffMs?: number; maxPages?: number; maxEmptyPolls?: number; signal?: AbortSignal; }; /** * One yielded page from {@link StreamsClient.consume} — the * `GET /v1/streams/events` envelope verbatim, with `next_cursor` renamed to * `cursor` (the checkpoint to persist and resume from). */ type StreamsBatch = { /** Canonical events of this page, in cursor order. */ events: TEvent[]; /** Checkpoint after this page — pass back as `consume({ cursor })` to resume. */ cursor: string | null; tip: StreamsTip; /** Chain reorgs reported alongside this page; empty when none. */ reorgs: StreamsReorg[]; }; type StreamsConsumeParams = { /** Resume strictly after this cursor; omit to start from the oldest seekable page. */ cursor?: string | null; types?: readonly StreamsEventType[]; notTypes?: readonly StreamsEventType[]; contractId?: StreamsFilterValue; sender?: StreamsFilterValue; recipient?: StreamsFilterValue; assetIdentifier?: string; /** Labelled OR-groups — same semantics as `events.consume`; each returned * event echoes the labels it matched. */ filters?: StreamsFilterMap; /** Events per page (the `limit` query param). Default 100. */ batchSize?: number; /** Poll interval while caught up at the tip, in ms. Default 2000. */ intervalMs?: number; /** Abort to end the iteration. */ signal?: AbortSignal; }; type StreamsEventsConsumeResult = { cursor: string | null; pages: number; emptyPolls: number; }; type StreamsEventsReplayParams = { /** Start point: `"genesis"` (default) or a `:` cursor. */ from?: "genesis" | string; /** * Called once per finalized dump file, in block order, before live tailing. * Process the parquet with your own tooling (e.g. DuckDB) — the SDK does not * decode parquet. Use `client.dumps.download(file)` to fetch + verify bytes. * * Delivery is file-granular and at-least-once: a file whose range straddles * `from` is handed over whole. `ctx.from` is that cursor (`null` from * genesis); skip rows at or below it, or key rows by `cursor` so a re-run * is an idempotent no-op. Files ending at or below `from` are not handed * over at all. */ onDumpFile: (file: StreamsDumpFile, ctx: { from: string | null; }) => Promise | void; /** Called per live page after the dump phase, like `consume`. */ onBatch: (events: StreamsEvent[], envelope: StreamsEventsEnvelope) => Promise | string | null | undefined; /** * Called when the live-tail phase (after the dump backfill) crosses a reorg, * before the cursor rewinds and re-reads the now-canonical events — same * contract as `consume`. The dump-backfill phase is finalized and never * reorgs; omit this to leave stale rows from an orphaned fork in place. */ onReorg?: (reorg: StreamsReorg, ctx: StreamsReorgContext) => Promise | void; mode?: "tail" | "bounded"; batchSize?: number; emptyBackoffMs?: number; maxPages?: number; maxEmptyPolls?: number; signal?: AbortSignal; /** Narrow the LIVE TAIL after the dump phase. The dump files themselves are * always all-type (block-partitioned parquet) — filter those in your own * tooling while processing each file. */ types?: readonly StreamsEventType[]; notTypes?: readonly StreamsEventType[]; /** Labelled OR-groups for the live tail, like `consume`. */ filters?: StreamsFilterMap; }; type FetchLike2 = (input: string | URL | Request, init?: RequestInit) => Promise; /** One bulk parquet file in the dumps manifest. `path` is the object key under * the dumps base URL. */ type StreamsDumpFile = { path: string; from_block: number; to_block: number; min_cursor: string | null; max_cursor: string | null; row_count: number; byte_size: number; sha256: string; schema_version: number; created_at: string; }; type StreamsDumpsManifest = { dataset: string; network: string; version: string; schema_version: number; generated_at: string; producer_version: string; finality_lag_blocks: number; /** Cursor at the end of the finalized bulk coverage — hand to live tailing. */ latest_finalized_cursor: string | null; coverage: { from_block: number; to_block: number; }; files: StreamsDumpFile[]; /** ed25519 signature over the manifest's canonical bytes. Absent on legacy * unsigned manifests. Verified by `list()` when `verifyDumpsManifest` is on. */ signature?: string; /** Short id of the signing public key. */ key_id?: string; }; type StreamsDumps = { /** Fetch and parse the latest dumps manifest. */ list(): Promise; /** Absolute URL for a manifest file. */ fileUrl(file: StreamsDumpFile): string; /** Download a parquet file and verify its sha256 against the manifest. */ download(file: StreamsDumpFile): Promise; }; type StreamsClient = { /** * Follow Streams as an async iterator of page batches. * * Yields one {@link StreamsBatch} per `GET /v1/streams/events` page — the * existing envelope (`events`, `next_cursor` → `cursor`, `tip`, `reorgs`) * with zero extra API calls. Batches are chosen over per-block groupings * because the envelope is page-keyed, so every yield is exactly one fetch. * Empty pages are skipped; at the tip the iterator re-polls every * `intervalMs` (default 2000) until aborted via `signal`. * * Reorgs are surfaced on the batch (`batch.reorgs`) but the cursor is not * rewound automatically — use `events.consume` with `onReorg` for managed * rollback semantics. */ /** Narrowing overload: a literal `types` array narrows every batch's * event union to exactly those members. */ consume(params: StreamsConsumeParams & { types: T; }): AsyncIterableIterator>>; consume(params?: StreamsConsumeParams): AsyncIterableIterator; events: { /** Narrowing overload, matching `consume`. */ list(params: StreamsEventsListParams & { types: T; }): Promise>>; list(params?: StreamsEventsListParams): Promise; byTxId(txId: string): Promise; /** * Pull pages from Streams and call `onBatch` after each page. * * Use `consume` for indexers and ETL jobs that own checkpointing. Return * the checkpoint cursor from `onBatch`. Default `mode: "tail"` keeps * polling when caught up; `mode: "bounded"` exits on the first empty page. * The consumer also exits when `maxPages`, `maxEmptyPolls`, or `signal` * stops it. */ consume< const F extends StreamsFilterMap = Record, TTx = never, D extends boolean = false >(params: StreamsEventsConsumeParams & { sink?: ConsumerSink; }): Promise; /** * Backfill from bulk dumps, then continue live from the dump→live seam in * one call. Iterates finalized dump files (via `onDumpFile`) in block * order, then tails live from the manifest's `latest_finalized_cursor` * (exclusive input → no gap or duplicate at the seam). Requires * `dumpsBaseUrl`. */ replay(params: StreamsEventsReplayParams): Promise; /** * Follow Streams as an async iterator. * * Use `stream` for live processors and watch-style apps. It tails * indefinitely by default and stops when its `AbortSignal`, `maxPages`, or * `maxEmptyPolls` stops it. */ /** Narrowing overload, matching `consume`. */ stream(params: StreamsEventsStreamParams & { types: T; }): AsyncIterable>; stream(params?: StreamsEventsStreamParams): AsyncIterable; /** * Subscribe to the real-time SSE push surface. Calls `onEvent` for each new * canonical event as the server pushes it (chain cadence, not poll-bounded), * and verifies each frame's inline ed25519 signature when the client was * created with `verify`. Returns an unsubscribe function whose `done` * settles when the loop ends. Not reorg-aware: an event delivered then * orphaned is never retracted. Durable writers use `consume()`. */ subscribe(params: StreamsEventsSubscribeParams): StreamsSubscription; }; blocks: { events(heightOrHash: number | string): Promise; }; reorgs: { list(params: StreamsReorgsListParams): Promise; }; /** Bulk parquet dumps. Requires `dumpsBaseUrl` on the client. */ dumps: StreamsDumps; canonical(height: number): Promise; tip(): Promise; }; type Sleep = (ms: number, signal?: AbortSignal) => Promise; /** Options shared by both consume loops' page-fetch retry. Vocabulary matches * `@secondlayer/stacks` transports (`retryCount`/`retryDelay`) — one retry * language across the family, not a third. */ type PageRetryOptions = { /** Retries after the first failure. Default 3; `0` disables. */ retryCount?: number; /** Base delay in ms; the n-th retry waits `retryDelay * n` (matches the * stacks transport). A server `Retry-After` overrides it. Default 1000. */ retryDelay?: number; /** Void observer, called before each retry sleep (metrics/logging). The * retry policy owns the decision; this cannot change it. */ onError?: (err: unknown, ctx: { attempt: number; retriesLeft: number; delayMs: number; }) => void; }; /** Minimum shape a consumed Index row must expose. */ type IndexFeedItem = { cursor: string; block_height: number; }; /** Minimum envelope shape of a consumable Index feed page. */ type IndexFeedEnvelope = { next_cursor: string | null; tip: IndexTip; reorgs: IndexReorg[]; }; /** One page fetch. `fromHeight` is only set on the first page of a fresh * consume (no cursor yet) — cursor and from_height are mutually exclusive * on the API. */ type IndexFeedFetcher = (params: { cursor: string | null; fromHeight?: number; limit: number; }) => Promise; /** Consumer options shared by `index.events.consume` and * `index.contractCalls.consume`. Same contract as the Streams consumer: * commit your writes inside `onBatch`, return the cursor you committed — * or attach a `sink` and let it own checkpointing and rollback entirely. */ type IndexConsumeOptions< TItem extends IndexFeedItem, TEnvelope extends IndexFeedEnvelope, TTx = never > = { /** Resume from a committed checkpoint. Without it (and without * `fromHeight`) the API serves only the recent default window. */ fromCursor?: string | null; /** Start a fresh sweep at this height (e.g. `0` for genesis backfill). * Ignored once a cursor exists (including a sink's committed cursor). */ fromHeight?: number; /** `tail` (default) keeps polling at the tip; `bounded` returns on the * first empty page. */ mode?: "tail" | "bounded"; /** Emit only rows at or below the tip's `finalized_height`; the * unfinalized tail is re-read each poll until it settles. Finalized data * never reorgs, so `onReorg` is skipped entirely. */ finalizedOnly?: boolean; batchSize?: number; /** * Destination adapter that owns the checkpoint + rollback transaction * (e.g. `kyselySink` from `@secondlayer/sdk/sinks/kysely`). With a sink: * the loop resumes from the sink's committed cursor, `onBatch` receives * `ctx.tx` and must write ONLY through it (rows and cursor commit in one * transaction — a throw aborts both), reorg rollback is automatic (no * `onReorg` needed), and `onBatch`'s return value is ignored. Folds * (balances) invert in the sink's `onRollback`, not in `onReorg`. */ sink?: ConsumerSink; /** Fires once per page, before `onBatch` and before any early return — * an empty page still proves the loop is alive. Feed it to * `consumerHealth().record`. */ onProgress?: (ctx: ConsumerBatchContext) => void; onBatch: (items: TItem[], envelope: TEnvelope, ctx: ConsumerBatchContext & WithSinkTx) => void | string | null | undefined | Promise | Promise; /** Called once per new reorg at or below the checkpoint, before the loop * rewinds to `ctx.cursor` (`null` = pre-genesis) and re-reads. A reorg * whose fork sits ABOVE the checkpoint is not a rollback: nothing past * it was written, so it is noted and the page is read as normal. */ onReorg?: (reorg: IndexReorg, ctx: { cursor: string | null; }) => Promise | void; /** Deepest rewind one reorg may make below the checkpoint before the loop * refuses with `ValidationError`. Default 1000 blocks. The fork point is * server supplied and drives a delete on every declared table, so raise * this only for a source you trust. */ maxRollbackDepth?: number; sleep?: Sleep; emptyBackoffMs?: number; maxPages?: number; maxEmptyPolls?: number; signal?: AbortSignal; /** Page-fetch retries after the first failure (429/5xx/network only — * 4xx and handler throws always propagate). Default 3; `0` disables. */ retryCount?: number; /** Base retry delay in ms; the n-th retry waits `retryDelay * n`. A server * `Retry-After` overrides it. Default 1000. */ retryDelay?: number; /** Void observer, called before each retry sleep. Cannot change the retry * decision — the policy owns it. */ onError?: PageRetryOptions["onError"]; }; /** * Checkpointed pull loop over a cursor-paginated Index feed — the Index port * of `consumeStreamsEvents`, sharing its contract: at-least-once delivery, * client-owned checkpoints (`onBatch` may return the cursor it committed), * and automatic reorg rewind to the lowest fresh fork point. * * Differs from Streams in how finality is read: Index rows carry no * per-event `finalized` flag, so `finalizedOnly` gates by * `block_height <= tip.finalized_height` instead. */ declare function consumeIndexFeed< TItem extends IndexFeedItem, TEnvelope extends IndexFeedEnvelope, TTx = never >(opts: IndexConsumeOptions & { fetchPage: IndexFeedFetcher; itemsOf: (envelope: TEnvelope) => TItem[]; }): Promise<{ cursor: string | null; pages: number; emptyPolls: number; }>; import { SbtcEventTopic as SbtcEventTopic2 } from "@secondlayer/stacks/sbtc"; type IndexTip = { block_height: number; /** Highest height treated as immutable (past the burn-confirmation * finality boundary). Rows at or below it never reorg — `finalizedOnly` * consumers gate on this, since Index rows carry no per-event flag. */ finalized_height: number; lag_seconds: number; }; /** * A chain reorg overlapping a returned page's height range. Height-keyed feeds * (`/transactions`, `/contract-calls`, `/stacking`) populate this so a consumer * can reconcile: roll back every row at `block_height >= fork_point_height` * (the whole fork block is replaced, so the rollback is inclusive of the fork * height), then re-read the canonical run from the foot of `fork_point_height`. * The SDK consumers do exactly this — they rewind to `Cursor.atHeight( * fork_point_height)`, an exclusive cursor that re-reads from `fork:0` * inclusive. Empty when the page spans no reorg. */ type IndexReorg = { id: string; detected_at: string; fork_point_height: number; old_index_block_hash: string | null; new_index_block_hash: string | null; /** Orphaned cursor span `:`, inclusive. */ orphaned_range: { from: string; to: string; }; /** * First position of the new canonical chain at the fork, `fork:0` * (INCLUSIVE). Not an exclusive resume token — resuming a `(bh,ei) > cursor` * read directly from it would skip `fork:0`. To re-read the new run, rewind * to the foot of `fork_point_height` (`Cursor.atHeight`), not to this value. */ new_canonical_tip: string; }; type FtTransfer = { cursor: string; block_height: number; block_time?: string | null; tx_id: string; tx_index: number; event_index: number; event_type: "ft_transfer"; contract_id: string; asset_identifier: string; sender: string; recipient: string; amount: string; }; type FtTransfersEnvelope = { events: TRow[]; next_cursor: string | null; tip: IndexTip; reorgs: IndexReorg[]; }; /** Transfer columns that survive any projection — the consume contract * (`cursor`, `block_height`) plus the `event_type` discriminant. */ type FtTransferAlwaysFields = "cursor" | "block_height" | "event_type"; /** An ft-transfer row narrowed to the requested columns plus the always-present ones. */ type FtTransferFields = Pick; type FtTransfersListParams = { /** * Columns to return. The server projects the row, so an unrequested column * is physically absent — and the narrowing overload on `list` makes reading * one a compile error rather than `undefined` at runtime. Omitting * `block_time` also lets the server skip the blocks join. */ fields?: readonly (keyof FtTransfer & string)[]; cursor?: string | null; fromCursor?: string | null; limit?: number; contractId?: string; assetIdentifier?: string; sender?: string; recipient?: string; fromHeight?: number; toHeight?: number; }; /** Largest page any Index list route serves. The server clamps `limit` above * this without saying so, so `walk` refuses a bigger `batchSize` up front * rather than paging in silently smaller steps. */ declare const INDEX_MAX_PAGE_SIZE = 1e3; /** Options every `walk*` feed shares on top of its list filters. Page fetches * retry with the same `retryCount`/`retryDelay`/`onError` vocabulary as * `consume()`; the tradeoff is that a walk can pause for the retry delays * instead of failing fast on the first 429. */ type WalkOptions = PageRetryOptions & { /** Rows per page request. Default 200, max `INDEX_MAX_PAGE_SIZE` (1000). */ batchSize?: number; /** Stops the walk. The iterator always rejects with the signal's reason (an * `AbortError` by default), whether the abort lands during a page request * (which is cancelled) or between yields, so a caller can tell a finished * walk from a stopped one. */ signal?: AbortSignal; }; type FtTransfersWalkParams = Omit & WalkOptions; type NftTransfer = { cursor: string; block_height: number; block_time?: string | null; tx_id: string; tx_index: number; event_index: number; event_type: "nft_transfer"; contract_id: string; asset_identifier: string; sender: string; recipient: string; value: string; }; type NftTransfersEnvelope = { events: TRow[]; next_cursor: string | null; tip: IndexTip; reorgs: IndexReorg[]; }; /** See {@link FtTransferAlwaysFields}. */ type NftTransferAlwaysFields = "cursor" | "block_height" | "event_type"; /** An nft-transfer row narrowed to the requested columns plus the always-present ones. */ type NftTransferFields = Pick; type NftTransfersListParams = { /** Columns to return (see {@link FtTransfersListParams.fields}). */ fields?: readonly (keyof NftTransfer & string)[]; cursor?: string | null; fromCursor?: string | null; limit?: number; contractId?: string; assetIdentifier?: string; sender?: string; recipient?: string; fromHeight?: number; toHeight?: number; }; type NftTransfersWalkParams = Omit & WalkOptions; type IndexEventBase = { cursor: string; block_height: number; block_time?: string | null; tx_id: string; tx_index: number; event_index: number; contract_id: string | null; /** Submitting-transaction context, present only when `txContext: true` was * requested. `tx_sender` is the real tx sender — distinct from a transfer's * asset `sender`, and the only place a `print` event's sender is available. * Lets a consumer build per-event tx context without a `/v1/index/transactions` * call per event. */ tx_sender?: string | null; tx_type?: string | null; tx_status?: string | null; tx_contract_id?: string | null; tx_function_name?: string | null; }; type IndexFtTransfer = IndexEventBase & { event_type: "ft_transfer"; asset_identifier: string; sender: string; recipient: string; amount: string; }; type IndexNftTransfer = IndexEventBase & { event_type: "nft_transfer"; asset_identifier: string; sender: string; recipient: string; value: string; }; type IndexStxTransfer = IndexEventBase & { event_type: "stx_transfer"; sender: string; recipient: string; amount: string; memo: string | null; }; type IndexStxMint = IndexEventBase & { event_type: "stx_mint"; recipient: string; amount: string; }; type IndexStxBurn = IndexEventBase & { event_type: "stx_burn"; sender: string; amount: string; }; type IndexStxLock = IndexEventBase & { event_type: "stx_lock"; sender: string; amount: string; payload: { unlock_height: string | null; }; }; type IndexFtMint = IndexEventBase & { event_type: "ft_mint"; asset_identifier: string; recipient: string; amount: string; }; type IndexFtBurn = IndexEventBase & { event_type: "ft_burn"; asset_identifier: string; sender: string; amount: string; }; type IndexNftMint = IndexEventBase & { event_type: "nft_mint"; asset_identifier: string; recipient: string; value: string; }; type IndexNftBurn = IndexEventBase & { event_type: "nft_burn"; asset_identifier: string; sender: string; value: string; }; type IndexPrint = IndexEventBase & { event_type: "print"; payload: { topic: string | null; value: unknown; raw_value: string | null; }; }; /** Decoded chain event, discriminated by `event_type`. */ type IndexEvent = IndexFtTransfer | IndexNftTransfer | IndexStxTransfer | IndexStxMint | IndexStxBurn | IndexStxLock | IndexFtMint | IndexFtBurn | IndexNftMint | IndexNftBurn | IndexPrint; type IndexEventType = IndexEvent["event_type"]; /** Fields every read returns regardless of `fields` — the consume contract * (`cursor`, `block_height`) plus the union discriminant. */ type IndexAlwaysFields = "cursor" | "block_height" | "event_type"; /** An event row narrowed to a `fields` selection (plus what always ships). */ type IndexEventFields< T extends IndexEventType, F extends keyof IndexEventOf & string > = Pick, (F | IndexAlwaysFields) & keyof IndexEventOf>; type EventsEnvelope< T extends IndexEventType = IndexEventType, TRow = IndexEventOf > = { events: TRow[]; next_cursor: string | null; tip: IndexTip; reorgs: IndexReorg[]; }; /** The single decoded-event member matching one `event_type` literal. * `IndexEventOf<"ft_transfer">` is `IndexFtTransfer`. */ type IndexEventOf = Extract; type EventsListParams = { /** Required. One of the decoded event types. Passing a literal narrows the * rows every surface hands back to that event's own shape. */ eventType: T; cursor?: string | null; fromCursor?: string | null; limit?: number; /** One id, or several to scope the sweep to a set of contracts (max 20). * Mutually exclusive with `trait`. */ contractId?: string | readonly string[]; assetIdentifier?: string; sender?: string; recipient?: string; fromHeight?: number; toHeight?: number; /** Restrict to contracts conforming to a trait/standard (e.g. "sip-010"). * Mutually exclusive with contractId; contract-keyed event types only. */ trait?: string; /** Join the submitting transaction into each event — populates `tx_sender`, * `tx_type`, `tx_status`, `tx_contract_id`, `tx_function_name`. Off by default. * Avoids a `/v1/index/transactions` call per event; for `print` events it's * the only source of the submitting sender. */ txContext?: boolean; /** * Return only these columns. The server projects the SELECT, so an * unrequested field is physically absent — and the returned row type * narrows to match, making a read of one a compile error rather than * `undefined`. * * `cursor`, `block_height`, and `event_type` come back regardless: the * first two are the consume contract, the third carries the discriminant. * Don't list them. * * Cost note: this does NOT change your bill — Index meters per row read, * not per field. What it buys is wire bytes, plus skipping the `blocks` * join when `block_time` is omitted. */ fields?: readonly (keyof IndexEventOf & string)[]; }; type EventsWalkParams = Omit, "limit"> & WalkOptions; type EventsConsumeParams< T extends IndexEventType = IndexEventType, TTx = never > = Omit, "cursor" | "fromCursor" | "limit" | "fields"> & IndexConsumeOptions, EventsEnvelope, TTx>; type IndexContractCall = { cursor: string; block_height: number; block_time?: string | null; tx_id: string; tx_index: number; contract_id: string; function_name: string; sender: string; status: string; args: unknown[]; result: unknown; result_hex: string | null; }; type ContractCallsEnvelope = { contract_calls: IndexContractCall[]; next_cursor: string | null; tip: IndexTip; reorgs: IndexReorg[]; }; type ContractCallsListParams = { cursor?: string | null; fromCursor?: string | null; limit?: number; /** One id, or several to scope the sweep to a set of contracts (max 20). * Mutually exclusive with `trait`. */ contractId?: string | readonly string[]; functionName?: string; sender?: string; fromHeight?: number; toHeight?: number; /** Restrict to contracts conforming to a trait/standard (e.g. "sip-010"). * Mutually exclusive with contractId. */ trait?: string; }; type ContractCallsWalkParams = Omit & WalkOptions; type ContractCallsConsumeParams = Omit & IndexConsumeOptions; /** One canonical block in the sync map. Lean by design — block + parent hash * for chain linkage, burn anchor for Bitcoin confirmations. Use `blocks` for * the full block resource. */ type IndexCanonicalBlock = { cursor: string; block_height: number; block_hash: string; parent_hash: string; burn_block_height: number; burn_block_hash: string | null; }; type CanonicalEnvelope = { canonical: IndexCanonicalBlock[]; next_cursor: string | null; tip: IndexTip; }; type CanonicalListParams = { cursor?: string | null; fromCursor?: string | null; limit?: number; fromHeight?: number; toHeight?: number; }; type CanonicalWalkParams = Omit & WalkOptions; /** A block resource. Metadata is intentionally thin — only chain-linkage and * burn-anchor fields are persisted (no miner / tx_count / signer). */ type IndexBlock = { cursor: string; block_height: number; block_hash: string; parent_hash: string; burn_block_height: number; burn_block_hash: string | null; block_time: string | null; canonical: boolean; }; type BlocksEnvelope = { blocks: IndexBlock[]; next_cursor: string | null; tip: IndexTip; }; type BlockEnvelope = { block: IndexBlock; tip: IndexTip; }; type BlocksListParams = { cursor?: string | null; fromCursor?: string | null; limit?: number; fromHeight?: number; toHeight?: number; }; type BlocksWalkParams = Omit & WalkOptions; type IndexPostCondition = { type: "stx"; principal: string; condition_code: number; condition_code_name: string | null; amount: string; } | { type: "ft"; principal: string; asset_identifier: string; condition_code: number; condition_code_name: string | null; amount: string; } | { type: "nft"; principal: string; asset_identifier: string; asset_value: unknown; condition_code: number; condition_code_name: string | null; } | { type: "staking"; principal: string; condition_code: number; condition_code_name: string | null; amount: string; } | { type: "pox"; principal: string; condition_code: number; condition_code_name: string | null; }; /** Full transaction document: columnar fields plus `raw_tx`-decoded enrichment. * Payload sub-objects are present only for the matching `tx_type`; enrichment * fields are null when `raw_tx` isn't decodable (e.g. burnchain ops). */ type IndexTransaction = { cursor: string; tx_id: string; block_height: number; block_time?: string | null; tx_index: number; tx_type: string; sender: string; status: string; fee: string | null; nonce: string | null; sponsored: boolean | null; anchor_mode: string | null; post_condition_mode: string | null; post_conditions: IndexPostCondition[]; contract_call?: { contract_id: string; function_name: string; function_args: unknown[]; /** Raw hex-encoded ClarityValues; decode(function_args_hex[i]) === function_args[i]. */ function_args_hex: string[]; result: unknown; result_hex: string | null; }; token_transfer?: { recipient: string; amount: string; memo: string; }; smart_contract?: { contract_id: string | null; clarity_version: number | null; }; coinbase?: { alt_recipient: string | null; }; tenure_change?: { cause: number; }; }; type TransactionsEnvelope = { transactions: TRow[]; next_cursor: string | null; tip: IndexTip; reorgs: IndexReorg[]; }; type TransactionEnvelope = { transaction: IndexTransaction; tip: IndexTip; }; /** Transaction columns that survive any projection. */ type TransactionAlwaysFields = "cursor" | "block_height" | "tx_id"; /** A Transaction row narrowed to the requested columns plus the always-present ones. */ type TransactionFields = Pick; type TransactionsListParams = { /** * Columns to return. The server projects the row, so an unrequested column * is physically absent — and the narrowing overload on `list` makes reading * one a compile error rather than `undefined` at runtime. */ fields?: readonly (keyof IndexTransaction & string)[]; cursor?: string | null; fromCursor?: string | null; limit?: number; type?: string; sender?: string; contractId?: string; fromHeight?: number; toHeight?: number; }; type TransactionsWalkParams = Omit & WalkOptions; /** A decoded PoX-4 stacking action (one per stacking contract call). */ type IndexStackingAction = { cursor: string; block_height: number; block_time?: string | null; burn_block_height: number; tx_id: string; tx_index: number; function_name: string; caller: string; stacker: string | null; delegate_to: string | null; amount_ustx: string | null; lock_period: number | null; pox_addr: { version: number | null; hashbytes: string | null; btc: string | null; }; start_cycle: number | null; end_cycle: number | null; reward_cycle: number | null; signer_key: string | null; result_ok: boolean; }; type StackingEnvelope = { stacking: IndexStackingAction[]; next_cursor: string | null; tip: IndexTip; reorgs: IndexReorg[]; /** Present only when the PoX-4 decoder is disabled, explaining an empty feed. */ notes?: string; }; type StackingListParams = { cursor?: string | null; fromCursor?: string | null; limit?: number; functionName?: string; stacker?: string; caller?: string; fromHeight?: number; toHeight?: number; }; type StackingWalkParams = Omit & WalkOptions; /** A pending (unconfirmed) transaction. Like a transaction document but * pre-chain — no block_height/tx_index/result/events — with `received_at` and * a sequence cursor instead of a block position. */ type IndexMempoolTransaction = { cursor: string; tx_id: string; tx_type: string; sender: string; received_at?: string | null; fee: string | null; nonce: string | null; sponsored: boolean | null; anchor_mode: string | null; post_condition_mode: string | null; post_conditions: IndexPostCondition[]; contract_call?: { contract_id: string; function_name: string; function_args: unknown[]; }; token_transfer?: { recipient: string; amount: string; memo: string; }; smart_contract?: { clarity_version: number | null; }; coinbase?: { alt_recipient: string | null; }; tenure_change?: { cause: number; }; }; type MempoolEnvelope = { mempool: IndexMempoolTransaction[]; next_cursor: string | null; tip: IndexTip; }; type MempoolTransactionEnvelope = { transaction: IndexMempoolTransaction; tip: IndexTip; }; type MempoolListParams = { cursor?: string | null; fromCursor?: string | null; limit?: number; sender?: string; type?: string; /** Filter to pending calls to a single contract (e.g. `SP….contract`). */ contractId?: string; /** Filter to pending calls to a single function (composes with `contractId`). */ functionName?: string; }; type MempoolWalkParams = Omit & WalkOptions; /** * Empirical per-topic print payload schema for a contract, inferred from * sampled on-chain events. `topics` is sorted by count desc; `sampled` is true * when the contract has more print events than the windows examined. */ type PrintSchemaResponse = { contract_id: string; topics: InferredTopicSchema[]; sampled: boolean; total_events: number; /** True when the count hit the server-side cap (total_events is the cap). */ total_events_capped: boolean; sample: { size: number; newest_height: number | null; oldest_height: number | null; }; tip: IndexTip; }; type SbtcWithdrawalStatus = "REQUESTED" | "ACCEPTED" | "REJECTED"; /** A completed sBTC peg-in, keyed by `bitcoin_txid` (deposits carry no * `request_id`). One terminal `completed-deposit` event per deposit. */ type IndexSbtcDeposit = { cursor: string; block_height: number; block_time?: string | null; tx_id: string; tx_index: number; event_index: number; amount: string | null; sender: string | null; bitcoin_txid: string | null; output_index: number | null; recipient_btc_version: number | null; recipient_btc_hashbytes: string | null; }; /** A deposit fetched by Bitcoin txid — always terminal, hence `status`. */ type IndexSbtcDepositDetail = IndexSbtcDeposit & { status: "COMPLETED"; }; /** A peg-out collapsed to one row per `request_id`, with lifecycle `status` * derived from the latest accept/reject. */ type IndexSbtcWithdrawal = { cursor: string; request_id: number; status: SbtcWithdrawalStatus; amount: string | null; sender: string | null; recipient_btc_version: number | null; recipient_btc_hashbytes: string | null; sweep_txid: string | null; /** BTC L1 settlement of the committed sweep: true once confirmed, false while * pending, null when there is no sweep yet (REQUESTED). */ settlement_confirmed: boolean | null; /** BTC confirmations on the sweep tx, null when no settlement row yet. */ btc_confirmations: number | null; /** Confirming BTC block height, null while unconfirmed / no sweep. */ btc_block_height: number | null; /** When the sweep crossed the confirmation threshold (ISO), null otherwise. */ confirmed_at: string | null; requested_at?: string | null; resolved_at?: string | null; }; /** One phase of a withdrawal's lifecycle (the on-Stacks event that drove it). */ type IndexSbtcWithdrawalPhase = { block_height: number; block_time: string | null; tx_id: string; }; /** A single peg-out's full assembled lifecycle, fetched by `request_id`. * `finalized` is true once terminal and past the reorg margin. */ type IndexSbtcWithdrawalDetail = { request_id: number; status: SbtcWithdrawalStatus; amount: string | null; sender: string | null; recipient_btc_version: number | null; recipient_btc_hashbytes: string | null; requested: IndexSbtcWithdrawalPhase; accepted: (IndexSbtcWithdrawalPhase & { sweep_txid: string | null; signer_bitmap: string | null; }) | null; rejected: IndexSbtcWithdrawalPhase | null; settlement: { sweep_txid: string | null; btc_confirmations: number | null; settlement_confirmed: boolean | null; /** Confirming Bitcoin block height; null until the sweep confirms. */ btc_block_height: number | null; /** ISO timestamp the sweep first crossed the confirmation threshold. */ confirmed_at: string | null; }; finalized: boolean; }; /** A raw decoded sBTC protocol-state event — the full `sbtc_events` row, one * per event across all topics. */ type IndexSbtcEvent = { cursor: string; block_height: number; block_time?: string | null; tx_id: string; tx_index: number; event_index: number; topic: SbtcEventTopic; request_id: number | null; amount: string | null; sender: string | null; recipient_btc_version: number | null; recipient_btc_hashbytes: string | null; bitcoin_txid: string | null; output_index: number | null; sweep_txid: string | null; burn_hash: string | null; burn_height: number | null; signer_bitmap: string | null; max_fee: string | null; fee: string | null; governance_contract_type: number | null; governance_new_contract: string | null; signer_aggregate_pubkey: string | null; signer_threshold: number | null; signer_address: string | null; signer_keys_count: number | null; }; /** The peg "scoreboard" — a single all-time canonical aggregate. */ type IndexSbtcSummary = { total_deposits: number; total_withdrawals_requested: number; total_withdrawals_accepted: number; total_withdrawals_rejected: number; net_peg_flow_sats: string; total_locked_sats: string; sbtc_supply_sats: string | null; }; type SbtcDepositsEnvelope = { deposits: TRow[]; next_cursor: string | null; tip: IndexTip; reorgs: IndexReorg[]; /** Present only when the sBTC decoder is disabled, explaining an empty feed. */ notes?: string; }; type SbtcDepositEnvelope = { deposit: IndexSbtcDepositDetail; tip: IndexTip; }; type SbtcWithdrawalsEnvelope = { withdrawals: TRow[]; next_cursor: string | null; tip: IndexTip; reorgs: IndexReorg[]; notes?: string; }; type SbtcWithdrawalEnvelope = { withdrawal: IndexSbtcWithdrawalDetail; tip: IndexTip; }; type SbtcEventsEnvelope = { events: IndexSbtcEvent[]; next_cursor: string | null; tip: IndexTip; reorgs: IndexReorg[]; notes?: string; }; type SbtcSummaryEnvelope = { summary: IndexSbtcSummary; tip: IndexTip; notes?: string; }; type SbtcDepositsListParams = { cursor?: string | null; fromCursor?: string | null; limit?: number; /** Clamp to the finality boundary — only settled deposits past the reorg margin. */ confirmed?: boolean; sender?: string; bitcoinTxid?: string; fromHeight?: number; toHeight?: number; /** * Columns to return. The server projects the row, so an unrequested column * is physically absent — and the narrowing overload on `list` makes reading * one a compile error rather than `undefined` at runtime. */ fields?: readonly (keyof IndexSbtcDeposit & string)[]; }; /** Deposit columns that survive any projection — pagination needs them. */ type SbtcDepositAlwaysFields = "cursor" | "block_height"; /** A deposit row narrowed to the requested columns plus the always-present ones. */ type SbtcDepositFields = Pick; type SbtcDepositsWalkParams = Omit & WalkOptions; /** SbtcWithdrawal columns that survive any projection. */ type SbtcWithdrawalAlwaysFields = "cursor" | "request_id"; /** A SbtcWithdrawal row narrowed to the requested columns plus the always-present ones. */ type SbtcWithdrawalFields = Pick; type SbtcWithdrawalsListParams = { /** * Columns to return. The server projects the row, so an unrequested column * is physically absent — and the narrowing overload on `list` makes reading * one a compile error rather than `undefined` at runtime. */ fields?: readonly (keyof IndexSbtcWithdrawal & string)[]; cursor?: string | null; fromCursor?: string | null; limit?: number; confirmed?: boolean; status?: SbtcWithdrawalStatus; sender?: string; requestId?: number; /** Filter by BTC L1 settlement: true → only confirmed sweeps; false → not * yet confirmed (pending or no sweep). */ settlementConfirmed?: boolean; fromHeight?: number; toHeight?: number; }; type SbtcWithdrawalsWalkParams = Omit & WalkOptions; type SbtcEventsListParams = { cursor?: string | null; fromCursor?: string | null; limit?: number; confirmed?: boolean; topic?: SbtcEventTopic; sender?: string; requestId?: number; bitcoinTxid?: string; fromHeight?: number; toHeight?: number; }; type SbtcEventsWalkParams = Omit & WalkOptions; type SbtcEventsConsumeParams = Omit & IndexConsumeOptions; type SbtcDepositsConsumeParams = Omit & IndexConsumeOptions; /** `index.sbtc` — the decoded sBTC peg surface (deposits, withdrawals, raw * events, scoreboard). The only productized decoded sBTC peg feed on Stacks. */ interface SbtcResource { deposits: { /** Narrowing overload: `fields` shrinks the row type to exactly the * requested columns plus {@link SbtcDepositAlwaysFields}. */ list(params: SbtcDepositsListParams & { fields: readonly F[]; }): Promise>>; list(params?: SbtcDepositsListParams): Promise; walk(params?: SbtcDepositsWalkParams): AsyncIterable; /** Checkpointed sweep of completed deposits — append-only, so safe to * mirror. See {@link IndexConsumeOptions}. */ consume(params: SbtcDepositsConsumeParams & { sink?: ConsumerSink; }): Promise<{ cursor: string | null; pages: number; emptyPolls: number; }>; /** Fetch a completed deposit by Bitcoin txid; 404 → null. */ get(bitcoinTxid: string): Promise; }; withdrawals: { /** Narrowing overload: `fields` shrinks the row type to exactly the * requested columns plus {@link SbtcWithdrawalAlwaysFields}. */ list(params: SbtcWithdrawalsListParams & { fields: readonly F[]; }): Promise>>; list(params?: SbtcWithdrawalsListParams): Promise; walk(params?: SbtcWithdrawalsWalkParams): AsyncIterable; /** Fetch a withdrawal's full lifecycle by request id; 404 → null. */ get(requestId: number): Promise; }; events: { list(params?: SbtcEventsListParams): Promise; walk(params?: SbtcEventsWalkParams): AsyncIterable; /** Checkpointed sweep of the raw peg event log — append-only across all * six topics. See {@link IndexConsumeOptions}. */ consume(params: SbtcEventsConsumeParams & { sink?: ConsumerSink; }): Promise<{ cursor: string | null; pages: number; emptyPolls: number; }>; }; /** The peg scoreboard — a single all-time aggregate (no pagination). */ summary(): Promise; } /** Per-function action count within a reward cycle. */ type IndexPoxFunctionCount = { function_name: string; count: number; }; /** Aggregate stats for one PoX reward cycle — distinct from `index.stacking` * (decoded per-call PoX-4 actions); this is the reward-cycle rollup. */ type IndexPoxCycle = { reward_cycle: number; /** Total ustx locked across all stack-* calls in this cycle (bigint-safe string). */ total_stacked_ustx: string; unique_stackers: number; unique_delegators: number; action_count: number; start_block_height: number; end_block_height: number; /** True when this is the latest reward cycle (still accumulating new actions). */ is_current: boolean; function_breakdown: IndexPoxFunctionCount[]; }; type PoxCyclesEnvelope = { cycles: IndexPoxCycle[]; /** The next `reward_cycle` to page from (cycles descend), or null at the end. */ next_cursor: number | null; tip: IndexTip; /** Present only when the PoX-4 decoder is disabled, explaining an empty feed. */ notes?: string; }; type PoxCycleEnvelope = { cycle: IndexPoxCycle; tip: IndexTip; notes?: string; }; type PoxCyclesListParams = { /** A `reward_cycle` to page from; the page returns cycles below it (descending). */ cursor?: number | null; limit?: number; }; type PoxCyclesWalkParams = Omit & WalkOptions; /** `index.pox` — PoX reward-cycle aggregates. */ interface PoxResource { cycles: { list(params?: PoxCyclesListParams): Promise; walk(params?: PoxCyclesWalkParams): AsyncIterable; /** Fetch one reward cycle's aggregate by number; 404 → null. */ get(rewardCycle: number): Promise; }; } /** The decoded `pox-5` boot-contract print topics (SIP-045 Bitcoin Staking) — * one source of truth in `@secondlayer/stacks/pox5` (already a dependency; * the old "SDK owns its own dependency surface" rationale predated that). */ type IndexPox5EventTopic = Pox5EventTopic; /** A raw decoded PoX-5 print event. Promoted fields cover the hot query paths; * `data` always carries the full decoded tuple, including nested shapes * (`btc-lockup`, `bond-rewards`, `bond-periods`) that have no flat column. */ type IndexPox5Event = { cursor: string; block_height: number; block_time: string | null; tx_id: string; tx_index: number; event_index: number; topic: IndexPox5EventTopic; staker: string | null; signer: string | null; signer_manager: string | null; bond_index: number | null; /** ustx, bigint-safe string. */ amount_ustx: string | null; /** sats, bigint-safe string. */ amount_sats: string | null; reward_cycle: number | null; first_reward_cycle: number | null; unlock_cycle: number | null; unlock_burn_height: number | null; is_l1_lock: boolean | null; signer_key: string | null; /** The full decoded print tuple as JSON. */ data: unknown; }; type Pox5EventsEnvelope = { events: TRow[]; next_cursor: string | null; tip: IndexTip; reorgs: IndexReorg[]; /** Present only when the PoX-5 decoder is disabled, explaining an empty feed. */ notes?: string; }; /** Pox5Event columns that survive any projection. */ type Pox5EventAlwaysFields = "cursor" | "block_height" | "topic"; /** A Pox5Event row narrowed to the requested columns plus the always-present ones. */ type Pox5EventFields = Pick; type Pox5EventsListParams = { /** * Columns to return. The server projects the row, so an unrequested column * is physically absent — and the narrowing overload on `list` makes reading * one a compile error rather than `undefined` at runtime. */ fields?: readonly (keyof IndexPox5Event & string)[]; cursor?: string | null; fromCursor?: string | null; limit?: number; /** Clamp to the finality boundary — only rows past the reorg margin. */ confirmed?: boolean; topic?: IndexPox5EventTopic; staker?: string; signer?: string; signerManager?: string; bondIndex?: number; rewardCycle?: number; fromHeight?: number; toHeight?: number; }; type Pox5EventsWalkParams = Omit & WalkOptions; /** `index.pox5` — decoded PoX-5 print events, the staking primitive from the * epoch 4.0 hard fork onward (PoX-4's `index.stacking` stream ends there). */ interface Pox5Resource { events: { /** Narrowing overload: `fields` shrinks the row type to exactly the * requested columns plus {@link Pox5EventAlwaysFields}. */ list(params: Pox5EventsListParams & { fields: readonly F[]; }): Promise>>; list(params?: Pox5EventsListParams): Promise; walk(params?: Pox5EventsWalkParams): AsyncIterable; }; } /** * `index.ftTransfers` — callable shorthand for `.list()`, with `.list`/`.walk` * still available: `await sl.index.ftTransfers({ contractId })`. * * The API accepts `contract_id`/`sender`/`recipient` equality filters only — * no amount filtering and no asset-slug resolution on /v1/index/ft-transfers. */ interface FtTransfersResource { /** Narrowing overload for the callable shorthand — same wire path as * `list`, so it must narrow identically. */ (params: FtTransfersListParams & { fields: readonly F[]; }): Promise>>; (params?: FtTransfersListParams): Promise; /** Narrowing overload: `fields` shrinks the row type to exactly the * requested columns plus {@link FtTransferAlwaysFields}. */ list(params: FtTransfersListParams & { fields: readonly F[]; }): Promise>>; list(params?: FtTransfersListParams): Promise; /** Narrowing overload, matching `list`: `walk` forwards `fields` to the * wire, so without this the yielded rows are stripped while the type * still promises every column. */ walk(params: FtTransfersWalkParams & { fields: readonly F[]; }): AsyncIterable>; walk(params?: FtTransfersWalkParams): AsyncIterable; } /** `index.nftTransfers` — callable shorthand for `.list()` (see {@link FtTransfersResource}). */ interface NftTransfersResource { /** Narrowing overload for the callable shorthand — same wire path as * `list`, so it must narrow identically. */ (params: NftTransfersListParams & { fields: readonly F[]; }): Promise>>; (params?: NftTransfersListParams): Promise; /** Narrowing overload: `fields` shrinks the row type to exactly the * requested columns plus {@link NftTransferAlwaysFields}. */ list(params: NftTransfersListParams & { fields: readonly F[]; }): Promise>>; list(params?: NftTransfersListParams): Promise; /** Narrowing overload, matching `list` (see {@link FtTransfersResource}). */ walk(params: NftTransfersWalkParams & { fields: readonly F[]; }): AsyncIterable>; walk(params?: NftTransfersWalkParams): AsyncIterable; } /** `index.events` — callable shorthand for `.list()`; `eventType` is required. */ interface IndexEventsResource { /** Narrowing overload for the callable shorthand — same wire path as * `list`, so it must narrow identically. */ < T extends IndexEventType, const F extends keyof IndexEventOf & string >(params: EventsListParams & { fields: readonly F[]; }): Promise>>; (params: EventsListParams): Promise>; /** Narrowing overload: `fields` shrinks the returned row type to exactly * the requested columns plus {@link IndexAlwaysFields}. `const F` means no * `as const` at the call site. */ list< T extends IndexEventType, const F extends keyof IndexEventOf & string >(params: EventsListParams & { fields: readonly F[]; }): Promise>>; list(params: EventsListParams): Promise>; /** Narrowing overload, matching `list`: `walk` forwards `fields` to the * wire, so without this the yielded rows are stripped while the type * still promises every column. */ walk< T extends IndexEventType, const F extends keyof IndexEventOf & string >(params: EventsWalkParams & { fields: readonly F[]; }): AsyncIterable>; walk(params: EventsWalkParams): AsyncIterable>; consume< T extends IndexEventType, TTx = never >(params: EventsConsumeParams & { sink?: ConsumerSink; }): Promise<{ cursor: string | null; pages: number; emptyPolls: number; }>; } /** Per-event-type filter vocabulary in the {@link IndexDiscovery} doc. */ type IndexEventTypeFilters = { columns?: string[]; allowed_filters?: string[]; equality_filters?: string[]; required_non_null?: string[]; }; /** The `GET /v1/index` discovery doc — live endpoint + filter vocabulary. * Shape is intentionally open (the server may add fields); the agent-relevant * parts are the per-type filter rules. */ type IndexDiscovery = { event_type_filters?: Record; [key: string]: unknown; }; declare class Index extends BaseClient { constructor(options?: Partial); /** * Index discovery doc — the live vocabulary: every endpoint, each event type's * columns, allowed/equality filters, and required-non-null fields. Read this to * learn what's queryable (and which types accept `trait`) instead of hardcoding. */ discover(): Promise; /** * Empirical per-topic print payload schema for a contract — what topics it * emits and each field's observed Clarity/TS/column types. Anonymous read; * 404 → null. */ printSchema(contractId: string): Promise; /** Callable: `index.ftTransfers(params)` ≡ `index.ftTransfers.list(params)`. */ readonly ftTransfers: FtTransfersResource; /** Callable: `index.nftTransfers(params)` ≡ `index.nftTransfers.list(params)`. */ readonly nftTransfers: NftTransfersResource; /** Generic decoded events by `event_type` (the full /v1/index/events surface). * Callable: `index.events(params)` ≡ `index.events.list(params)`. */ readonly events: IndexEventsResource; readonly contractCalls: { list: (params?: ContractCallsListParams) => Promise; walk: (params?: ContractCallsWalkParams) => AsyncIterable; consume: (params: ContractCallsConsumeParams & { sink?: ConsumerSink; }) => Promise<{ cursor: string | null; pages: number; emptyPolls: number; }>; }; /** Canonical block-hash map — sync only the current canonical chain. */ readonly canonical: { list: (params?: CanonicalListParams) => Promise; walk: (params?: CanonicalWalkParams) => AsyncIterable; }; /** Canonical blocks: paginated `list`/`walk`, plus `get` by height or hash * (resolves to null on 404). */ readonly blocks: { list: (params?: BlocksListParams) => Promise; walk: (params?: BlocksWalkParams) => AsyncIterable; get: (ref: string | number) => Promise; }; /** Full transaction documents: paginated `list`/`walk`, plus `get` by tx_id * (resolves to null on 404). */ readonly transactions: { /** Narrowing overload: `fields` shrinks the row type to exactly the * requested columns plus {@link TransactionAlwaysFields}. Declared as * an overloaded call signature because an arrow property cannot carry * overloads. */ list: { (params: TransactionsListParams & { fields: readonly F[]; }): Promise>>; (params?: TransactionsListParams): Promise; }; walk: (params?: TransactionsWalkParams) => AsyncIterable; get: (txId: string) => Promise; getProof: (txId: string) => Promise; }; /** Decoded PoX-4 stacking actions. Empty (with a `notes` hint) when the * platform's PoX-4 decoder is disabled. */ readonly stacking: { list: (params?: StackingListParams) => Promise; walk: (params?: StackingWalkParams) => AsyncIterable; }; /** Pending (unconfirmed) transactions: paginated `list`/`walk`, plus `get` by * tx_id (resolves to null when the tx has confirmed or dropped). */ readonly mempool: { list: (params?: MempoolListParams) => Promise; walk: (params?: MempoolWalkParams) => AsyncIterable; get: (txId: string) => Promise; }; /** Decoded sBTC peg surface — the only productized decoded sBTC peg feed on * Stacks. `deposits`/`withdrawals`/`events` paginate; `summary` is a scalar. */ readonly sbtc: SbtcResource; /** PoX reward-cycle aggregates. Distinct from `stacking` (per-call PoX-4 * actions); `cycles` is the reward-cycle rollup. */ readonly pox: PoxResource; /** Decoded PoX-5 print events (SIP-045 Bitcoin Staking) — the raw pox-5 * boot-contract log, all 19 topics. Distinct from `stacking` (PoX-4 era). */ readonly pox5: Pox5Resource; private listFtTransfers; private listNftTransfers; /** Shared keyset-pagination loop for every `walk*` feed: seed the cursor from * `cursor`/`fromCursor` (and `fromHeight` 0 on the first page), then page until * the server stops advancing the cursor (`next_cursor` null or unchanged). A * short page is not a tail signal: the server clamps oversized limits without * saying so, so trusting page length truncated backfills. The cost is one * extra empty fetch at the end of feeds that never return a null cursor. * `list` merges the resource's filter params; `itemsOf` selects the envelope's * item array. Each page fetch retries under `fetchPageWithRetry`; the cursor * is a string keyset for every feed except PoX cycles, which use a number. */ private keysetWalk; private walkFtTransfers; private walkNftTransfers; private listEvents; private walkEvents; private listContractCalls; private walkContractCalls; private listCanonical; private walkCanonical; private listBlocks; private getBlock; private walkBlocks; private listTransactions; private getTransaction; /** Fetch the inclusion proof for a tx (raw tx + Nakamoto header + merkle path) * to verify client-side with `verifyTransactionProof`. 404 → null. A 503 * (`PROOF_TX_SET_INCOMPLETE` / `PROOF_NODE_UNAVAILABLE`) surfaces as an * ApiError — the proof can't be assembled on this deployment right now. */ private getTransactionProof; private walkTransactions; private listStacking; private walkStacking; private listMempool; private getMempoolTx; private walkMempool; private listSbtcDeposits; private walkSbtcDeposits; private getSbtcDeposit; private listSbtcWithdrawals; private walkSbtcWithdrawals; private getSbtcWithdrawal; private listSbtcEvents; private walkSbtcEvents; private getSbtcSummary; private listPox5Events; private walkPox5Events; private listPoxCycles; private walkPoxCycles; private getPoxCycle; } import { CreateWebhookRequest, CreateWebhookResponse, DeadRow, DeliveryRow, ReplayResult, RotateSecretResponse, UpdateWebhookRequest, WebhookDetail, WebhookSummary, WebhookTestResult } from "@secondlayer/shared/schemas/webhooks"; import { ChainTrigger, ChainTriggerType, CreateWebhookRequest as CreateWebhookRequest2, CreateWebhookResponse as CreateWebhookResponse2, DeadRow as DeadRow2, DeliveryRow as DeliveryRow2, ReplayResult as ReplayResult2, RotateSecretResponse as RotateSecretResponse2, WebhookDetail as WebhookDetail2, WebhookFormat, WebhookKind, WebhookRuntime, WebhookStatus, WebhookSummary as WebhookSummary2, WebhookTestResult as WebhookTestResult2, UpdateWebhookRequest as UpdateWebhookRequest2 } from "@secondlayer/shared/schemas/webhooks"; import { trigger } from "@secondlayer/shared/schemas/webhooks"; declare class Webhooks extends BaseClient { list(): Promise<{ data: WebhookSummary[]; }>; get(id: string): Promise; create(input: CreateWebhookRequest): Promise; update(id: string, patch: UpdateWebhookRequest): Promise; pause(id: string): Promise; resume(id: string): Promise; delete(id: string): Promise<{ ok: true; }>; rotateSecret(id: string): Promise; /** Send a one-off test webhook to the webhook's URL (built for its * format, SSRF-guarded). Logged as a delivery row, visible via deliveries. */ test(id: string): Promise; /** The last 100 delivery attempts, newest first. The server caps the window; * there is nothing to page. */ deliveries(id: string): Promise<{ data: DeliveryRow[]; }>; replay(id: string, range: { fromBlock: number; toBlock: number; force?: string; }): Promise; dead(id: string): Promise<{ data: DeadRow[]; }>; /** Push one dead-lettered event back onto the delivery queue. `outboxId` is * the `id` of a row from {@link dead}. */ requeue(id: string, outboxId: string): Promise<{ ok: true; }>; } interface ContextAccount { email: string; } interface ActiveSubgraphOperation { subgraph: string; operationId: string; kind: SubgraphOperationStatus["kind"]; status: SubgraphOperationStatus["status"]; progress: number | null; } /** Why one snapshot field could not be read. Serialized from the SDK error * family so a snapshot stays plain data. */ interface ContextFieldError { message: string; /** Stable code from the API envelope (`UNAUTHORIZED`, `INDEX_NOT_READY`) * or the SDK (`REQUEST_TIMEOUT`); absent for a bare transport failure. */ code?: string; /** HTTP status; `0` when the API could not be reached. */ status?: number; /** Whether the same read can succeed on a retry (429, 5xx, network). */ retryable: boolean; } /** One snapshot field: the value, or `null` plus the error that produced it. * `value: null` with no `error` means the read succeeded and found nothing. */ interface ContextField { value: T | null; error?: ContextFieldError; } /** * A point-in-time orientation snapshot for an agent: the live tips and what * this instance holds. Every field is a {@link ContextField}, so a missing * value says why it is missing (API unreachable, token rejected, index still * catching up) instead of a bare `null`. `context()` itself never throws. */ interface ContextSnapshot { /** Token identity from `/api/accounts/me`. A self-hosted instance has no * account system: the read 404s and the field carries that error. */ account: ContextField; streamsTip: ContextField; indexTip: ContextField; subgraphs: ContextField; webhooks: ContextField<{ count: number; byStatus: Record; }>; /** In-flight reindex operations (bounded to subgraphs reporting `reindexing`). */ activeOperations: ContextField; /** Decoder lag / empty-index from `GET /public/status`. */ instance: ContextField; } /** Fold one read into a {@link ContextField}: the value, or `null` with the * failure described. Exported for callers that assemble their own snapshot * from extra reads and want the same shape. */ declare function contextField(read: Promise): Promise>; declare class SecondLayer extends BaseClient { readonly streams: StreamsClient; readonly index: Index; readonly contracts: Contracts; readonly subgraphs: Subgraphs; readonly webhooks: Webhooks; readonly archive: SecondLayerArchive; readonly instance: InstanceClient; constructor(options?: Partial); /** * Up to 10 public reads in one round trip (`POST /v1/batch`). Each item * is authorized on its own; the client's bearer token applies to every item. */ batch(requests: Array<{ path: string; params?: Record; }>): Promise<{ results: Array<{ path: string | null; status: number; body: unknown; }>; }>; /** * Assemble a {@link ContextSnapshot}: the same orientation an MCP agent * reads from `secondlayer://context`, available to any SDK/CLI consumer. * Reads run concurrently; a failed read lands as `{ value: null, error }` * on its field rather than rejecting the whole snapshot. */ context(): Promise; } /** * Returns a typed client for a subgraph defined with `defineSubgraph()`. * * Accepts a plain options object, a `SecondLayer` instance, or a `Subgraphs` instance. * * @example * ```ts * import mySubgraph from './subgraphs/my-subgraph' * import { getSubgraph } from '@secondlayer/sdk' * * const client = getSubgraph(mySubgraph, { apiKey: 'sl_...' }) * const rows = await client.transfers.findMany({ where: { sender: 'SP...' } }) * ``` */ declare function getSubgraph; }>(def: T, options?: Partial | SecondLayer | Subgraphs): InferSubgraphClient2; type CreateStreamsClientOptions = { apiKey?: string; baseUrl?: string; /** Deploy origin label sent as `x-sl-origin` (telemetry). Defaults to `cli`. */ origin?: "cli" | "mcp" | "session"; fetchImpl?: FetchLike2; /** * Public base URL for bulk parquet dumps (the R2/CDN bucket root). Required * to use `client.dumps`. See `GET /public/streams/dumps/manifest`. */ dumpsBaseUrl?: string; /** * Verify the ed25519 `X-Signature` on every REST response and per-frame SSE * signature. Three states: * - **default (omitted)** — *lenient*: verify when the server signs (the * hosted API signs every response), and pass through when no signature is * present (e.g. a self-hosted instance with no `STREAMS_SIGNING_PRIVATE_KEY`). * So verification is on by default against the hosted API without breaking * unsigned self-host deployments. An *invalid* signature always throws. * - **`true`** (or `{ publicKey }` to pin a known PEM) — *strict*: a missing * OR invalid signature throws `StreamsSignatureError`. Use this when you * require a portable, non-repudiable attestation and won't accept unsigned * data (it also closes the lenient mode's strip-the-header downgrade). * - **`false`** — off. * * The key is fetched once from `/public/streams/signing-key` (cached; a * rotated `X-Signature-KeyId` triggers one refresh) unless a PEM is pinned. */ verify?: boolean | { publicKey: string; }; /** * Verify the bulk dumps manifest's ed25519 signature in `client.dumps.list()` * before trusting any file sha256 (default ON). Uses the same key source as * `verify` (fetches `/public/streams/signing-key`, or a pinned PEM). Pass * `false` to opt out. A missing or invalid signature throws * `StreamsSignatureError`. */ verifyDumpsManifest?: boolean; }; declare function createStreamsClient(options: CreateStreamsClientOptions): StreamsClient; /** * Decode a raw Streams event into the SAME flat, `event_type`-discriminated * row shape Index serves — so Streams consumption reads identically to Index * consumption, with one call instead of eleven guard+decode pairs. * * ```ts * for (const event of envelope.events) { * const row = decode(event); * if (row.event_type === "ft_transfer") row.amount; // string, narrowed * } * ``` * * Prefer `decoded: true` on `streams.events.consume` — then decoding never * appears in your code at all. */ declare function decode(event: StreamsEvent): IndexEvent; /** Options accepted by every error in the family. */ interface SecondLayerErrorOptions { cause?: Error; details?: string; /** Stable machine-readable code (e.g. `UPGRADE_REQUIRED`). */ code?: string; /** Docs page that explains this failure and its fix. */ docsUrl?: string; /** Extra context lines appended to the message (DDL to run, next steps). */ metaMessages?: string[]; /** Whether retrying the SAME request can succeed (429/5xx/network). */ retryable?: boolean; /** Parsed `Retry-After`, in seconds — set on rate limits that carry it. */ retryAfterSeconds?: number; } /** * Root of the SDK error family. Implements the same protocol as `BaseError` * in `@secondlayer/stacks` (`shortMessage`, `cause`, `toJSON`) so a failed * chain read and a failed platform read serialize and introspect identically. * It intentionally does NOT extend that class: cross-package `instanceof` is * unreliable under per-package bundling anyway (match on `code`/`name` across * packages). * * `retryable` is the signal the consume loops act on: 429/5xx/network are * retryable, 4xx and body-serialization failures are not. */ declare class SecondLayerError extends Error { name: string; /** The one-line failure, without the appended context blocks. */ shortMessage: string; code?: string; docsUrl?: string; metaMessages?: string[]; retryable: boolean; retryAfterSeconds?: number; constructor(shortMessage: string, options?: SecondLayerErrorOptions); /** * Walk the cause chain. With a predicate, returns the first error matching * it (or `null`); without one, returns the deepest cause. Mirrors viem's * `error.walk()` so cross-library error handling reads the same. */ walk(fn?: (err: unknown) => boolean): unknown; toJSON(): { name: string; message: string; shortMessage: string; cause: string | undefined; code: string | undefined; docsUrl: string | undefined; retryable: boolean; retryAfterSeconds: number | undefined; }; } /** * Error thrown by {@link SecondLayer} when an API request fails. * Includes the HTTP status code for programmatic error handling. * * @example * ```ts * try { * await client.subgraphs.status("my-subgraph"); * } catch (err) { * if (err instanceof ApiError && err.status === 404) { * console.log("Subgraph not found"); * } * } * ``` */ declare class ApiError extends SecondLayerError { /** HTTP status code (0 for network errors). */ status: number; /** Raw response body (parsed JSON if possible) — preserved for callers that need error details. */ body?: unknown; constructor(status: number, message: string, body?: unknown, code?: string, options?: SecondLayerErrorOptions); } /** Thrown on a 401 by both the instance clients and Streams. Carries the * server's `{error, code}` envelope when it sent one, so a revoked token reads * differently from a missing one. */ declare class AuthError extends ApiError { readonly status: 401; constructor(message?: string, body?: unknown, code?: string); } /** Thrown on a 429 by both the instance clients and Streams. `retryable`, * with `retryAfterSeconds` parsed from the `Retry-After` header when sent. */ declare class RateLimitError extends ApiError { /** Raw `Retry-After` header value (seconds or HTTP-date). */ readonly retryAfter?: string | undefined; readonly status: 429; constructor(message?: string, retryAfter?: string | undefined, body?: unknown, code?: string); } /** Thrown on a 4xx the caller can fix (bad cursor, bad params). Never retried. * `code` is the server's machine-readable code when the envelope carried one. */ declare class ValidationError extends ApiError { constructor(message: string, status: number, body?: unknown, code?: string); } /** Thrown when an archive manifest signature is missing or does not verify. */ declare class ArchiveSignatureError extends SecondLayerError { constructor(message?: string); } /** Thrown on a 401 from the archive ops host (quote/fetch/credits). */ declare class ArchiveAuthError extends SecondLayerError { constructor(message?: string); } /** Thrown on a 402 from archive fetch — prepaid credits do not cover the quote. */ declare class InsufficientArchiveCreditsError extends SecondLayerError { shortfallUsdMicros?: number; constructor(message?: string, shortfallUsdMicros?: number); } /** Thrown on a 503 when the archive fetch gate is unconfigured. */ declare class ArchiveGateNotConfiguredError extends SecondLayerError { constructor(message?: string); } /** Parse a `Retry-After` header: delta-seconds or an HTTP-date. */ declare function parseRetryAfter(value?: string | null): number | undefined; /** Thrown on a 5xx from the Streams API, and on a failed signing-key fetch. * `retryable`: the page retry policy tries again. */ declare class StreamsServerError extends ApiError { constructor(message: string, status: number, body?: unknown, code?: string); } /** Thrown when response signature verification is enabled and fails. */ declare class StreamsSignatureError extends SecondLayerError { constructor(message?: string); } import { decodeFtTransfer, isFtTransfer, decodeNftTransfer, isNftTransfer, decodeStxBurn, decodeStxLock, decodeStxMint, decodeStxTransfer, isStxBurn, isStxLock, isStxMint, isStxTransfer, decodeFtBurn, decodeFtMint, decodeNftBurn, decodeNftMint, isFtBurn, isFtMint, isNftBurn, isNftMint, decodePrint, isPrint } from "@secondlayer/shared/streams-rows"; import { DecodedEventColumns, DecodedEventRow, DecodedFtBurn, DecodedFtBurnPayload, DecodedFtMint, DecodedFtMintPayload, DecodedFtTransfer, DecodedFtTransferPayload, DecodedNftBurn, DecodedNftBurnPayload, DecodedNftMint, DecodedNftMintPayload, DecodedNftTransfer, DecodedNftTransferPayload, DecodedPrint, DecodedPrintPayload, DecodedPrintValue, DecodedStxBurn, DecodedStxBurnPayload, DecodedStxLock, DecodedStxLockPayload, DecodedStxMint, DecodedStxMintPayload, DecodedStxTransfer, DecodedStxTransferPayload, FtTransferEvent, FtTransferPayload as FtTransferPayload2, NftTransferEvent, NftTransferPayload as NftTransferPayload2 } from "@secondlayer/shared/streams-rows"; /** * Helpers for Streams cursors. A cursor is the opaque `:` string * that marks a position in the event stream; treat the format as an * implementation detail and go through these helpers instead of string-building * it at call sites. Encode/decode and the rewind sentinel come from the * canonical codec in `@secondlayer/shared` so the SDK cannot accept a spelling * the server would 400, or rewind to a different foot than Index/Streams. */ declare const Cursor: { /** * Cursor at the foot of `height` — a position that sorts strictly below the * first event of block `height` (`height:0`) and strictly above every event * of block `height - 1`. Cursors are exclusive (`(bh,ei) > after`), so * resuming from it re-reads the entire canonical run starting at `height:0` * inclusive. This is the position to rewind to after a reorg whose fork point * is `height`: the new canonical block at `height` carries a fresh first * event at `(height, 0)` that the consumer MUST re-read. * * Encoded as `blockEndCursor(height - 1)` rather than the seemingly-natural * `${height}:0` — that earlier form was an off-by-one: being exclusive, it * skipped `(height, 0)`, silently dropping the fork block's first row on * every reorg. The sentinel is int4 max (the `event_index`/`tx_index` column * type), larger than any real index, so nothing at `height - 1` survives the * keyset and the next returned row is exactly `(height, 0)`. * * `atHeight(0)` is `null`: the pre-genesis position both consume loops * accept as "read everything from the first event". There is no string * cursor below `0:0`, and `0:0` itself is exclusive, so returning it here * skipped the genesis event. */ atHeight(height: number): string | null; /** Parse a `:` cursor. Throws `ValidationError` if malformed. */ parse(cursor: string): { blockHeight: number; eventIndex: number; }; }; import { InstanceDiagnosis as InstanceDiagnosis3, InstanceIssue, InstanceState, PublicStatus as PublicStatus2 } from "@secondlayer/shared/archive/instance-diagnosis"; import { SubgraphAgentSchema as SubgraphAgentSchema3, SubgraphSpecFormat as SubgraphSpecFormat2, SubgraphSpecOptions as SubgraphSpecOptions3 } from "@secondlayer/shared/subgraphs/spec"; /** * The two pieces every "deploy a consumer" guide leaves out, packaged: * a liveness endpoint and a graceful-shutdown signal. Both are * runtime-agnostic — the SDK never binds a port or assumes Bun/Node. */ interface ConsumerHealth { /** Feed this to the consume loop's `onProgress` — it stamps arrival time * and keeps the latest progress ctx for the health body. */ record: (ctx: ConsumerBatchContext) => void; /** Fetch-style handler for `GET /health`. Mount it on whatever serves * HTTP in your runtime: * `Bun.serve({ port, fetch: health.handler })` · * `http.createServer(...)` via an adapter · a Workers route. */ handler: (req: Request) => Response; } /** * Liveness for a consume loop. The gate is "did a page land recently", NOT * "are we near the tip": during a genesis backfill the loop is millions of * blocks behind and perfectly healthy, so lag can't be the signal. A wedged * loop stops reporting pages and flips to 503 — that's what platforms * should restart on. The body carries position for dashboards: * `blocks_behind` is the actual backlog (`tip - scanned_height`, ~0 for a * caught-up tail even on a quiet contract), while `last_delivered_height` * is the last DELIVERED row — its distance from the tip is event age, not * lag. */ declare function consumerHealth(options?: { staleAfterMs?: number; }): ConsumerHealth; /** * An `AbortSignal` wired to SIGTERM/SIGINT (redeploys arrive as SIGTERM). * Pass it as the consume loop's `signal`: aborting is checked at the top of * the loop, never mid-batch, so the in-flight transaction — rows AND * checkpoint — always commits before the process exits. Nothing is * half-written; the next run resumes from exactly that cursor. * * No-op (never fires) on runtimes without `process` signal handling. */ declare function shutdownSignal(options?: { signals?: readonly ("SIGTERM" | "SIGINT")[]; }): AbortSignal; type CreateArchiveClientOptions = { /** @deprecated Prefer `accountKey`. Kept one release as an alias. */ apiKey?: string; accountKey?: string; fetchImpl?: FetchLike; archiveBaseUrl?: string; archiveOpsUrl?: string; verifyManifest?: boolean; publicKeyPem?: string; }; declare function createArchiveClient(options?: CreateArchiveClientOptions): ArchiveClient; import { ChainWebhookDelivery } from "@secondlayer/shared"; import { StandardWebhooksHeaders } from "@secondlayer/shared/crypto/standard-webhooks"; import { StandardWebhooksHeaders as StandardWebhooksHeaders2, verify } from "@secondlayer/shared/crypto/standard-webhooks"; import { ChainApplyDeliveryOf, ChainApplyEnvelope, ChainApplyEnvelopeOf, ChainEventEnvelope, ChainFtBurnData, ChainFtMintData, ChainFtTransferData, ChainNftBurnData, ChainNftMintData, ChainNftTransferData, ChainPrintEventData, ChainReorgOrphanedEntry, ChainReorgRollbackDelivery, ChainReorgRollbackEnvelope, ChainStxBurnData, ChainStxLockData, ChainStxMintData, ChainStxTransferData, ChainTestDelivery, ChainTxLevelEvent, ChainWebhookDelivery as ChainWebhookDelivery2, ChainWebhookEnvelope, SbtcDepositEvent, SbtcWithdrawalEvent, SbtcWithdrawalSweptConfirmedEvent } from "@secondlayer/shared"; type HeaderLookup = (name: string) => string | null | undefined; type WebhookHeaderInput = HeaderLookup | StandardWebhooksHeaders | Record | { get: (name: string) => string | null; }; /** * Verify a Secondlayer webhook delivery signature. * * Every delivery whose webhook `format` is `"standard-webhooks"` (the * default) carries three Standard Webhooks headers: * * webhook-id — UUID for the delivery (stable across retries; use as * your dedup key) * webhook-timestamp — unix seconds at dispatch time * webhook-signature — space-separated list of `v1,` tuples * * The signed content is `${id}.${timestamp}.${rawBody}` HMAC-SHA256 with the * signing secret. Secrets returned by `secondlayer webhooks create` (or * `rotate-secret`) are a bare 64-character hex string used directly as the * HMAC key (its UTF-8 bytes) — this helper handles that. A `whsec_`-prefixed * base64 secret (the Svix convention) is also accepted and base64-decoded after * the prefix is stripped. Note: because the issued secret is bare hex (no * `whsec_` prefix), a generic Svix / Standard Webhooks library will base64- * decode it and derive the wrong key — verify with this helper (or with * {@link verifySecondlayerSignature}, the format-agnostic ed25519 path). * * @param rawBody The raw request body as a string. NEVER pass * `JSON.stringify(req.body)` — re-stringifying drops * key ordering and whitespace, breaking the HMAC. * Use the raw body bytes/string your framework hands * you (Express raw body middleware, Hono `c.req.text()`, * Bun `await req.text()`, etc.). * @param headers The request headers. Accepts a plain object * (Express / Node), a Fetch `Headers` instance * (Bun / Hono / Workers), or a callback that returns * a header value by name. Header name matching is * case-insensitive. * @param secret The signing secret returned by * `secondlayer webhooks create` / `rotateSecret` (a bare * 64-char hex string). Pass it through verbatim — the * helper accepts both bare hex and `whsec_`-prefixed * base64 secrets. * @param toleranceSeconds Max age of `webhook-timestamp` in seconds. Default * 300 (5 min) per the Standard Webhooks spec. * @returns true if every header is present, the timestamp is within * tolerance, and a `v1` signature matches. * * @example * ```ts * // Hono / Bun * import { verifyWebhookSignature } from "@secondlayer/sdk"; * * app.post("/webhook", async (c) => { * const raw = await c.req.text(); * if (!verifyWebhookSignature(raw, c.req.raw.headers, process.env.SIGNING_SECRET!)) { * return c.text("Invalid signature", 401); * } * const { type, timestamp, data } = JSON.parse(raw); * // ... process data ... * return c.body(null, 204); * }); * ``` * * @example * ```ts * // Express with raw-body middleware * import express from "express"; * import { verifyWebhookSignature } from "@secondlayer/sdk"; * * app.post( * "/webhook", * express.raw({ type: "application/json" }), * (req, res) => { * const raw = req.body.toString("utf8"); * if (!verifyWebhookSignature(raw, req.headers, process.env.SIGNING_SECRET!)) { * return res.status(401).end(); * } * // ... process raw ... * res.status(204).end(); * }, * ); * ``` */ declare function verifyWebhookSignature(rawBody: string, headers: WebhookHeaderInput, secret: string, toleranceSeconds?: number): boolean; /** * Verify the universal Secondlayer authenticity signature that every delivery * carries, regardless of body format (`raw`, `cloudevents`, `standard-webhooks`, * …). This is the format-agnostic alternative to {@link verifyWebhookSignature}: * instead of a per-webhook HMAC secret, it checks an ed25519 signature over * `${webhook-id}.${rawBody}` against Secondlayer's published public key — so one * key proves authenticity for any format. * * @param rawBody The raw request body string (never re-stringify the parsed * JSON — whitespace/key-order changes break the signature). * @param headers Request headers — plain object, Fetch `Headers`, or a * lookup callback. Reads `webhook-id` + `x-secondlayer-signature`. * @param publicKeyPem Secondlayer's published ed25519 public key (SPKI PEM). * @returns true when the signature header is present and verifies. * * @example * ```ts * import { verifySecondlayerSignature } from "@secondlayer/sdk"; * * app.post("/webhook", async (c) => { * const raw = await c.req.text(); * if (!verifySecondlayerSignature(raw, c.req.raw.headers, SECONDLAYER_PUBLIC_KEY)) { * return c.text("Invalid signature", 401); * } * // ... process raw ... * return c.body(null, 204); * }); * ``` */ declare function verifySecondlayerSignature(rawBody: string, headers: WebhookHeaderInput, publicKeyPem: string): boolean; /** * Decode + narrow a chain webhook delivery body into a typed * {@link ChainWebhookDelivery}. Verify the signature first with * {@link verifyWebhookSignature} (or {@link verifySecondlayerSignature}), then * decode the same raw body — this does not check authenticity, only shape. * * Only understands the `format: "standard-webhooks"` envelope (`{ type, * timestamp, data }`) — the webhook default, and the only format * `verifyWebhookSignature` covers. Other formats (`raw`, `cloudevents`, …) * carry the same `data` value under a different envelope; see the "Chain * webhook payloads" doc for how to unwrap those. * * A chain webhook delivery is NOT a Streams/Index event — do not run this * over a `StreamsEvent` body (`{ event_type, payload }`) or vice versa. * * @param rawBody The raw request body string (same bytes passed to * {@link verifyWebhookSignature}). * @throws {Error} if the body isn't a `chain.*` delivery, or the envelope is * internally inconsistent (e.g. `type` and `data.trigger` disagree — * a sign the wire shape drifted from this decoder). * * @example * ```ts * import { decodeChainWebhook, verifyWebhookSignature } from "@secondlayer/sdk"; * * app.post("/webhook", async (c) => { * const raw = await c.req.text(); * if (!verifyWebhookSignature(raw, c.req.raw.headers, process.env.SIGNING_SECRET!)) { * return c.text("Invalid signature", 401); * } * const delivery = decodeChainWebhook(raw); * if (delivery.data.action === "apply" && delivery.data.trigger === "stx_transfer") { * delivery.data.event.data.amount; // typed * } * return c.body(null, 204); * }); * ``` */ declare function decodeChainWebhook(rawBody: string): ChainWebhookDelivery; import { RewardSet as RewardSet2 } from "@secondlayer/shared/node/consensus"; import { decodeClarityValue, toJsonSafe } from "@secondlayer/shared/streams-rows"; export { verifyWebhookSignature, verifyTransactionProof, verify as verifyStandardWebhooksHeaders, verifySecondlayerSignature, trigger, toJsonSafe, shutdownSignal, resolveBaseUrl, resolveApiKey, resolveAccountKey, parseRetryAfter, isStxTransfer, isStxMint, isStxLock, isStxBurn, isPrint, isNftTransfer, isNftMint, isNftBurn, isFtTransfer, isFtMint, isFtBurn, getSubgraph, fetchRewardSet, decodeStxTransfer, decodeStxMint, decodeStxLock, decodeStxBurn, decodePrint, decodeNftTransfer, decodeNftMint, decodeNftBurn, decodeFtTransfer, decodeFtMint, decodeFtBurn, decodeClarityValue, decodeChainWebhook, decode, createStreamsClient, createArchiveClient, contextField, consumerHealth, consumeIndexFeed, WithSinkTx, Webhooks, WebhookTestResult2 as WebhookTestResult, WebhookSummary2 as WebhookSummary, WebhookStatus, WebhookRuntime, WebhookKind, WebhookHeaderInput, WebhookFormat, WebhookDetail2 as WebhookDetail, WalkOptions, ValidationError, UpdateWebhookRequest2 as UpdateWebhookRequest, TransactionsWalkParams, TransactionsListParams, TransactionsEnvelope, TransactionProofVerifyResult, TransactionProof, TransactionEnvelope, Subgraphs, SubgraphSpecOptions3 as SubgraphSpecOptions, SubgraphSpecFormat2 as SubgraphSpecFormat, SubgraphOperationStatus, SubgraphAgentSchema3 as SubgraphAgentSchema, StreamsTip, StreamsSubscription, StreamsSignatureError, StreamsServerError, StreamsReorgsListParams, StreamsReorgsListEnvelope, StreamsReorgContext, StreamsReorg, StreamsEventsSubscribeParams, StreamsEventsStreamParams, StreamsEventsListParams, StreamsEventsListEnvelope, StreamsEventsEnvelope, StreamsEventsConsumeResult, StreamsEventsConsumeParams, StreamsEventType, StreamsEventPayload, StreamsEvent, StreamsDumpsManifest, StreamsDumps, StreamsDumpFile, StreamsConsumeParams, StreamsClient, StreamsCanonicalBlock, StreamsBatchContext, StreamsBatch, StandardWebhooksHeaders2 as StandardWebhooksHeaders, StackingWalkParams, StackingListParams, StackingEnvelope, SinkTx, SecondLayerOptions, SecondLayerErrorOptions, SecondLayerError, SecondLayerArchive, SecondLayer, SbtcWithdrawalsWalkParams, SbtcWithdrawalsListParams, SbtcWithdrawalsEnvelope, SbtcWithdrawalSweptConfirmedEvent, SbtcWithdrawalStatus, SbtcWithdrawalEvent, SbtcWithdrawalEnvelope, SbtcSummaryEnvelope, SbtcResource, SbtcEventsWalkParams, SbtcEventsListParams, SbtcEventsEnvelope, SbtcEventsConsumeParams, SbtcEventTopic2 as SbtcEventTopic, SbtcDepositsWalkParams, SbtcDepositsListParams, SbtcDepositsEnvelope, SbtcDepositsConsumeParams, SbtcDepositEvent, SbtcDepositEnvelope, RotateSecretResponse2 as RotateSecretResponse, RewardSet2 as RewardSet, RequestOptions, ReplayResult2 as ReplayResult, RateLimitError, PublicStatus2 as PublicStatus, PrintSchemaResponse, PoxResource, PoxCyclesWalkParams, PoxCyclesListParams, PoxCyclesEnvelope, PoxCycleEnvelope, Pox5Resource, Pox5EventsWalkParams, Pox5EventsListParams, Pox5EventsEnvelope, NftTransfersWalkParams, NftTransfersResource, NftTransfersListParams, NftTransfersEnvelope, NftTransferPayload2 as NftTransferPayload, NftTransferEvent, NftTransfer, MempoolWalkParams, MempoolTransactionEnvelope, MempoolListParams, MempoolEnvelope, LoadedArchive, LOCAL_API_URL, InsufficientArchiveCreditsError, InstanceStatus, InstanceState, InstanceIssue, InstanceDiagnosis3 as InstanceDiagnosis, InstanceClient, IndexTransaction, IndexTip, IndexStxTransfer, IndexStxMint, IndexStxLock, IndexStxBurn, IndexStackingAction, IndexSbtcWithdrawalPhase, IndexSbtcWithdrawalDetail, IndexSbtcWithdrawal, IndexSbtcSummary, IndexSbtcEvent, IndexSbtcDepositDetail, IndexSbtcDeposit, IndexReorg, IndexPrint, IndexPoxFunctionCount, IndexPoxCycle, IndexPox5EventTopic, IndexPox5Event, IndexPostCondition, IndexNftTransfer, IndexNftMint, IndexNftBurn, IndexMempoolTransaction, IndexFtTransfer, IndexFtMint, IndexFtBurn, IndexFeedItem, IndexFeedFetcher, IndexFeedEnvelope, IndexEventsResource, IndexEventTypeFilters, IndexEventType, IndexEventOf, IndexEventFields, IndexEvent, IndexDiscovery, IndexContractCall, IndexConsumeOptions, IndexCanonicalBlock, IndexBlock, IndexAlwaysFields, Index, INSTANCE_TOKEN_ENV, INDEX_MAX_PAGE_SIZE, FtTransfersWalkParams, FtTransfersResource, FtTransfersListParams, FtTransfersEnvelope, FtTransferPayload2 as FtTransferPayload, FtTransferEvent, FtTransfer, FetchLike2 as FetchLike, EventsWalkParams, EventsListParams, EventsEnvelope, EventsConsumeParams, DeliveryRow2 as DeliveryRow, DecodedStxTransferPayload, DecodedStxTransfer, DecodedStxMintPayload, DecodedStxMint, DecodedStxLockPayload, DecodedStxLock, DecodedStxBurnPayload, DecodedStxBurn, DecodedPrintValue, DecodedPrintPayload, DecodedPrint, DecodedNftTransferPayload, DecodedNftTransfer, DecodedNftMintPayload, DecodedNftMint, DecodedNftBurnPayload, DecodedNftBurn, DecodedFtTransferPayload, DecodedFtTransfer, DecodedFtMintPayload, DecodedFtMint, DecodedFtBurnPayload, DecodedFtBurn, DecodedEventRow, DecodedEventColumns, DeadRow2 as DeadRow, DEFAULT_REQUEST_TIMEOUT_MS, Cursor, CreateWebhookResponse2 as CreateWebhookResponse, CreateWebhookRequest2 as CreateWebhookRequest, CreateArchiveClientOptions, ContractsListParams, ContractsEnvelope, Contracts, ContractSummary, ContractConformance, ContractCallsWalkParams, ContractCallsListParams, ContractCallsEnvelope, ContractCallsConsumeParams, ContextSnapshot, ContextFieldError, ContextField, ContextAccount, ConsumerSink, ConsumerHealth, ConsumerBatchContext, ChainWebhookEnvelope, ChainWebhookDelivery2 as ChainWebhookDelivery, ChainTxLevelEvent, ChainTriggerType, ChainTrigger, ChainTestDelivery, ChainStxTransferData, ChainStxMintData, ChainStxLockData, ChainStxBurnData, ChainReorgRollbackEnvelope, ChainReorgRollbackDelivery, ChainReorgOrphanedEntry, ChainPrintEventData, ChainNftTransferData, ChainNftMintData, ChainNftBurnData, ChainFtTransferData, ChainFtMintData, ChainFtBurnData, ChainEventEnvelope, ChainApplyEnvelopeOf, ChainApplyEnvelope, ChainApplyDeliveryOf, CanonicalWalkParams, CanonicalListParams, CanonicalEnvelope, BlocksWalkParams, BlocksListParams, BlocksEnvelope, BlockEnvelope, AuthError, ArchiveVerifyResult, ArchiveVerifyRangeStatus, ArchiveVerifyInput, ArchiveStatus, ArchiveSignatureError, ArchiveQuote, ArchivePartition, ArchiveManifest, ArchiveLoadOptions, ArchiveGateNotConfiguredError, ArchiveFlow, ArchiveFetchResult, ArchiveFetchItem, ArchiveDataset, ArchiveCreditsBalance, ArchiveClient, ArchiveAuthError, ApiError, ActiveSubgraphOperation, ACCOUNT_KEY_ENV };