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; } /** 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; }; 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; }; } /** * 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; } /** `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"; 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; }; /** 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[]; }; /** 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"]; }; 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; }; /** 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"; 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; } 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; import { SubgraphAgentSchema as SubgraphAgentSchema2, SubgraphSpecFormat, SubgraphSpecOptions as SubgraphSpecOptions2 } from "@secondlayer/shared/subgraphs/spec"; export { getSubgraph, Subgraphs, SubgraphSpecOptions2 as SubgraphSpecOptions, SubgraphSpecFormat, SubgraphAgentSchema2 as SubgraphAgentSchema, SecondLayerOptions };