@inversealtruism/cairn-sdk 0.4.5 → 0.5.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-SWCUE26T.js → chunk-5RGFC2JR.js} +2 -2
- package/dist/{chunk-WZFWAEXL.js → chunk-BL6VFALH.js} +36 -5
- package/dist/{chunk-FX5LLMNP.js → chunk-CZAQNZZG.js} +19 -8
- package/dist/{chunk-J5J4JEJ6.js → chunk-K5MGEYEU.js} +13 -3
- package/dist/{chunk-XW3IFE6K.js → chunk-OE5BEEQY.js} +6 -3
- package/dist/{chunk-LOAOBV36.js → chunk-OSPAN7GX.js} +7 -3
- package/dist/connect.cjs +8 -4
- 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 +13 -6
- package/dist/controller.js +3 -3
- package/dist/index.cjs +220 -229
- package/dist/index.d.cts +52 -22
- package/dist/index.d.ts +52 -22
- 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 +18 -7
- package/dist/react.js +8 -4
- package/dist/registry.cjs +34 -5
- package/dist/registry.js +2 -2
- package/package.json +3 -9
- 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-IGJ46AYP.js +0 -163
package/dist/index.js
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Http,
|
|
3
|
-
RegistryClient,
|
|
4
|
-
readCapped
|
|
5
|
-
} from "./chunk-WZFWAEXL.js";
|
|
6
|
-
import {
|
|
7
|
-
BoardClient
|
|
8
|
-
} from "./chunk-IGJ46AYP.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-CZAQNZZG.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-OE5BEEQY.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-5RGFC2JR.js";
|
|
22
|
+
import {
|
|
23
|
+
Http,
|
|
24
|
+
RegistryClient,
|
|
25
|
+
RetryWaitBudget,
|
|
26
|
+
readCapped,
|
|
27
|
+
retryAfterMs
|
|
28
|
+
} from "./chunk-BL6VFALH.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-OSPAN7GX.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.5.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. */
|
|
@@ -101,7 +101,7 @@ var SubmitInFlightError = class extends CairnError {
|
|
|
101
101
|
}
|
|
102
102
|
};
|
|
103
103
|
var NATIVE_CODES = /* @__PURE__ */ new Set(["FIRST_PARTY_ONLY", "RATE_LIMITED", "FORBIDDEN", "INTERNAL"]);
|
|
104
|
-
var NESTED_TRANSIENT = /* @__PURE__ */ new Set(["VERIFY_UNAVAILABLE", "OFFER_UNKNOWN"]);
|
|
104
|
+
var NESTED_TRANSIENT = /* @__PURE__ */ new Set(["VERIFY_UNAVAILABLE", "OFFER_UNKNOWN", "WALLET_UNAVAILABLE", "REVIEW_REQUIRED"]);
|
|
105
105
|
var NESTED_TERMINAL = /* @__PURE__ */ new Set([
|
|
106
106
|
"FILL_UNSAFE",
|
|
107
107
|
"FILL_WRONG_TARGET",
|
|
@@ -119,7 +119,11 @@ var NESTED_TERMINAL = /* @__PURE__ */ new Set([
|
|
|
119
119
|
"FEE_TOO_LOW",
|
|
120
120
|
"ACCOUNT_CHANGED",
|
|
121
121
|
"GHOST_INPUTS_SKIPPED",
|
|
122
|
-
"NOTHING_TO_CONSOLIDATE"
|
|
122
|
+
"NOTHING_TO_CONSOLIDATE",
|
|
123
|
+
"OFFER_SOURCE_CONFLICT",
|
|
124
|
+
"FILL_LEGACY_SUNSET",
|
|
125
|
+
"VERIFY_HORIZON",
|
|
126
|
+
"VERIFY_UNREPRESENTABLE"
|
|
123
127
|
]);
|
|
124
128
|
function mapProviderError(error, code) {
|
|
125
129
|
if (code === "USER_REJECTED" || code === "APPROVAL_CLOSED") return new UserRejectedError(error);
|
|
@@ -345,7 +349,7 @@ var WalletConnection = class {
|
|
|
345
349
|
send(params) {
|
|
346
350
|
return this.call(() => this.provider.send(params), "send").then(unwrapWrite);
|
|
347
351
|
}
|
|
348
|
-
/** Low-level Propose (always prompts).
|
|
352
|
+
/** Low-level Propose (always prompts). The caller computes the payload hash, uri and expiry epoch. */
|
|
349
353
|
propose(params) {
|
|
350
354
|
return this.call(() => this.provider.propose(params), "propose").then(unwrapWrite);
|
|
351
355
|
}
|
|
@@ -374,8 +378,9 @@ var DISCONNECTED = Object.freeze({ status: "disconnected", account: null, error:
|
|
|
374
378
|
var CairnController = class {
|
|
375
379
|
state = DISCONNECTED;
|
|
376
380
|
conn = null;
|
|
377
|
-
//
|
|
378
|
-
//
|
|
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.
|
|
379
384
|
epoch = 0;
|
|
380
385
|
listeners = /* @__PURE__ */ new Set();
|
|
381
386
|
getWalletFn;
|
|
@@ -406,6 +411,7 @@ var CairnController = class {
|
|
|
406
411
|
}
|
|
407
412
|
onAccounts = (accounts) => {
|
|
408
413
|
if (!Array.isArray(accounts) || accounts.length === 0) {
|
|
414
|
+
this.epoch++;
|
|
409
415
|
this.setState({ status: "disconnected", account: null });
|
|
410
416
|
return;
|
|
411
417
|
}
|
|
@@ -417,6 +423,7 @@ var CairnController = class {
|
|
|
417
423
|
}
|
|
418
424
|
};
|
|
419
425
|
onDisconnect = () => {
|
|
426
|
+
this.epoch++;
|
|
420
427
|
this.setState({ status: "disconnected", account: null });
|
|
421
428
|
};
|
|
422
429
|
// Detach the provider listeners (ghost-reconnect fix). Idempotent + best-effort.
|
|
@@ -472,7 +479,7 @@ var CairnController = class {
|
|
|
472
479
|
|
|
473
480
|
// src/react.ts
|
|
474
481
|
function createCairnHooks(React, controllerOrOpts) {
|
|
475
|
-
const controller = controllerOrOpts
|
|
482
|
+
const controller = isController(controllerOrOpts) ? controllerOrOpts : new CairnController(controllerOrOpts);
|
|
476
483
|
const useCairnState = () => React.useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
|
|
477
484
|
const useCairnAccount = () => useCairnState().account;
|
|
478
485
|
const useCairn = () => ({
|
|
@@ -483,6 +490,10 @@ function createCairnHooks(React, controllerOrOpts) {
|
|
|
483
490
|
});
|
|
484
491
|
return { controller, useCairnState, useCairnAccount, useCairn };
|
|
485
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
|
+
}
|
|
486
497
|
// Annotate the CommonJS export names for ESM import in node:
|
|
487
498
|
0 && (module.exports = {
|
|
488
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-OE5BEEQY.js";
|
|
4
|
+
import "./chunk-5RGFC2JR.js";
|
|
5
|
+
import "./chunk-OSPAN7GX.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
|