@inversealtruism/cairn-sdk 0.1.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.
@@ -0,0 +1,105 @@
1
+ import { F as FetchLike } from './http-D2CzfOsm.cjs';
2
+ export { H as Http, a as HttpOptions } from './http-D2CzfOsm.cjs';
3
+ import { Chain } from './chain.cjs';
4
+ import { BoardClient } from './board.cjs';
5
+ export { BoardItemContent, BoardWindow, ProposeInput, ProposeResult, RankedItem, SupportInput } from './board.cjs';
6
+ import { IndexerClient } from './indexer.cjs';
7
+ export { InclusionResult, IndexEvent, MerkleProof, StreamHandle, StreamHandlers, TrustLevel } from './indexer.cjs';
8
+ import { ContentClient } from './content.cjs';
9
+ export { ContentClientOptions, PreparedContent } from './content.cjs';
10
+ import { RegistryClient } from './registry.cjs';
11
+ import { WalletConnection, DetectOptions } from './connect.cjs';
12
+ export { AttestParams, CairnProvider, ConnectResult, ProposeParams, SealClaimParams, SendParams, SignInResult, TxResult, connect, detectProvider, getWallet, isInstalled } from './connect.cjs';
13
+ import '@inversealtruism/csd-client';
14
+ import '@inversealtruism/csd-tx';
15
+ import '@inversealtruism/csd-codec';
16
+ import '@inversealtruism/csd-crypto';
17
+ import '@inversealtruism/csd-light';
18
+ import '@inversealtruism/csd-registry';
19
+
20
+ declare class CairnError extends Error {
21
+ constructor(message: string);
22
+ }
23
+ /** The Cairn Wallet extension is not installed (no `window.cairn` appeared). */
24
+ declare class NotInstalledError extends CairnError {
25
+ constructor(message?: string);
26
+ }
27
+ /** The user rejected the request in the wallet's approval window. */
28
+ declare class UserRejectedError extends CairnError {
29
+ constructor(message?: string);
30
+ }
31
+ /** The wallet is locked; the user must unlock it before approving. */
32
+ declare class WalletLockedError extends CairnError {
33
+ constructor(message?: string);
34
+ }
35
+ /** The method is not exposed to dApps by the wallet boundary. */
36
+ declare class UnsupportedMethodError extends CairnError {
37
+ constructor(message?: string);
38
+ }
39
+ /** An HTTP request to a Cairn service failed (network or non-2xx). */
40
+ declare class HttpError extends CairnError {
41
+ status: number;
42
+ url: string;
43
+ body?: string;
44
+ constructor(status: number, url: string, body?: string);
45
+ }
46
+ /** Self-certification failed: received bytes do not hash to the expected payload hash. */
47
+ declare class ContentVerificationError extends CairnError {
48
+ expected: string;
49
+ constructor(expected: string);
50
+ }
51
+
52
+ interface CairnBaseUrls {
53
+ /** Cairn board server (board + /api/rpc proxy + /content origin). Default https://cairn-substrate.com */
54
+ cairn?: string;
55
+ /** Node RPC base. Default `${cairn}/api/rpc` (the public proxy — no local node needed). */
56
+ rpc?: string;
57
+ /**
58
+ * csd-indexer base. Default `${cairn}/explorer/api` — the cairn server's existing
59
+ * hardened, read-only, rate-limited reverse proxy to the localhost indexer (the
60
+ * same one the block explorer uses). SSE works through it; WebSocket subscribe()
61
+ * needs a direct indexer endpoint (set this to a localhost/private indexer URL).
62
+ */
63
+ indexer?: string;
64
+ /**
65
+ * Optional direct csd-swarm gateway base. Not set by default — content is reached
66
+ * via the indexer proxy (which fronts the swarm) and the cairn origin, both verified.
67
+ * Set this only if you run a public swarm gateway.
68
+ */
69
+ swarm?: string;
70
+ }
71
+ interface CairnConfig {
72
+ /** Currently only "mainnet". Selects the default base URLs. */
73
+ network?: "mainnet";
74
+ baseUrls?: CairnBaseUrls;
75
+ /** Custom fetch (defaults to global fetch). */
76
+ fetch?: FetchLike;
77
+ /** Per-request HTTP timeout in ms (default 15000). */
78
+ timeoutMs?: number;
79
+ /** A pre-connected wallet (otherwise call `cairn.connect()` in the browser). */
80
+ wallet?: WalletConnection;
81
+ /** WebSocket implementation for indexer subscriptions (defaults to global). */
82
+ WebSocketImpl?: typeof WebSocket;
83
+ }
84
+ declare class Cairn {
85
+ readonly config: CairnConfig;
86
+ readonly chain: Chain;
87
+ readonly index: IndexerClient;
88
+ readonly content: ContentClient;
89
+ readonly registry: RegistryClient;
90
+ board: BoardClient;
91
+ wallet?: WalletConnection;
92
+ private readonly cairnHttp;
93
+ constructor(config?: CairnConfig);
94
+ /**
95
+ * Detect + connect the Cairn Wallet (browser only). Wires the wallet into
96
+ * `cairn.board` for signed writes. Resolves to the connected address.
97
+ */
98
+ connect(opts?: DetectOptions): Promise<string>;
99
+ /** Attach an already-connected wallet (e.g. one you constructed yourself). */
100
+ attachWallet(wallet: WalletConnection): void;
101
+ /** True if a wallet is attached. */
102
+ get connected(): boolean;
103
+ }
104
+
105
+ export { BoardClient, Cairn, type CairnBaseUrls, type CairnConfig, CairnError, Chain, ContentClient, ContentVerificationError, DetectOptions, FetchLike, HttpError, IndexerClient, NotInstalledError, RegistryClient, UnsupportedMethodError, UserRejectedError, WalletConnection, WalletLockedError };
@@ -0,0 +1,105 @@
1
+ import { F as FetchLike } from './http-D2CzfOsm.js';
2
+ export { H as Http, a as HttpOptions } from './http-D2CzfOsm.js';
3
+ import { Chain } from './chain.js';
4
+ import { BoardClient } from './board.js';
5
+ export { BoardItemContent, BoardWindow, ProposeInput, ProposeResult, RankedItem, SupportInput } from './board.js';
6
+ import { IndexerClient } from './indexer.js';
7
+ export { InclusionResult, IndexEvent, MerkleProof, StreamHandle, StreamHandlers, TrustLevel } from './indexer.js';
8
+ import { ContentClient } from './content.js';
9
+ export { ContentClientOptions, PreparedContent } from './content.js';
10
+ import { RegistryClient } from './registry.js';
11
+ import { WalletConnection, DetectOptions } from './connect.js';
12
+ export { AttestParams, CairnProvider, ConnectResult, ProposeParams, SealClaimParams, SendParams, SignInResult, TxResult, connect, detectProvider, getWallet, isInstalled } from './connect.js';
13
+ import '@inversealtruism/csd-client';
14
+ import '@inversealtruism/csd-tx';
15
+ import '@inversealtruism/csd-codec';
16
+ import '@inversealtruism/csd-crypto';
17
+ import '@inversealtruism/csd-light';
18
+ import '@inversealtruism/csd-registry';
19
+
20
+ declare class CairnError extends Error {
21
+ constructor(message: string);
22
+ }
23
+ /** The Cairn Wallet extension is not installed (no `window.cairn` appeared). */
24
+ declare class NotInstalledError extends CairnError {
25
+ constructor(message?: string);
26
+ }
27
+ /** The user rejected the request in the wallet's approval window. */
28
+ declare class UserRejectedError extends CairnError {
29
+ constructor(message?: string);
30
+ }
31
+ /** The wallet is locked; the user must unlock it before approving. */
32
+ declare class WalletLockedError extends CairnError {
33
+ constructor(message?: string);
34
+ }
35
+ /** The method is not exposed to dApps by the wallet boundary. */
36
+ declare class UnsupportedMethodError extends CairnError {
37
+ constructor(message?: string);
38
+ }
39
+ /** An HTTP request to a Cairn service failed (network or non-2xx). */
40
+ declare class HttpError extends CairnError {
41
+ status: number;
42
+ url: string;
43
+ body?: string;
44
+ constructor(status: number, url: string, body?: string);
45
+ }
46
+ /** Self-certification failed: received bytes do not hash to the expected payload hash. */
47
+ declare class ContentVerificationError extends CairnError {
48
+ expected: string;
49
+ constructor(expected: string);
50
+ }
51
+
52
+ interface CairnBaseUrls {
53
+ /** Cairn board server (board + /api/rpc proxy + /content origin). Default https://cairn-substrate.com */
54
+ cairn?: string;
55
+ /** Node RPC base. Default `${cairn}/api/rpc` (the public proxy — no local node needed). */
56
+ rpc?: string;
57
+ /**
58
+ * csd-indexer base. Default `${cairn}/explorer/api` — the cairn server's existing
59
+ * hardened, read-only, rate-limited reverse proxy to the localhost indexer (the
60
+ * same one the block explorer uses). SSE works through it; WebSocket subscribe()
61
+ * needs a direct indexer endpoint (set this to a localhost/private indexer URL).
62
+ */
63
+ indexer?: string;
64
+ /**
65
+ * Optional direct csd-swarm gateway base. Not set by default — content is reached
66
+ * via the indexer proxy (which fronts the swarm) and the cairn origin, both verified.
67
+ * Set this only if you run a public swarm gateway.
68
+ */
69
+ swarm?: string;
70
+ }
71
+ interface CairnConfig {
72
+ /** Currently only "mainnet". Selects the default base URLs. */
73
+ network?: "mainnet";
74
+ baseUrls?: CairnBaseUrls;
75
+ /** Custom fetch (defaults to global fetch). */
76
+ fetch?: FetchLike;
77
+ /** Per-request HTTP timeout in ms (default 15000). */
78
+ timeoutMs?: number;
79
+ /** A pre-connected wallet (otherwise call `cairn.connect()` in the browser). */
80
+ wallet?: WalletConnection;
81
+ /** WebSocket implementation for indexer subscriptions (defaults to global). */
82
+ WebSocketImpl?: typeof WebSocket;
83
+ }
84
+ declare class Cairn {
85
+ readonly config: CairnConfig;
86
+ readonly chain: Chain;
87
+ readonly index: IndexerClient;
88
+ readonly content: ContentClient;
89
+ readonly registry: RegistryClient;
90
+ board: BoardClient;
91
+ wallet?: WalletConnection;
92
+ private readonly cairnHttp;
93
+ constructor(config?: CairnConfig);
94
+ /**
95
+ * Detect + connect the Cairn Wallet (browser only). Wires the wallet into
96
+ * `cairn.board` for signed writes. Resolves to the connected address.
97
+ */
98
+ connect(opts?: DetectOptions): Promise<string>;
99
+ /** Attach an already-connected wallet (e.g. one you constructed yourself). */
100
+ attachWallet(wallet: WalletConnection): void;
101
+ /** True if a wallet is attached. */
102
+ get connected(): boolean;
103
+ }
104
+
105
+ export { BoardClient, Cairn, type CairnBaseUrls, type CairnConfig, CairnError, Chain, ContentClient, ContentVerificationError, DetectOptions, FetchLike, HttpError, IndexerClient, NotInstalledError, RegistryClient, UnsupportedMethodError, UserRejectedError, WalletConnection, WalletLockedError };
package/dist/index.js ADDED
@@ -0,0 +1,181 @@
1
+ import {
2
+ BoardClient
3
+ } from "./chunk-OGP2W7DL.js";
4
+ import {
5
+ Chain
6
+ } from "./chunk-6VWLWF7R.js";
7
+ import {
8
+ WalletConnection,
9
+ connect,
10
+ detectProvider,
11
+ getWallet,
12
+ isInstalled
13
+ } from "./chunk-SMAPCKVG.js";
14
+ import {
15
+ ContentClient
16
+ } from "./chunk-BZVJVL4Q.js";
17
+ import {
18
+ CairnError,
19
+ ContentVerificationError,
20
+ HttpError,
21
+ NotInstalledError,
22
+ UnsupportedMethodError,
23
+ UserRejectedError,
24
+ WalletLockedError
25
+ } from "./chunk-PHMU3CG5.js";
26
+ import {
27
+ IndexerClient
28
+ } from "./chunk-ETZYBDFC.js";
29
+ import {
30
+ RegistryClient
31
+ } from "./chunk-G7FABIF4.js";
32
+
33
+ // src/http.ts
34
+ var Http = class {
35
+ baseUrl;
36
+ _fetch;
37
+ timeoutMs;
38
+ headers;
39
+ constructor(opts) {
40
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
41
+ const f = opts.fetch ?? globalThis.fetch;
42
+ if (!f) throw new Error("No fetch available \u2014 pass `fetch` in the SDK options.");
43
+ this._fetch = opts.fetch ? opts.fetch : f.bind(globalThis);
44
+ this.timeoutMs = opts.timeoutMs ?? 15e3;
45
+ this.headers = opts.headers ?? {};
46
+ }
47
+ /** Build an absolute URL from a path + optional query params. */
48
+ url(path, query) {
49
+ const p = path.startsWith("/") ? path : `/${path}`;
50
+ const u = new URL(this.baseUrl + p);
51
+ if (query) {
52
+ for (const [k, v] of Object.entries(query)) {
53
+ if (v !== void 0 && v !== null) u.searchParams.set(k, String(v));
54
+ }
55
+ }
56
+ return u.toString();
57
+ }
58
+ async raw(method, path, opts) {
59
+ const url = this.url(path, opts?.query);
60
+ const ctrl = new AbortController();
61
+ const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
62
+ try {
63
+ const res = await this._fetch(url, {
64
+ method,
65
+ signal: ctrl.signal,
66
+ headers: {
67
+ ...opts?.body !== void 0 ? { "content-type": "application/json" } : {},
68
+ ...this.headers,
69
+ ...opts?.headers ?? {}
70
+ },
71
+ body: opts?.body !== void 0 ? JSON.stringify(opts.body) : void 0
72
+ });
73
+ return res;
74
+ } finally {
75
+ clearTimeout(timer);
76
+ }
77
+ }
78
+ async getJson(path, query) {
79
+ const res = await this.raw("GET", path, { query });
80
+ if (!res.ok) throw new HttpError(res.status, this.url(path, query), await safeText(res));
81
+ return await res.json();
82
+ }
83
+ async postJson(path, body, headers) {
84
+ const res = await this.raw("POST", path, { body, headers });
85
+ if (!res.ok) throw new HttpError(res.status, this.url(path), await safeText(res));
86
+ return await res.json();
87
+ }
88
+ /** GET raw bytes (for content retrieval). Returns null on 404. */
89
+ async getBytes(path) {
90
+ const res = await this.raw("GET", path);
91
+ if (res.status === 404) return null;
92
+ if (!res.ok) throw new HttpError(res.status, this.url(path), await safeText(res));
93
+ const buf = new Uint8Array(await res.arrayBuffer());
94
+ return { bytes: buf, headers: res.headers };
95
+ }
96
+ };
97
+ async function safeText(res) {
98
+ try {
99
+ return await res.text();
100
+ } catch {
101
+ return "";
102
+ }
103
+ }
104
+
105
+ // src/index.ts
106
+ var DEFAULT_CAIRN = "https://cairn-substrate.com";
107
+ var Cairn = class {
108
+ config;
109
+ chain;
110
+ index;
111
+ content;
112
+ registry;
113
+ board;
114
+ wallet;
115
+ cairnHttp;
116
+ constructor(config = {}) {
117
+ this.config = config;
118
+ const fetch = config.fetch;
119
+ const timeoutMs = config.timeoutMs;
120
+ const cairn = (config.baseUrls?.cairn ?? DEFAULT_CAIRN).replace(/\/+$/, "");
121
+ const rpc = config.baseUrls?.rpc ?? `${cairn}/api/rpc`;
122
+ const indexer = (config.baseUrls?.indexer ?? `${cairn}/explorer/api`).replace(/\/+$/, "");
123
+ this.cairnHttp = new Http({ baseUrl: cairn, fetch, timeoutMs });
124
+ const indexerHttp = new Http({ baseUrl: indexer, fetch, timeoutMs });
125
+ const swarmHttp = config.baseUrls?.swarm ? new Http({ baseUrl: config.baseUrls.swarm.replace(/\/+$/, ""), fetch, timeoutMs }) : void 0;
126
+ this.chain = new Chain({ rpcUrl: rpc, fetch, timeoutMs });
127
+ this.wallet = config.wallet;
128
+ this.board = new BoardClient(this.cairnHttp, this.wallet);
129
+ this.index = new IndexerClient(indexerHttp, {
130
+ fetch,
131
+ WebSocketImpl: config.WebSocketImpl,
132
+ // Upgrade inclusion proofs to fully trust-minimized by cross-checking the
133
+ // proof root against the on-chain header merkle at that height.
134
+ headerMerkleAt: async (h) => {
135
+ const block = await this.chain.client.blockByHeight(h);
136
+ return String(block.header.merkle);
137
+ }
138
+ });
139
+ this.content = new ContentClient({ swarm: swarmHttp, indexer: indexerHttp, cairn: this.cairnHttp });
140
+ this.registry = new RegistryClient({ baseUrl: indexer, fetch });
141
+ }
142
+ /**
143
+ * Detect + connect the Cairn Wallet (browser only). Wires the wallet into
144
+ * `cairn.board` for signed writes. Resolves to the connected address.
145
+ */
146
+ async connect(opts = {}) {
147
+ const { wallet, address } = await connect(opts);
148
+ this.attachWallet(wallet);
149
+ return address;
150
+ }
151
+ /** Attach an already-connected wallet (e.g. one you constructed yourself). */
152
+ attachWallet(wallet) {
153
+ this.wallet = wallet;
154
+ this.board = new BoardClient(this.cairnHttp, wallet);
155
+ }
156
+ /** True if a wallet is attached. */
157
+ get connected() {
158
+ return !!this.wallet;
159
+ }
160
+ };
161
+ export {
162
+ BoardClient,
163
+ Cairn,
164
+ CairnError,
165
+ Chain,
166
+ ContentClient,
167
+ ContentVerificationError,
168
+ Http,
169
+ HttpError,
170
+ IndexerClient,
171
+ NotInstalledError,
172
+ RegistryClient,
173
+ UnsupportedMethodError,
174
+ UserRejectedError,
175
+ WalletConnection,
176
+ WalletLockedError,
177
+ connect,
178
+ detectProvider,
179
+ getWallet,
180
+ isInstalled
181
+ };
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/indexer.ts
21
+ var indexer_exports = {};
22
+ __export(indexer_exports, {
23
+ IndexerClient: () => IndexerClient
24
+ });
25
+ module.exports = __toCommonJS(indexer_exports);
26
+ var import_csd_codec = require("@inversealtruism/csd-codec");
27
+ var IndexerClient = class {
28
+ http;
29
+ opts;
30
+ constructor(http, opts = {}) {
31
+ this.http = http;
32
+ this.opts = opts;
33
+ }
34
+ // ---- REST --------------------------------------------------------------
35
+ health() {
36
+ return this.http.getJson("/health");
37
+ }
38
+ tipHeight() {
39
+ return this.http.getJson("/blocks/tip/height");
40
+ }
41
+ tipHash() {
42
+ return this.http.getJson("/blocks/tip/hash");
43
+ }
44
+ blockHashAtHeight(h) {
45
+ return this.http.getJson(`/block-height/${h}`);
46
+ }
47
+ block(hash) {
48
+ return this.http.getJson(`/block/${hash}`);
49
+ }
50
+ blockTxids(hash) {
51
+ return this.http.getJson(`/block/${hash}/txids`);
52
+ }
53
+ tx(id) {
54
+ return this.http.getJson(`/tx/${id}`);
55
+ }
56
+ txStatus(id) {
57
+ return this.http.getJson(`/tx/${id}/status`);
58
+ }
59
+ txMerkleProof(id) {
60
+ return this.http.getJson(`/tx/${id}/merkle-proof`);
61
+ }
62
+ address(a) {
63
+ return this.http.getJson(`/address/${a}`);
64
+ }
65
+ addressTxs(a) {
66
+ return this.http.getJson(`/address/${a}/txs`);
67
+ }
68
+ addressUtxo(a) {
69
+ return this.http.getJson(`/address/${a}/utxo`);
70
+ }
71
+ reputation(a) {
72
+ return this.http.getJson(`/address/${a}/reputation`);
73
+ }
74
+ domains() {
75
+ return this.http.getJson("/domains");
76
+ }
77
+ domainProposals(d) {
78
+ return this.http.getJson(`/domain/${encodeURIComponent(d)}/proposals`);
79
+ }
80
+ proposal(id) {
81
+ return this.http.getJson(`/proposal/${id}`);
82
+ }
83
+ attestations(id) {
84
+ return this.http.getJson(`/proposal/${id}/attestations`);
85
+ }
86
+ registryPeers() {
87
+ return this.http.getJson("/registry/peers");
88
+ }
89
+ registryGateways() {
90
+ return this.http.getJson("/registry/gateways");
91
+ }
92
+ identity(handle) {
93
+ return this.http.getJson(`/identity/${encodeURIComponent(handle)}`);
94
+ }
95
+ reverseIdentity(addr) {
96
+ return this.http.getJson(`/address/${addr}/identity`);
97
+ }
98
+ // ---- trust-minimized inclusion ----------------------------------------
99
+ /**
100
+ * Verify a tx is included in a block by folding its merkle branch to the root
101
+ * with csd-codec.verifyMerkleProof. If `headerMerkleAt` was provided, the
102
+ * proof's root is cross-checked against the ON-CHAIN header merkle at that
103
+ * height → trustLevel "verified-inclusion". Otherwise the proof is only
104
+ * internally consistent → "proof-consistent" (still useful, but the indexer is trusted).
105
+ */
106
+ async verifyInclusion(txid) {
107
+ let proof;
108
+ try {
109
+ proof = await this.txMerkleProof(txid);
110
+ } catch {
111
+ return { included: false, trustLevel: "not-found", reason: "no merkle proof (tx not indexed)" };
112
+ }
113
+ const folds = (0, import_csd_codec.verifyMerkleProof)(txid, proof.pos, proof.merkle, proof.merkle_root);
114
+ if (!folds) {
115
+ return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "merkle branch does not fold to the claimed root" };
116
+ }
117
+ if (this.opts.headerMerkleAt) {
118
+ try {
119
+ const onchain = await this.opts.headerMerkleAt(proof.block_height);
120
+ if (norm(onchain) !== norm(proof.merkle_root)) {
121
+ return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "proof root != on-chain header merkle (indexer disagrees with chain)" };
122
+ }
123
+ return { included: true, trustLevel: "verified-inclusion", blockHeight: proof.block_height, pos: proof.pos, merkleRoot: proof.merkle_root };
124
+ } catch {
125
+ }
126
+ }
127
+ return { included: true, trustLevel: "proof-consistent", blockHeight: proof.block_height, pos: proof.pos, merkleRoot: proof.merkle_root };
128
+ }
129
+ // ---- live streams ------------------------------------------------------
130
+ /** Stream all events (blocks, proposals, attestations, reorgs) via SSE. */
131
+ streamAll(handlers) {
132
+ return this.sse("/stream/all", handlers);
133
+ }
134
+ /** Stream blocks + reorgs only. */
135
+ streamBlocks(handlers) {
136
+ return this.sse("/stream/blocks", handlers);
137
+ }
138
+ /** Stream proposals (+ reorgs) for a domain. */
139
+ streamDomain(domain, handlers) {
140
+ return this.sse(`/stream/domain/${encodeURIComponent(domain)}`, handlers);
141
+ }
142
+ /**
143
+ * Subscribe over WebSocket with selective tracking. Returns a handle with
144
+ * `.close()`. `onEvent` receives every matching IndexEvent (incl. reorgs).
145
+ */
146
+ subscribe(sub, onEvent, onError) {
147
+ const WS = this.opts.WebSocketImpl ?? globalThis.WebSocket;
148
+ if (!WS) {
149
+ throw new Error("No WebSocket available \u2014 pass WebSocketImpl in the indexer options (e.g. in older Node).");
150
+ }
151
+ const wsUrl = this.http.url("/ws").replace(/^http/, "ws");
152
+ let closed = false;
153
+ const ws = new WS(wsUrl);
154
+ ws.addEventListener("open", () => {
155
+ const msg = {};
156
+ if (sub.all) msg["track-all"] = true;
157
+ if (sub.domains?.length) msg["track-domain"] = sub.domains;
158
+ if (sub.addresses?.length) msg["track-address"] = sub.addresses;
159
+ if (sub.proposals?.length) msg["track-proposal"] = sub.proposals;
160
+ ws.send(JSON.stringify(Object.keys(msg).length ? msg : { "track-all": true }));
161
+ });
162
+ ws.addEventListener("message", (ev) => {
163
+ try {
164
+ const data = JSON.parse(typeof ev.data === "string" ? ev.data : String(ev.data));
165
+ if (data && data.kind) onEvent(data);
166
+ } catch {
167
+ }
168
+ });
169
+ if (onError) ws.addEventListener("error", (e) => onError(e));
170
+ return {
171
+ close() {
172
+ if (closed) return;
173
+ closed = true;
174
+ try {
175
+ ws.close();
176
+ } catch {
177
+ }
178
+ }
179
+ };
180
+ }
181
+ // ---- internal SSE over fetch (portable + auto-reconnect) ---------------
182
+ sse(path, handlers) {
183
+ const fetchImpl = this.opts.fetch ?? globalThis.fetch;
184
+ const url = this.http.url(path);
185
+ let closed = false;
186
+ let ctrl = null;
187
+ const run = async () => {
188
+ while (!closed) {
189
+ ctrl = new AbortController();
190
+ try {
191
+ const res = await fetchImpl(url, { signal: ctrl.signal, headers: { accept: "text/event-stream" } });
192
+ if (!res.ok || !res.body) throw new Error(`SSE ${res.status}`);
193
+ handlers.onOpen?.();
194
+ const reader = res.body.getReader();
195
+ const decoder = new TextDecoder();
196
+ let buf = "";
197
+ for (; ; ) {
198
+ const { value, done } = await reader.read();
199
+ if (done) break;
200
+ buf += decoder.decode(value, { stream: true });
201
+ let idx;
202
+ while ((idx = buf.indexOf("\n\n")) >= 0) {
203
+ const frame = buf.slice(0, idx);
204
+ buf = buf.slice(idx + 2);
205
+ dispatchFrame(frame, handlers);
206
+ }
207
+ }
208
+ } catch (err) {
209
+ if (closed) return;
210
+ handlers.onError?.(err);
211
+ }
212
+ if (closed) return;
213
+ await new Promise((r) => setTimeout(r, 2e3));
214
+ }
215
+ };
216
+ void run();
217
+ return {
218
+ close() {
219
+ if (closed) return;
220
+ closed = true;
221
+ try {
222
+ ctrl?.abort();
223
+ } catch {
224
+ }
225
+ }
226
+ };
227
+ }
228
+ };
229
+ function dispatchFrame(frame, h) {
230
+ let event = "message";
231
+ const dataLines = [];
232
+ for (const line of frame.split("\n")) {
233
+ if (line.startsWith(":")) continue;
234
+ if (line.startsWith("event:")) event = line.slice(6).trim();
235
+ else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim());
236
+ }
237
+ if (event === "hello" || dataLines.length === 0) return;
238
+ let parsed;
239
+ try {
240
+ parsed = JSON.parse(dataLines.join("\n"));
241
+ } catch {
242
+ return;
243
+ }
244
+ h.onEvent?.(parsed);
245
+ if (parsed.kind === "block") h.onBlock?.(parsed);
246
+ else if (parsed.kind === "proposal") h.onProposal?.(parsed);
247
+ else if (parsed.kind === "attestation") h.onAttestation?.(parsed);
248
+ else if (parsed.kind === "reorg") h.onReorg?.(parsed);
249
+ }
250
+ function norm(h) {
251
+ return String(h).toLowerCase().replace(/^0x/, "");
252
+ }
253
+ // Annotate the CommonJS export names for ESM import in node:
254
+ 0 && (module.exports = {
255
+ IndexerClient
256
+ });