@secondlayer/sdk 6.26.0 → 6.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +278 -1
- package/dist/index.js +200 -16
- package/dist/index.js.map +5 -5
- package/dist/streams/index.js.map +1 -1
- package/dist/subgraphs/index.d.ts +277 -0
- package/dist/subgraphs/index.js +200 -16
- package/dist/subgraphs/index.js.map +5 -5
- package/package.json +2 -4
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import { ed25519 } from \"@secondlayer/shared\";\nimport { buildQuery } from \"../base.ts\";\nimport {\n\ttype StreamsEventsFetcher,\n\tconsumeStreamsEvents,\n\titerateStreamsBatches,\n\tstreamStreamsEvents,\n} from \"./consumer.ts\";\nimport { createStreamsDumps } from \"./dumps.ts\";\nimport {\n\tAuthError,\n\tRateLimitError,\n\tStreamsServerError,\n\tStreamsSignatureError,\n\tValidationError,\n} from \"./errors.ts\";\nimport { subscribeStreamsEvents } from \"./subscribe.ts\";\nimport type {\n\tFetchLike,\n\tStreamsCanonicalBlock,\n\tStreamsClient,\n\tStreamsConsumeParams,\n\tStreamsEventsConsumeParams,\n\tStreamsEventsEnvelope,\n\tStreamsEventsListEnvelope,\n\tStreamsEventsListParams,\n\tStreamsEventsReplayParams,\n\tStreamsEventsStreamParams,\n\tStreamsEventsSubscribeParams,\n\tStreamsReorgsListEnvelope,\n\tStreamsReorgsListParams,\n\tStreamsTip,\n\tStreamsUsage,\n} from \"./types.ts\";\n\n/** Parse a `<block>:<index>` cursor; null sorts before genesis. */\nfunction cursorTuple(cursor: string | null): [number, number] {\n\tif (!cursor) return [-1, -1];\n\tconst parts = cursor.split(\":\");\n\tconst [block, index] = parts.map(Number);\n\tif (\n\t\tparts.length !== 2 ||\n\t\t!Number.isInteger(block) ||\n\t\t!Number.isInteger(index)\n\t) {\n\t\tthrow new ValidationError(\n\t\t\t`Invalid stream cursor \"${cursor}\"; expected \"<block>:<index>\" (e.g. \"951475:3\").`,\n\t\t\t400,\n\t\t);\n\t}\n\treturn [block, index];\n}\n\n/** The greater of two cursors (later in the stream). */\nfunction maxCursor(a: string | null, b: string | null): string | null {\n\tconst [ah, ai] = cursorTuple(a);\n\tconst [bh, bi] = cursorTuple(b);\n\treturn ah > bh || (ah === bh && ai >= bi) ? a : b;\n}\n\nconst DEFAULT_STREAMS_BASE_URL = \"https://api.secondlayer.tools\";\n\nexport type CreateStreamsClientOptions = {\n\tapiKey: string;\n\tbaseUrl?: string;\n\tfetchImpl?: FetchLike;\n\t/**\n\t * Public base URL for bulk parquet dumps (the R2/CDN bucket root). Required\n\t * to use `client.dumps`. See `GET /public/streams/dumps/manifest`.\n\t */\n\tdumpsBaseUrl?: string;\n\t/**\n\t * Verify the ed25519 `X-Signature` on every REST response and per-frame SSE\n\t * signature. Three states:\n\t * - **default (omitted)** — *lenient*: verify when the server signs (the\n\t * hosted API signs every response), and pass through when no signature is\n\t * present (e.g. a self-hosted instance with no `STREAMS_SIGNING_PRIVATE_KEY`).\n\t * So verification is on by default against the hosted API without breaking\n\t * unsigned self-host deployments. An *invalid* signature always throws.\n\t * - **`true`** (or `{ publicKey }` to pin a known PEM) — *strict*: a missing\n\t * OR invalid signature throws `StreamsSignatureError`. Use this when you\n\t * require a portable, non-repudiable attestation and won't accept unsigned\n\t * data (it also closes the lenient mode's strip-the-header downgrade).\n\t * - **`false`** — off.\n\t *\n\t * The key is fetched once from `/public/streams/signing-key` (cached; a\n\t * rotated `X-Signature-KeyId` triggers one refresh) unless a PEM is pinned.\n\t */\n\tverify?: boolean | { publicKey: string };\n\t/**\n\t * Verify the bulk dumps manifest's ed25519 signature in `client.dumps.list()`\n\t * before trusting any file sha256 (default ON). Uses the same key source as\n\t * `verify` (fetches `/public/streams/signing-key`, or a pinned PEM). Pass\n\t * `false` to opt out. A missing or invalid signature throws\n\t * `StreamsSignatureError`.\n\t */\n\tverifyDumpsManifest?: boolean;\n};\n\nfunction normalizeBaseUrl(baseUrl: string): string {\n\treturn baseUrl.replace(/\\/+$/, \"\");\n}\n\nasync function responseBody(response: Response): Promise<unknown> {\n\tconst text = await response.text();\n\tif (text.length === 0) return undefined;\n\ttry {\n\t\treturn JSON.parse(text);\n\t} catch {\n\t\treturn text;\n\t}\n}\n\nfunction errorMessage(body: unknown, fallback: string): string {\n\tif (body && typeof body === \"object\") {\n\t\tconst record = body as Record<string, unknown>;\n\t\tconst message = record.error ?? record.message;\n\t\tif (typeof message === \"string\" && message.length > 0) return message;\n\t}\n\tif (typeof body === \"string\" && body.length > 0) return body;\n\treturn fallback;\n}\n\nasync function mapStreamsError(response: Response): Promise<never> {\n\tconst body = await responseBody(response);\n\n\tif (response.status === 401) {\n\t\tthrow new AuthError(errorMessage(body, \"API key invalid or expired.\"));\n\t}\n\n\tif (response.status === 429) {\n\t\tconst retryAfter = response.headers.get(\"Retry-After\") ?? undefined;\n\t\tthrow new RateLimitError(\n\t\t\terrorMessage(body, \"Rate limited. Try again later.\"),\n\t\t\tretryAfter,\n\t\t);\n\t}\n\n\tif (response.status >= 500) {\n\t\tthrow new StreamsServerError(\n\t\t\terrorMessage(body, `Streams server returned ${response.status}.`),\n\t\t\tresponse.status,\n\t\t\tbody,\n\t\t);\n\t}\n\n\tthrow new ValidationError(\n\t\terrorMessage(body, `Streams request returned ${response.status}.`),\n\t\tresponse.status,\n\t\tbody,\n\t);\n}\n\nexport function createStreamsClient(\n\toptions: CreateStreamsClientOptions,\n): StreamsClient {\n\tconst baseUrl = normalizeBaseUrl(options.baseUrl ?? DEFAULT_STREAMS_BASE_URL);\n\tconst fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));\n\tconst verify = options.verify;\n\t// On by default, but lenient: the hosted API signs every response, while a\n\t// self-hosted instance without STREAMS_SIGNING_PRIVATE_KEY serves none.\n\t// Lenient verifies when a signature is present and passes through when it is\n\t// absent — so the default neither skips hosted verification nor breaks OSS.\n\t// `verify: true` / `{ publicKey }` is strict (missing signature throws);\n\t// `verify: false` is off. An invalid signature always throws.\n\tconst verifyMode: \"off\" | \"lenient\" | \"strict\" =\n\t\tverify === false ? \"off\" : verify === undefined ? \"lenient\" : \"strict\";\n\n\t// Lazily resolve and cache the verification key alongside its id, so a\n\t// rotation (signalled by a changed `X-Signature-KeyId`) can be detected.\n\ttype VerificationKey = {\n\t\tkeyId: string;\n\t\tpublicKeyPem: string;\n\t\tpublicKey: ReturnType<typeof ed25519.loadEd25519PublicKey>;\n\t};\n\tlet keyPromise: Promise<VerificationKey> | null = null;\n\tfunction loadKey(): Promise<VerificationKey> {\n\t\tif (keyPromise) return keyPromise;\n\t\tkeyPromise = (async () => {\n\t\t\tif (typeof verify === \"object\") {\n\t\t\t\treturn {\n\t\t\t\t\tkeyId: ed25519.ed25519KeyId(verify.publicKey),\n\t\t\t\t\tpublicKeyPem: verify.publicKey,\n\t\t\t\t\tpublicKey: ed25519.loadEd25519PublicKey(verify.publicKey),\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst res = await fetchImpl(`${baseUrl}/public/streams/signing-key`);\n\t\t\tif (!res.ok) {\n\t\t\t\tthrow new StreamsSignatureError(\n\t\t\t\t\t`Could not fetch signing key (${res.status}).`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst body = (await res.json()) as {\n\t\t\t\tpublic_key_pem?: string;\n\t\t\t\tkey_id?: string;\n\t\t\t};\n\t\t\tif (!body.public_key_pem) {\n\t\t\t\tthrow new StreamsSignatureError(\"Signing key response missing key.\");\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tkeyId: body.key_id ?? ed25519.ed25519KeyId(body.public_key_pem),\n\t\t\t\tpublicKeyPem: body.public_key_pem,\n\t\t\t\tpublicKey: ed25519.loadEd25519PublicKey(body.public_key_pem),\n\t\t\t};\n\t\t})();\n\t\treturn keyPromise;\n\t}\n\n\tconst dumps = createStreamsDumps({\n\t\tbaseUrl: options.dumpsBaseUrl,\n\t\tfetchImpl,\n\t\tverifyManifest: options.verifyDumpsManifest ?? true,\n\t\tloadPublicKeyPem: async () => (await loadKey()).publicKeyPem,\n\t});\n\n\tasync function request<T>(path: string): Promise<T> {\n\t\tconst response = await fetchImpl(`${baseUrl}${path}`, {\n\t\t\theaders: { Authorization: `Bearer ${options.apiKey}` },\n\t\t});\n\t\tif (!response.ok) await mapStreamsError(response);\n\t\tconst text = await response.text();\n\t\tif (verifyMode !== \"off\") {\n\t\t\tconst signature = response.headers.get(\"X-Signature\");\n\t\t\tif (!signature) {\n\t\t\t\t// Strict requires a signature; lenient (default) lets an unsigned\n\t\t\t\t// response through — e.g. self-host with no signing key configured.\n\t\t\t\tif (verifyMode === \"strict\") {\n\t\t\t\t\tthrow new StreamsSignatureError(\"Response is missing X-Signature.\");\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst responseKeyId = response.headers.get(\"X-Signature-KeyId\");\n\t\t\t\tlet key = await loadKey();\n\t\t\t\t// The server rotated to a key we haven't seen.\n\t\t\t\tif (responseKeyId && responseKeyId !== key.keyId) {\n\t\t\t\t\tif (typeof verify === \"object\") {\n\t\t\t\t\t\t// Pinned key: a different id is never the pinned key — fail closed.\n\t\t\t\t\t\tthrow new StreamsSignatureError(\n\t\t\t\t\t\t\t`Response signed with key '${responseKeyId}', expected pinned key '${key.keyId}'.`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\t// Fetched key: refresh once. A still-mismatched id (no re-loop)\n\t\t\t\t\t// means the endpoint doesn't serve the signing key — fail closed.\n\t\t\t\t\tkeyPromise = null;\n\t\t\t\t\tkey = await loadKey();\n\t\t\t\t\tif (responseKeyId !== key.keyId) {\n\t\t\t\t\t\tthrow new StreamsSignatureError(\n\t\t\t\t\t\t\t`Response signed with key '${responseKeyId}' not served by the signing-key endpoint.`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// A signature is present, so verify it regardless of strict/lenient —\n\t\t\t\t// an invalid signature always fails closed.\n\t\t\t\tif (!ed25519.verifyEd25519(text, signature, key.publicKey)) {\n\t\t\t\t\tthrow new StreamsSignatureError();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn JSON.parse(text) as T;\n\t}\n\n\tconst fetchEvents: StreamsEventsFetcher = async ({\n\t\tcursor,\n\t\tlimit,\n\t\ttypes,\n\t\tnotTypes,\n\t\tcontractId,\n\t\tsender,\n\t\trecipient,\n\t\tassetIdentifier,\n\t}) => {\n\t\treturn listEvents({\n\t\t\tcursor,\n\t\t\tlimit,\n\t\t\ttypes,\n\t\t\tnotTypes,\n\t\t\tcontractId,\n\t\t\tsender,\n\t\t\trecipient,\n\t\t\tassetIdentifier,\n\t\t});\n\t};\n\n\tasync function listEvents(\n\t\tparams: StreamsEventsListParams = {},\n\t): Promise<StreamsEventsEnvelope> {\n\t\treturn request<StreamsEventsEnvelope>(\n\t\t\t`/v1/streams/events${buildQuery({\n\t\t\t\tcursor: params.cursor,\n\t\t\t\tfrom_height: params.fromHeight,\n\t\t\t\tto_height: params.toHeight,\n\t\t\t\tlimit: params.limit,\n\t\t\t\tcontract_id: params.contractId,\n\t\t\t\tsender: params.sender,\n\t\t\t\trecipient: params.recipient,\n\t\t\t\tasset_identifier: params.assetIdentifier,\n\t\t\t\ttypes: params.types,\n\t\t\t\tnot_types: params.notTypes,\n\t\t\t})}`,\n\t\t);\n\t}\n\n\treturn {\n\t\tconsume(params: StreamsConsumeParams = {}) {\n\t\t\treturn iterateStreamsBatches({\n\t\t\t\tfromCursor: params.cursor,\n\t\t\t\tbatchSize: params.batchSize ?? 100,\n\t\t\t\tintervalMs: params.intervalMs ?? 2000,\n\t\t\t\ttypes: params.types,\n\t\t\t\tnotTypes: params.notTypes,\n\t\t\t\tcontractId: params.contractId,\n\t\t\t\tsender: params.sender,\n\t\t\t\trecipient: params.recipient,\n\t\t\t\tassetIdentifier: params.assetIdentifier,\n\t\t\t\tsignal: params.signal,\n\t\t\t\tfetchEvents,\n\t\t\t});\n\t\t},\n\t\tevents: {\n\t\t\tlist: listEvents,\n\t\t\tbyTxId(txId: string) {\n\t\t\t\treturn request<StreamsEventsListEnvelope>(\n\t\t\t\t\t`/v1/streams/events/${encodeURIComponent(txId)}`,\n\t\t\t\t);\n\t\t\t},\n\t\t\tconsume(params: StreamsEventsConsumeParams) {\n\t\t\t\treturn consumeStreamsEvents({\n\t\t\t\t\tfromCursor: params.fromCursor,\n\t\t\t\t\tmode: params.mode,\n\t\t\t\t\tfinalizedOnly: params.finalizedOnly,\n\t\t\t\t\ttypes: params.types,\n\t\t\t\t\tnotTypes: params.notTypes,\n\t\t\t\t\tcontractId: params.contractId,\n\t\t\t\t\tsender: params.sender,\n\t\t\t\t\trecipient: params.recipient,\n\t\t\t\t\tassetIdentifier: params.assetIdentifier,\n\t\t\t\t\tbatchSize: params.batchSize ?? 100,\n\t\t\t\t\tfetchEvents,\n\t\t\t\t\tonBatch: params.onBatch,\n\t\t\t\t\tonReorg: params.onReorg,\n\t\t\t\t\temptyBackoffMs: params.emptyBackoffMs,\n\t\t\t\t\tmaxPages: params.maxPages,\n\t\t\t\t\tmaxEmptyPolls: params.maxEmptyPolls,\n\t\t\t\t\tsignal: params.signal,\n\t\t\t\t});\n\t\t\t},\n\t\t\tstream(params: StreamsEventsStreamParams = {}) {\n\t\t\t\treturn streamStreamsEvents({\n\t\t\t\t\tfromCursor: params.fromCursor,\n\t\t\t\t\ttypes: params.types,\n\t\t\t\t\tnotTypes: params.notTypes,\n\t\t\t\t\tcontractId: params.contractId,\n\t\t\t\t\tsender: params.sender,\n\t\t\t\t\trecipient: params.recipient,\n\t\t\t\t\tassetIdentifier: params.assetIdentifier,\n\t\t\t\t\tbatchSize: params.batchSize ?? 100,\n\t\t\t\t\temptyBackoffMs: params.emptyBackoffMs,\n\t\t\t\t\tmaxPages: params.maxPages,\n\t\t\t\t\tmaxEmptyPolls: params.maxEmptyPolls,\n\t\t\t\t\tsignal: params.signal,\n\t\t\t\t\tfetchEvents,\n\t\t\t\t});\n\t\t\t},\n\t\t\tsubscribe(params: StreamsEventsSubscribeParams) {\n\t\t\t\treturn subscribeStreamsEvents({\n\t\t\t\t\tbaseUrl,\n\t\t\t\t\tapiKey: options.apiKey,\n\t\t\t\t\tfetchImpl,\n\t\t\t\t\tverify: verifyMode,\n\t\t\t\t\tloadKey,\n\t\t\t\t\tparams,\n\t\t\t\t});\n\t\t\t},\n\t\t\tasync replay(params: StreamsEventsReplayParams) {\n\t\t\t\tconst fromCursor =\n\t\t\t\t\tparams.from === \"genesis\" ? null : (params.from ?? null);\n\t\t\t\tconst fromBlock = fromCursor ? cursorTuple(fromCursor)[0] : 0;\n\t\t\t\tconst manifest = await dumps.list();\n\n\t\t\t\t// Hydrate finalized history from dumps, in block order.\n\t\t\t\tconst files = manifest.files\n\t\t\t\t\t.filter((file) => file.to_block >= fromBlock)\n\t\t\t\t\t.sort(\n\t\t\t\t\t\t(a, b) => a.from_block - b.from_block || a.to_block - b.to_block,\n\t\t\t\t\t);\n\t\t\t\tfor (const file of files) {\n\t\t\t\t\tif (params.signal?.aborted) break;\n\t\t\t\t\tawait params.onDumpFile(file);\n\t\t\t\t}\n\n\t\t\t\t// Seam: tail live from just past the dumped coverage. Cursor input is\n\t\t\t\t// exclusive, so the first live event is strictly after the last dump.\n\t\t\t\tconst seam = maxCursor(fromCursor, manifest.latest_finalized_cursor);\n\t\t\t\treturn consumeStreamsEvents({\n\t\t\t\t\tfromCursor: seam,\n\t\t\t\t\tmode: params.mode ?? \"tail\",\n\t\t\t\t\tbatchSize: params.batchSize ?? 100,\n\t\t\t\t\tfetchEvents,\n\t\t\t\t\tonBatch: params.onBatch,\n\t\t\t\t\tonReorg: params.onReorg,\n\t\t\t\t\temptyBackoffMs: params.emptyBackoffMs,\n\t\t\t\t\tmaxPages: params.maxPages,\n\t\t\t\t\tmaxEmptyPolls: params.maxEmptyPolls,\n\t\t\t\t\tsignal: params.signal,\n\t\t\t\t});\n\t\t\t},\n\t\t},\n\t\tblocks: {\n\t\t\tevents(heightOrHash: number | string) {\n\t\t\t\treturn request<StreamsEventsListEnvelope>(\n\t\t\t\t\t`/v1/streams/blocks/${encodeURIComponent(String(heightOrHash))}/events`,\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t\treorgs: {\n\t\t\tlist(params: StreamsReorgsListParams) {\n\t\t\t\treturn request<StreamsReorgsListEnvelope>(\n\t\t\t\t\t`/v1/streams/reorgs${buildQuery({\n\t\t\t\t\t\tsince: params.since,\n\t\t\t\t\t\tlimit: params.limit,\n\t\t\t\t\t})}`,\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t\tdumps,\n\t\tcanonical(height: number) {\n\t\t\treturn request<StreamsCanonicalBlock>(`/v1/streams/canonical/${height}`);\n\t\t},\n\t\ttip() {\n\t\t\treturn request<StreamsTip>(\"/v1/streams/tip\");\n\t\t},\n\t\tusage() {\n\t\t\treturn request<StreamsUsage>(\"/v1/streams/usage\");\n\t\t},\n\t};\n}\n",
|
|
6
6
|
"import type { ByoBreakingChangeDetails } from \"@secondlayer/shared/errors\";\n\nexport type { ByoBreakingChangeDetails };\n\n/**\n * Error thrown by {@link SecondLayer} when an API request fails.\n * Includes the HTTP status code for programmatic error handling.\n *\n * @example\n * ```ts\n * try {\n * await client.subgraphs.get(\"my-subgraph\");\n * } catch (err) {\n * if (err instanceof ApiError && err.status === 404) {\n * console.log(\"Subgraph not found\");\n * }\n * }\n * ```\n */\nexport class ApiError extends Error {\n\tconstructor(\n\t\t/** HTTP status code (0 for network errors). */\n\t\tpublic status: number,\n\t\tmessage: string,\n\t\t/** Raw response body (parsed JSON if possible) — preserved for callers that need error details. */\n\t\tpublic body?: unknown,\n\t\t/** Stable machine-readable code from the API's `{error, code}` error envelope. */\n\t\tpublic code?: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"ApiError\";\n\t}\n}\n\n/**\n * Thrown on optimistic-concurrency conflict when a deploy supplies an\n * `expectedVersion` that no longer matches the server's stored version.\n */\nexport class VersionConflictError extends ApiError {\n\tconstructor(\n\t\tpublic currentVersion: string,\n\t\tpublic expectedVersion: string,\n\t\tmessage = `Version conflict: expected ${expectedVersion}, current ${currentVersion}`,\n\t) {\n\t\tsuper(409, message, { currentVersion, expectedVersion });\n\t\tthis.name = \"VersionConflictError\";\n\t}\n}\n\n/**\n * Thrown when a BYO subgraph deploy is refused for a breaking schema change.\n * The deploy did NOT run — `details.plan` carries the DROP + rebuild DDL to run\n * manually on your own database, plus the breaking `reasons` and the `diff`.\n *\n * @example\n * ```ts\n * try {\n * await client.subgraphs.deploy(bundle);\n * } catch (err) {\n * if (err instanceof ByoBreakingChangeError) {\n * console.log(err.details.plan.dropStatement);\n * console.log(err.details.plan.statements.join(\";\\n\"));\n * }\n * }\n * ```\n */\nexport class ByoBreakingChangeError extends ApiError {\n\treadonly details: ByoBreakingChangeDetails;\n\tconstructor(message: string, details: ByoBreakingChangeDetails) {\n\t\tsuper(422, message, details, \"BYO_BREAKING_CHANGE\");\n\t\tthis.name = \"ByoBreakingChangeError\";\n\t\tthis.details = details;\n\t}\n}\n\n/** Narrow an unknown error body's `details` to {@link ByoBreakingChangeDetails}. */\nexport function isByoBreakingDetails(\n\tx: unknown,\n): x is ByoBreakingChangeDetails {\n\tif (!x || typeof x !== \"object\") return false;\n\tconst d = x as Record<string, unknown>;\n\tconst plan = d.plan as Record<string, unknown> | undefined;\n\treturn (\n\t\tArray.isArray(d.reasons) &&\n\t\t!!plan &&\n\t\ttypeof plan === \"object\" &&\n\t\ttypeof plan.dropStatement === \"string\"\n\t);\n}\n",
|
|
7
|
-
"import {\n\tApiError,\n\ttype ByoBreakingChangeDetails,\n\tByoBreakingChangeError,\n\tisByoBreakingDetails,\n} from \"./errors.ts\";\n\nexport type FetchLike = (\n\tinput: string | URL | Request,\n\tinit?: RequestInit,\n) => Promise<Response>;\n\nexport interface SecondLayerOptions {\n\t/** Base URL of the Secondlayer platform API (trailing slashes are stripped). */\n\tbaseUrl: string;\n\t/** Bearer token for authenticated requests. */\n\tapiKey?: string;\n\t/** Fetch implementation. Tests and edge runtimes can provide their own. */\n\tfetchImpl?: FetchLike;\n\t/** Public base URL for Streams bulk parquet dumps (the cold backfill plane).\n\t * Required for `streams.dumps.*`; without it the dumps client falls back to\n\t * its built-in default. */\n\tdumpsBaseUrl?: string;\n\t/** Deploy origin label sent as `x-sl-origin` (telemetry). Defaults to `cli`. */\n\torigin?: \"cli\" | \"mcp\" | \"session\";\n}\n\nconst DEFAULT_BASE_URL = \"https://api.secondlayer.tools\";\n\n/** Build a query-string suffix from name→value pairs. Skips null/undefined and\n * empty values; arrays are comma-joined. Returns \"\" (never a dangling \"?\") or\n * \"?a=1&b=2\" — the one canonical builder every list endpoint shares, so the\n * empty-query guard can't be forgotten per call site. */\nexport function buildQuery(\n\tparams: Record<\n\t\tstring,\n\t\tnumber | string | readonly string[] | null | undefined\n\t>,\n): string {\n\tconst search = new URLSearchParams();\n\tfor (const [name, value] of Object.entries(params)) {\n\t\tif (value === undefined || value === null) continue;\n\t\tconst serialized = Array.isArray(value) ? value.join(\",\") : String(value);\n\t\tif (serialized.length === 0) continue;\n\t\tsearch.set(name, serialized);\n\t}\n\tconst query = search.toString();\n\treturn query ? `?${query}` : \"\";\n}\n\nexport abstract class BaseClient {\n\tprotected baseUrl: string;\n\tprotected apiKey?: string;\n\tprotected origin: \"cli\" | \"mcp\" | \"session\";\n\n\tconstructor(options: Partial<SecondLayerOptions> = {}) {\n\t\tthis.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n\t\tthis.apiKey = options.apiKey;\n\t\tthis.origin = options.origin ?? \"cli\";\n\t}\n\n\tstatic authHeaders(apiKey?: string): Record<string, string> {\n\t\tconst headers: Record<string, string> = {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t};\n\t\tif (apiKey) {\n\t\t\theaders.Authorization = `Bearer ${apiKey}`;\n\t\t}\n\t\treturn headers;\n\t}\n\n\tprotected async request<T>(\n\t\tmethod: string,\n\t\tpath: string,\n\t\tbody?: unknown,\n\t): Promise<T> {\n\t\tconst response = await this.fetchResponse(method, path, body);\n\t\tif (response.status === 204) {\n\t\t\treturn undefined as T;\n\t\t}\n\t\treturn response.json() as Promise<T>;\n\t}\n\n\tprotected async requestText(\n\t\tmethod: string,\n\t\tpath: string,\n\t\tbody?: unknown,\n\t): Promise<string> {\n\t\tconst response = await this.fetchResponse(method, path, body);\n\t\treturn response.text();\n\t}\n\n\tprivate async fetchResponse(\n\t\tmethod: string,\n\t\tpath: string,\n\t\tbody?: unknown,\n\t): Promise<Response> {\n\t\tconst url = `${this.baseUrl}${path}`;\n\t\tconst headers = BaseClient.authHeaders(this.apiKey);\n\t\theaders[\"x-sl-origin\"] = this.origin;\n\n\t\t// Serialize the body BEFORE the network try so a body-encoding error\n\t\t// (e.g. unsupported BigInt) isn't misreported as \"Cannot reach API\".\n\t\t// BigInts are stringified — server schemas accept jsonb so the value\n\t\t// reaches the server intact, and any field that needs an actual bigint\n\t\t// at runtime is rehydrated by the consuming module (subgraph handler\n\t\t// code preserves the literal). See packages/subgraphs source-matcher\n\t\t// for the post-load shape.\n\t\tlet serializedBody: string | undefined;\n\t\tif (body !== undefined && body !== null) {\n\t\t\ttry {\n\t\t\t\tserializedBody = JSON.stringify(body, (_key, value) =>\n\t\t\t\t\ttypeof value === \"bigint\" ? value.toString() : value,\n\t\t\t\t);\n\t\t\t} catch (err) {\n\t\t\t\tconst detail = err instanceof Error ? err.message : String(err);\n\t\t\t\tthrow new ApiError(0, `Failed to serialize request body: ${detail}`);\n\t\t\t}\n\t\t}\n\n\t\tlet response: Response;\n\t\ttry {\n\t\t\tresponse = await fetch(url, {\n\t\t\t\tmethod,\n\t\t\t\theaders,\n\t\t\t\tbody: serializedBody,\n\t\t\t});\n\t\t} catch {\n\t\t\tthrow new ApiError(\n\t\t\t\t0,\n\t\t\t\t`Cannot reach API at ${this.baseUrl}. Check your connection or try again.`,\n\t\t\t);\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tif (response.status === 401) {\n\t\t\t\tthrow new ApiError(401, \"API key invalid or expired.\");\n\t\t\t}\n\n\t\t\tif (response.status === 429) {\n\t\t\t\tconst retryAfter = response.headers.get(\"Retry-After\");\n\t\t\t\tconst msg = retryAfter\n\t\t\t\t\t? `Rate limited. Wait ${retryAfter} seconds.`\n\t\t\t\t\t: \"Rate limited. Try again later.\";\n\t\t\t\tthrow new ApiError(429, msg);\n\t\t\t}\n\n\t\t\tif (response.status >= 500) {\n\t\t\t\tthrow new ApiError(\n\t\t\t\t\tresponse.status,\n\t\t\t\t\t`Server error. Try again or check status at ${this.baseUrl}/health`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst errorBody = await response.text();\n\t\t\tlet message = `HTTP ${response.status}`;\n\t\t\tlet parsedBody: unknown = errorBody;\n\t\t\tlet code: string | undefined;\n\t\t\ttry {\n\t\t\t\tconst json = JSON.parse(errorBody);\n\t\t\t\tparsedBody = json;\n\t\t\t\tconst err = json.error ?? json.message;\n\t\t\t\tif (typeof err === \"string\") {\n\t\t\t\t\tmessage = err;\n\t\t\t\t} else if (err && typeof err === \"object\") {\n\t\t\t\t\tmessage = JSON.stringify(err);\n\t\t\t\t}\n\t\t\t\tif (typeof json.code === \"string\") {\n\t\t\t\t\tcode = json.code;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tif (errorBody) message = errorBody;\n\t\t\t}\n\t\t\tif (\n\t\t\t\tresponse.status === 422 &&\n\t\t\t\tcode === \"BYO_BREAKING_CHANGE\" &&\n\t\t\t\tparsedBody &&\n\t\t\t\ttypeof parsedBody === \"object\" &&\n\t\t\t\tisByoBreakingDetails((parsedBody as { details?: unknown }).details)\n\t\t\t) {\n\t\t\t\tthrow new ByoBreakingChangeError(\n\t\t\t\t\tmessage,\n\t\t\t\t\t(parsedBody as { details: ByoBreakingChangeDetails }).details,\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow new ApiError(response.status, message, parsedBody, code);\n\t\t}\n\n\t\treturn response;\n\t}\n}\n",
|
|
7
|
+
"import {\n\tApiError,\n\ttype ByoBreakingChangeDetails,\n\tByoBreakingChangeError,\n\tisByoBreakingDetails,\n} from \"./errors.ts\";\n\nexport type FetchLike = (\n\tinput: string | URL | Request,\n\tinit?: RequestInit,\n) => Promise<Response>;\n\nexport interface SecondLayerOptions {\n\t/** Base URL of the Secondlayer platform API (trailing slashes are stripped). */\n\tbaseUrl: string;\n\t/** Bearer token for authenticated requests. */\n\tapiKey?: string;\n\t/** Fetch implementation. Tests and edge runtimes can provide their own. */\n\tfetchImpl?: FetchLike;\n\t/** Public base URL for Streams bulk parquet dumps (the cold backfill plane).\n\t * Required for `streams.dumps.*`; without it the dumps client falls back to\n\t * its built-in default. */\n\tdumpsBaseUrl?: string;\n\t/** Deploy origin label sent as `x-sl-origin` (telemetry). Defaults to `cli`. */\n\torigin?: \"cli\" | \"mcp\" | \"session\";\n}\n\nconst DEFAULT_BASE_URL = \"https://api.secondlayer.tools\";\n\n/** Build a query-string suffix from name→value pairs. Skips null/undefined and\n * empty values; arrays are comma-joined. Returns \"\" (never a dangling \"?\") or\n * \"?a=1&b=2\" — the one canonical builder every list endpoint shares, so the\n * empty-query guard can't be forgotten per call site. */\nexport function buildQuery(\n\tparams: Record<\n\t\tstring,\n\t\tnumber | string | boolean | readonly string[] | null | undefined\n\t>,\n): string {\n\tconst search = new URLSearchParams();\n\tfor (const [name, value] of Object.entries(params)) {\n\t\tif (value === undefined || value === null) continue;\n\t\tconst serialized = Array.isArray(value) ? value.join(\",\") : String(value);\n\t\tif (serialized.length === 0) continue;\n\t\tsearch.set(name, serialized);\n\t}\n\tconst query = search.toString();\n\treturn query ? `?${query}` : \"\";\n}\n\nexport abstract class BaseClient {\n\tprotected baseUrl: string;\n\tprotected apiKey?: string;\n\tprotected origin: \"cli\" | \"mcp\" | \"session\";\n\n\tconstructor(options: Partial<SecondLayerOptions> = {}) {\n\t\tthis.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n\t\tthis.apiKey = options.apiKey;\n\t\tthis.origin = options.origin ?? \"cli\";\n\t}\n\n\tstatic authHeaders(apiKey?: string): Record<string, string> {\n\t\tconst headers: Record<string, string> = {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t};\n\t\tif (apiKey) {\n\t\t\theaders.Authorization = `Bearer ${apiKey}`;\n\t\t}\n\t\treturn headers;\n\t}\n\n\tprotected async request<T>(\n\t\tmethod: string,\n\t\tpath: string,\n\t\tbody?: unknown,\n\t): Promise<T> {\n\t\tconst response = await this.fetchResponse(method, path, body);\n\t\tif (response.status === 204) {\n\t\t\treturn undefined as T;\n\t\t}\n\t\treturn response.json() as Promise<T>;\n\t}\n\n\tprotected async requestText(\n\t\tmethod: string,\n\t\tpath: string,\n\t\tbody?: unknown,\n\t): Promise<string> {\n\t\tconst response = await this.fetchResponse(method, path, body);\n\t\treturn response.text();\n\t}\n\n\tprivate async fetchResponse(\n\t\tmethod: string,\n\t\tpath: string,\n\t\tbody?: unknown,\n\t): Promise<Response> {\n\t\tconst url = `${this.baseUrl}${path}`;\n\t\tconst headers = BaseClient.authHeaders(this.apiKey);\n\t\theaders[\"x-sl-origin\"] = this.origin;\n\n\t\t// Serialize the body BEFORE the network try so a body-encoding error\n\t\t// (e.g. unsupported BigInt) isn't misreported as \"Cannot reach API\".\n\t\t// BigInts are stringified — server schemas accept jsonb so the value\n\t\t// reaches the server intact, and any field that needs an actual bigint\n\t\t// at runtime is rehydrated by the consuming module (subgraph handler\n\t\t// code preserves the literal). See packages/subgraphs source-matcher\n\t\t// for the post-load shape.\n\t\tlet serializedBody: string | undefined;\n\t\tif (body !== undefined && body !== null) {\n\t\t\ttry {\n\t\t\t\tserializedBody = JSON.stringify(body, (_key, value) =>\n\t\t\t\t\ttypeof value === \"bigint\" ? value.toString() : value,\n\t\t\t\t);\n\t\t\t} catch (err) {\n\t\t\t\tconst detail = err instanceof Error ? err.message : String(err);\n\t\t\t\tthrow new ApiError(0, `Failed to serialize request body: ${detail}`);\n\t\t\t}\n\t\t}\n\n\t\tlet response: Response;\n\t\ttry {\n\t\t\tresponse = await fetch(url, {\n\t\t\t\tmethod,\n\t\t\t\theaders,\n\t\t\t\tbody: serializedBody,\n\t\t\t});\n\t\t} catch {\n\t\t\tthrow new ApiError(\n\t\t\t\t0,\n\t\t\t\t`Cannot reach API at ${this.baseUrl}. Check your connection or try again.`,\n\t\t\t);\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tif (response.status === 401) {\n\t\t\t\tthrow new ApiError(401, \"API key invalid or expired.\");\n\t\t\t}\n\n\t\t\tif (response.status === 429) {\n\t\t\t\tconst retryAfter = response.headers.get(\"Retry-After\");\n\t\t\t\tconst msg = retryAfter\n\t\t\t\t\t? `Rate limited. Wait ${retryAfter} seconds.`\n\t\t\t\t\t: \"Rate limited. Try again later.\";\n\t\t\t\tthrow new ApiError(429, msg);\n\t\t\t}\n\n\t\t\tif (response.status >= 500) {\n\t\t\t\tthrow new ApiError(\n\t\t\t\t\tresponse.status,\n\t\t\t\t\t`Server error. Try again or check status at ${this.baseUrl}/health`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst errorBody = await response.text();\n\t\t\tlet message = `HTTP ${response.status}`;\n\t\t\tlet parsedBody: unknown = errorBody;\n\t\t\tlet code: string | undefined;\n\t\t\ttry {\n\t\t\t\tconst json = JSON.parse(errorBody);\n\t\t\t\tparsedBody = json;\n\t\t\t\tconst err = json.error ?? json.message;\n\t\t\t\tif (typeof err === \"string\") {\n\t\t\t\t\tmessage = err;\n\t\t\t\t} else if (err && typeof err === \"object\") {\n\t\t\t\t\tmessage = JSON.stringify(err);\n\t\t\t\t}\n\t\t\t\tif (typeof json.code === \"string\") {\n\t\t\t\t\tcode = json.code;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tif (errorBody) message = errorBody;\n\t\t\t}\n\t\t\tif (\n\t\t\t\tresponse.status === 422 &&\n\t\t\t\tcode === \"BYO_BREAKING_CHANGE\" &&\n\t\t\t\tparsedBody &&\n\t\t\t\ttypeof parsedBody === \"object\" &&\n\t\t\t\tisByoBreakingDetails((parsedBody as { details?: unknown }).details)\n\t\t\t) {\n\t\t\t\tthrow new ByoBreakingChangeError(\n\t\t\t\t\tmessage,\n\t\t\t\t\t(parsedBody as { details: ByoBreakingChangeDetails }).details,\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow new ApiError(response.status, message, parsedBody, code);\n\t\t}\n\n\t\treturn response;\n\t}\n}\n",
|
|
8
8
|
"export class AuthError extends Error {\n\treadonly status = 401;\n\n\tconstructor(message = \"API key invalid or expired.\") {\n\t\tsuper(message);\n\t\tthis.name = \"AuthError\";\n\t}\n}\n\nexport class RateLimitError extends Error {\n\treadonly status = 429;\n\n\tconstructor(\n\t\tmessage = \"Rate limited. Try again later.\",\n\t\treadonly retryAfter?: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"RateLimitError\";\n\t}\n}\n\nexport class ValidationError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\treadonly status: number,\n\t\treadonly body?: unknown,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"ValidationError\";\n\t}\n}\n\nexport class StreamsServerError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\treadonly status: number,\n\t\treadonly body?: unknown,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"StreamsServerError\";\n\t}\n}\n\n/** Thrown when response signature verification is enabled and fails. */\nexport class StreamsSignatureError extends Error {\n\tconstructor(message = \"Streams response signature verification failed.\") {\n\t\tsuper(message);\n\t\tthis.name = \"StreamsSignatureError\";\n\t}\n}\n",
|
|
9
9
|
"import { ValidationError } from \"./errors.ts\";\n\n/**\n * Largest value the `event_index` / `tx_index` cursor component can take —\n * Postgres int4 max. Used as the foot-of-block sentinel in {@link Cursor.atHeight}\n * so a rewind cursor sorts just below `height:0`. Mirrors the same int4-max\n * sentinel the server uses for reorg height-range scans and empty-range advance.\n */\nconst REWIND_FOOT_INDEX_SENTINEL = 2_147_483_647;\n\n/**\n * Helpers for Streams cursors. A cursor is the opaque `<block>:<index>` string\n * that marks a position in the event stream; treat the format as an\n * implementation detail and go through these helpers instead of string-building\n * it at call sites.\n */\nexport const Cursor = {\n\t/**\n\t * Cursor at the foot of `height` — a position that sorts strictly below the\n\t * first event of block `height` (`height:0`) and strictly above every event\n\t * of block `height - 1`. Cursors are exclusive (`(bh,ei) > after`), so\n\t * resuming from it re-reads the entire canonical run starting at `height:0`\n\t * inclusive. This is the position to rewind to after a reorg whose fork point\n\t * is `height`: the new canonical block at `height` carries a fresh first\n\t * event at `(height, 0)` that the consumer MUST re-read.\n\t *\n\t * Encoded as `${height-1}:${SENTINEL}` rather than the seemingly-natural\n\t * `${height}:0` — that earlier form was an off-by-one: being exclusive, it\n\t * skipped `(height, 0)`, silently dropping the fork block's first row on\n\t * every reorg. The sentinel is int4 max (the `event_index`/`tx_index` column\n\t * type), larger than any real index, so nothing at `height - 1` survives the\n\t * keyset and the next returned row is exactly `(height, 0)`.\n\t */\n\tatHeight(height: number): string {\n\t\t// Genesis can't reorg; degenerate-guard so `height - 1` never goes negative\n\t\t// (the cursor parsers reject negative components).\n\t\tif (height <= 0) return \"0:0\";\n\t\treturn `${height - 1}:${REWIND_FOOT_INDEX_SENTINEL}`;\n\t},\n\n\t/** Parse a `<block>:<index>` cursor. Throws `ValidationError` if malformed. */\n\tparse(cursor: string): { blockHeight: number; eventIndex: number } {\n\t\tconst parts = cursor.split(\":\");\n\t\tconst blockHeight = Number(parts[0]);\n\t\tconst eventIndex = Number(parts[1]);\n\t\tif (\n\t\t\tparts.length !== 2 ||\n\t\t\t!Number.isInteger(blockHeight) ||\n\t\t\t!Number.isInteger(eventIndex)\n\t\t) {\n\t\t\tthrow new ValidationError(\n\t\t\t\t`Invalid stream cursor \"${cursor}\"; expected \"<block>:<index>\" (e.g. \"951475:3\").`,\n\t\t\t\t400,\n\t\t\t);\n\t\t}\n\t\treturn { blockHeight, eventIndex };\n\t},\n};\n",
|
|
10
10
|
"import { Cursor } from \"./cursor.ts\";\nimport type {\n\tStreamsBatch,\n\tStreamsEvent,\n\tStreamsEventType,\n\tStreamsEventsEnvelope,\n\tStreamsFilterValue,\n\tStreamsReorg,\n} from \"./types.ts\";\n\n/** Stable identity of a reorg, for in-memory dedup across re-reported pages. */\nfunction reorgKey(reorg: StreamsReorg): string {\n\treturn `${reorg.detected_at}|${reorg.fork_point_height}|${reorg.new_canonical_tip}`;\n}\n\ntype StreamsEventsFetchParams = {\n\tcursor?: string | null;\n\tlimit: number;\n\ttypes?: readonly StreamsEventType[];\n\tnotTypes?: readonly StreamsEventType[];\n\tcontractId?: StreamsFilterValue;\n\tsender?: StreamsFilterValue;\n\trecipient?: StreamsFilterValue;\n\tassetIdentifier?: string;\n};\n\nexport type StreamsEventsFetcher = (\n\tparams: StreamsEventsFetchParams,\n) => Promise<StreamsEventsEnvelope>;\n\nexport type Sleep = (ms: number, signal?: AbortSignal) => Promise<void>;\n\nexport async function defaultSleep(\n\tms: number,\n\tsignal?: AbortSignal,\n): Promise<void> {\n\tif (signal?.aborted) return;\n\n\tawait new Promise<void>((resolve) => {\n\t\tconst timeout = setTimeout(resolve, ms);\n\t\tif (!signal) return;\n\t\tsignal.addEventListener(\n\t\t\t\"abort\",\n\t\t\t() => {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\tresolve();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t});\n}\n\nexport async function consumeStreamsEvents(opts: {\n\tfromCursor?: string | null;\n\tmode?: \"tail\" | \"bounded\";\n\tfinalizedOnly?: boolean;\n\tbatchSize: number;\n\ttypes?: readonly StreamsEventType[];\n\tnotTypes?: readonly StreamsEventType[];\n\tcontractId?: StreamsFilterValue;\n\tsender?: StreamsFilterValue;\n\trecipient?: StreamsFilterValue;\n\tassetIdentifier?: string;\n\tfetchEvents: StreamsEventsFetcher;\n\tonBatch: (\n\t\tevents: StreamsEvent[],\n\t\tenvelope: StreamsEventsEnvelope,\n\t\tctx: { cursor: string | null },\n\t) =>\n\t\t| void\n\t\t| string\n\t\t| null\n\t\t| undefined\n\t\t| Promise<void>\n\t\t| Promise<string | null | undefined>;\n\tonReorg?: (\n\t\treorg: StreamsReorg,\n\t\tctx: { cursor: string },\n\t) => Promise<void> | void;\n\tsleep?: Sleep;\n\temptyBackoffMs?: number;\n\tmaxPages?: number;\n\tmaxEmptyPolls?: number;\n\tsignal?: AbortSignal;\n}): Promise<{ cursor: string | null; pages: number; emptyPolls: number }> {\n\tconst sleep = opts.sleep ?? defaultSleep;\n\tconst mode = opts.mode ?? \"tail\";\n\tconst finalizedOnly = opts.finalizedOnly ?? false;\n\tconst emptyBackoffMs = opts.emptyBackoffMs ?? 500;\n\tconst maxPages = opts.maxPages ?? Number.POSITIVE_INFINITY;\n\tconst maxEmptyPolls = opts.maxEmptyPolls ?? Number.POSITIVE_INFINITY;\n\tlet cursor = opts.fromCursor ?? null;\n\t// In-memory only: rollback is idempotent, so a crash before the rewind is\n\t// re-detected and re-applied harmlessly on restart — no need to persist.\n\tconst handledReorgs = new Set<string>();\n\tlet pages = 0;\n\tlet emptyPolls = 0;\n\n\twhile (\n\t\tpages < maxPages &&\n\t\temptyPolls < maxEmptyPolls &&\n\t\t!opts.signal?.aborted\n\t) {\n\t\tconst envelope = await opts.fetchEvents({\n\t\t\tcursor,\n\t\t\tlimit: opts.batchSize,\n\t\t\ttypes: opts.types,\n\t\t\tnotTypes: opts.notTypes,\n\t\t\tcontractId: opts.contractId,\n\t\t\tsender: opts.sender,\n\t\t\trecipient: opts.recipient,\n\t\t\tassetIdentifier: opts.assetIdentifier,\n\t\t});\n\t\tpages++;\n\n\t\t// Reorgs: roll back each new fork, then rewind to the lowest fork point\n\t\t// and re-read the now-canonical run. Finalized data never reorgs, so\n\t\t// `finalizedOnly` skips this entirely.\n\t\tif (!finalizedOnly && opts.onReorg) {\n\t\t\tconst fresh = envelope.reorgs\n\t\t\t\t.filter((reorg) => !handledReorgs.has(reorgKey(reorg)))\n\t\t\t\t.sort((a, b) => a.fork_point_height - b.fork_point_height);\n\t\t\tif (fresh.length > 0) {\n\t\t\t\tconst forkPoint = Math.min(\n\t\t\t\t\t...fresh.map((reorg) => reorg.fork_point_height),\n\t\t\t\t);\n\t\t\t\tconst rewind = Cursor.atHeight(forkPoint);\n\t\t\t\tfor (const reorg of fresh) {\n\t\t\t\t\tawait opts.onReorg(reorg, { cursor: rewind });\n\t\t\t\t\thandledReorgs.add(reorgKey(reorg));\n\t\t\t\t}\n\t\t\t\tcursor = rewind;\n\t\t\t\temptyPolls = 0;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\n\t\tconst emitted = finalizedOnly\n\t\t\t? envelope.events.filter((event) => event.finalized)\n\t\t\t: envelope.events;\n\t\t// Only advance to the last finalized event in finalizedOnly mode; the\n\t\t// unfinalized tail is re-read next poll until it settles.\n\t\tconst checkpoint = finalizedOnly\n\t\t\t? (emitted.at(-1)?.cursor ?? cursor)\n\t\t\t: envelope.next_cursor;\n\n\t\tconst returnedCursor = await opts.onBatch(emitted, envelope, {\n\t\t\tcursor: checkpoint,\n\t\t});\n\t\tconst nextCursor = returnedCursor ?? checkpoint;\n\n\t\tif (nextCursor && nextCursor !== cursor) {\n\t\t\tcursor = nextCursor;\n\t\t\temptyPolls = 0;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (emitted.length === 0) {\n\t\t\temptyPolls++;\n\t\t\tif (mode === \"bounded\") {\n\t\t\t\treturn { cursor, pages, emptyPolls };\n\t\t\t}\n\t\t\tawait sleep(emptyBackoffMs, opts.signal);\n\t\t\tcontinue;\n\t\t}\n\n\t\treturn { cursor, pages, emptyPolls };\n\t}\n\n\treturn { cursor, pages, emptyPolls };\n}\n\n/**\n * Async-iterator form of the Streams pull loop, yielding one {@link StreamsBatch}\n * per fetched page. Each yield maps 1:1 onto a `GET /v1/streams/events` envelope\n * (`{ events, next_cursor, tip, reorgs }` → `{ events, cursor, tip, reorgs }`) —\n * no extra API calls, no regrouping. Pages with no events and no reorgs are not\n * yielded; the iterator sleeps `intervalMs` and re-polls the tip instead.\n */\nexport async function* iterateStreamsBatches(opts: {\n\tfromCursor?: string | null;\n\tbatchSize: number;\n\tintervalMs: number;\n\ttypes?: readonly StreamsEventType[];\n\tnotTypes?: readonly StreamsEventType[];\n\tcontractId?: StreamsFilterValue;\n\tsender?: StreamsFilterValue;\n\trecipient?: StreamsFilterValue;\n\tassetIdentifier?: string;\n\tfetchEvents: StreamsEventsFetcher;\n\tsleep?: Sleep;\n\tsignal?: AbortSignal;\n}): AsyncGenerator<StreamsBatch> {\n\tconst sleep = opts.sleep ?? defaultSleep;\n\tlet cursor = opts.fromCursor ?? null;\n\n\twhile (!opts.signal?.aborted) {\n\t\tconst envelope = await opts.fetchEvents({\n\t\t\tcursor,\n\t\t\tlimit: opts.batchSize,\n\t\t\ttypes: opts.types,\n\t\t\tnotTypes: opts.notTypes,\n\t\t\tcontractId: opts.contractId,\n\t\t\tsender: opts.sender,\n\t\t\trecipient: opts.recipient,\n\t\t\tassetIdentifier: opts.assetIdentifier,\n\t\t});\n\n\t\tconst checkpoint = envelope.next_cursor ?? cursor;\n\t\tif (envelope.events.length > 0 || envelope.reorgs.length > 0) {\n\t\t\tyield {\n\t\t\t\tevents: envelope.events,\n\t\t\t\tcursor: checkpoint,\n\t\t\t\ttip: envelope.tip,\n\t\t\t\treorgs: envelope.reorgs,\n\t\t\t};\n\t\t}\n\n\t\tconst advanced = checkpoint !== null && checkpoint !== cursor;\n\t\tcursor = checkpoint;\n\t\t// Caught up at the tip: wait one poll interval before re-reading.\n\t\tif (!advanced && envelope.events.length === 0) {\n\t\t\tif (opts.signal?.aborted) return;\n\t\t\tawait sleep(opts.intervalMs, opts.signal);\n\t\t}\n\t}\n}\n\nexport async function* streamStreamsEvents(opts: {\n\tfromCursor?: string | null;\n\tbatchSize: number;\n\ttypes?: readonly StreamsEventType[];\n\tnotTypes?: readonly StreamsEventType[];\n\tcontractId?: StreamsFilterValue;\n\tsender?: StreamsFilterValue;\n\trecipient?: StreamsFilterValue;\n\tassetIdentifier?: string;\n\tfetchEvents: StreamsEventsFetcher;\n\tsleep?: Sleep;\n\temptyBackoffMs?: number;\n\tmaxPages?: number;\n\tmaxEmptyPolls?: number;\n\tsignal?: AbortSignal;\n}): AsyncGenerator<StreamsEvent> {\n\tconst sleep = opts.sleep ?? defaultSleep;\n\tconst emptyBackoffMs = opts.emptyBackoffMs ?? 500;\n\tconst maxPages = opts.maxPages ?? Number.POSITIVE_INFINITY;\n\tconst maxEmptyPolls = opts.maxEmptyPolls ?? Number.POSITIVE_INFINITY;\n\tlet cursor = opts.fromCursor ?? null;\n\tlet pages = 0;\n\tlet emptyPolls = 0;\n\n\twhile (\n\t\tpages < maxPages &&\n\t\temptyPolls < maxEmptyPolls &&\n\t\t!opts.signal?.aborted\n\t) {\n\t\tconst envelope = await opts.fetchEvents({\n\t\t\tcursor,\n\t\t\tlimit: opts.batchSize,\n\t\t\ttypes: opts.types,\n\t\t\tnotTypes: opts.notTypes,\n\t\t\tcontractId: opts.contractId,\n\t\t\tsender: opts.sender,\n\t\t\trecipient: opts.recipient,\n\t\t\tassetIdentifier: opts.assetIdentifier,\n\t\t});\n\t\tpages++;\n\n\t\tfor (const event of envelope.events) {\n\t\t\tif (opts.signal?.aborted) return;\n\t\t\tyield event;\n\t\t}\n\n\t\tconst nextCursor = envelope.next_cursor;\n\t\tif (nextCursor && nextCursor !== cursor) {\n\t\t\tcursor = nextCursor;\n\t\t\temptyPolls = 0;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (envelope.events.length === 0) {\n\t\t\temptyPolls++;\n\t\t\tif (emptyPolls >= maxEmptyPolls || pages >= maxPages) return;\n\t\t\tawait sleep(emptyBackoffMs, opts.signal);\n\t\t\tcontinue;\n\t\t}\n\n\t\treturn;\n\t}\n}\n",
|
|
@@ -1337,6 +1337,265 @@ type PrintSchemaResponse = {
|
|
|
1337
1337
|
}
|
|
1338
1338
|
tip: IndexTip
|
|
1339
1339
|
};
|
|
1340
|
+
/** The decoded `sbtc_events` topics. */
|
|
1341
|
+
type SbtcEventTopic = "completed-deposit" | "withdrawal-create" | "withdrawal-accept" | "withdrawal-reject" | "key-rotation" | "update-protocol-contract";
|
|
1342
|
+
type SbtcWithdrawalStatus = "REQUESTED" | "ACCEPTED" | "REJECTED";
|
|
1343
|
+
/** A completed sBTC peg-in, keyed by `bitcoin_txid` (deposits carry no
|
|
1344
|
+
* `request_id`). One terminal `completed-deposit` event per deposit. */
|
|
1345
|
+
type IndexSbtcDeposit = {
|
|
1346
|
+
cursor: string
|
|
1347
|
+
block_height: number
|
|
1348
|
+
block_time?: string | null
|
|
1349
|
+
tx_id: string
|
|
1350
|
+
tx_index: number
|
|
1351
|
+
event_index: number
|
|
1352
|
+
amount: string | null
|
|
1353
|
+
sender: string | null
|
|
1354
|
+
bitcoin_txid: string | null
|
|
1355
|
+
output_index: number | null
|
|
1356
|
+
recipient_btc_version: number | null
|
|
1357
|
+
recipient_btc_hashbytes: string | null
|
|
1358
|
+
};
|
|
1359
|
+
/** A deposit fetched by Bitcoin txid — always terminal, hence `status`. */
|
|
1360
|
+
type IndexSbtcDepositDetail = IndexSbtcDeposit & {
|
|
1361
|
+
status: "COMPLETED"
|
|
1362
|
+
};
|
|
1363
|
+
/** A peg-out collapsed to one row per `request_id`, with lifecycle `status`
|
|
1364
|
+
* derived from the latest accept/reject. */
|
|
1365
|
+
type IndexSbtcWithdrawal = {
|
|
1366
|
+
cursor: string
|
|
1367
|
+
request_id: number
|
|
1368
|
+
status: SbtcWithdrawalStatus
|
|
1369
|
+
amount: string | null
|
|
1370
|
+
sender: string | null
|
|
1371
|
+
recipient_btc_version: number | null
|
|
1372
|
+
recipient_btc_hashbytes: string | null
|
|
1373
|
+
sweep_txid: string | null
|
|
1374
|
+
requested_at?: string | null
|
|
1375
|
+
resolved_at?: string | null
|
|
1376
|
+
};
|
|
1377
|
+
/** One phase of a withdrawal's lifecycle (the on-Stacks event that drove it). */
|
|
1378
|
+
type IndexSbtcWithdrawalPhase = {
|
|
1379
|
+
block_height: number
|
|
1380
|
+
block_time: string | null
|
|
1381
|
+
tx_id: string
|
|
1382
|
+
};
|
|
1383
|
+
/** A single peg-out's full assembled lifecycle, fetched by `request_id`.
|
|
1384
|
+
* `finalized` is true once terminal and past the reorg margin. */
|
|
1385
|
+
type IndexSbtcWithdrawalDetail = {
|
|
1386
|
+
request_id: number
|
|
1387
|
+
status: SbtcWithdrawalStatus
|
|
1388
|
+
amount: string | null
|
|
1389
|
+
sender: string | null
|
|
1390
|
+
recipient_btc_version: number | null
|
|
1391
|
+
recipient_btc_hashbytes: string | null
|
|
1392
|
+
requested: IndexSbtcWithdrawalPhase
|
|
1393
|
+
accepted: (IndexSbtcWithdrawalPhase & {
|
|
1394
|
+
sweep_txid: string | null
|
|
1395
|
+
signer_bitmap: string | null
|
|
1396
|
+
}) | null
|
|
1397
|
+
rejected: IndexSbtcWithdrawalPhase | null
|
|
1398
|
+
settlement: {
|
|
1399
|
+
sweep_txid: string | null
|
|
1400
|
+
btc_confirmations: number | null
|
|
1401
|
+
settlement_confirmed: boolean | null
|
|
1402
|
+
}
|
|
1403
|
+
finalized: boolean
|
|
1404
|
+
};
|
|
1405
|
+
/** A raw decoded sBTC protocol-state event — the full `sbtc_events` row, one
|
|
1406
|
+
* per event across all topics. */
|
|
1407
|
+
type IndexSbtcEvent = {
|
|
1408
|
+
cursor: string
|
|
1409
|
+
block_height: number
|
|
1410
|
+
block_time?: string | null
|
|
1411
|
+
tx_id: string
|
|
1412
|
+
tx_index: number
|
|
1413
|
+
event_index: number
|
|
1414
|
+
topic: SbtcEventTopic
|
|
1415
|
+
request_id: number | null
|
|
1416
|
+
amount: string | null
|
|
1417
|
+
sender: string | null
|
|
1418
|
+
recipient_btc_version: number | null
|
|
1419
|
+
recipient_btc_hashbytes: string | null
|
|
1420
|
+
bitcoin_txid: string | null
|
|
1421
|
+
output_index: number | null
|
|
1422
|
+
sweep_txid: string | null
|
|
1423
|
+
burn_hash: string | null
|
|
1424
|
+
burn_height: number | null
|
|
1425
|
+
signer_bitmap: string | null
|
|
1426
|
+
max_fee: string | null
|
|
1427
|
+
fee: string | null
|
|
1428
|
+
governance_contract_type: number | null
|
|
1429
|
+
governance_new_contract: string | null
|
|
1430
|
+
signer_aggregate_pubkey: string | null
|
|
1431
|
+
signer_threshold: number | null
|
|
1432
|
+
signer_address: string | null
|
|
1433
|
+
signer_keys_count: number | null
|
|
1434
|
+
};
|
|
1435
|
+
/** The peg "scoreboard" — a single all-time canonical aggregate. */
|
|
1436
|
+
type IndexSbtcSummary = {
|
|
1437
|
+
total_deposits: number
|
|
1438
|
+
total_withdrawals_requested: number
|
|
1439
|
+
total_withdrawals_accepted: number
|
|
1440
|
+
total_withdrawals_rejected: number
|
|
1441
|
+
net_peg_flow_sats: string
|
|
1442
|
+
total_locked_sats: string
|
|
1443
|
+
sbtc_supply_sats: string | null
|
|
1444
|
+
};
|
|
1445
|
+
type SbtcDepositsEnvelope = {
|
|
1446
|
+
deposits: IndexSbtcDeposit[]
|
|
1447
|
+
next_cursor: string | null
|
|
1448
|
+
tip: IndexTip
|
|
1449
|
+
reorgs: IndexReorg[]
|
|
1450
|
+
/** Present only when the sBTC decoder is disabled, explaining an empty feed. */
|
|
1451
|
+
notes?: string
|
|
1452
|
+
};
|
|
1453
|
+
type SbtcDepositEnvelope = {
|
|
1454
|
+
deposit: IndexSbtcDepositDetail
|
|
1455
|
+
tip: IndexTip
|
|
1456
|
+
};
|
|
1457
|
+
type SbtcWithdrawalsEnvelope = {
|
|
1458
|
+
withdrawals: IndexSbtcWithdrawal[]
|
|
1459
|
+
next_cursor: string | null
|
|
1460
|
+
tip: IndexTip
|
|
1461
|
+
reorgs: IndexReorg[]
|
|
1462
|
+
notes?: string
|
|
1463
|
+
};
|
|
1464
|
+
type SbtcWithdrawalEnvelope = {
|
|
1465
|
+
withdrawal: IndexSbtcWithdrawalDetail
|
|
1466
|
+
tip: IndexTip
|
|
1467
|
+
};
|
|
1468
|
+
type SbtcEventsEnvelope = {
|
|
1469
|
+
events: IndexSbtcEvent[]
|
|
1470
|
+
next_cursor: string | null
|
|
1471
|
+
tip: IndexTip
|
|
1472
|
+
reorgs: IndexReorg[]
|
|
1473
|
+
notes?: string
|
|
1474
|
+
};
|
|
1475
|
+
type SbtcSummaryEnvelope = {
|
|
1476
|
+
summary: IndexSbtcSummary
|
|
1477
|
+
tip: IndexTip
|
|
1478
|
+
notes?: string
|
|
1479
|
+
};
|
|
1480
|
+
type SbtcDepositsListParams = {
|
|
1481
|
+
cursor?: string | null
|
|
1482
|
+
fromCursor?: string | null
|
|
1483
|
+
limit?: number
|
|
1484
|
+
/** Clamp to the finality boundary — only settled deposits past the reorg margin. */
|
|
1485
|
+
confirmed?: boolean
|
|
1486
|
+
sender?: string
|
|
1487
|
+
bitcoinTxid?: string
|
|
1488
|
+
fromHeight?: number
|
|
1489
|
+
toHeight?: number
|
|
1490
|
+
};
|
|
1491
|
+
type SbtcDepositsWalkParams = Omit<SbtcDepositsListParams, "limit"> & {
|
|
1492
|
+
batchSize?: number
|
|
1493
|
+
signal?: AbortSignal
|
|
1494
|
+
};
|
|
1495
|
+
type SbtcWithdrawalsListParams = {
|
|
1496
|
+
cursor?: string | null
|
|
1497
|
+
fromCursor?: string | null
|
|
1498
|
+
limit?: number
|
|
1499
|
+
confirmed?: boolean
|
|
1500
|
+
status?: SbtcWithdrawalStatus
|
|
1501
|
+
sender?: string
|
|
1502
|
+
requestId?: number
|
|
1503
|
+
fromHeight?: number
|
|
1504
|
+
toHeight?: number
|
|
1505
|
+
};
|
|
1506
|
+
type SbtcWithdrawalsWalkParams = Omit<SbtcWithdrawalsListParams, "limit"> & {
|
|
1507
|
+
batchSize?: number
|
|
1508
|
+
signal?: AbortSignal
|
|
1509
|
+
};
|
|
1510
|
+
type SbtcEventsListParams = {
|
|
1511
|
+
cursor?: string | null
|
|
1512
|
+
fromCursor?: string | null
|
|
1513
|
+
limit?: number
|
|
1514
|
+
confirmed?: boolean
|
|
1515
|
+
topic?: SbtcEventTopic
|
|
1516
|
+
sender?: string
|
|
1517
|
+
requestId?: number
|
|
1518
|
+
bitcoinTxid?: string
|
|
1519
|
+
fromHeight?: number
|
|
1520
|
+
toHeight?: number
|
|
1521
|
+
};
|
|
1522
|
+
type SbtcEventsWalkParams = Omit<SbtcEventsListParams, "limit"> & {
|
|
1523
|
+
batchSize?: number
|
|
1524
|
+
signal?: AbortSignal
|
|
1525
|
+
};
|
|
1526
|
+
/** `index.sbtc` — the decoded sBTC peg surface (deposits, withdrawals, raw
|
|
1527
|
+
* events, scoreboard). The only productized decoded sBTC peg feed on Stacks. */
|
|
1528
|
+
interface SbtcResource {
|
|
1529
|
+
deposits: {
|
|
1530
|
+
list(params?: SbtcDepositsListParams): Promise<SbtcDepositsEnvelope>
|
|
1531
|
+
walk(params?: SbtcDepositsWalkParams): AsyncIterable<IndexSbtcDeposit>
|
|
1532
|
+
/** Fetch a completed deposit by Bitcoin txid; 404 → null. */
|
|
1533
|
+
get(bitcoinTxid: string): Promise<SbtcDepositEnvelope | null>
|
|
1534
|
+
};
|
|
1535
|
+
withdrawals: {
|
|
1536
|
+
list(params?: SbtcWithdrawalsListParams): Promise<SbtcWithdrawalsEnvelope>
|
|
1537
|
+
walk(params?: SbtcWithdrawalsWalkParams): AsyncIterable<IndexSbtcWithdrawal>
|
|
1538
|
+
/** Fetch a withdrawal's full lifecycle by request id; 404 → null. */
|
|
1539
|
+
get(requestId: number): Promise<SbtcWithdrawalEnvelope | null>
|
|
1540
|
+
};
|
|
1541
|
+
events: {
|
|
1542
|
+
list(params?: SbtcEventsListParams): Promise<SbtcEventsEnvelope>
|
|
1543
|
+
walk(params?: SbtcEventsWalkParams): AsyncIterable<IndexSbtcEvent>
|
|
1544
|
+
};
|
|
1545
|
+
/** The peg scoreboard — a single all-time aggregate (no pagination). */
|
|
1546
|
+
summary(): Promise<SbtcSummaryEnvelope>;
|
|
1547
|
+
}
|
|
1548
|
+
/** Per-function action count within a reward cycle. */
|
|
1549
|
+
type IndexPoxFunctionCount = {
|
|
1550
|
+
function_name: string
|
|
1551
|
+
count: number
|
|
1552
|
+
};
|
|
1553
|
+
/** Aggregate stats for one PoX reward cycle — distinct from `index.stacking`
|
|
1554
|
+
* (decoded per-call PoX-4 actions); this is the reward-cycle rollup. */
|
|
1555
|
+
type IndexPoxCycle = {
|
|
1556
|
+
reward_cycle: number
|
|
1557
|
+
/** Total ustx locked across all stack-* calls in this cycle (bigint-safe string). */
|
|
1558
|
+
total_stacked_ustx: string
|
|
1559
|
+
unique_stackers: number
|
|
1560
|
+
unique_delegators: number
|
|
1561
|
+
action_count: number
|
|
1562
|
+
start_block_height: number
|
|
1563
|
+
end_block_height: number
|
|
1564
|
+
/** True when this is the latest reward cycle (still accumulating new actions). */
|
|
1565
|
+
is_current: boolean
|
|
1566
|
+
function_breakdown: IndexPoxFunctionCount[]
|
|
1567
|
+
};
|
|
1568
|
+
type PoxCyclesEnvelope = {
|
|
1569
|
+
cycles: IndexPoxCycle[]
|
|
1570
|
+
/** The next `reward_cycle` to page from (cycles descend), or null at the end. */
|
|
1571
|
+
next_cursor: number | null
|
|
1572
|
+
tip: IndexTip
|
|
1573
|
+
/** Present only when the PoX-4 decoder is disabled, explaining an empty feed. */
|
|
1574
|
+
notes?: string
|
|
1575
|
+
};
|
|
1576
|
+
type PoxCycleEnvelope = {
|
|
1577
|
+
cycle: IndexPoxCycle
|
|
1578
|
+
tip: IndexTip
|
|
1579
|
+
notes?: string
|
|
1580
|
+
};
|
|
1581
|
+
type PoxCyclesListParams = {
|
|
1582
|
+
/** A `reward_cycle` to page from; the page returns cycles below it (descending). */
|
|
1583
|
+
cursor?: number | null
|
|
1584
|
+
limit?: number
|
|
1585
|
+
};
|
|
1586
|
+
type PoxCyclesWalkParams = Omit<PoxCyclesListParams, "limit"> & {
|
|
1587
|
+
batchSize?: number
|
|
1588
|
+
signal?: AbortSignal
|
|
1589
|
+
};
|
|
1590
|
+
/** `index.pox` — PoX reward-cycle aggregates. */
|
|
1591
|
+
interface PoxResource {
|
|
1592
|
+
cycles: {
|
|
1593
|
+
list(params?: PoxCyclesListParams): Promise<PoxCyclesEnvelope>
|
|
1594
|
+
walk(params?: PoxCyclesWalkParams): AsyncIterable<IndexPoxCycle>
|
|
1595
|
+
/** Fetch one reward cycle's aggregate by number; 404 → null. */
|
|
1596
|
+
get(rewardCycle: number): Promise<PoxCycleEnvelope | null>
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1340
1599
|
/**
|
|
1341
1600
|
* `index.ftTransfers` — callable shorthand for `.list()`, with `.list`/`.walk`
|
|
1342
1601
|
* still available: `await sl.index.ftTransfers({ contractId })`.
|
|
@@ -1445,6 +1704,12 @@ declare class Index extends BaseClient {
|
|
|
1445
1704
|
walk: (params?: MempoolWalkParams) => AsyncIterable<IndexMempoolTransaction>
|
|
1446
1705
|
get: (txId: string) => Promise<MempoolTransactionEnvelope | null>
|
|
1447
1706
|
};
|
|
1707
|
+
/** Decoded sBTC peg surface — the only productized decoded sBTC peg feed on
|
|
1708
|
+
* Stacks. `deposits`/`withdrawals`/`events` paginate; `summary` is a scalar. */
|
|
1709
|
+
readonly sbtc: SbtcResource;
|
|
1710
|
+
/** PoX reward-cycle aggregates. Distinct from `stacking` (per-call PoX-4
|
|
1711
|
+
* actions); `cycles` is the reward-cycle rollup. */
|
|
1712
|
+
readonly pox: PoxResource;
|
|
1448
1713
|
private listFtTransfers;
|
|
1449
1714
|
private listNftTransfers;
|
|
1450
1715
|
private walkFtTransfers;
|
|
@@ -1471,6 +1736,18 @@ declare class Index extends BaseClient {
|
|
|
1471
1736
|
private listMempool;
|
|
1472
1737
|
private getMempoolTx;
|
|
1473
1738
|
private walkMempool;
|
|
1739
|
+
private listSbtcDeposits;
|
|
1740
|
+
private walkSbtcDeposits;
|
|
1741
|
+
private getSbtcDeposit;
|
|
1742
|
+
private listSbtcWithdrawals;
|
|
1743
|
+
private walkSbtcWithdrawals;
|
|
1744
|
+
private getSbtcWithdrawal;
|
|
1745
|
+
private listSbtcEvents;
|
|
1746
|
+
private walkSbtcEvents;
|
|
1747
|
+
private getSbtcSummary;
|
|
1748
|
+
private listPoxCycles;
|
|
1749
|
+
private walkPoxCycles;
|
|
1750
|
+
private getPoxCycle;
|
|
1474
1751
|
}
|
|
1475
1752
|
/**
|
|
1476
1753
|
* Typed client for project management (`/api/projects/*`).
|
package/dist/subgraphs/index.js
CHANGED
|
@@ -234,11 +234,7 @@ function buildAggregateQueryString(params) {
|
|
|
234
234
|
return str ? `?${str}` : "";
|
|
235
235
|
}
|
|
236
236
|
function buildSpecQueryString(options) {
|
|
237
|
-
|
|
238
|
-
if (options?.serverUrl)
|
|
239
|
-
qs.set("server", options.serverUrl);
|
|
240
|
-
const str = qs.toString();
|
|
241
|
-
return str ? `?${str}` : "";
|
|
237
|
+
return buildQuery({ server: options?.serverUrl });
|
|
242
238
|
}
|
|
243
239
|
|
|
244
240
|
class Subgraphs extends BaseClient {
|
|
@@ -267,18 +263,15 @@ class Subgraphs extends BaseClient {
|
|
|
267
263
|
return this.request("POST", `/api/subgraphs/${name}/backfill`, options);
|
|
268
264
|
}
|
|
269
265
|
async gaps(name, opts) {
|
|
270
|
-
const qs =
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
qs.set("resolved", String(opts.resolved));
|
|
277
|
-
const query = qs.toString();
|
|
278
|
-
return this.request("GET", `/api/subgraphs/${name}/gaps${query ? `?${query}` : ""}`);
|
|
266
|
+
const qs = buildQuery({
|
|
267
|
+
_limit: opts?.limit,
|
|
268
|
+
_offset: opts?.offset,
|
|
269
|
+
resolved: opts?.resolved
|
|
270
|
+
});
|
|
271
|
+
return this.request("GET", `/api/subgraphs/${name}/gaps${qs}`);
|
|
279
272
|
}
|
|
280
273
|
async delete(name, options) {
|
|
281
|
-
const qs = options?.force ?
|
|
274
|
+
const qs = buildQuery({ force: options?.force ? true : undefined });
|
|
282
275
|
return this.request("DELETE", `/api/subgraphs/${name}${qs}`);
|
|
283
276
|
}
|
|
284
277
|
async publish(name) {
|
|
@@ -818,6 +811,30 @@ class Index extends BaseClient {
|
|
|
818
811
|
walk: (params = {}) => this.walkMempool(params),
|
|
819
812
|
get: (txId) => this.getMempoolTx(txId)
|
|
820
813
|
};
|
|
814
|
+
sbtc = {
|
|
815
|
+
deposits: {
|
|
816
|
+
list: (params = {}) => this.listSbtcDeposits(params),
|
|
817
|
+
walk: (params = {}) => this.walkSbtcDeposits(params),
|
|
818
|
+
get: (bitcoinTxid) => this.getSbtcDeposit(bitcoinTxid)
|
|
819
|
+
},
|
|
820
|
+
withdrawals: {
|
|
821
|
+
list: (params = {}) => this.listSbtcWithdrawals(params),
|
|
822
|
+
walk: (params = {}) => this.walkSbtcWithdrawals(params),
|
|
823
|
+
get: (requestId) => this.getSbtcWithdrawal(requestId)
|
|
824
|
+
},
|
|
825
|
+
events: {
|
|
826
|
+
list: (params = {}) => this.listSbtcEvents(params),
|
|
827
|
+
walk: (params = {}) => this.walkSbtcEvents(params)
|
|
828
|
+
},
|
|
829
|
+
summary: () => this.getSbtcSummary()
|
|
830
|
+
};
|
|
831
|
+
pox = {
|
|
832
|
+
cycles: {
|
|
833
|
+
list: (params = {}) => this.listPoxCycles(params),
|
|
834
|
+
walk: (params = {}) => this.walkPoxCycles(params),
|
|
835
|
+
get: (rewardCycle) => this.getPoxCycle(rewardCycle)
|
|
836
|
+
}
|
|
837
|
+
};
|
|
821
838
|
async listFtTransfers(params = {}) {
|
|
822
839
|
return this.request("GET", `/v1/index/ft-transfers${buildQuery({
|
|
823
840
|
cursor: params.cursor,
|
|
@@ -1184,6 +1201,173 @@ class Index extends BaseClient {
|
|
|
1184
1201
|
firstPage = false;
|
|
1185
1202
|
}
|
|
1186
1203
|
}
|
|
1204
|
+
async listSbtcDeposits(params = {}) {
|
|
1205
|
+
return this.request("GET", `/v1/index/sbtc/deposits${buildQuery({
|
|
1206
|
+
cursor: params.cursor,
|
|
1207
|
+
from_cursor: params.fromCursor,
|
|
1208
|
+
limit: params.limit,
|
|
1209
|
+
confirmed: params.confirmed,
|
|
1210
|
+
sender: params.sender,
|
|
1211
|
+
bitcoin_txid: params.bitcoinTxid,
|
|
1212
|
+
from_height: params.fromHeight,
|
|
1213
|
+
to_height: params.toHeight
|
|
1214
|
+
})}`);
|
|
1215
|
+
}
|
|
1216
|
+
async* walkSbtcDeposits(params = {}) {
|
|
1217
|
+
const batchSize = params.batchSize ?? 200;
|
|
1218
|
+
let cursor = params.cursor ?? params.fromCursor ?? null;
|
|
1219
|
+
let firstPage = true;
|
|
1220
|
+
while (!params.signal?.aborted) {
|
|
1221
|
+
const envelope = await this.listSbtcDeposits({
|
|
1222
|
+
...params,
|
|
1223
|
+
limit: batchSize,
|
|
1224
|
+
cursor: firstPage ? params.cursor : cursor,
|
|
1225
|
+
fromCursor: firstPage ? params.fromCursor : undefined,
|
|
1226
|
+
fromHeight: firstPage ? firstWalkFromHeight(params) : undefined
|
|
1227
|
+
});
|
|
1228
|
+
for (const deposit of envelope.deposits) {
|
|
1229
|
+
if (params.signal?.aborted)
|
|
1230
|
+
return;
|
|
1231
|
+
yield deposit;
|
|
1232
|
+
}
|
|
1233
|
+
const nextCursor = envelope.next_cursor;
|
|
1234
|
+
if (!nextCursor || nextCursor === cursor || envelope.deposits.length < batchSize) {
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
cursor = nextCursor;
|
|
1238
|
+
firstPage = false;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
async getSbtcDeposit(bitcoinTxid) {
|
|
1242
|
+
try {
|
|
1243
|
+
return await this.request("GET", `/v1/index/sbtc/deposits/${encodeURIComponent(bitcoinTxid)}`);
|
|
1244
|
+
} catch (err) {
|
|
1245
|
+
if (err instanceof ApiError && err.status === 404)
|
|
1246
|
+
return null;
|
|
1247
|
+
throw err;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
async listSbtcWithdrawals(params = {}) {
|
|
1251
|
+
return this.request("GET", `/v1/index/sbtc/withdrawals${buildQuery({
|
|
1252
|
+
cursor: params.cursor,
|
|
1253
|
+
from_cursor: params.fromCursor,
|
|
1254
|
+
limit: params.limit,
|
|
1255
|
+
confirmed: params.confirmed,
|
|
1256
|
+
status: params.status,
|
|
1257
|
+
sender: params.sender,
|
|
1258
|
+
request_id: params.requestId,
|
|
1259
|
+
from_height: params.fromHeight,
|
|
1260
|
+
to_height: params.toHeight
|
|
1261
|
+
})}`);
|
|
1262
|
+
}
|
|
1263
|
+
async* walkSbtcWithdrawals(params = {}) {
|
|
1264
|
+
const batchSize = params.batchSize ?? 200;
|
|
1265
|
+
let cursor = params.cursor ?? params.fromCursor ?? null;
|
|
1266
|
+
let firstPage = true;
|
|
1267
|
+
while (!params.signal?.aborted) {
|
|
1268
|
+
const envelope = await this.listSbtcWithdrawals({
|
|
1269
|
+
...params,
|
|
1270
|
+
limit: batchSize,
|
|
1271
|
+
cursor: firstPage ? params.cursor : cursor,
|
|
1272
|
+
fromCursor: firstPage ? params.fromCursor : undefined,
|
|
1273
|
+
fromHeight: firstPage ? firstWalkFromHeight(params) : undefined
|
|
1274
|
+
});
|
|
1275
|
+
for (const withdrawal of envelope.withdrawals) {
|
|
1276
|
+
if (params.signal?.aborted)
|
|
1277
|
+
return;
|
|
1278
|
+
yield withdrawal;
|
|
1279
|
+
}
|
|
1280
|
+
const nextCursor = envelope.next_cursor;
|
|
1281
|
+
if (!nextCursor || nextCursor === cursor || envelope.withdrawals.length < batchSize) {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
cursor = nextCursor;
|
|
1285
|
+
firstPage = false;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
async getSbtcWithdrawal(requestId) {
|
|
1289
|
+
try {
|
|
1290
|
+
return await this.request("GET", `/v1/index/sbtc/withdrawals/${requestId}`);
|
|
1291
|
+
} catch (err) {
|
|
1292
|
+
if (err instanceof ApiError && err.status === 404)
|
|
1293
|
+
return null;
|
|
1294
|
+
throw err;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
async listSbtcEvents(params = {}) {
|
|
1298
|
+
return this.request("GET", `/v1/index/sbtc/events${buildQuery({
|
|
1299
|
+
cursor: params.cursor,
|
|
1300
|
+
from_cursor: params.fromCursor,
|
|
1301
|
+
limit: params.limit,
|
|
1302
|
+
confirmed: params.confirmed,
|
|
1303
|
+
topic: params.topic,
|
|
1304
|
+
sender: params.sender,
|
|
1305
|
+
request_id: params.requestId,
|
|
1306
|
+
bitcoin_txid: params.bitcoinTxid,
|
|
1307
|
+
from_height: params.fromHeight,
|
|
1308
|
+
to_height: params.toHeight
|
|
1309
|
+
})}`);
|
|
1310
|
+
}
|
|
1311
|
+
async* walkSbtcEvents(params = {}) {
|
|
1312
|
+
const batchSize = params.batchSize ?? 200;
|
|
1313
|
+
let cursor = params.cursor ?? params.fromCursor ?? null;
|
|
1314
|
+
let firstPage = true;
|
|
1315
|
+
while (!params.signal?.aborted) {
|
|
1316
|
+
const envelope = await this.listSbtcEvents({
|
|
1317
|
+
...params,
|
|
1318
|
+
limit: batchSize,
|
|
1319
|
+
cursor: firstPage ? params.cursor : cursor,
|
|
1320
|
+
fromCursor: firstPage ? params.fromCursor : undefined,
|
|
1321
|
+
fromHeight: firstPage ? firstWalkFromHeight(params) : undefined
|
|
1322
|
+
});
|
|
1323
|
+
for (const event of envelope.events) {
|
|
1324
|
+
if (params.signal?.aborted)
|
|
1325
|
+
return;
|
|
1326
|
+
yield event;
|
|
1327
|
+
}
|
|
1328
|
+
const nextCursor = envelope.next_cursor;
|
|
1329
|
+
if (!nextCursor || nextCursor === cursor || envelope.events.length < batchSize) {
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
cursor = nextCursor;
|
|
1333
|
+
firstPage = false;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
async getSbtcSummary() {
|
|
1337
|
+
return this.request("GET", "/v1/index/sbtc/summary");
|
|
1338
|
+
}
|
|
1339
|
+
async listPoxCycles(params = {}) {
|
|
1340
|
+
return this.request("GET", `/v1/index/pox/cycles${buildQuery({
|
|
1341
|
+
cursor: params.cursor,
|
|
1342
|
+
limit: params.limit
|
|
1343
|
+
})}`);
|
|
1344
|
+
}
|
|
1345
|
+
async* walkPoxCycles(params = {}) {
|
|
1346
|
+
const batchSize = params.batchSize ?? 100;
|
|
1347
|
+
let cursor = params.cursor ?? null;
|
|
1348
|
+
while (!params.signal?.aborted) {
|
|
1349
|
+
const envelope = await this.listPoxCycles({ cursor, limit: batchSize });
|
|
1350
|
+
for (const cycle of envelope.cycles) {
|
|
1351
|
+
if (params.signal?.aborted)
|
|
1352
|
+
return;
|
|
1353
|
+
yield cycle;
|
|
1354
|
+
}
|
|
1355
|
+
const nextCursor = envelope.next_cursor;
|
|
1356
|
+
if (nextCursor === null || nextCursor === cursor || envelope.cycles.length < batchSize) {
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
cursor = nextCursor;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
async getPoxCycle(rewardCycle) {
|
|
1363
|
+
try {
|
|
1364
|
+
return await this.request("GET", `/v1/index/pox/cycles/${rewardCycle}`);
|
|
1365
|
+
} catch (err) {
|
|
1366
|
+
if (err instanceof ApiError && err.status === 404)
|
|
1367
|
+
return null;
|
|
1368
|
+
throw err;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1187
1371
|
}
|
|
1188
1372
|
|
|
1189
1373
|
// src/projects/client.ts
|
|
@@ -1840,5 +2024,5 @@ export {
|
|
|
1840
2024
|
Subgraphs
|
|
1841
2025
|
};
|
|
1842
2026
|
|
|
1843
|
-
//# debugId=
|
|
2027
|
+
//# debugId=AF49D51938E916C064756E2164756E21
|
|
1844
2028
|
//# sourceMappingURL=index.js.map
|