@inversealtruism/cairn-sdk 0.4.6 → 0.6.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 +13 -21
- package/dist/{chunk-ZFVE6MU7.js → chunk-2A4AJBO5.js} +1 -1
- package/dist/{chunk-2HP3ZXBO.js → chunk-67QBDZPW.js} +36 -5
- package/dist/{chunk-FWTPXLGV.js → chunk-BMH2MACB.js} +2 -2
- package/dist/{chunk-J5J4JEJ6.js → chunk-K5MGEYEU.js} +13 -3
- package/dist/{chunk-OB33SSGA.js → chunk-MLNW2RQB.js} +6 -3
- package/dist/{chunk-E4YQRWZZ.js → chunk-QEYBWBXC.js} +19 -8
- package/dist/connect.cjs +2 -2
- package/dist/connect.d.cts +2 -2
- package/dist/connect.d.ts +2 -2
- package/dist/connect.js +2 -2
- package/dist/content.cjs +18 -8
- package/dist/content.d.cts +8 -5
- package/dist/content.d.ts +8 -5
- package/dist/content.js +2 -2
- package/dist/controller.cjs +7 -4
- package/dist/controller.js +3 -3
- package/dist/index.cjs +214 -227
- package/dist/index.d.cts +54 -23
- package/dist/index.d.ts +54 -23
- package/dist/index.js +144 -53
- package/dist/indexer.cjs +13 -3
- package/dist/indexer.d.cts +3 -0
- package/dist/indexer.d.ts +3 -0
- package/dist/indexer.js +1 -1
- package/dist/react.cjs +12 -5
- package/dist/react.js +8 -4
- package/dist/registry.cjs +34 -5
- package/dist/registry.js +2 -2
- package/package.json +11 -17
- package/dist/board.cjs +0 -231
- package/dist/board.d.cts +0 -151
- package/dist/board.d.ts +0 -151
- package/dist/board.js +0 -7
- package/dist/chunk-E2MA75S3.js +0 -163
package/dist/index.js
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Http,
|
|
3
|
-
RegistryClient,
|
|
4
|
-
readCapped
|
|
5
|
-
} from "./chunk-2HP3ZXBO.js";
|
|
6
|
-
import {
|
|
7
|
-
BoardClient
|
|
8
|
-
} from "./chunk-E2MA75S3.js";
|
|
9
1
|
import {
|
|
10
2
|
Chain,
|
|
11
3
|
LightClient
|
|
12
4
|
} from "./chunk-C452UODV.js";
|
|
13
5
|
import {
|
|
14
6
|
ContentClient
|
|
15
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-QEYBWBXC.js";
|
|
16
8
|
import {
|
|
17
9
|
IndexerClient
|
|
18
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-K5MGEYEU.js";
|
|
19
11
|
import {
|
|
20
12
|
CairnController
|
|
21
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-MLNW2RQB.js";
|
|
22
14
|
import {
|
|
23
15
|
WalletConnection,
|
|
24
16
|
connect,
|
|
@@ -26,7 +18,14 @@ import {
|
|
|
26
18
|
discoverProviders,
|
|
27
19
|
getWallet,
|
|
28
20
|
isInstalled
|
|
29
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-BMH2MACB.js";
|
|
22
|
+
import {
|
|
23
|
+
Http,
|
|
24
|
+
RegistryClient,
|
|
25
|
+
RetryWaitBudget,
|
|
26
|
+
readCapped,
|
|
27
|
+
retryAfterMs
|
|
28
|
+
} from "./chunk-67QBDZPW.js";
|
|
30
29
|
import {
|
|
31
30
|
CairnError,
|
|
32
31
|
ContentVerificationError,
|
|
@@ -40,16 +39,51 @@ import {
|
|
|
40
39
|
errorCode,
|
|
41
40
|
mapProviderError,
|
|
42
41
|
mapSubmitResultError
|
|
43
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-2A4AJBO5.js";
|
|
44
43
|
|
|
45
44
|
// src/names.ts
|
|
46
|
-
import { NAME_RE, RESERVED_NAMES } from "@inversealtruism/cairnx-core";
|
|
45
|
+
import { NAME_RE, RESERVED_NAMES, TICKER_RE, AMOUNT_RE, HASH_RE } from "@inversealtruism/cairnx-core";
|
|
47
46
|
var nameOk = (n) => typeof n === "string" && NAME_RE.test(n) && !RESERVED_NAMES.has(n);
|
|
47
|
+
var amountOk = (v) => typeof v === "string" && AMOUNT_RE.test(v);
|
|
48
|
+
var heightOk = (v) => Number.isSafeInteger(v) && v >= 0;
|
|
49
|
+
function nameHistoryProblem(h, name) {
|
|
50
|
+
if (!h || typeof h !== "object" || Array.isArray(h)) return "not an object";
|
|
51
|
+
const r = h;
|
|
52
|
+
if (r.name !== name) return `name is ${JSON.stringify(r.name)}, expected ${JSON.stringify(name)}`;
|
|
53
|
+
if (!heightOk(r.tipHeight)) return "bad tipHeight";
|
|
54
|
+
if (!r.resolve || typeof r.resolve !== "object") return "resolve is not an object";
|
|
55
|
+
if (typeof r.scopedReplaySufficient !== "boolean") return "scopedReplaySufficient is not a boolean";
|
|
56
|
+
if (!Array.isArray(r.events)) return "events is not an array";
|
|
57
|
+
for (const [i, e] of r.events.entries()) {
|
|
58
|
+
const ev = e;
|
|
59
|
+
if (!ev || typeof ev.txid !== "string" || !HASH_RE.test(ev.txid) || !heightOk(ev.height) || !heightOk(ev.pos) || typeof ev.kind !== "string")
|
|
60
|
+
return `event ${i} is malformed`;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function tokenProblem(t) {
|
|
65
|
+
if (!t || typeof t !== "object") return "not an object";
|
|
66
|
+
const r = t;
|
|
67
|
+
if (typeof r.ticker !== "string" || !TICKER_RE.test(r.ticker)) return "bad ticker";
|
|
68
|
+
if (!Number.isSafeInteger(r.decimals) || r.decimals < 0) return `bad decimals (${String(r.decimals)})`;
|
|
69
|
+
if (!amountOk(r.supply)) return "supply is not a base-unit integer string";
|
|
70
|
+
if (!amountOk(r.minted)) return "minted is not a base-unit integer string";
|
|
71
|
+
if (r.mintLimit !== void 0 && !amountOk(r.mintLimit)) return "mintLimit is not a base-unit integer string";
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
48
74
|
var NamesClient = class {
|
|
49
75
|
constructor(http) {
|
|
50
76
|
this.http = http;
|
|
51
77
|
}
|
|
52
78
|
http;
|
|
79
|
+
malformed(path, what) {
|
|
80
|
+
return new HttpError(200, this.http.url(path), `malformed response: ${what}`);
|
|
81
|
+
}
|
|
82
|
+
async list(path) {
|
|
83
|
+
const body = await this.http.getJson(path);
|
|
84
|
+
if (!Array.isArray(body)) throw this.malformed(path, "expected an array");
|
|
85
|
+
return body;
|
|
86
|
+
}
|
|
53
87
|
/** Resolve a .csd name for DISPLAY (server-trusted; see the module trust note). null = unknown/invalid name. */
|
|
54
88
|
async resolve(name) {
|
|
55
89
|
if (!nameOk(name)) return null;
|
|
@@ -70,15 +104,21 @@ var NamesClient = class {
|
|
|
70
104
|
throw e;
|
|
71
105
|
}
|
|
72
106
|
}
|
|
73
|
-
/** The event history of one name (registrations, transfers, sets, renewals)
|
|
107
|
+
/** The event history of one name (registrations, transfers, sets, renewals) with the server's current
|
|
108
|
+
* claim. null = invalid or unregistered name. Throws a typed HttpError on a malformed body. */
|
|
74
109
|
async nameHistory(name) {
|
|
75
110
|
if (!nameOk(name)) return null;
|
|
111
|
+
const path = `/trade/api/cairnx/name-history/${encodeURIComponent(name)}`;
|
|
112
|
+
let h;
|
|
76
113
|
try {
|
|
77
|
-
|
|
114
|
+
h = await this.http.getJson(path);
|
|
78
115
|
} catch (e) {
|
|
79
116
|
if (e?.status === 404) return null;
|
|
80
117
|
throw e;
|
|
81
118
|
}
|
|
119
|
+
const p = nameHistoryProblem(h, name);
|
|
120
|
+
if (p) throw this.malformed(path, p);
|
|
121
|
+
return h;
|
|
82
122
|
}
|
|
83
123
|
/** Reverse lookup: an address's chosen primary .csd name (null name = none set). */
|
|
84
124
|
async primary(addr) {
|
|
@@ -102,29 +142,40 @@ var NamesClient = class {
|
|
|
102
142
|
}
|
|
103
143
|
/** All registered names (display list; short-edge-cached server-side). */
|
|
104
144
|
names() {
|
|
105
|
-
return this.
|
|
145
|
+
return this.list(`/trade/api/cairnx/names`);
|
|
106
146
|
}
|
|
107
|
-
/** All deployed tokens. */
|
|
108
|
-
tokens() {
|
|
109
|
-
|
|
147
|
+
/** All deployed tokens. Throws a typed HttpError if any row has a malformed ticker, decimals or amount. */
|
|
148
|
+
async tokens() {
|
|
149
|
+
const path = `/trade/api/cairnx/tokens`;
|
|
150
|
+
const rows = await this.list(path);
|
|
151
|
+
rows.forEach((t, i) => {
|
|
152
|
+
const p = tokenProblem(t);
|
|
153
|
+
if (p) throw this.malformed(path, `token row ${i}: ${p}`);
|
|
154
|
+
});
|
|
155
|
+
return rows;
|
|
110
156
|
}
|
|
111
|
-
/** One token by ticker. null = unknown/invalid ticker. */
|
|
157
|
+
/** One token by ticker. null = unknown/invalid ticker. Throws a typed HttpError on a malformed body. */
|
|
112
158
|
async token(ticker) {
|
|
113
|
-
if (typeof ticker !== "string" ||
|
|
159
|
+
if (typeof ticker !== "string" || !TICKER_RE.test(ticker)) return null;
|
|
160
|
+
const path = `/trade/api/cairnx/token/${ticker}`;
|
|
161
|
+
let t;
|
|
114
162
|
try {
|
|
115
|
-
|
|
163
|
+
t = await this.http.getJson(path);
|
|
116
164
|
} catch (e) {
|
|
117
165
|
if (e?.status === 404) return null;
|
|
118
166
|
throw e;
|
|
119
167
|
}
|
|
168
|
+
const p = tokenProblem(t);
|
|
169
|
+
if (p) throw this.malformed(path, p);
|
|
170
|
+
return t;
|
|
120
171
|
}
|
|
121
172
|
/** Open offers (the market's display list). */
|
|
122
173
|
offers() {
|
|
123
|
-
return this.
|
|
174
|
+
return this.list(`/trade/api/cairnx/offers`);
|
|
124
175
|
}
|
|
125
176
|
/** Open bids. */
|
|
126
177
|
bids() {
|
|
127
|
-
return this.
|
|
178
|
+
return this.list(`/trade/api/cairnx/bids`);
|
|
128
179
|
}
|
|
129
180
|
/** The full canonical CairnX state (large; prefer the scoped reads above). */
|
|
130
181
|
state() {
|
|
@@ -151,6 +202,7 @@ import {
|
|
|
151
202
|
epochOf
|
|
152
203
|
} from "@inversealtruism/cairnx-core";
|
|
153
204
|
var ADDR = /^0x[0-9a-f]{40}$/;
|
|
205
|
+
var BELOW_BASE = /^tx below the synced base \((\d+)\)/;
|
|
154
206
|
var HASH = /^0x[0-9a-f]{64}$/;
|
|
155
207
|
var COINBASE = "0x" + "00".repeat(32);
|
|
156
208
|
function bindPlannedSums(planned, proven) {
|
|
@@ -201,8 +253,12 @@ async function preverifyOffer(opts) {
|
|
|
201
253
|
} catch (e) {
|
|
202
254
|
return { ok: false, trust: "unverified", transient: true, reason: `couldn't reach the chain to verify the offer (${e?.message ?? e})` };
|
|
203
255
|
}
|
|
204
|
-
if (!incl.included || incl.trustLevel !== "verified-inclusion" || !incl.tx || !incl.appPayloadHash)
|
|
256
|
+
if (!incl.included || incl.trustLevel !== "verified-inclusion" || !incl.tx || !incl.appPayloadHash) {
|
|
257
|
+
const below = BELOW_BASE.exec(incl.reason ?? "");
|
|
258
|
+
if (below)
|
|
259
|
+
return { ok: false, trust: "unverified", blockHeight: incl.blockHeight, reason: `the offer was mined below this SDK's SPV checkpoint window (base ${below[1]}), so it cannot be merkle-proven here and retrying will not change that; pass an older anchor in config.spvCheckpoint to verify it` };
|
|
205
260
|
return { ok: false, trust: "unverified", transient: true, blockHeight: incl.blockHeight, reason: incl.reason ?? `offer not merkle-proven on-chain (${incl.trustLevel})` };
|
|
261
|
+
}
|
|
206
262
|
const blockHeight = Number(incl.blockHeight);
|
|
207
263
|
let provenTx, provenId;
|
|
208
264
|
try {
|
|
@@ -238,8 +294,15 @@ async function preverifyOffer(opts) {
|
|
|
238
294
|
return { ok: false, trust: "verified", blockHeight, reason: "the offer was already expired when it was mined - the chain rejects it, and a fill would move CSD for nothing" };
|
|
239
295
|
let outputPlan;
|
|
240
296
|
let sumsMismatch;
|
|
241
|
-
|
|
242
|
-
|
|
297
|
+
let planReason;
|
|
298
|
+
let planBound = false;
|
|
299
|
+
const has = (v) => v !== void 0 && v !== null;
|
|
300
|
+
const servedStatus = opts.servedOffer?.status;
|
|
301
|
+
if (!has(opts.pay)) planReason = "no `pay` was given, so no output plan was sized";
|
|
302
|
+
else if (isTokenWant(offerRec.want)) planReason = "the offer is token-priced: the payment is a token transfer, not CSD outputs";
|
|
303
|
+
else if (offerRec.min !== void 0) planReason = "the offer is partially fillable: its running paid/delivered is not provable here (the wallet's fill-SPV sizes it)";
|
|
304
|
+
else if (typeof servedStatus !== "string") planReason = "no served offer status: liveness is not provable here, so no output plan was sized (pass the resolver's offer object)";
|
|
305
|
+
else {
|
|
243
306
|
const provenState = {
|
|
244
307
|
id,
|
|
245
308
|
seller,
|
|
@@ -247,7 +310,7 @@ async function preverifyOffer(opts) {
|
|
|
247
310
|
want: { ...offerRec.want, payto },
|
|
248
311
|
taker: offerRec.taker,
|
|
249
312
|
bid: offerRec.bid,
|
|
250
|
-
status:
|
|
313
|
+
status: servedStatus,
|
|
251
314
|
expiresEpoch: 0,
|
|
252
315
|
height: blockHeight,
|
|
253
316
|
feeBps: terms.feeBps
|
|
@@ -256,16 +319,25 @@ async function preverifyOffer(opts) {
|
|
|
256
319
|
const plan = fillOutputPlan(provenState, opts.pay);
|
|
257
320
|
if (plan.kind === "csd-outputs") {
|
|
258
321
|
outputPlan = plan.outputs;
|
|
259
|
-
if (opts.plannedOutputs
|
|
322
|
+
if (has(opts.plannedOutputs)) {
|
|
260
323
|
sumsMismatch = bindPlannedSums(opts.plannedOutputs, plan.outputs);
|
|
324
|
+
planBound = true;
|
|
325
|
+
}
|
|
261
326
|
} else if (plan.kind === "undeliverable") {
|
|
262
327
|
sumsMismatch = `this pay would not deliver against the proven offer (${plan.reason}) - refusing (the CSD would be lost)`;
|
|
328
|
+
} else {
|
|
329
|
+
planReason = `the proven offer settles without CSD outputs (${plan.kind})`;
|
|
263
330
|
}
|
|
264
331
|
} catch (e) {
|
|
265
332
|
sumsMismatch = `couldn't size the fill against the proven offer (${e?.message ?? e}) - refusing rather than sizing from served fields`;
|
|
266
333
|
}
|
|
267
334
|
}
|
|
268
|
-
const
|
|
335
|
+
const planFields = {
|
|
336
|
+
...outputPlan ? { outputPlan } : {},
|
|
337
|
+
...has(opts.plannedOutputs) ? { planBound } : {},
|
|
338
|
+
...!outputPlan && planReason && (has(opts.pay) || has(opts.plannedOutputs)) ? { planReason } : {}
|
|
339
|
+
};
|
|
340
|
+
const base = { payto, seller, terms, blockHeight, ...planFields };
|
|
269
341
|
if (opts.servedOffer !== void 0 && opts.servedOffer !== null) {
|
|
270
342
|
const so = opts.servedOffer;
|
|
271
343
|
if (so?.want?.payto === void 0 || so?.want?.payto === null)
|
|
@@ -286,7 +358,7 @@ async function preverifyOffer(opts) {
|
|
|
286
358
|
}
|
|
287
359
|
|
|
288
360
|
// src/index.ts
|
|
289
|
-
var DEFAULT_SPV_CHECKPOINT = { height:
|
|
361
|
+
var DEFAULT_SPV_CHECKPOINT = { height: 115403, hash: "0x00000000000052e29166597d598827b137af1dea7542123b848637fee0b67347" };
|
|
290
362
|
var DEFAULT_CAIRN = "https://cairn-substrate.com";
|
|
291
363
|
var SPV_HTTP_TIMEOUT_MS = 15e3;
|
|
292
364
|
var SPV_HTTP_MAX_RETRIES = 4;
|
|
@@ -298,7 +370,10 @@ var spvIsTransient = (e) => {
|
|
|
298
370
|
return SPV_TRANSIENT_RE.test(msg) || SPV_TRANSIENT_JSON_RE.test(msg);
|
|
299
371
|
};
|
|
300
372
|
var spvSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
301
|
-
var
|
|
373
|
+
var spvBackoffMs = (attempt) => Math.min(4e3, 700 * (attempt + 1));
|
|
374
|
+
var SPV_RETRY_WAIT_BUDGET_MS = 16e3;
|
|
375
|
+
var SPV_RECHECK_DEPTH = 12;
|
|
376
|
+
var spvTipOf = (lc) => lc.baseHeight + lc.chain.length - 1;
|
|
302
377
|
var Cairn = class {
|
|
303
378
|
config;
|
|
304
379
|
chain;
|
|
@@ -307,7 +382,6 @@ var Cairn = class {
|
|
|
307
382
|
registry;
|
|
308
383
|
/** .csd names + CairnX market READS (server-trusted display reads; see names.ts trust note). */
|
|
309
384
|
names;
|
|
310
|
-
board;
|
|
311
385
|
wallet;
|
|
312
386
|
cairnHttp;
|
|
313
387
|
// Lazily-seeded SPV light client + its checkpoint (audit M3 — PoW-verified header source).
|
|
@@ -333,7 +407,6 @@ var Cairn = class {
|
|
|
333
407
|
this._spvCp = config.spvCheckpoint ?? DEFAULT_SPV_CHECKPOINT;
|
|
334
408
|
this.chain = new Chain({ rpcUrl: rpc, fetch, timeoutMs });
|
|
335
409
|
this.wallet = config.wallet;
|
|
336
|
-
this.board = new BoardClient(this.cairnHttp, this.wallet);
|
|
337
410
|
this.index = new IndexerClient(indexerHttp, {
|
|
338
411
|
fetch,
|
|
339
412
|
WebSocketImpl: config.WebSocketImpl,
|
|
@@ -351,16 +424,17 @@ var Cairn = class {
|
|
|
351
424
|
* to the honest "proof-consistent" instead of over-claiming "verified-inclusion". The verified
|
|
352
425
|
* header chain is cached across calls; the first call near tip syncs the post-checkpoint span.
|
|
353
426
|
* MF-03: a STRUCTURAL forward-sync break (a reorg orphaned the cached tip) reseeds ONCE via
|
|
354
|
-
* withSpvReseed.
|
|
355
|
-
*
|
|
356
|
-
*
|
|
427
|
+
* withSpvReseed. A height within SPV_RECHECK_DEPTH of the cached tip is re-checked against the served
|
|
428
|
+
* chain first (recheckCachedHeader), so a block orphaned after it was cached stops backing
|
|
429
|
+
* "verified-inclusion". Deeper heights answer from the cache with no network touch.
|
|
357
430
|
*/
|
|
358
431
|
async verifiedHeaderMerkleAt(height) {
|
|
359
432
|
if (!Number.isInteger(height) || height < this._spvCp.height)
|
|
360
433
|
throw new Error(`height ${height} is below the SPV checkpoint ${this._spvCp.height} (cannot PoW-verify backward)`);
|
|
361
434
|
return this.withSpvReseed(async (lc) => {
|
|
362
|
-
const haveTo = lc
|
|
435
|
+
const haveTo = spvTipOf(lc);
|
|
363
436
|
if (haveTo < height) await lc.sync(height);
|
|
437
|
+
else await this.recheckCachedHeader(lc, height, haveTo);
|
|
364
438
|
const vh = lc.chain[height - lc.baseHeight];
|
|
365
439
|
if (!vh) throw new Error(`light client has no verified header at ${height}`);
|
|
366
440
|
return String(vh.header.merkle);
|
|
@@ -372,11 +446,8 @@ var Cairn = class {
|
|
|
372
446
|
// this._spvLight first - the wallet G8 lesson, same shape as the MF-14 seed). A TRANSIENT failure
|
|
373
447
|
// (rate limit / gateway / timeout / non-dense / CF 200-HTML) keeps the cache and rethrows: wiping
|
|
374
448
|
// on those turns one /api/headers 429 into a reseed storm (DOS-HDR-3). A second structural
|
|
375
|
-
// failure propagates fail-closed; there is no loop.
|
|
376
|
-
//
|
|
377
|
-
// branch with NO network touch, so after a reorg "verified-inclusion" can still be over-claimed
|
|
378
|
-
// for a tx in an orphaned block until some read past the tip trips this reseed. This fix closes
|
|
379
|
-
// the permanent-brick leg only.
|
|
449
|
+
// failure propagates fail-closed; there is no loop. recheckCachedHeader feeds this path: a near-tip
|
|
450
|
+
// cached header that the served chain no longer carries is a structural failure.
|
|
380
451
|
async withSpvReseed(op) {
|
|
381
452
|
const lc = await this.seededSpvLight();
|
|
382
453
|
try {
|
|
@@ -389,6 +460,16 @@ var Cairn = class {
|
|
|
389
460
|
return out;
|
|
390
461
|
}
|
|
391
462
|
}
|
|
463
|
+
// A near-tip cached header the served chain no longer carries is a reorg: throw a structural error so
|
|
464
|
+
// withSpvReseed rebuilds. The message must not match SPV_TRANSIENT_RE, or it would be retried on the stale cache.
|
|
465
|
+
async recheckCachedHeader(lc, height, haveTo) {
|
|
466
|
+
if (height <= haveTo - SPV_RECHECK_DEPTH) return;
|
|
467
|
+
const cached = lc.chain[height - lc.baseHeight];
|
|
468
|
+
if (!cached) return;
|
|
469
|
+
const [served] = await this.fetchHeadersBatch(height, 1);
|
|
470
|
+
if (String(served?.hash ?? "").toLowerCase() !== String(cached.hash).toLowerCase())
|
|
471
|
+
throw new Error(`SPV cached block at height ${height} is no longer on the served chain (reorg); reseeding`);
|
|
472
|
+
}
|
|
392
473
|
// Build + seed + tip-advance a FRESH light client into a LOCAL. Never touches this._spvLight:
|
|
393
474
|
// the caller commits the result only on success (MF-14; the wallet G8 posture - never null or
|
|
394
475
|
// dirty the shared field while an attempt is in flight).
|
|
@@ -401,7 +482,7 @@ var Cairn = class {
|
|
|
401
482
|
await lc.syncFromCheckpoint(this._spvCp.height, this._spvCp.hash);
|
|
402
483
|
try {
|
|
403
484
|
const tipHeight = Number((await this.chain.tip()).height);
|
|
404
|
-
const have = lc
|
|
485
|
+
const have = spvTipOf(lc);
|
|
405
486
|
if (Number.isFinite(tipHeight) && tipHeight > have) await lc.sync(tipHeight);
|
|
406
487
|
} catch {
|
|
407
488
|
}
|
|
@@ -446,6 +527,7 @@ var Cairn = class {
|
|
|
446
527
|
if (!g) throw new Error("No fetch available for SPV header sync - pass `fetch` in the SDK options.");
|
|
447
528
|
const doFetch = this.config.fetch ? this.config.fetch : g.bind(globalThis);
|
|
448
529
|
const url = `${this._cairnBase}/api/headers/${from}/${count}`;
|
|
530
|
+
const budget = new RetryWaitBudget(SPV_RETRY_WAIT_BUDGET_MS);
|
|
449
531
|
for (let attempt = 0; ; attempt++) {
|
|
450
532
|
let res;
|
|
451
533
|
let parsed;
|
|
@@ -457,8 +539,9 @@ var Cairn = class {
|
|
|
457
539
|
parsed = JSON.parse(new TextDecoder().decode(await readCapped(res, SPV_MAX_BATCH_BYTES)));
|
|
458
540
|
}
|
|
459
541
|
} catch (e) {
|
|
460
|
-
|
|
461
|
-
|
|
542
|
+
const waitMs = attempt < SPV_HTTP_MAX_RETRIES ? budget.take(spvBackoffMs(attempt), 0) : null;
|
|
543
|
+
if (waitMs === null) throw e;
|
|
544
|
+
await spvSleep(waitMs);
|
|
462
545
|
continue;
|
|
463
546
|
} finally {
|
|
464
547
|
clearTimeout(timer);
|
|
@@ -471,7 +554,9 @@ var Cairn = class {
|
|
|
471
554
|
if ((res.status === 429 || res.status === 502 || res.status === 503) && attempt < SPV_HTTP_MAX_RETRIES) {
|
|
472
555
|
res.body?.cancel?.().catch(() => {
|
|
473
556
|
});
|
|
474
|
-
|
|
557
|
+
const waitMs = budget.take(spvBackoffMs(attempt), retryAfterMs(res));
|
|
558
|
+
if (waitMs === null) throw new Error(`/api/headers ${res.status}: Retry-After exceeds the remaining retry budget`);
|
|
559
|
+
await spvSleep(waitMs);
|
|
475
560
|
continue;
|
|
476
561
|
}
|
|
477
562
|
throw new Error(`/api/headers ${res.status}`);
|
|
@@ -494,12 +579,20 @@ var Cairn = class {
|
|
|
494
579
|
* EXACTLY the outputPlan, never resolver-served outputs.
|
|
495
580
|
*/
|
|
496
581
|
async verifyOfferForFill(offerId, servedOffer, opts) {
|
|
497
|
-
const light = {
|
|
582
|
+
const light = {
|
|
583
|
+
verifyTxInclusion: (txidHex) => this.withSpvReseed(async (lc) => {
|
|
584
|
+
const haveTo = spvTipOf(lc);
|
|
585
|
+
const r = await lc.verifyTxInclusion(txidHex);
|
|
586
|
+
if (r.trustLevel === "verified-inclusion" && typeof r.blockHeight === "number" && r.blockHeight <= haveTo)
|
|
587
|
+
await this.recheckCachedHeader(lc, r.blockHeight, haveTo);
|
|
588
|
+
return r;
|
|
589
|
+
})
|
|
590
|
+
};
|
|
498
591
|
return preverifyOffer({ light, client: this.chain.client, offerId, servedOffer, pay: opts?.pay, plannedOutputs: opts?.plannedOutputs });
|
|
499
592
|
}
|
|
500
593
|
/**
|
|
501
|
-
* Detect + connect the Cairn Wallet (browser only)
|
|
502
|
-
*
|
|
594
|
+
* Detect + connect the Cairn Wallet (browser only) and attach it as `cairn.wallet`.
|
|
595
|
+
* Resolves to the connected address.
|
|
503
596
|
*/
|
|
504
597
|
async connect(opts = {}) {
|
|
505
598
|
const { wallet, address } = await connect(opts);
|
|
@@ -509,7 +602,6 @@ var Cairn = class {
|
|
|
509
602
|
/** Attach an already-connected wallet (e.g. one you constructed yourself). */
|
|
510
603
|
attachWallet(wallet) {
|
|
511
604
|
this.wallet = wallet;
|
|
512
|
-
this.board = new BoardClient(this.cairnHttp, wallet);
|
|
513
605
|
}
|
|
514
606
|
/** True if a wallet is attached. */
|
|
515
607
|
get connected() {
|
|
@@ -517,7 +609,6 @@ var Cairn = class {
|
|
|
517
609
|
}
|
|
518
610
|
};
|
|
519
611
|
export {
|
|
520
|
-
BoardClient,
|
|
521
612
|
Cairn,
|
|
522
613
|
CairnController,
|
|
523
614
|
CairnError,
|
package/dist/indexer.cjs
CHANGED
|
@@ -137,17 +137,27 @@ var IndexerClient = class {
|
|
|
137
137
|
if (!folds) {
|
|
138
138
|
return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "merkle branch does not fold to the claimed root" };
|
|
139
139
|
}
|
|
140
|
+
let unverified;
|
|
140
141
|
if (this.opts.headerMerkleAt) {
|
|
142
|
+
let onchain;
|
|
141
143
|
try {
|
|
142
|
-
|
|
144
|
+
onchain = await this.opts.headerMerkleAt(proof.block_height);
|
|
145
|
+
} catch (e) {
|
|
146
|
+
onchain = "";
|
|
147
|
+
unverified = `header at ${proof.block_height} not verified (${e?.message ?? String(e)})`;
|
|
148
|
+
}
|
|
149
|
+
if (onchain) {
|
|
143
150
|
if (norm(onchain) !== norm(proof.merkle_root)) {
|
|
144
151
|
return { included: false, trustLevel: "not-found", equivocation: true, blockHeight: proof.block_height, reason: "proof root != on-chain header merkle (indexer disagrees with chain)" };
|
|
145
152
|
}
|
|
146
153
|
return { included: true, trustLevel: "verified-inclusion", blockHeight: proof.block_height, pos: proof.pos, merkleRoot: proof.merkle_root };
|
|
147
|
-
} catch {
|
|
148
154
|
}
|
|
155
|
+
unverified ??= `header source returned no merkle root for ${proof.block_height}`;
|
|
156
|
+
} else {
|
|
157
|
+
unverified = "no header source configured (headerMerkleAt)";
|
|
149
158
|
}
|
|
150
|
-
|
|
159
|
+
const trivial = proof.merkle.length === 0 && norm(proof.merkle_root) === norm(txid) ? "; empty branch (root equals the txid)" : "";
|
|
160
|
+
return { included: false, trustLevel: "proof-consistent", blockHeight: proof.block_height, pos: proof.pos, merkleRoot: proof.merkle_root, reason: `indexer proof only, ${unverified}${trivial}` };
|
|
151
161
|
}
|
|
152
162
|
// ---- live streams ------------------------------------------------------
|
|
153
163
|
/** Stream all events (blocks, proposals, attestations, reorgs) via SSE. */
|
package/dist/indexer.d.cts
CHANGED
|
@@ -8,6 +8,9 @@ interface MerkleProof {
|
|
|
8
8
|
}
|
|
9
9
|
type TrustLevel = "verified-inclusion" | "proof-consistent" | "not-found";
|
|
10
10
|
interface InclusionResult {
|
|
11
|
+
/** True only when the proof root matched a header the `headerMerkleAt` source verified (trustLevel
|
|
12
|
+
* "verified-inclusion"). A proof that merely folds is "proof-consistent" with `included: false` and a
|
|
13
|
+
* `reason`: the indexer alone vouches for it, and an empty branch whose root is the txid folds trivially. */
|
|
11
14
|
included: boolean;
|
|
12
15
|
trustLevel: TrustLevel;
|
|
13
16
|
blockHeight?: number;
|
package/dist/indexer.d.ts
CHANGED
|
@@ -8,6 +8,9 @@ interface MerkleProof {
|
|
|
8
8
|
}
|
|
9
9
|
type TrustLevel = "verified-inclusion" | "proof-consistent" | "not-found";
|
|
10
10
|
interface InclusionResult {
|
|
11
|
+
/** True only when the proof root matched a header the `headerMerkleAt` source verified (trustLevel
|
|
12
|
+
* "verified-inclusion"). A proof that merely folds is "proof-consistent" with `included: false` and a
|
|
13
|
+
* `reason`: the indexer alone vouches for it, and an empty branch whose root is the txid folds trivially. */
|
|
11
14
|
included: boolean;
|
|
12
15
|
trustLevel: TrustLevel;
|
|
13
16
|
blockHeight?: number;
|
package/dist/indexer.js
CHANGED
package/dist/react.cjs
CHANGED
|
@@ -26,7 +26,7 @@ __export(react_exports, {
|
|
|
26
26
|
module.exports = __toCommonJS(react_exports);
|
|
27
27
|
|
|
28
28
|
// src/errors.ts
|
|
29
|
-
var SDK_VERSION = "0.
|
|
29
|
+
var SDK_VERSION = "0.6.0";
|
|
30
30
|
var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
|
|
31
31
|
var CairnError = class extends Error {
|
|
32
32
|
/** Stable machine code for programmatic branching. */
|
|
@@ -349,7 +349,7 @@ var WalletConnection = class {
|
|
|
349
349
|
send(params) {
|
|
350
350
|
return this.call(() => this.provider.send(params), "send").then(unwrapWrite);
|
|
351
351
|
}
|
|
352
|
-
/** Low-level Propose (always prompts).
|
|
352
|
+
/** Low-level Propose (always prompts). The caller computes the payload hash, uri and expiry epoch. */
|
|
353
353
|
propose(params) {
|
|
354
354
|
return this.call(() => this.provider.propose(params), "propose").then(unwrapWrite);
|
|
355
355
|
}
|
|
@@ -378,8 +378,9 @@ var DISCONNECTED = Object.freeze({ status: "disconnected", account: null, error:
|
|
|
378
378
|
var CairnController = class {
|
|
379
379
|
state = DISCONNECTED;
|
|
380
380
|
conn = null;
|
|
381
|
-
//
|
|
382
|
-
//
|
|
381
|
+
// Connect/disconnect generation token. disconnect(), the wallet's `disconnect` event and an empty
|
|
382
|
+
// accountsChanged (lock, account switch, revoke) bump it; a connect() whose await straddled the bump must
|
|
383
|
+
// never commit "connected" over the torn-down session.
|
|
383
384
|
epoch = 0;
|
|
384
385
|
listeners = /* @__PURE__ */ new Set();
|
|
385
386
|
getWalletFn;
|
|
@@ -410,6 +411,7 @@ var CairnController = class {
|
|
|
410
411
|
}
|
|
411
412
|
onAccounts = (accounts) => {
|
|
412
413
|
if (!Array.isArray(accounts) || accounts.length === 0) {
|
|
414
|
+
this.epoch++;
|
|
413
415
|
this.setState({ status: "disconnected", account: null });
|
|
414
416
|
return;
|
|
415
417
|
}
|
|
@@ -421,6 +423,7 @@ var CairnController = class {
|
|
|
421
423
|
}
|
|
422
424
|
};
|
|
423
425
|
onDisconnect = () => {
|
|
426
|
+
this.epoch++;
|
|
424
427
|
this.setState({ status: "disconnected", account: null });
|
|
425
428
|
};
|
|
426
429
|
// Detach the provider listeners (ghost-reconnect fix). Idempotent + best-effort.
|
|
@@ -476,7 +479,7 @@ var CairnController = class {
|
|
|
476
479
|
|
|
477
480
|
// src/react.ts
|
|
478
481
|
function createCairnHooks(React, controllerOrOpts) {
|
|
479
|
-
const controller = controllerOrOpts
|
|
482
|
+
const controller = isController(controllerOrOpts) ? controllerOrOpts : new CairnController(controllerOrOpts);
|
|
480
483
|
const useCairnState = () => React.useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
|
|
481
484
|
const useCairnAccount = () => useCairnState().account;
|
|
482
485
|
const useCairn = () => ({
|
|
@@ -487,6 +490,10 @@ function createCairnHooks(React, controllerOrOpts) {
|
|
|
487
490
|
});
|
|
488
491
|
return { controller, useCairnState, useCairnAccount, useCairn };
|
|
489
492
|
}
|
|
493
|
+
function isController(v) {
|
|
494
|
+
const c = v;
|
|
495
|
+
return !!c && typeof c.subscribe === "function" && typeof c.getSnapshot === "function" && typeof c.connect === "function" && typeof c.disconnect === "function" && typeof c.signInWithCsd === "function";
|
|
496
|
+
}
|
|
490
497
|
// Annotate the CommonJS export names for ESM import in node:
|
|
491
498
|
0 && (module.exports = {
|
|
492
499
|
CairnController,
|
package/dist/react.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CairnController
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-MLNW2RQB.js";
|
|
4
|
+
import "./chunk-BMH2MACB.js";
|
|
5
|
+
import "./chunk-2A4AJBO5.js";
|
|
6
6
|
|
|
7
7
|
// src/react.ts
|
|
8
8
|
function createCairnHooks(React, controllerOrOpts) {
|
|
9
|
-
const controller = controllerOrOpts
|
|
9
|
+
const controller = isController(controllerOrOpts) ? controllerOrOpts : new CairnController(controllerOrOpts);
|
|
10
10
|
const useCairnState = () => React.useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
|
|
11
11
|
const useCairnAccount = () => useCairnState().account;
|
|
12
12
|
const useCairn = () => ({
|
|
@@ -17,6 +17,10 @@ function createCairnHooks(React, controllerOrOpts) {
|
|
|
17
17
|
});
|
|
18
18
|
return { controller, useCairnState, useCairnAccount, useCairn };
|
|
19
19
|
}
|
|
20
|
+
function isController(v) {
|
|
21
|
+
const c = v;
|
|
22
|
+
return !!c && typeof c.subscribe === "function" && typeof c.getSnapshot === "function" && typeof c.connect === "function" && typeof c.disconnect === "function" && typeof c.signInWithCsd === "function";
|
|
23
|
+
}
|
|
20
24
|
export {
|
|
21
25
|
CairnController,
|
|
22
26
|
createCairnHooks
|
package/dist/registry.cjs
CHANGED
|
@@ -36,7 +36,7 @@ module.exports = __toCommonJS(registry_exports);
|
|
|
36
36
|
var import_csd_registry = require("@inversealtruism/csd-registry");
|
|
37
37
|
|
|
38
38
|
// src/errors.ts
|
|
39
|
-
var SDK_VERSION = "0.
|
|
39
|
+
var SDK_VERSION = "0.6.0";
|
|
40
40
|
var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
|
|
41
41
|
var CairnError = class extends Error {
|
|
42
42
|
/** Stable machine code for programmatic branching. */
|
|
@@ -95,6 +95,26 @@ var HttpError = class extends CairnError {
|
|
|
95
95
|
// src/http.ts
|
|
96
96
|
var DEFAULT_MAX_BYTES = 16 * 1024 * 1024;
|
|
97
97
|
var ERR_TEXT_CAP = 8 * 1024;
|
|
98
|
+
var MAX_RETRY_AFTER_MS = 6e4;
|
|
99
|
+
function retryAfterMs(res) {
|
|
100
|
+
const secs = Number(res.headers.get("retry-after"));
|
|
101
|
+
return Number.isFinite(secs) && secs > 0 ? Math.min(secs * 1e3, MAX_RETRY_AFTER_MS) : 0;
|
|
102
|
+
}
|
|
103
|
+
var RetryWaitBudget = class {
|
|
104
|
+
left;
|
|
105
|
+
constructor(totalMs) {
|
|
106
|
+
this.left = totalMs;
|
|
107
|
+
}
|
|
108
|
+
/** The ms to sleep before the next attempt (`ownMs` is the caller's backoff, clipped to what is left), or
|
|
109
|
+
* null when the server's hint exceeds the remaining budget. */
|
|
110
|
+
take(ownMs, serverMs) {
|
|
111
|
+
if (serverMs > this.left) return null;
|
|
112
|
+
const ms = Math.min(Math.max(ownMs, serverMs), this.left);
|
|
113
|
+
this.left -= ms;
|
|
114
|
+
return ms;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
var HTTP_RETRY_WAIT_BUDGET_MS = 3e4;
|
|
98
118
|
async function readCapped(res, maxBytes) {
|
|
99
119
|
const cl = Number(res.headers.get("content-length"));
|
|
100
120
|
if (Number.isFinite(cl) && cl > maxBytes) throw new HttpError(res.status, res.url, `response too large (content-length ${cl} > ${maxBytes})`);
|
|
@@ -143,9 +163,10 @@ var Http = class {
|
|
|
143
163
|
if (!f) throw new Error("No fetch available \u2014 pass `fetch` in the SDK options.");
|
|
144
164
|
this._fetch = opts.fetch ? opts.fetch : f.bind(globalThis);
|
|
145
165
|
this.timeoutMs = opts.timeoutMs ?? 15e3;
|
|
146
|
-
this.maxBytes = opts.maxBytes
|
|
166
|
+
this.maxBytes = Number.isFinite(opts.maxBytes) && opts.maxBytes >= 1 ? Math.floor(opts.maxBytes) : DEFAULT_MAX_BYTES;
|
|
147
167
|
this.headers = opts.headers ?? {};
|
|
148
|
-
|
|
168
|
+
const retries = Number(opts.retries ?? 0);
|
|
169
|
+
this.retries = Number.isNaN(retries) ? 0 : Math.min(10, Math.max(0, Math.floor(retries)));
|
|
149
170
|
}
|
|
150
171
|
/** Build an absolute URL from a path + optional query params. */
|
|
151
172
|
url(path, query) {
|
|
@@ -194,11 +215,16 @@ var Http = class {
|
|
|
194
215
|
}
|
|
195
216
|
async getJson(path, query) {
|
|
196
217
|
const url = this.url(path, query);
|
|
218
|
+
const budget = new RetryWaitBudget(HTTP_RETRY_WAIT_BUDGET_MS);
|
|
197
219
|
for (let attempt = 0; ; attempt++) {
|
|
220
|
+
let serverWaitMs = 0;
|
|
221
|
+
let last;
|
|
198
222
|
try {
|
|
199
223
|
const { res, body } = await this.raw("GET", path, { query });
|
|
200
224
|
if (!res.ok) {
|
|
201
|
-
|
|
225
|
+
last = new HttpError(res.status, url, errText(body));
|
|
226
|
+
if (!(res.status >= 500 && attempt < this.retries)) throw last;
|
|
227
|
+
serverWaitMs = retryAfterMs(res);
|
|
202
228
|
} else {
|
|
203
229
|
return this.parseJson(res, new TextDecoder().decode(body), url);
|
|
204
230
|
}
|
|
@@ -206,7 +232,10 @@ var Http = class {
|
|
|
206
232
|
if (e instanceof HttpError && e.status < 500) throw e;
|
|
207
233
|
if (attempt >= this.retries) throw e;
|
|
208
234
|
}
|
|
209
|
-
|
|
235
|
+
const jitterMs = Math.floor(Math.random() * Math.min(4e3, 250 * 2 ** attempt));
|
|
236
|
+
const waitMs = budget.take(jitterMs, serverWaitMs);
|
|
237
|
+
if (waitMs === null) throw last ?? new HttpError(0, url, "retry wait budget exhausted");
|
|
238
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
210
239
|
}
|
|
211
240
|
}
|
|
212
241
|
async postJson(path, body, headers) {
|
package/dist/registry.js
CHANGED