@inversealtruism/cairn-sdk 0.1.2 → 0.1.4
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/board.cjs +33 -2
- package/dist/board.d.cts +1 -1
- package/dist/board.d.ts +1 -1
- package/dist/board.js +2 -2
- package/dist/chain.cjs +29 -0
- package/dist/chain.d.cts +24 -3
- package/dist/chain.d.ts +24 -3
- package/dist/chain.js +5 -1
- package/dist/{chunk-G7FABIF4.js → chunk-BGJZF7S2.js} +11 -2
- package/dist/{chunk-6VWLWF7R.js → chunk-C452UODV.js} +31 -2
- package/dist/{chunk-OGP2W7DL.js → chunk-FSQZVNEM.js} +1 -1
- package/dist/chunk-K7SJOW3O.js +213 -0
- package/dist/chunk-MBSPHWZG.js +96 -0
- package/dist/{chunk-BZVJVL4Q.js → chunk-MVX7VQT4.js} +4 -3
- package/dist/{chunk-FNBE2FPE.js → chunk-OG4KA3LS.js} +49 -17
- package/dist/chunk-RELP23CH.js +99 -0
- package/dist/connect.cjs +145 -18
- package/dist/connect.d.cts +140 -2
- package/dist/connect.d.ts +140 -2
- package/dist/connect.js +4 -2
- package/dist/content.cjs +37 -5
- package/dist/content.d.cts +1 -1
- package/dist/content.d.ts +1 -1
- package/dist/content.js +2 -2
- package/dist/controller.cjs +355 -0
- package/dist/controller.d.cts +37 -0
- package/dist/controller.d.ts +37 -0
- package/dist/controller.js +8 -0
- package/dist/{http-D2CzfOsm.d.cts → http-CQsNL8w9.d.cts} +5 -0
- package/dist/{http-D2CzfOsm.d.ts → http-CQsNL8w9.d.ts} +5 -0
- package/dist/index.cjs +386 -48
- package/dist/index.d.cts +39 -6
- package/dist/index.d.ts +39 -6
- package/dist/index.js +75 -20
- package/dist/indexer.cjs +49 -17
- package/dist/indexer.d.cts +15 -4
- package/dist/indexer.d.ts +15 -4
- package/dist/indexer.js +1 -1
- package/dist/react.cjs +371 -0
- package/dist/react.d.cts +30 -0
- package/dist/react.d.ts +30 -0
- package/dist/react.js +23 -0
- 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 +21 -10
- package/dist/chunk-PHMU3CG5.js +0 -63
- package/dist/chunk-SMAPCKVG.js +0 -118
package/dist/index.cjs
CHANGED
|
@@ -22,51 +22,88 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
BoardClient: () => BoardClient,
|
|
24
24
|
Cairn: () => Cairn,
|
|
25
|
+
CairnController: () => CairnController,
|
|
25
26
|
CairnError: () => CairnError,
|
|
26
27
|
Chain: () => Chain,
|
|
27
28
|
ContentClient: () => ContentClient,
|
|
28
29
|
ContentVerificationError: () => ContentVerificationError,
|
|
30
|
+
DEFAULT_SPV_CHECKPOINT: () => DEFAULT_SPV_CHECKPOINT,
|
|
29
31
|
Http: () => Http,
|
|
30
32
|
HttpError: () => HttpError,
|
|
31
33
|
IndexerClient: () => IndexerClient,
|
|
32
34
|
NotInstalledError: () => NotInstalledError,
|
|
33
35
|
RegistryClient: () => RegistryClient,
|
|
36
|
+
SDK_VERSION: () => SDK_VERSION,
|
|
34
37
|
UnsupportedMethodError: () => UnsupportedMethodError,
|
|
35
38
|
UserRejectedError: () => UserRejectedError,
|
|
36
39
|
WalletConnection: () => WalletConnection,
|
|
37
40
|
WalletLockedError: () => WalletLockedError,
|
|
38
41
|
connect: () => connect,
|
|
39
42
|
detectProvider: () => detectProvider,
|
|
43
|
+
discoverProviders: () => discoverProviders,
|
|
44
|
+
errorCode: () => errorCode,
|
|
40
45
|
getWallet: () => getWallet,
|
|
41
|
-
isInstalled: () => isInstalled
|
|
46
|
+
isInstalled: () => isInstalled,
|
|
47
|
+
mapProviderError: () => mapProviderError
|
|
42
48
|
});
|
|
43
49
|
module.exports = __toCommonJS(index_exports);
|
|
44
50
|
|
|
45
51
|
// src/errors.ts
|
|
52
|
+
var SDK_VERSION = "0.1.4";
|
|
53
|
+
var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
|
|
46
54
|
var CairnError = class extends Error {
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
/** Stable machine code for programmatic branching. */
|
|
56
|
+
code;
|
|
57
|
+
/** The human message without the appended docs/version footer. */
|
|
58
|
+
shortMessage;
|
|
59
|
+
/** Relative docs path (under the SDK docs base), if any. */
|
|
60
|
+
docsPath;
|
|
61
|
+
/** The cairn-sdk version that produced this error. */
|
|
62
|
+
version = SDK_VERSION;
|
|
63
|
+
constructor(shortMessage, opts = {}) {
|
|
64
|
+
const docs = opts.docsPath;
|
|
65
|
+
super(
|
|
66
|
+
[shortMessage, docs ? `Docs: ${DOCS_BASE}${docs}` : "", `Version: cairn-sdk@${SDK_VERSION}`].filter(Boolean).join("\n"),
|
|
67
|
+
{ cause: opts.cause }
|
|
68
|
+
);
|
|
49
69
|
this.name = new.target.name;
|
|
70
|
+
this.code = opts.code ?? "UNKNOWN";
|
|
71
|
+
this.shortMessage = shortMessage;
|
|
72
|
+
this.docsPath = docs;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Walk the `cause` chain. With a predicate, returns the FIRST matching error (or null). Without one,
|
|
76
|
+
* returns the DEEPEST cause (the root). Mirrors viem's `BaseError.walk()`.
|
|
77
|
+
*/
|
|
78
|
+
walk(fn) {
|
|
79
|
+
let current = this;
|
|
80
|
+
let last = this;
|
|
81
|
+
while (current != null) {
|
|
82
|
+
if (fn && fn(current)) return current;
|
|
83
|
+
last = current;
|
|
84
|
+
current = current?.cause;
|
|
85
|
+
}
|
|
86
|
+
return fn ? null : last;
|
|
50
87
|
}
|
|
51
88
|
};
|
|
52
89
|
var NotInstalledError = class extends CairnError {
|
|
53
90
|
constructor(message = "Cairn Wallet not detected. Install it from cairn-substrate.com.") {
|
|
54
|
-
super(message);
|
|
91
|
+
super(message, { code: "NOT_INSTALLED", docsPath: "/install" });
|
|
55
92
|
}
|
|
56
93
|
};
|
|
57
94
|
var UserRejectedError = class extends CairnError {
|
|
58
95
|
constructor(message = "Request rejected by user.") {
|
|
59
|
-
super(message);
|
|
96
|
+
super(message, { code: "USER_REJECTED", docsPath: "/errors#user-rejected" });
|
|
60
97
|
}
|
|
61
98
|
};
|
|
62
99
|
var WalletLockedError = class extends CairnError {
|
|
63
100
|
constructor(message = "Wallet is locked.") {
|
|
64
|
-
super(message);
|
|
101
|
+
super(message, { code: "WALLET_LOCKED", docsPath: "/errors#wallet-locked" });
|
|
65
102
|
}
|
|
66
103
|
};
|
|
67
104
|
var UnsupportedMethodError = class extends CairnError {
|
|
68
105
|
constructor(message = "Method not supported by the wallet.") {
|
|
69
|
-
super(message);
|
|
106
|
+
super(message, { code: "UNSUPPORTED_METHOD", docsPath: "/errors#unsupported-method" });
|
|
70
107
|
}
|
|
71
108
|
};
|
|
72
109
|
var HttpError = class extends CairnError {
|
|
@@ -74,7 +111,7 @@ var HttpError = class extends CairnError {
|
|
|
74
111
|
url;
|
|
75
112
|
body;
|
|
76
113
|
constructor(status, url, body) {
|
|
77
|
-
super(`HTTP ${status} for ${url}${body ? `: ${String(body).slice(0, 200)}` : ""}
|
|
114
|
+
super(`HTTP ${status} for ${url}${body ? `: ${String(body).slice(0, 200)}` : ""}`, { code: "HTTP_ERROR" });
|
|
78
115
|
this.status = status;
|
|
79
116
|
this.url = url;
|
|
80
117
|
this.body = body;
|
|
@@ -83,7 +120,7 @@ var HttpError = class extends CairnError {
|
|
|
83
120
|
var ContentVerificationError = class extends CairnError {
|
|
84
121
|
expected;
|
|
85
122
|
constructor(expected) {
|
|
86
|
-
super(`Content does not match its payload hash ${expected} \u2014 bytes were tampered with in transit
|
|
123
|
+
super(`Content does not match its payload hash ${expected} \u2014 bytes were tampered with in transit.`, { code: "CONTENT_VERIFICATION", docsPath: "/content#verification" });
|
|
87
124
|
this.expected = expected;
|
|
88
125
|
}
|
|
89
126
|
};
|
|
@@ -91,15 +128,56 @@ function mapProviderError(error) {
|
|
|
91
128
|
const e = String(error || "").toLowerCase();
|
|
92
129
|
if (e.includes("rejected by user")) return new UserRejectedError(error);
|
|
93
130
|
if (e.includes("wallet locked")) return new WalletLockedError(error);
|
|
94
|
-
if (e.includes("unsupported dapp method")) return new UnsupportedMethodError(error);
|
|
131
|
+
if (e.includes("unsupported dapp method") || e.includes("predates") || e.includes("too old")) return new UnsupportedMethodError(error);
|
|
95
132
|
return new CairnError(error || "Unknown wallet error");
|
|
96
133
|
}
|
|
134
|
+
function errorCode(err) {
|
|
135
|
+
return err instanceof CairnError ? err.code : "UNKNOWN";
|
|
136
|
+
}
|
|
97
137
|
|
|
98
138
|
// src/http.ts
|
|
139
|
+
var DEFAULT_MAX_BYTES = 16 * 1024 * 1024;
|
|
140
|
+
var ERR_TEXT_CAP = 8 * 1024;
|
|
141
|
+
async function readCapped(res, maxBytes) {
|
|
142
|
+
const cl = Number(res.headers.get("content-length"));
|
|
143
|
+
if (Number.isFinite(cl) && cl > maxBytes) throw new HttpError(res.status, res.url, `response too large (content-length ${cl} > ${maxBytes})`);
|
|
144
|
+
const body = res.body;
|
|
145
|
+
if (!body || typeof body.getReader !== "function") {
|
|
146
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
147
|
+
if (buf.byteLength > maxBytes) throw new HttpError(res.status, res.url, `response too large (${buf.byteLength} > ${maxBytes})`);
|
|
148
|
+
return buf;
|
|
149
|
+
}
|
|
150
|
+
const reader = body.getReader();
|
|
151
|
+
const chunks = [];
|
|
152
|
+
let total = 0;
|
|
153
|
+
for (; ; ) {
|
|
154
|
+
const { done, value } = await reader.read();
|
|
155
|
+
if (done) break;
|
|
156
|
+
if (value) {
|
|
157
|
+
total += value.byteLength;
|
|
158
|
+
if (total > maxBytes) {
|
|
159
|
+
try {
|
|
160
|
+
await reader.cancel();
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
throw new HttpError(res.status, res.url, `response exceeded ${maxBytes} bytes`);
|
|
164
|
+
}
|
|
165
|
+
chunks.push(value);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const out = new Uint8Array(total);
|
|
169
|
+
let off = 0;
|
|
170
|
+
for (const c of chunks) {
|
|
171
|
+
out.set(c, off);
|
|
172
|
+
off += c.byteLength;
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
99
176
|
var Http = class {
|
|
100
177
|
baseUrl;
|
|
101
178
|
_fetch;
|
|
102
179
|
timeoutMs;
|
|
180
|
+
maxBytes;
|
|
103
181
|
headers;
|
|
104
182
|
constructor(opts) {
|
|
105
183
|
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
@@ -107,12 +185,17 @@ var Http = class {
|
|
|
107
185
|
if (!f) throw new Error("No fetch available \u2014 pass `fetch` in the SDK options.");
|
|
108
186
|
this._fetch = opts.fetch ? opts.fetch : f.bind(globalThis);
|
|
109
187
|
this.timeoutMs = opts.timeoutMs ?? 15e3;
|
|
188
|
+
this.maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
110
189
|
this.headers = opts.headers ?? {};
|
|
111
190
|
}
|
|
112
191
|
/** Build an absolute URL from a path + optional query params. */
|
|
113
192
|
url(path, query) {
|
|
114
193
|
const p = path.startsWith("/") ? path : `/${path}`;
|
|
115
194
|
const u = new URL(this.baseUrl + p);
|
|
195
|
+
const basePath = new URL(this.baseUrl + "/").pathname;
|
|
196
|
+
if (basePath !== "/" && !(u.pathname + "/").startsWith(basePath)) {
|
|
197
|
+
throw new Error(`illegal path '${path}' \u2014 escapes base ${basePath}`);
|
|
198
|
+
}
|
|
116
199
|
if (query) {
|
|
117
200
|
for (const [k, v] of Object.entries(query)) {
|
|
118
201
|
if (v !== void 0 && v !== null) u.searchParams.set(k, String(v));
|
|
@@ -143,25 +226,24 @@ var Http = class {
|
|
|
143
226
|
async getJson(path, query) {
|
|
144
227
|
const res = await this.raw("GET", path, { query });
|
|
145
228
|
if (!res.ok) throw new HttpError(res.status, this.url(path, query), await safeText(res));
|
|
146
|
-
return await res.
|
|
229
|
+
return JSON.parse(new TextDecoder().decode(await readCapped(res, this.maxBytes)));
|
|
147
230
|
}
|
|
148
231
|
async postJson(path, body, headers) {
|
|
149
232
|
const res = await this.raw("POST", path, { body, headers });
|
|
150
233
|
if (!res.ok) throw new HttpError(res.status, this.url(path), await safeText(res));
|
|
151
|
-
return await res.
|
|
234
|
+
return JSON.parse(new TextDecoder().decode(await readCapped(res, this.maxBytes)));
|
|
152
235
|
}
|
|
153
236
|
/** GET raw bytes (for content retrieval). Returns null on 404. */
|
|
154
237
|
async getBytes(path) {
|
|
155
238
|
const res = await this.raw("GET", path);
|
|
156
239
|
if (res.status === 404) return null;
|
|
157
240
|
if (!res.ok) throw new HttpError(res.status, this.url(path), await safeText(res));
|
|
158
|
-
|
|
159
|
-
return { bytes: buf, headers: res.headers };
|
|
241
|
+
return { bytes: await readCapped(res, this.maxBytes), headers: res.headers };
|
|
160
242
|
}
|
|
161
243
|
};
|
|
162
244
|
async function safeText(res) {
|
|
163
245
|
try {
|
|
164
|
-
return await res
|
|
246
|
+
return new TextDecoder().decode(await readCapped(res, ERR_TEXT_CAP));
|
|
165
247
|
} catch {
|
|
166
248
|
return "";
|
|
167
249
|
}
|
|
@@ -175,6 +257,8 @@ var import_csd_crypto = require("@inversealtruism/csd-crypto");
|
|
|
175
257
|
var import_csd_light = require("@inversealtruism/csd-light");
|
|
176
258
|
var import_csd_client2 = require("@inversealtruism/csd-client");
|
|
177
259
|
var import_csd_light2 = require("@inversealtruism/csd-light");
|
|
260
|
+
var import_csd_tx2 = require("@inversealtruism/csd-tx");
|
|
261
|
+
var import_csd_crypto2 = require("@inversealtruism/csd-crypto");
|
|
178
262
|
var Chain = class {
|
|
179
263
|
client;
|
|
180
264
|
opts;
|
|
@@ -195,6 +279,29 @@ var Chain = class {
|
|
|
195
279
|
submit(nodeJsonTx) {
|
|
196
280
|
return this.client.submit(nodeJsonTx);
|
|
197
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* Build + sign (+ optionally broadcast) a transfer with INPUT-VALUE VERIFICATION (the H2-safe send).
|
|
284
|
+
* Fetches the signer's UTXOs, selects, then re-verifies each selected input's REAL value against the
|
|
285
|
+
* chain (`verifyInputValues` recomputes each source tx's txid, so a lying RPC can't under-report) and
|
|
286
|
+
* computes change from the VERIFIED total — closing the UTXO-VALUE-1 implicit-fee-burn. Fail-closed:
|
|
287
|
+
* if any source can't be verified it signs nothing. Prefer this over the pure `buildSend`. Pass
|
|
288
|
+
* `broadcast:false` to get the signed tx back without submitting.
|
|
289
|
+
*/
|
|
290
|
+
async send(p) {
|
|
291
|
+
const from = (0, import_csd_crypto2.addrFromPriv)(p.priv);
|
|
292
|
+
const u = await this.client.utxos(from);
|
|
293
|
+
const built = await (0, import_csd_tx2.buildSendVerified)({
|
|
294
|
+
outputs: p.outputs,
|
|
295
|
+
fee: p.fee,
|
|
296
|
+
utxos: u.utxos,
|
|
297
|
+
priv: p.priv,
|
|
298
|
+
maxFee: p.maxFee,
|
|
299
|
+
verify: (inputs) => (0, import_csd_client2.verifyInputValues)(this.client, inputs)
|
|
300
|
+
});
|
|
301
|
+
if (!built.ok || p.broadcast === false) return built;
|
|
302
|
+
const submitResult = await this.client.submit(built.nodeJson);
|
|
303
|
+
return { ...built, ok: built.ok && submitResult.ok !== false, submitResult };
|
|
304
|
+
}
|
|
198
305
|
/** A verifying light client over the same RPC (built once, reused). */
|
|
199
306
|
light() {
|
|
200
307
|
if (!this._light) {
|
|
@@ -362,6 +469,13 @@ function sleep(ms) {
|
|
|
362
469
|
|
|
363
470
|
// src/indexer.ts
|
|
364
471
|
var import_csd_codec4 = require("@inversealtruism/csd-codec");
|
|
472
|
+
var HEX64 = /^0x[0-9a-fA-F]{64}$/;
|
|
473
|
+
var MAX_SSE_BUF = 1024 * 1024;
|
|
474
|
+
function isValidMerkleProof(p) {
|
|
475
|
+
if (!p || typeof p !== "object") return false;
|
|
476
|
+
const m = p;
|
|
477
|
+
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));
|
|
478
|
+
}
|
|
365
479
|
var IndexerClient = class {
|
|
366
480
|
http;
|
|
367
481
|
opts;
|
|
@@ -382,32 +496,35 @@ var IndexerClient = class {
|
|
|
382
496
|
blockHashAtHeight(h) {
|
|
383
497
|
return this.http.getJson(`/block-height/${h}`);
|
|
384
498
|
}
|
|
499
|
+
// CAIRNSDK-CONTENT-HASH-PATH-1: encode every caller-supplied id segment. Raw interpolation let an id like
|
|
500
|
+
// `../../api/rpc/tip` walk `new URL` to a DIFFERENT same-origin endpoint whose body was then returned
|
|
501
|
+
// verbatim as the requested object (worse than the content case, which discards the walked-to response).
|
|
385
502
|
block(hash) {
|
|
386
|
-
return this.http.getJson(`/block/${hash}`);
|
|
503
|
+
return this.http.getJson(`/block/${encodeURIComponent(hash)}`);
|
|
387
504
|
}
|
|
388
505
|
blockTxids(hash) {
|
|
389
|
-
return this.http.getJson(`/block/${hash}/txids`);
|
|
506
|
+
return this.http.getJson(`/block/${encodeURIComponent(hash)}/txids`);
|
|
390
507
|
}
|
|
391
508
|
tx(id) {
|
|
392
|
-
return this.http.getJson(`/tx/${id}`);
|
|
509
|
+
return this.http.getJson(`/tx/${encodeURIComponent(id)}`);
|
|
393
510
|
}
|
|
394
511
|
txStatus(id) {
|
|
395
|
-
return this.http.getJson(`/tx/${id}/status`);
|
|
512
|
+
return this.http.getJson(`/tx/${encodeURIComponent(id)}/status`);
|
|
396
513
|
}
|
|
397
514
|
txMerkleProof(id) {
|
|
398
|
-
return this.http.getJson(`/tx/${id}/merkle-proof`);
|
|
515
|
+
return this.http.getJson(`/tx/${encodeURIComponent(id)}/merkle-proof`);
|
|
399
516
|
}
|
|
400
517
|
address(a) {
|
|
401
|
-
return this.http.getJson(`/address/${a}`);
|
|
518
|
+
return this.http.getJson(`/address/${encodeURIComponent(a)}`);
|
|
402
519
|
}
|
|
403
520
|
addressTxs(a) {
|
|
404
|
-
return this.http.getJson(`/address/${a}/txs`);
|
|
521
|
+
return this.http.getJson(`/address/${encodeURIComponent(a)}/txs`);
|
|
405
522
|
}
|
|
406
523
|
addressUtxo(a) {
|
|
407
|
-
return this.http.getJson(`/address/${a}/utxo`);
|
|
524
|
+
return this.http.getJson(`/address/${encodeURIComponent(a)}/utxo`);
|
|
408
525
|
}
|
|
409
526
|
reputation(a) {
|
|
410
|
-
return this.http.getJson(`/address/${a}/reputation`);
|
|
527
|
+
return this.http.getJson(`/address/${encodeURIComponent(a)}/reputation`);
|
|
411
528
|
}
|
|
412
529
|
domains() {
|
|
413
530
|
return this.http.getJson("/domains");
|
|
@@ -416,10 +533,10 @@ var IndexerClient = class {
|
|
|
416
533
|
return this.http.getJson(`/domain/${encodeURIComponent(d)}/proposals`);
|
|
417
534
|
}
|
|
418
535
|
proposal(id) {
|
|
419
|
-
return this.http.getJson(`/proposal/${id}`);
|
|
536
|
+
return this.http.getJson(`/proposal/${encodeURIComponent(id)}`);
|
|
420
537
|
}
|
|
421
538
|
attestations(id) {
|
|
422
|
-
return this.http.getJson(`/proposal/${id}/attestations`);
|
|
539
|
+
return this.http.getJson(`/proposal/${encodeURIComponent(id)}/attestations`);
|
|
423
540
|
}
|
|
424
541
|
registryPeers() {
|
|
425
542
|
return this.http.getJson("/registry/peers");
|
|
@@ -431,7 +548,7 @@ var IndexerClient = class {
|
|
|
431
548
|
return this.http.getJson(`/identity/${encodeURIComponent(handle)}`);
|
|
432
549
|
}
|
|
433
550
|
reverseIdentity(addr) {
|
|
434
|
-
return this.http.getJson(`/address/${addr}/identity`);
|
|
551
|
+
return this.http.getJson(`/address/${encodeURIComponent(addr)}/identity`);
|
|
435
552
|
}
|
|
436
553
|
// ---- trust-minimized inclusion ----------------------------------------
|
|
437
554
|
/**
|
|
@@ -440,9 +557,11 @@ var IndexerClient = class {
|
|
|
440
557
|
* proof's root is cross-checked against the header merkle at that height →
|
|
441
558
|
* trustLevel "verified-inclusion" (only as trustworthy as that header source —
|
|
442
559
|
* see headerMerkleAt). Otherwise the proof is only internally consistent →
|
|
443
|
-
* "proof-consistent" (still useful, but the indexer is trusted).
|
|
444
|
-
*
|
|
445
|
-
*
|
|
560
|
+
* "proof-consistent" (still useful, but the indexer is trusted). INDEXER-DOC-STALE-1: the Cairn facade now
|
|
561
|
+
* wires headerMerkleAt UNCONDITIONALLY via a PoW-verifying checkpoint light client (index.ts), so the
|
|
562
|
+
* default reaches "verified-inclusion" when the proof root matches the PoW-verified header — what makes a
|
|
563
|
+
* same-origin RPC safe is the forward PoW re-verification, not an origin gate (which no longer exists).
|
|
564
|
+
* Fails closed: a malformed proof or a chain-disagreement returns "not-found" (the latter with equivocation:true).
|
|
446
565
|
*/
|
|
447
566
|
async verifyInclusion(txid2) {
|
|
448
567
|
let proof;
|
|
@@ -451,7 +570,15 @@ var IndexerClient = class {
|
|
|
451
570
|
} catch {
|
|
452
571
|
return { included: false, trustLevel: "not-found", reason: "no merkle proof (tx not indexed)" };
|
|
453
572
|
}
|
|
454
|
-
|
|
573
|
+
if (!isValidMerkleProof(proof)) {
|
|
574
|
+
return { included: false, trustLevel: "not-found", reason: "malformed merkle proof" };
|
|
575
|
+
}
|
|
576
|
+
let folds = false;
|
|
577
|
+
try {
|
|
578
|
+
folds = (0, import_csd_codec4.verifyMerkleProof)(txid2, proof.pos, proof.merkle, proof.merkle_root);
|
|
579
|
+
} catch {
|
|
580
|
+
return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "malformed merkle proof" };
|
|
581
|
+
}
|
|
455
582
|
if (!folds) {
|
|
456
583
|
return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "merkle branch does not fold to the claimed root" };
|
|
457
584
|
}
|
|
@@ -459,7 +586,7 @@ var IndexerClient = class {
|
|
|
459
586
|
try {
|
|
460
587
|
const onchain = await this.opts.headerMerkleAt(proof.block_height);
|
|
461
588
|
if (norm(onchain) !== norm(proof.merkle_root)) {
|
|
462
|
-
return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "proof root != on-chain header merkle (indexer disagrees with chain)" };
|
|
589
|
+
return { included: false, trustLevel: "not-found", equivocation: true, blockHeight: proof.block_height, reason: "proof root != on-chain header merkle (indexer disagrees with chain)" };
|
|
463
590
|
}
|
|
464
591
|
return { included: true, trustLevel: "verified-inclusion", blockHeight: proof.block_height, pos: proof.pos, merkleRoot: proof.merkle_root };
|
|
465
592
|
} catch {
|
|
@@ -483,6 +610,11 @@ var IndexerClient = class {
|
|
|
483
610
|
/**
|
|
484
611
|
* Subscribe over WebSocket with selective tracking. Returns a handle with
|
|
485
612
|
* `.close()`. `onEvent` receives every matching IndexEvent (incl. reorgs).
|
|
613
|
+
*
|
|
614
|
+
* WS-NORECONNECT-1: unlike the SSE feeds (streamAll/streamBlocks/streamDomain, which auto-reconnect with
|
|
615
|
+
* backoff), this WebSocket subscription does NOT auto-reconnect — it surfaces a terminal error via
|
|
616
|
+
* `onError` (when provided) and stops. A consumer tracking reorgs over WS MUST re-`subscribe()` on error
|
|
617
|
+
* to avoid a silently-stale view; for built-in reconnect prefer the SSE `stream*` methods.
|
|
486
618
|
*/
|
|
487
619
|
subscribe(sub, onEvent, onError) {
|
|
488
620
|
const WS = this.opts.WebSocketImpl ?? globalThis.WebSocket;
|
|
@@ -545,6 +677,13 @@ var IndexerClient = class {
|
|
|
545
677
|
buf = buf.slice(idx + 2);
|
|
546
678
|
dispatchFrame(frame, handlers);
|
|
547
679
|
}
|
|
680
|
+
if (buf.length > MAX_SSE_BUF) {
|
|
681
|
+
try {
|
|
682
|
+
await reader.cancel();
|
|
683
|
+
} catch {
|
|
684
|
+
}
|
|
685
|
+
throw new Error(`SSE buffer exceeded ${MAX_SSE_BUF} bytes with no frame delimiter \u2014 aborting`);
|
|
686
|
+
}
|
|
548
687
|
}
|
|
549
688
|
} catch (err) {
|
|
550
689
|
if (closed) return;
|
|
@@ -657,8 +796,9 @@ var ContentClient = class {
|
|
|
657
796
|
}
|
|
658
797
|
};
|
|
659
798
|
function normalizeHash(hash) {
|
|
660
|
-
const h = hash.startsWith("0x") || hash.startsWith("0X") ? hash : `0x${hash}
|
|
661
|
-
|
|
799
|
+
const h = (hash.startsWith("0x") || hash.startsWith("0X") ? hash : `0x${hash}`).toLowerCase();
|
|
800
|
+
if (!/^0x[0-9a-f]{64}$/.test(h)) throw new Error(`invalid content hash: ${hash}`);
|
|
801
|
+
return h;
|
|
662
802
|
}
|
|
663
803
|
|
|
664
804
|
// src/registry.ts
|
|
@@ -677,11 +817,20 @@ var RegistryClient = class {
|
|
|
677
817
|
peers() {
|
|
678
818
|
return (0, import_csd_registry.discoverPeers)(this.src);
|
|
679
819
|
}
|
|
680
|
-
/**
|
|
820
|
+
/**
|
|
821
|
+
* Resolve a handle → identity (GET /identity/:handle). Returns null if unresolved.
|
|
822
|
+
*
|
|
823
|
+
* ⚠ TRUST (CAIRN-SDK-RESOLVE-VERIFIED-FOOTGUN): the returned `verified` flag is the INDEXER's assertion,
|
|
824
|
+
* NOT a client-side proof. A hostile/compromised indexer (or a MITM of the read proxy) can return
|
|
825
|
+
* `verified:true` for a wrong address. Do NOT use this directly as a payee/login target without either (a)
|
|
826
|
+
* recomputing from raw records via `fromRecords.name(...)`, or (b) cross-checking a second independent
|
|
827
|
+
* source (the wallet's namespv union cure). Treat `verified` here as a hint, not a guarantee.
|
|
828
|
+
*/
|
|
681
829
|
resolveName(handle) {
|
|
682
830
|
return (0, import_csd_registry.resolveName)(this.src, handle);
|
|
683
831
|
}
|
|
684
|
-
/** Reverse-resolve an address → identity (GET /address/:addr/identity).
|
|
832
|
+
/** Reverse-resolve an address → identity (GET /address/:addr/identity).
|
|
833
|
+
* ⚠ Same indexer-trust caveat as resolveName — `verified` is the indexer's claim, not a client proof. */
|
|
685
834
|
reverseName(address) {
|
|
686
835
|
return (0, import_csd_registry.reverseName)(this.src, address);
|
|
687
836
|
}
|
|
@@ -692,6 +841,26 @@ var RegistryClient = class {
|
|
|
692
841
|
};
|
|
693
842
|
|
|
694
843
|
// src/connect.ts
|
|
844
|
+
function discoverProviders(opts = {}) {
|
|
845
|
+
const timeoutMs = opts.timeoutMs ?? 1e3;
|
|
846
|
+
return new Promise((resolve) => {
|
|
847
|
+
if (typeof window === "undefined") {
|
|
848
|
+
resolve([]);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const found = /* @__PURE__ */ new Map();
|
|
852
|
+
const onAnnounce = (ev) => {
|
|
853
|
+
const d = ev?.detail;
|
|
854
|
+
if (d?.info?.uuid && d?.provider?.isCairn) found.set(d.info.uuid, d);
|
|
855
|
+
};
|
|
856
|
+
window.addEventListener("csd:announceProvider", onAnnounce);
|
|
857
|
+
window.dispatchEvent(new CustomEvent("csd:requestProvider"));
|
|
858
|
+
setTimeout(() => {
|
|
859
|
+
window.removeEventListener("csd:announceProvider", onAnnounce);
|
|
860
|
+
resolve([...found.values()]);
|
|
861
|
+
}, timeoutMs);
|
|
862
|
+
});
|
|
863
|
+
}
|
|
695
864
|
function detectProvider(opts = {}) {
|
|
696
865
|
const timeoutMs = opts.timeoutMs ?? 3e3;
|
|
697
866
|
return new Promise((resolve, reject) => {
|
|
@@ -699,18 +868,19 @@ function detectProvider(opts = {}) {
|
|
|
699
868
|
reject(new NotInstalledError("Not running in a browser \u2014 `window.cairn` is unavailable."));
|
|
700
869
|
return;
|
|
701
870
|
}
|
|
702
|
-
if (window.cairn?.isCairn) {
|
|
703
|
-
resolve(window.cairn);
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
871
|
let done = false;
|
|
707
|
-
const
|
|
872
|
+
const finish = (p) => {
|
|
708
873
|
if (done) return;
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
874
|
+
done = true;
|
|
875
|
+
cleanup();
|
|
876
|
+
resolve(p);
|
|
877
|
+
};
|
|
878
|
+
const onInit = () => {
|
|
879
|
+
if (!done && window.cairn?.isCairn) finish(window.cairn);
|
|
880
|
+
};
|
|
881
|
+
const onAnnounce = (ev) => {
|
|
882
|
+
const p = ev?.detail?.provider;
|
|
883
|
+
if (p?.isCairn) finish(p);
|
|
714
884
|
};
|
|
715
885
|
const timer = setTimeout(() => {
|
|
716
886
|
if (done) return;
|
|
@@ -721,8 +891,15 @@ function detectProvider(opts = {}) {
|
|
|
721
891
|
const cleanup = () => {
|
|
722
892
|
clearTimeout(timer);
|
|
723
893
|
window.removeEventListener("cairn#initialized", onInit);
|
|
894
|
+
window.removeEventListener("csd:announceProvider", onAnnounce);
|
|
724
895
|
};
|
|
725
896
|
window.addEventListener("cairn#initialized", onInit);
|
|
897
|
+
window.addEventListener("csd:announceProvider", onAnnounce);
|
|
898
|
+
if (window.cairn?.isCairn) {
|
|
899
|
+
finish(window.cairn);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
window.dispatchEvent(new CustomEvent("csd:requestProvider"));
|
|
726
903
|
onInit();
|
|
727
904
|
});
|
|
728
905
|
}
|
|
@@ -759,10 +936,76 @@ var WalletConnection = class {
|
|
|
759
936
|
this._addr = addr;
|
|
760
937
|
return addr;
|
|
761
938
|
}
|
|
762
|
-
/**
|
|
939
|
+
/**
|
|
940
|
+
* @deprecated First-party-only legacy login (it authenticates against the wallet's own configured
|
|
941
|
+
* server and is rejected from third-party origins). Use {@link signInWithCsd} for any site.
|
|
942
|
+
* Passwordless sign-in: a signature over a login nonce (always prompts; structurally cannot sign a tx).
|
|
943
|
+
*/
|
|
763
944
|
signIn() {
|
|
764
945
|
return Promise.resolve(this.provider.signIn()).then(unwrap);
|
|
765
946
|
}
|
|
947
|
+
/**
|
|
948
|
+
* Audience-bound "Sign in with CSD" (SIWC) — the secure, replay-resistant third-party login.
|
|
949
|
+
* The wallet binds the REAL requesting origin + your single-use server `nonce` into the signed
|
|
950
|
+
* message and returns the artifact; it never mints a session. VERIFY the result SERVER-SIDE with
|
|
951
|
+
* `verifySiwc` from `@inversealtruism/csd-siwc` (domain/nonce/chain/time/signature/account), then
|
|
952
|
+
* issue your OWN session. The signature is proof-of-control, NOT a bearer token. Always prompts.
|
|
953
|
+
*/
|
|
954
|
+
signInWithCsd(params) {
|
|
955
|
+
if (!params || typeof params.nonce !== "string" || !params.nonce) {
|
|
956
|
+
return Promise.reject(new UnsupportedMethodError("signInWithCsd requires a server-issued { nonce }"));
|
|
957
|
+
}
|
|
958
|
+
if (typeof this.provider.signInWithCsd !== "function") {
|
|
959
|
+
return Promise.reject(new UnsupportedMethodError("this wallet predates Sign-in-with-CSD \u2014 ask the user to update the Cairn Wallet"));
|
|
960
|
+
}
|
|
961
|
+
return Promise.resolve(this.provider.signInWithCsd(params)).then(unwrap);
|
|
962
|
+
}
|
|
963
|
+
/** True if this wallet supports audience-bound SIWC (safe to call `signInWithCsd`). */
|
|
964
|
+
get supportsSiwc() {
|
|
965
|
+
return typeof this.provider.signInWithCsd === "function";
|
|
966
|
+
}
|
|
967
|
+
/** Feature/capability detection. Returns null if the wallet predates `getCapabilities`. */
|
|
968
|
+
async getCapabilities() {
|
|
969
|
+
if (typeof this.provider.getCapabilities !== "function") return null;
|
|
970
|
+
try {
|
|
971
|
+
return await this.provider.getCapabilities();
|
|
972
|
+
} catch {
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
/** Atomic fill (CairnX DvP): pay + attest in ONE tx. Always prompts with clear-signing. */
|
|
977
|
+
fillOffer(params) {
|
|
978
|
+
return Promise.resolve(this.provider.fillOffer(params)).then(unwrap);
|
|
979
|
+
}
|
|
980
|
+
/** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
|
|
981
|
+
async getPermissions() {
|
|
982
|
+
if (typeof this.provider.getPermissions !== "function") return [];
|
|
983
|
+
return unwrap(await this.provider.getPermissions());
|
|
984
|
+
}
|
|
985
|
+
/** Explicitly request permission to see the address (prompts, like connect). */
|
|
986
|
+
requestPermissions() {
|
|
987
|
+
if (typeof this.provider.requestPermissions !== "function") {
|
|
988
|
+
return Promise.reject(new UnsupportedMethodError("this wallet predates requestPermissions \u2014 use connect()"));
|
|
989
|
+
}
|
|
990
|
+
return Promise.resolve(this.provider.requestPermissions()).then(unwrap);
|
|
991
|
+
}
|
|
992
|
+
/** Revoke THIS origin's own access (silent). Returns { revoked }. Safe no-op on older wallets. */
|
|
993
|
+
async revokePermissions() {
|
|
994
|
+
if (typeof this.provider.revokePermissions !== "function") return { revoked: false };
|
|
995
|
+
return unwrap(await this.provider.revokePermissions());
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Subscribe to provider events. `accountsChanged` fires with `[]` when this site loses access
|
|
999
|
+
* (wallet lock / account switch / revoke) — re-call connect() to (re)share; `disconnect` likewise.
|
|
1000
|
+
* No-op if the wallet predates events. Use the SAME handler reference with off() to unsubscribe.
|
|
1001
|
+
*/
|
|
1002
|
+
on(event, handler) {
|
|
1003
|
+
this.provider.on?.(event, handler);
|
|
1004
|
+
}
|
|
1005
|
+
/** Remove a previously-added event handler (must be the same reference passed to on()). */
|
|
1006
|
+
off(event, handler) {
|
|
1007
|
+
this.provider.removeListener?.(event, handler);
|
|
1008
|
+
}
|
|
766
1009
|
/** Send CSD (always prompts with clear-signing; wallet picks inputs + returns change to itself). */
|
|
767
1010
|
send(params) {
|
|
768
1011
|
return Promise.resolve(this.provider.send(params)).then(unwrap);
|
|
@@ -796,6 +1039,95 @@ async function connect(opts = {}) {
|
|
|
796
1039
|
return { wallet, address };
|
|
797
1040
|
}
|
|
798
1041
|
|
|
1042
|
+
// src/controller.ts
|
|
1043
|
+
var DISCONNECTED = Object.freeze({ status: "disconnected", account: null, error: null });
|
|
1044
|
+
var CairnController = class {
|
|
1045
|
+
state = DISCONNECTED;
|
|
1046
|
+
conn = null;
|
|
1047
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1048
|
+
getWalletFn;
|
|
1049
|
+
detectOpts;
|
|
1050
|
+
constructor(opts = {}) {
|
|
1051
|
+
const { getWallet: gw, ...detect } = opts;
|
|
1052
|
+
this.getWalletFn = gw ?? getWallet;
|
|
1053
|
+
this.detectOpts = detect;
|
|
1054
|
+
}
|
|
1055
|
+
/** useSyncExternalStore-compatible. Returns an unsubscribe fn. */
|
|
1056
|
+
subscribe = (cb) => {
|
|
1057
|
+
this.listeners.add(cb);
|
|
1058
|
+
return () => {
|
|
1059
|
+
this.listeners.delete(cb);
|
|
1060
|
+
};
|
|
1061
|
+
};
|
|
1062
|
+
/** useSyncExternalStore-compatible snapshot — stable reference until the state actually changes. */
|
|
1063
|
+
getSnapshot = () => this.state;
|
|
1064
|
+
/** The underlying WalletConnection (for advanced use: send/propose/permissions/…). Null until connect(). */
|
|
1065
|
+
get connection() {
|
|
1066
|
+
return this.conn;
|
|
1067
|
+
}
|
|
1068
|
+
setState(patch) {
|
|
1069
|
+
const next = { ...this.state, ...patch };
|
|
1070
|
+
if (next.status === this.state.status && next.account === this.state.account && next.error === this.state.error) return;
|
|
1071
|
+
this.state = Object.freeze(next);
|
|
1072
|
+
for (const l of [...this.listeners]) l();
|
|
1073
|
+
}
|
|
1074
|
+
onAccounts = (accounts) => {
|
|
1075
|
+
if (!Array.isArray(accounts) || accounts.length === 0) {
|
|
1076
|
+
this.setState({ status: "disconnected", account: null });
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
const next = String(accounts[0]);
|
|
1080
|
+
if (this.state.status === "connected" && this.state.account && next.toLowerCase() === this.state.account.toLowerCase()) {
|
|
1081
|
+
this.setState({ status: "connected", account: next, error: null });
|
|
1082
|
+
} else {
|
|
1083
|
+
this.setState({ status: "disconnected", account: null });
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
onDisconnect = () => {
|
|
1087
|
+
this.setState({ status: "disconnected", account: null });
|
|
1088
|
+
};
|
|
1089
|
+
// Detach the provider listeners (ghost-reconnect fix). Idempotent + best-effort.
|
|
1090
|
+
detach() {
|
|
1091
|
+
try {
|
|
1092
|
+
this.conn?.off?.("accountsChanged", this.onAccounts);
|
|
1093
|
+
this.conn?.off?.("disconnect", this.onDisconnect);
|
|
1094
|
+
} catch {
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
/** Detect + connect (prompts the user the first time per origin). Resolves the connected address. */
|
|
1098
|
+
connect = async () => {
|
|
1099
|
+
this.setState({ status: "connecting", error: null });
|
|
1100
|
+
try {
|
|
1101
|
+
if (!this.conn) {
|
|
1102
|
+
this.conn = await this.getWalletFn(this.detectOpts);
|
|
1103
|
+
this.conn.on("accountsChanged", this.onAccounts);
|
|
1104
|
+
this.conn.on("disconnect", this.onDisconnect);
|
|
1105
|
+
}
|
|
1106
|
+
const addr = await this.conn.connect();
|
|
1107
|
+
this.setState({ status: "connected", account: addr, error: null });
|
|
1108
|
+
return addr;
|
|
1109
|
+
} catch (e) {
|
|
1110
|
+
this.setState({ status: "disconnected", account: null, error: e?.message ?? String(e) });
|
|
1111
|
+
throw e;
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
/** Forget the connection locally AND revoke this origin's wallet-side permission (best-effort). */
|
|
1115
|
+
disconnect = async () => {
|
|
1116
|
+
try {
|
|
1117
|
+
await this.conn?.revokePermissions();
|
|
1118
|
+
} catch {
|
|
1119
|
+
}
|
|
1120
|
+
this.detach();
|
|
1121
|
+
this.conn = null;
|
|
1122
|
+
this.setState({ status: "disconnected", account: null, error: null });
|
|
1123
|
+
};
|
|
1124
|
+
/** Audience-bound sign-in (must connect() first). Returns the signed artifact to verify server-side. */
|
|
1125
|
+
signInWithCsd = (params) => {
|
|
1126
|
+
if (!this.conn) return Promise.reject(new Error("call connect() before signInWithCsd()"));
|
|
1127
|
+
return this.conn.signInWithCsd(params);
|
|
1128
|
+
};
|
|
1129
|
+
};
|
|
1130
|
+
|
|
799
1131
|
// src/index.ts
|
|
800
1132
|
var DEFAULT_SPV_CHECKPOINT = { height: 31310, hash: "0x000000000000138bd3eee4b8d2fbacb8d5433ac3040ddbf1c45a5e3e0cc9e814" };
|
|
801
1133
|
var DEFAULT_CAIRN = "https://cairn-substrate.com";
|
|
@@ -880,21 +1212,27 @@ var Cairn = class {
|
|
|
880
1212
|
0 && (module.exports = {
|
|
881
1213
|
BoardClient,
|
|
882
1214
|
Cairn,
|
|
1215
|
+
CairnController,
|
|
883
1216
|
CairnError,
|
|
884
1217
|
Chain,
|
|
885
1218
|
ContentClient,
|
|
886
1219
|
ContentVerificationError,
|
|
1220
|
+
DEFAULT_SPV_CHECKPOINT,
|
|
887
1221
|
Http,
|
|
888
1222
|
HttpError,
|
|
889
1223
|
IndexerClient,
|
|
890
1224
|
NotInstalledError,
|
|
891
1225
|
RegistryClient,
|
|
1226
|
+
SDK_VERSION,
|
|
892
1227
|
UnsupportedMethodError,
|
|
893
1228
|
UserRejectedError,
|
|
894
1229
|
WalletConnection,
|
|
895
1230
|
WalletLockedError,
|
|
896
1231
|
connect,
|
|
897
1232
|
detectProvider,
|
|
1233
|
+
discoverProviders,
|
|
1234
|
+
errorCode,
|
|
898
1235
|
getWallet,
|
|
899
|
-
isInstalled
|
|
1236
|
+
isInstalled,
|
|
1237
|
+
mapProviderError
|
|
900
1238
|
});
|