@inversealtruism/cairn-sdk 0.1.3 → 0.2.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.
Files changed (46) hide show
  1. package/README.md +16 -16
  2. package/dist/board.cjs +1 -1
  3. package/dist/board.d.cts +1 -1
  4. package/dist/board.d.ts +1 -1
  5. package/dist/board.js +2 -2
  6. package/dist/chain.cjs +1 -1
  7. package/dist/chain.d.cts +1 -1
  8. package/dist/chain.d.ts +1 -1
  9. package/dist/chain.js +1 -1
  10. package/dist/{chunk-G7FABIF4.js → chunk-BGJZF7S2.js} +11 -2
  11. package/dist/{chunk-5KAUQH72.js → chunk-C452UODV.js} +1 -1
  12. package/dist/{chunk-FNBE2FPE.js → chunk-J5J4JEJ6.js} +50 -18
  13. package/dist/{chunk-UMGWVOOT.js → chunk-JGXD3WIC.js} +7 -2
  14. package/dist/{chunk-ZAE2BIH5.js → chunk-PYREGAB3.js} +21 -3
  15. package/dist/{chunk-645CYBVN.js → chunk-RYZJDZLJ.js} +1 -1
  16. package/dist/{chunk-OSXS26RA.js → chunk-SIUUY6FZ.js} +4 -3
  17. package/dist/{chunk-G4OZKZSJ.js → chunk-UIEOBVFI.js} +8 -2
  18. package/dist/connect.cjs +14 -3
  19. package/dist/connect.d.cts +7 -0
  20. package/dist/connect.d.ts +7 -0
  21. package/dist/connect.js +2 -2
  22. package/dist/content.cjs +4 -3
  23. package/dist/content.d.cts +1 -1
  24. package/dist/content.d.ts +1 -1
  25. package/dist/content.js +2 -2
  26. package/dist/controller.cjs +34 -5
  27. package/dist/controller.d.cts +1 -0
  28. package/dist/controller.d.ts +1 -0
  29. package/dist/controller.js +3 -3
  30. package/dist/{http-D2CzfOsm.d.cts → http-C9C4L4hz.d.cts} +11 -0
  31. package/dist/{http-D2CzfOsm.d.ts → http-C9C4L4hz.d.ts} +11 -0
  32. package/dist/index.cjs +272 -37
  33. package/dist/index.d.cts +99 -11
  34. package/dist/index.d.ts +99 -11
  35. package/dist/index.js +179 -17
  36. package/dist/indexer.cjs +50 -18
  37. package/dist/indexer.d.cts +15 -4
  38. package/dist/indexer.d.ts +15 -4
  39. package/dist/indexer.js +1 -1
  40. package/dist/react.cjs +34 -5
  41. package/dist/react.js +3 -3
  42. package/dist/registry.cjs +11 -2
  43. package/dist/registry.d.cts +12 -3
  44. package/dist/registry.d.ts +12 -3
  45. package/dist/registry.js +1 -1
  46. package/package.json +32 -23
package/dist/index.cjs CHANGED
@@ -27,9 +27,11 @@ __export(index_exports, {
27
27
  Chain: () => Chain,
28
28
  ContentClient: () => ContentClient,
29
29
  ContentVerificationError: () => ContentVerificationError,
30
+ DEFAULT_SPV_CHECKPOINT: () => DEFAULT_SPV_CHECKPOINT,
30
31
  Http: () => Http,
31
32
  HttpError: () => HttpError,
32
33
  IndexerClient: () => IndexerClient,
34
+ NamesClient: () => NamesClient,
33
35
  NotInstalledError: () => NotInstalledError,
34
36
  RegistryClient: () => RegistryClient,
35
37
  SDK_VERSION: () => SDK_VERSION,
@@ -48,7 +50,7 @@ __export(index_exports, {
48
50
  module.exports = __toCommonJS(index_exports);
49
51
 
50
52
  // src/errors.ts
51
- var SDK_VERSION = "0.1.2";
53
+ var SDK_VERSION = "0.2.0";
52
54
  var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
53
55
  var CairnError = class extends Error {
54
56
  /** Stable machine code for programmatic branching. */
@@ -123,7 +125,12 @@ var ContentVerificationError = class extends CairnError {
123
125
  this.expected = expected;
124
126
  }
125
127
  };
126
- function mapProviderError(error) {
128
+ var NATIVE_CODES = /* @__PURE__ */ new Set(["ACCOUNT_CHANGED", "FIRST_PARTY_ONLY", "RATE_LIMITED", "APPROVAL_CLOSED", "FORBIDDEN", "INTERNAL"]);
129
+ function mapProviderError(error, code) {
130
+ if (code === "USER_REJECTED" || code === "APPROVAL_CLOSED") return new UserRejectedError(error);
131
+ if (code === "WALLET_LOCKED") return new WalletLockedError(error);
132
+ if (code === "UNSUPPORTED_METHOD" || code === "UNKNOWN_KIND") return new UnsupportedMethodError(error);
133
+ if (code && NATIVE_CODES.has(code)) return new CairnError(error || code, { code });
127
134
  const e = String(error || "").toLowerCase();
128
135
  if (e.includes("rejected by user")) return new UserRejectedError(error);
129
136
  if (e.includes("wallet locked")) return new WalletLockedError(error);
@@ -135,23 +142,68 @@ function errorCode(err) {
135
142
  }
136
143
 
137
144
  // src/http.ts
145
+ var DEFAULT_MAX_BYTES = 16 * 1024 * 1024;
146
+ var ERR_TEXT_CAP = 8 * 1024;
147
+ async function readCapped(res, maxBytes) {
148
+ const cl = Number(res.headers.get("content-length"));
149
+ if (Number.isFinite(cl) && cl > maxBytes) throw new HttpError(res.status, res.url, `response too large (content-length ${cl} > ${maxBytes})`);
150
+ const body = res.body;
151
+ if (!body || typeof body.getReader !== "function") {
152
+ const buf = new Uint8Array(await res.arrayBuffer());
153
+ if (buf.byteLength > maxBytes) throw new HttpError(res.status, res.url, `response too large (${buf.byteLength} > ${maxBytes})`);
154
+ return buf;
155
+ }
156
+ const reader = body.getReader();
157
+ const chunks = [];
158
+ let total = 0;
159
+ for (; ; ) {
160
+ const { done, value } = await reader.read();
161
+ if (done) break;
162
+ if (value) {
163
+ total += value.byteLength;
164
+ if (total > maxBytes) {
165
+ try {
166
+ await reader.cancel();
167
+ } catch {
168
+ }
169
+ throw new HttpError(res.status, res.url, `response exceeded ${maxBytes} bytes`);
170
+ }
171
+ chunks.push(value);
172
+ }
173
+ }
174
+ const out = new Uint8Array(total);
175
+ let off = 0;
176
+ for (const c of chunks) {
177
+ out.set(c, off);
178
+ off += c.byteLength;
179
+ }
180
+ return out;
181
+ }
138
182
  var Http = class {
139
183
  baseUrl;
140
184
  _fetch;
141
185
  timeoutMs;
186
+ maxBytes;
142
187
  headers;
188
+ retries;
143
189
  constructor(opts) {
144
190
  this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
145
191
  const f = opts.fetch ?? globalThis.fetch;
146
192
  if (!f) throw new Error("No fetch available \u2014 pass `fetch` in the SDK options.");
147
193
  this._fetch = opts.fetch ? opts.fetch : f.bind(globalThis);
148
194
  this.timeoutMs = opts.timeoutMs ?? 15e3;
195
+ this.maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
149
196
  this.headers = opts.headers ?? {};
197
+ this.retries = Math.min(10, Math.max(0, Math.floor(opts.retries ?? 0)));
150
198
  }
151
199
  /** Build an absolute URL from a path + optional query params. */
152
200
  url(path, query) {
153
201
  const p = path.startsWith("/") ? path : `/${path}`;
154
202
  const u = new URL(this.baseUrl + p);
203
+ const basePath = new URL(this.baseUrl + "/").pathname;
204
+ if (basePath !== "/" && !(u.pathname + "/").startsWith(basePath)) {
205
+ throw new Error(`illegal path '${path}' \u2014 escapes base ${basePath}`);
206
+ }
155
207
  if (query) {
156
208
  for (const [k, v] of Object.entries(query)) {
157
209
  if (v !== void 0 && v !== null) u.searchParams.set(k, String(v));
@@ -179,28 +231,50 @@ var Http = class {
179
231
  clearTimeout(timer);
180
232
  }
181
233
  }
234
+ // A malformed 2xx body must surface as a TYPED HttpError, not a leaked SyntaxError (B10: dApp
235
+ // code branches on `instanceof CairnError`; a raw parse error escaped that contract).
236
+ parseJson(res, text, url) {
237
+ try {
238
+ return JSON.parse(text);
239
+ } catch (e) {
240
+ throw new HttpError(res.status, url, `invalid JSON body: ${text.slice(0, 120)}`);
241
+ }
242
+ }
182
243
  async getJson(path, query) {
183
- const res = await this.raw("GET", path, { query });
184
- if (!res.ok) throw new HttpError(res.status, this.url(path, query), await safeText(res));
185
- return await res.json();
244
+ const url = this.url(path, query);
245
+ for (let attempt = 0; ; attempt++) {
246
+ try {
247
+ const res = await this.raw("GET", path, { query });
248
+ if (!res.ok) {
249
+ const err = new HttpError(res.status, url, await safeText(res));
250
+ if (!(res.status >= 500 && attempt < this.retries)) throw err;
251
+ } else {
252
+ return this.parseJson(res, new TextDecoder().decode(await readCapped(res, this.maxBytes)), url);
253
+ }
254
+ } catch (e) {
255
+ if (e instanceof HttpError && e.status < 500) throw e;
256
+ if (attempt >= this.retries) throw e;
257
+ }
258
+ await new Promise((r) => setTimeout(r, Math.floor(Math.random() * Math.min(4e3, 250 * 2 ** attempt))));
259
+ }
186
260
  }
187
261
  async postJson(path, body, headers) {
262
+ const url = this.url(path);
188
263
  const res = await this.raw("POST", path, { body, headers });
189
- if (!res.ok) throw new HttpError(res.status, this.url(path), await safeText(res));
190
- return await res.json();
264
+ if (!res.ok) throw new HttpError(res.status, url, await safeText(res));
265
+ return this.parseJson(res, new TextDecoder().decode(await readCapped(res, this.maxBytes)), url);
191
266
  }
192
267
  /** GET raw bytes (for content retrieval). Returns null on 404. */
193
268
  async getBytes(path) {
194
269
  const res = await this.raw("GET", path);
195
270
  if (res.status === 404) return null;
196
271
  if (!res.ok) throw new HttpError(res.status, this.url(path), await safeText(res));
197
- const buf = new Uint8Array(await res.arrayBuffer());
198
- return { bytes: buf, headers: res.headers };
272
+ return { bytes: await readCapped(res, this.maxBytes), headers: res.headers };
199
273
  }
200
274
  };
201
275
  async function safeText(res) {
202
276
  try {
203
- return await res.text();
277
+ return new TextDecoder().decode(await readCapped(res, ERR_TEXT_CAP));
204
278
  } catch {
205
279
  return "";
206
280
  }
@@ -257,7 +331,7 @@ var Chain = class {
257
331
  });
258
332
  if (!built.ok || p.broadcast === false) return built;
259
333
  const submitResult = await this.client.submit(built.nodeJson);
260
- return { ...built, submitResult };
334
+ return { ...built, ok: built.ok && submitResult.ok !== false, submitResult };
261
335
  }
262
336
  /** A verifying light client over the same RPC (built once, reused). */
263
337
  light() {
@@ -426,6 +500,13 @@ function sleep(ms) {
426
500
 
427
501
  // src/indexer.ts
428
502
  var import_csd_codec4 = require("@inversealtruism/csd-codec");
503
+ var HEX64 = /^0x[0-9a-fA-F]{64}$/;
504
+ var MAX_SSE_BUF = 1024 * 1024;
505
+ function isValidMerkleProof(p) {
506
+ if (!p || typeof p !== "object") return false;
507
+ const m = p;
508
+ return Number.isInteger(m.block_height) && m.block_height >= 0 && Number.isInteger(m.pos) && m.pos >= 0 && typeof m.merkle_root === "string" && HEX64.test(m.merkle_root) && Array.isArray(m.merkle) && m.merkle.every((x) => typeof x === "string" && HEX64.test(x));
509
+ }
429
510
  var IndexerClient = class {
430
511
  http;
431
512
  opts;
@@ -446,32 +527,35 @@ var IndexerClient = class {
446
527
  blockHashAtHeight(h) {
447
528
  return this.http.getJson(`/block-height/${h}`);
448
529
  }
530
+ // CAIRNSDK-CONTENT-HASH-PATH-1: encode every caller-supplied id segment. Raw interpolation let an id like
531
+ // `../../api/rpc/tip` walk `new URL` to a DIFFERENT same-origin endpoint whose body was then returned
532
+ // verbatim as the requested object (worse than the content case, which discards the walked-to response).
449
533
  block(hash) {
450
- return this.http.getJson(`/block/${hash}`);
534
+ return this.http.getJson(`/block/${encodeURIComponent(hash)}`);
451
535
  }
452
536
  blockTxids(hash) {
453
- return this.http.getJson(`/block/${hash}/txids`);
537
+ return this.http.getJson(`/block/${encodeURIComponent(hash)}/txids`);
454
538
  }
455
539
  tx(id) {
456
- return this.http.getJson(`/tx/${id}`);
540
+ return this.http.getJson(`/tx/${encodeURIComponent(id)}`);
457
541
  }
458
542
  txStatus(id) {
459
- return this.http.getJson(`/tx/${id}/status`);
543
+ return this.http.getJson(`/tx/${encodeURIComponent(id)}/status`);
460
544
  }
461
545
  txMerkleProof(id) {
462
- return this.http.getJson(`/tx/${id}/merkle-proof`);
546
+ return this.http.getJson(`/tx/${encodeURIComponent(id)}/merkle-proof`);
463
547
  }
464
548
  address(a) {
465
- return this.http.getJson(`/address/${a}`);
549
+ return this.http.getJson(`/address/${encodeURIComponent(a)}`);
466
550
  }
467
551
  addressTxs(a) {
468
- return this.http.getJson(`/address/${a}/txs`);
552
+ return this.http.getJson(`/address/${encodeURIComponent(a)}/txs`);
469
553
  }
470
554
  addressUtxo(a) {
471
- return this.http.getJson(`/address/${a}/utxo`);
555
+ return this.http.getJson(`/address/${encodeURIComponent(a)}/utxo`);
472
556
  }
473
557
  reputation(a) {
474
- return this.http.getJson(`/address/${a}/reputation`);
558
+ return this.http.getJson(`/address/${encodeURIComponent(a)}/reputation`);
475
559
  }
476
560
  domains() {
477
561
  return this.http.getJson("/domains");
@@ -480,10 +564,10 @@ var IndexerClient = class {
480
564
  return this.http.getJson(`/domain/${encodeURIComponent(d)}/proposals`);
481
565
  }
482
566
  proposal(id) {
483
- return this.http.getJson(`/proposal/${id}`);
567
+ return this.http.getJson(`/proposal/${encodeURIComponent(id)}`);
484
568
  }
485
569
  attestations(id) {
486
- return this.http.getJson(`/proposal/${id}/attestations`);
570
+ return this.http.getJson(`/proposal/${encodeURIComponent(id)}/attestations`);
487
571
  }
488
572
  registryPeers() {
489
573
  return this.http.getJson("/registry/peers");
@@ -495,7 +579,7 @@ var IndexerClient = class {
495
579
  return this.http.getJson(`/identity/${encodeURIComponent(handle)}`);
496
580
  }
497
581
  reverseIdentity(addr) {
498
- return this.http.getJson(`/address/${addr}/identity`);
582
+ return this.http.getJson(`/address/${encodeURIComponent(addr)}/identity`);
499
583
  }
500
584
  // ---- trust-minimized inclusion ----------------------------------------
501
585
  /**
@@ -504,9 +588,11 @@ var IndexerClient = class {
504
588
  * proof's root is cross-checked against the header merkle at that height →
505
589
  * trustLevel "verified-inclusion" (only as trustworthy as that header source —
506
590
  * see headerMerkleAt). Otherwise the proof is only internally consistent →
507
- * "proof-consistent" (still useful, but the indexer is trusted). The Cairn
508
- * facade wires headerMerkleAt ONLY when the node RPC is an independent origin
509
- * from the indexer, so the default same-origin setup stays at "proof-consistent".
591
+ * "proof-consistent" (still useful, but the indexer is trusted). INDEXER-DOC-STALE-1: the Cairn facade now
592
+ * wires headerMerkleAt UNCONDITIONALLY via a PoW-verifying checkpoint light client (index.ts), so the
593
+ * default reaches "verified-inclusion" when the proof root matches the PoW-verified header — what makes a
594
+ * same-origin RPC safe is the forward PoW re-verification, not an origin gate (which no longer exists).
595
+ * Fails closed: a malformed proof or a chain-disagreement returns "not-found" (the latter with equivocation:true).
510
596
  */
511
597
  async verifyInclusion(txid2) {
512
598
  let proof;
@@ -515,7 +601,15 @@ var IndexerClient = class {
515
601
  } catch {
516
602
  return { included: false, trustLevel: "not-found", reason: "no merkle proof (tx not indexed)" };
517
603
  }
518
- const folds = (0, import_csd_codec4.verifyMerkleProof)(txid2, proof.pos, proof.merkle, proof.merkle_root);
604
+ if (!isValidMerkleProof(proof)) {
605
+ return { included: false, trustLevel: "not-found", reason: "malformed merkle proof" };
606
+ }
607
+ let folds = false;
608
+ try {
609
+ folds = (0, import_csd_codec4.verifyMerkleProof)(txid2, proof.pos, proof.merkle, proof.merkle_root);
610
+ } catch {
611
+ return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "malformed merkle proof" };
612
+ }
519
613
  if (!folds) {
520
614
  return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "merkle branch does not fold to the claimed root" };
521
615
  }
@@ -523,7 +617,7 @@ var IndexerClient = class {
523
617
  try {
524
618
  const onchain = await this.opts.headerMerkleAt(proof.block_height);
525
619
  if (norm(onchain) !== norm(proof.merkle_root)) {
526
- return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "proof root != on-chain header merkle (indexer disagrees with chain)" };
620
+ return { included: false, trustLevel: "not-found", equivocation: true, blockHeight: proof.block_height, reason: "proof root != on-chain header merkle (indexer disagrees with chain)" };
527
621
  }
528
622
  return { included: true, trustLevel: "verified-inclusion", blockHeight: proof.block_height, pos: proof.pos, merkleRoot: proof.merkle_root };
529
623
  } catch {
@@ -547,6 +641,11 @@ var IndexerClient = class {
547
641
  /**
548
642
  * Subscribe over WebSocket with selective tracking. Returns a handle with
549
643
  * `.close()`. `onEvent` receives every matching IndexEvent (incl. reorgs).
644
+ *
645
+ * WS-NORECONNECT-1: unlike the SSE feeds (streamAll/streamBlocks/streamDomain, which auto-reconnect with
646
+ * backoff), this WebSocket subscription does NOT auto-reconnect — it surfaces a terminal error via
647
+ * `onError` (when provided) and stops. A consumer tracking reorgs over WS MUST re-`subscribe()` on error
648
+ * to avoid a silently-stale view; for built-in reconnect prefer the SSE `stream*` methods.
550
649
  */
551
650
  subscribe(sub, onEvent, onError) {
552
651
  const WS = this.opts.WebSocketImpl ?? globalThis.WebSocket;
@@ -609,13 +708,20 @@ var IndexerClient = class {
609
708
  buf = buf.slice(idx + 2);
610
709
  dispatchFrame(frame, handlers);
611
710
  }
711
+ if (buf.length > MAX_SSE_BUF) {
712
+ try {
713
+ await reader.cancel();
714
+ } catch {
715
+ }
716
+ throw new Error(`SSE buffer exceeded ${MAX_SSE_BUF} bytes with no frame delimiter \u2014 aborting`);
717
+ }
612
718
  }
613
719
  } catch (err) {
614
720
  if (closed) return;
615
721
  handlers.onError?.(err);
616
722
  }
617
723
  if (closed) return;
618
- await new Promise((r) => setTimeout(r, 2e3));
724
+ await new Promise((r) => setTimeout(r, 1e3 + Math.floor(Math.random() * 2e3)));
619
725
  }
620
726
  };
621
727
  void run();
@@ -721,8 +827,9 @@ var ContentClient = class {
721
827
  }
722
828
  };
723
829
  function normalizeHash(hash) {
724
- const h = hash.startsWith("0x") || hash.startsWith("0X") ? hash : `0x${hash}`;
725
- return h.toLowerCase();
830
+ const h = (hash.startsWith("0x") || hash.startsWith("0X") ? hash : `0x${hash}`).toLowerCase();
831
+ if (!/^0x[0-9a-f]{64}$/.test(h)) throw new Error(`invalid content hash: ${hash}`);
832
+ return h;
726
833
  }
727
834
 
728
835
  // src/registry.ts
@@ -741,11 +848,20 @@ var RegistryClient = class {
741
848
  peers() {
742
849
  return (0, import_csd_registry.discoverPeers)(this.src);
743
850
  }
744
- /** Resolve a handle → identity (GET /identity/:handle). Returns null if unresolved. */
851
+ /**
852
+ * Resolve a handle → identity (GET /identity/:handle). Returns null if unresolved.
853
+ *
854
+ * ⚠ TRUST (CAIRN-SDK-RESOLVE-VERIFIED-FOOTGUN): the returned `verified` flag is the INDEXER's assertion,
855
+ * NOT a client-side proof. A hostile/compromised indexer (or a MITM of the read proxy) can return
856
+ * `verified:true` for a wrong address. Do NOT use this directly as a payee/login target without either (a)
857
+ * recomputing from raw records via `fromRecords.name(...)`, or (b) cross-checking a second independent
858
+ * source (the wallet's namespv union cure). Treat `verified` here as a hint, not a guarantee.
859
+ */
745
860
  resolveName(handle) {
746
861
  return (0, import_csd_registry.resolveName)(this.src, handle);
747
862
  }
748
- /** Reverse-resolve an address → identity (GET /address/:addr/identity). */
863
+ /** Reverse-resolve an address → identity (GET /address/:addr/identity).
864
+ * ⚠ Same indexer-trust caveat as resolveName — `verified` is the indexer's claim, not a client proof. */
749
865
  reverseName(address) {
750
866
  return (0, import_csd_registry.reverseName)(this.src, address);
751
867
  }
@@ -755,6 +871,96 @@ var RegistryClient = class {
755
871
  }
756
872
  };
757
873
 
874
+ // src/names.ts
875
+ var import_cairnx_core = require("@inversealtruism/cairnx-core");
876
+ var nameOk = (n) => typeof n === "string" && import_cairnx_core.NAME_RE.test(n) && !import_cairnx_core.RESERVED_NAMES.has(n);
877
+ var NamesClient = class {
878
+ constructor(http) {
879
+ this.http = http;
880
+ }
881
+ http;
882
+ /** Resolve a .csd name for DISPLAY (server-trusted; see the module trust note). null = unknown/invalid name. */
883
+ async resolve(name) {
884
+ if (!nameOk(name)) return null;
885
+ try {
886
+ return await this.http.getJson(`/trade/api/cairnx/resolve/${name}`);
887
+ } catch (e) {
888
+ if (e?.status === 404) return null;
889
+ throw e;
890
+ }
891
+ }
892
+ /** Full detail for one name (lease, lock, resting offer, recapture). null = not registered/invalid. */
893
+ async name(name) {
894
+ if (!nameOk(name)) return null;
895
+ try {
896
+ return await this.http.getJson(`/trade/api/cairnx/name/${name}`);
897
+ } catch (e) {
898
+ if (e?.status === 404) return null;
899
+ throw e;
900
+ }
901
+ }
902
+ /** The event history of one name (registrations, transfers, sets, renewals). null = invalid name. */
903
+ async nameHistory(name) {
904
+ if (!nameOk(name)) return null;
905
+ try {
906
+ return await this.http.getJson(`/trade/api/cairnx/name-history/${name}`);
907
+ } catch (e) {
908
+ if (e?.status === 404) return null;
909
+ throw e;
910
+ }
911
+ }
912
+ /** Reverse lookup: an address's chosen primary .csd name (null name = none set). */
913
+ async primary(addr) {
914
+ if (typeof addr !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(addr)) return null;
915
+ try {
916
+ return await this.http.getJson(`/trade/api/cairnx/primary/${addr.toLowerCase()}`);
917
+ } catch (e) {
918
+ if (e?.status === 404) return null;
919
+ throw e;
920
+ }
921
+ }
922
+ /** All names an address owns (plus its primary), as served by `/cairnx/address/:addr`. */
923
+ async namesOf(addr) {
924
+ if (typeof addr !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(addr)) return null;
925
+ try {
926
+ return await this.http.getJson(`/trade/api/cairnx/address/${addr.toLowerCase()}`);
927
+ } catch (e) {
928
+ if (e?.status === 404) return null;
929
+ throw e;
930
+ }
931
+ }
932
+ /** All registered names (display list; short-edge-cached server-side). */
933
+ names() {
934
+ return this.http.getJson(`/trade/api/cairnx/names`);
935
+ }
936
+ /** All deployed tokens. */
937
+ tokens() {
938
+ return this.http.getJson(`/trade/api/cairnx/tokens`);
939
+ }
940
+ /** One token by ticker. null = unknown/invalid ticker. */
941
+ async token(ticker) {
942
+ if (typeof ticker !== "string" || !/^[A-Z][A-Z0-9]{2,11}$/.test(ticker)) return null;
943
+ try {
944
+ return await this.http.getJson(`/trade/api/cairnx/token/${ticker}`);
945
+ } catch (e) {
946
+ if (e?.status === 404) return null;
947
+ throw e;
948
+ }
949
+ }
950
+ /** Open offers (the market's display list). */
951
+ offers() {
952
+ return this.http.getJson(`/trade/api/cairnx/offers`);
953
+ }
954
+ /** Open bids. */
955
+ bids() {
956
+ return this.http.getJson(`/trade/api/cairnx/bids`);
957
+ }
958
+ /** The full canonical CairnX state (large; prefer the scoped reads above). */
959
+ state() {
960
+ return this.http.getJson(`/trade/api/cairnx/state`);
961
+ }
962
+ };
963
+
758
964
  // src/connect.ts
759
965
  function discoverProviders(opts = {}) {
760
966
  const timeoutMs = opts.timeoutMs ?? 1e3;
@@ -823,7 +1029,7 @@ function isInstalled() {
823
1029
  }
824
1030
  function unwrap(reply) {
825
1031
  if (reply && reply.ok) return reply.result;
826
- throw mapProviderError(reply?.error ?? "Unknown wallet error");
1032
+ throw mapProviderError(reply?.error ?? "Unknown wallet error", reply?.code);
827
1033
  }
828
1034
  var WalletConnection = class {
829
1035
  provider;
@@ -857,6 +1063,9 @@ var WalletConnection = class {
857
1063
  * Passwordless sign-in: a signature over a login nonce (always prompts; structurally cannot sign a tx).
858
1064
  */
859
1065
  signIn() {
1066
+ if (typeof this.provider.signIn !== "function") {
1067
+ return Promise.reject(new UnsupportedMethodError("this wallet does not expose signIn()"));
1068
+ }
860
1069
  return Promise.resolve(this.provider.signIn()).then(unwrap);
861
1070
  }
862
1071
  /**
@@ -890,6 +1099,9 @@ var WalletConnection = class {
890
1099
  }
891
1100
  /** Atomic fill (CairnX DvP): pay + attest in ONE tx. Always prompts with clear-signing. */
892
1101
  fillOffer(params) {
1102
+ if (typeof this.provider.fillOffer !== "function") {
1103
+ return Promise.reject(new UnsupportedMethodError("this wallet predates fillOffer() \u2014 ask the user to update the Cairn Wallet"));
1104
+ }
893
1105
  return Promise.resolve(this.provider.fillOffer(params)).then(unwrap);
894
1106
  }
895
1107
  /** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
@@ -987,12 +1199,28 @@ var CairnController = class {
987
1199
  for (const l of [...this.listeners]) l();
988
1200
  }
989
1201
  onAccounts = (accounts) => {
990
- if (!Array.isArray(accounts) || accounts.length === 0) this.setState({ status: "disconnected", account: null });
991
- else this.setState({ status: "connected", account: String(accounts[0]), error: null });
1202
+ if (!Array.isArray(accounts) || accounts.length === 0) {
1203
+ this.setState({ status: "disconnected", account: null });
1204
+ return;
1205
+ }
1206
+ const next = String(accounts[0]);
1207
+ if (this.state.status === "connected" && this.state.account && next.toLowerCase() === this.state.account.toLowerCase()) {
1208
+ this.setState({ status: "connected", account: next, error: null });
1209
+ } else {
1210
+ this.setState({ status: "disconnected", account: null });
1211
+ }
992
1212
  };
993
1213
  onDisconnect = () => {
994
1214
  this.setState({ status: "disconnected", account: null });
995
1215
  };
1216
+ // Detach the provider listeners (ghost-reconnect fix). Idempotent + best-effort.
1217
+ detach() {
1218
+ try {
1219
+ this.conn?.off?.("accountsChanged", this.onAccounts);
1220
+ this.conn?.off?.("disconnect", this.onDisconnect);
1221
+ } catch {
1222
+ }
1223
+ }
996
1224
  /** Detect + connect (prompts the user the first time per origin). Resolves the connected address. */
997
1225
  connect = async () => {
998
1226
  this.setState({ status: "connecting", error: null });
@@ -1016,6 +1244,8 @@ var CairnController = class {
1016
1244
  await this.conn?.revokePermissions();
1017
1245
  } catch {
1018
1246
  }
1247
+ this.detach();
1248
+ this.conn = null;
1019
1249
  this.setState({ status: "disconnected", account: null, error: null });
1020
1250
  };
1021
1251
  /** Audience-bound sign-in (must connect() first). Returns the signed artifact to verify server-side. */
@@ -1026,7 +1256,7 @@ var CairnController = class {
1026
1256
  };
1027
1257
 
1028
1258
  // src/index.ts
1029
- var DEFAULT_SPV_CHECKPOINT = { height: 31310, hash: "0x000000000000138bd3eee4b8d2fbacb8d5433ac3040ddbf1c45a5e3e0cc9e814" };
1259
+ var DEFAULT_SPV_CHECKPOINT = { height: 38142, hash: "0x00000000000140f023cc0ee1457a40833f2fcb4de44291b9d373e50f17b97232" };
1030
1260
  var DEFAULT_CAIRN = "https://cairn-substrate.com";
1031
1261
  var Cairn = class {
1032
1262
  config;
@@ -1034,6 +1264,8 @@ var Cairn = class {
1034
1264
  index;
1035
1265
  content;
1036
1266
  registry;
1267
+ /** .csd names + CairnX market READS (server-trusted display reads; see names.ts trust note). */
1268
+ names;
1037
1269
  board;
1038
1270
  wallet;
1039
1271
  cairnHttp;
@@ -1062,6 +1294,7 @@ var Cairn = class {
1062
1294
  });
1063
1295
  this.content = new ContentClient({ swarm: swarmHttp, indexer: indexerHttp, cairn: this.cairnHttp });
1064
1296
  this.registry = new RegistryClient({ baseUrl: indexer, fetch });
1297
+ this.names = new NamesClient(this.cairnHttp);
1065
1298
  }
1066
1299
  /**
1067
1300
  * PoW-VERIFIED header merkle at a height (audit M3 — the no-PoW half). Lazily seeds a light
@@ -1114,9 +1347,11 @@ var Cairn = class {
1114
1347
  Chain,
1115
1348
  ContentClient,
1116
1349
  ContentVerificationError,
1350
+ DEFAULT_SPV_CHECKPOINT,
1117
1351
  Http,
1118
1352
  HttpError,
1119
1353
  IndexerClient,
1354
+ NamesClient,
1120
1355
  NotInstalledError,
1121
1356
  RegistryClient,
1122
1357
  SDK_VERSION,