@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.
- package/README.md +16 -16
- package/dist/board.cjs +1 -1
- package/dist/board.d.cts +1 -1
- package/dist/board.d.ts +1 -1
- package/dist/board.js +2 -2
- package/dist/chain.cjs +1 -1
- package/dist/chain.d.cts +1 -1
- package/dist/chain.d.ts +1 -1
- package/dist/chain.js +1 -1
- package/dist/{chunk-G7FABIF4.js → chunk-BGJZF7S2.js} +11 -2
- package/dist/{chunk-5KAUQH72.js → chunk-C452UODV.js} +1 -1
- package/dist/{chunk-FNBE2FPE.js → chunk-J5J4JEJ6.js} +50 -18
- package/dist/{chunk-UMGWVOOT.js → chunk-JGXD3WIC.js} +7 -2
- package/dist/{chunk-ZAE2BIH5.js → chunk-PYREGAB3.js} +21 -3
- package/dist/{chunk-645CYBVN.js → chunk-RYZJDZLJ.js} +1 -1
- package/dist/{chunk-OSXS26RA.js → chunk-SIUUY6FZ.js} +4 -3
- package/dist/{chunk-G4OZKZSJ.js → chunk-UIEOBVFI.js} +8 -2
- package/dist/connect.cjs +14 -3
- package/dist/connect.d.cts +7 -0
- package/dist/connect.d.ts +7 -0
- package/dist/connect.js +2 -2
- package/dist/content.cjs +4 -3
- package/dist/content.d.cts +1 -1
- package/dist/content.d.ts +1 -1
- package/dist/content.js +2 -2
- package/dist/controller.cjs +34 -5
- package/dist/controller.d.cts +1 -0
- package/dist/controller.d.ts +1 -0
- package/dist/controller.js +3 -3
- package/dist/{http-D2CzfOsm.d.cts → http-C9C4L4hz.d.cts} +11 -0
- package/dist/{http-D2CzfOsm.d.ts → http-C9C4L4hz.d.ts} +11 -0
- package/dist/index.cjs +272 -37
- package/dist/index.d.cts +99 -11
- package/dist/index.d.ts +99 -11
- package/dist/index.js +179 -17
- package/dist/indexer.cjs +50 -18
- package/dist/indexer.d.cts +15 -4
- package/dist/indexer.d.ts +15 -4
- package/dist/indexer.js +1 -1
- package/dist/react.cjs +34 -5
- package/dist/react.js +3 -3
- package/dist/registry.cjs +11 -2
- package/dist/registry.d.cts +12 -3
- package/dist/registry.d.ts +12 -3
- package/dist/registry.js +1 -1
- 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.
|
|
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
|
-
|
|
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
|
|
184
|
-
|
|
185
|
-
|
|
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,
|
|
190
|
-
return await res.
|
|
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
|
-
|
|
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
|
|
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).
|
|
508
|
-
*
|
|
509
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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)
|
|
991
|
-
|
|
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:
|
|
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,
|