@inversealtruism/cairn-sdk 0.8.1 → 0.8.2
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 +3 -3
- package/dist/chain.cjs +83 -3
- package/dist/chain.d.cts +9 -4
- package/dist/chain.d.ts +9 -4
- package/dist/chain.js +6 -1
- package/dist/{chunk-23ATXZKM.js → chunk-2RR4ZJPQ.js} +1 -1
- package/dist/{chunk-X7J6ECNV.js → chunk-37OHCH7R.js} +20 -8
- package/dist/{chunk-QWGR5J5D.js → chunk-AHPXOYCC.js} +1 -1
- package/dist/{chunk-F3KJPQXM.js → chunk-EVEBQSDK.js} +19 -14
- package/dist/{chunk-C452UODV.js → chunk-IPBMMJ3Z.js} +26 -4
- package/dist/{chunk-CI7YODPP.js → chunk-NRCTL7OI.js} +43 -3
- package/dist/connect.cjs +34 -20
- package/dist/connect.js +2 -2
- package/dist/content.cjs +1 -1
- package/dist/content.js +2 -2
- package/dist/controller.cjs +34 -20
- package/dist/controller.js +3 -3
- package/dist/index.cjs +262 -86
- package/dist/index.d.cts +77 -32
- package/dist/index.d.ts +77 -32
- package/dist/index.js +184 -67
- package/dist/indexer.cjs +98 -3
- package/dist/indexer.d.cts +5 -1
- package/dist/indexer.d.ts +5 -1
- package/dist/indexer.js +2 -1
- package/dist/react.cjs +34 -20
- package/dist/react.js +3 -3
- package/package.json +8 -5
package/dist/index.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Chain,
|
|
3
3
|
LightClient
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-IPBMMJ3Z.js";
|
|
5
5
|
import {
|
|
6
6
|
ContentClient
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-2RR4ZJPQ.js";
|
|
8
8
|
import {
|
|
9
9
|
IndexerClient
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-NRCTL7OI.js";
|
|
11
11
|
import {
|
|
12
12
|
CairnController
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-AHPXOYCC.js";
|
|
14
14
|
import {
|
|
15
15
|
WalletConnection,
|
|
16
16
|
connect,
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
discoverProviders,
|
|
19
19
|
getWallet,
|
|
20
20
|
isInstalled
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-EVEBQSDK.js";
|
|
22
22
|
import {
|
|
23
23
|
CairnError,
|
|
24
24
|
ContentVerificationError,
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
errorCode,
|
|
33
33
|
mapProviderError,
|
|
34
34
|
mapSubmitResultError
|
|
35
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-37OHCH7R.js";
|
|
36
36
|
|
|
37
37
|
// src/http.ts
|
|
38
38
|
var DEFAULT_MAX_BYTES = 16 * 1024 * 1024;
|
|
@@ -370,7 +370,11 @@ import {
|
|
|
370
370
|
fillEndorsement,
|
|
371
371
|
fillIsSafe,
|
|
372
372
|
requiredFillOutputs,
|
|
373
|
-
previewFill
|
|
373
|
+
previewFill,
|
|
374
|
+
offerExpiryHeightOf,
|
|
375
|
+
offerFillTooLate,
|
|
376
|
+
FILL_TIP_MARGIN,
|
|
377
|
+
fillTargetId as fillTargetOf
|
|
374
378
|
} from "@inversealtruism/cairnx-core";
|
|
375
379
|
var ADDR = /^0x[0-9a-f]{40}$/;
|
|
376
380
|
var BELOW_BASE = /^tx below the synced base \((\d+)\)/;
|
|
@@ -413,9 +417,114 @@ async function provenAuthor(client, provenTx) {
|
|
|
413
417
|
if (String(txid(src)).toLowerCase() !== String(in0.prevTxid).toLowerCase()) return null;
|
|
414
418
|
return authorOf(provenTx, src.outputs?.[Number(in0.vout)]?.scriptPubkey);
|
|
415
419
|
}
|
|
420
|
+
var has = (v) => v !== void 0 && v !== null;
|
|
421
|
+
function fillInputs(o) {
|
|
422
|
+
const base = { pay: o.pay, plannedOutputs: o.plannedOutputs };
|
|
423
|
+
if (!has(o.me) && !has(o.tip) && !has(o.fillTargetId)) return base;
|
|
424
|
+
const me = typeof o.me === "string" ? o.me.toLowerCase() : "";
|
|
425
|
+
const target = has(o.fillTargetId) ? String(o.fillTargetId).toLowerCase() : void 0;
|
|
426
|
+
if (!ADDR.test(me) || !Number.isSafeInteger(o.tip) || o.tip < 0 || target !== void 0 && !HASH.test(target) || !has(o.pay))
|
|
427
|
+
return "invalid fill inputs: pass me (your 0x address), tip (the chain tip height) and pay together; fillTargetId, when given, is a 0x txid";
|
|
428
|
+
return { ...base, me, tip: o.tip, fillTargetId: target };
|
|
429
|
+
}
|
|
430
|
+
function judgeFill(rec, proven, served, f) {
|
|
431
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
432
|
+
const status = served?.status;
|
|
433
|
+
const terms = proven.terms;
|
|
434
|
+
const state = {
|
|
435
|
+
id: proven.id,
|
|
436
|
+
seller: proven.seller,
|
|
437
|
+
give: rec.give,
|
|
438
|
+
want: { ...rec.want, payto: proven.payto },
|
|
439
|
+
taker: rec.taker,
|
|
440
|
+
bid: rec.bid,
|
|
441
|
+
min: rec.min,
|
|
442
|
+
status,
|
|
443
|
+
expiresEpoch: proven.expiresEpoch,
|
|
444
|
+
height: terms.height,
|
|
445
|
+
feeBps: terms.feeBps,
|
|
446
|
+
paid: str(served?.paid),
|
|
447
|
+
delivered: str(served?.delivered),
|
|
448
|
+
claimedBy: str(served?.claimedBy),
|
|
449
|
+
claimUntilHeight: typeof served?.claimUntilHeight === "number" ? served.claimUntilHeight : void 0,
|
|
450
|
+
claimTxid: str(served?.claimTxid)
|
|
451
|
+
};
|
|
452
|
+
const j = { planBound: false };
|
|
453
|
+
const err = (e) => e?.message ?? String(e);
|
|
454
|
+
const token = isTokenWant(rec.want);
|
|
455
|
+
const progress = state.paid !== void 0 && state.delivered !== void 0;
|
|
456
|
+
if (typeof status === "string" && status !== "open")
|
|
457
|
+
j.refusal = `the resolver serves this offer as ${status}, not open - refusing (CairnX does not apply a fill of it)`;
|
|
458
|
+
if (token && has(f.plannedOutputs)) {
|
|
459
|
+
const legs = bindPlannedSums(f.plannedOutputs, []);
|
|
460
|
+
if (legs) j.refusal ??= `the offer is token-priced, so a fill moves no CSD: ${legs}`;
|
|
461
|
+
}
|
|
462
|
+
if (!has(f.pay)) j.planReason = "no `pay` was given, so no output plan was sized";
|
|
463
|
+
else if (j.refusal) j.planReason = "the fill is refused, so no output plan was sized";
|
|
464
|
+
else if (token) j.planReason = "the offer is token-priced: the payment is a token transfer, not CSD outputs";
|
|
465
|
+
else if (rec.min !== void 0) {
|
|
466
|
+
j.planReason = "the offer is partially fillable: its running paid/delivered is not provable here (the wallet's fill-SPV sizes it)";
|
|
467
|
+
if (typeof status === "string" && progress) {
|
|
468
|
+
try {
|
|
469
|
+
const p = previewFill(state, f.pay);
|
|
470
|
+
if (!p.deliverable) j.refusal = `this pay would not deliver against the offer's served progress (${p.reason}) - refusing (the CSD would be lost)`;
|
|
471
|
+
} catch (e) {
|
|
472
|
+
j.refusal = `couldn't size the fill against the offer (${err(e)}) - refusing`;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
} else if (typeof status !== "string") j.planReason = "no served offer status: liveness is not provable here, so no output plan was sized (pass the resolver's offer object)";
|
|
476
|
+
else {
|
|
477
|
+
try {
|
|
478
|
+
const plan = fillOutputPlan(state, f.pay);
|
|
479
|
+
if (plan.kind === "csd-outputs") {
|
|
480
|
+
j.outputPlan = plan.outputs;
|
|
481
|
+
if (has(f.plannedOutputs)) {
|
|
482
|
+
j.refusal = bindPlannedSums(f.plannedOutputs, plan.outputs);
|
|
483
|
+
j.planBound = true;
|
|
484
|
+
}
|
|
485
|
+
} else if (plan.kind === "undeliverable") {
|
|
486
|
+
j.refusal = `this pay would not deliver against the proven offer (${plan.reason}) - refusing (the CSD would be lost)`;
|
|
487
|
+
} else {
|
|
488
|
+
j.planReason = `the proven offer settles without CSD outputs (${plan.kind})`;
|
|
489
|
+
}
|
|
490
|
+
} catch (e) {
|
|
491
|
+
j.refusal = `couldn't size the fill against the proven offer (${err(e)}) - refusing rather than sizing from served fields`;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (f.me !== void 0 && f.tip !== void 0 && has(f.pay)) {
|
|
495
|
+
const unplan = () => {
|
|
496
|
+
delete j.outputPlan;
|
|
497
|
+
j.planBound = false;
|
|
498
|
+
j.planReason = "the fill is refused, so no output plan was sized";
|
|
499
|
+
};
|
|
500
|
+
if (offerFillTooLate(proven.expiresAt, f.tip)) {
|
|
501
|
+
j.refusal ??= `the offer's last live block ${proven.expiresAt - 1} leaves less than the ${FILL_TIP_MARGIN}-block cushion above tip ${f.tip}, so the fill is too close to the offer's expiry - do not sign it`;
|
|
502
|
+
unplan();
|
|
503
|
+
}
|
|
504
|
+
if (typeof status !== "string") j.refusal ??= "the fill endorsement needs the resolver's offer object (its status, claim and progress) - pass servedOffer";
|
|
505
|
+
else if (rec.min !== void 0 && !progress) j.refusal ??= "the served offer is missing paid or delivered (a partial offer's progress) - pass the resolver's full offer object";
|
|
506
|
+
else {
|
|
507
|
+
try {
|
|
508
|
+
const e = fillEndorsement(state, f.me, f.pay, f.tip, f.fillTargetId !== void 0 ? { fillTargetId: f.fillTargetId } : void 0);
|
|
509
|
+
j.endorsement = { verdict: e.verdict, reason: e.reason };
|
|
510
|
+
j.fillTarget = fillTargetOf(state, f.tip);
|
|
511
|
+
if (e.verdict === "refused") {
|
|
512
|
+
j.refusal ??= `CairnX would refuse this fill (${e.reason}) - do not sign it`;
|
|
513
|
+
unplan();
|
|
514
|
+
}
|
|
515
|
+
} catch (e) {
|
|
516
|
+
j.refusal ??= `couldn't judge the fill against the offer (${err(e)}) - refusing`;
|
|
517
|
+
unplan();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return j;
|
|
522
|
+
}
|
|
416
523
|
async function preverifyOffer(opts) {
|
|
417
524
|
const id = String(opts.offerId).toLowerCase();
|
|
418
525
|
if (!HASH.test(id)) return { ok: false, trust: "unverified", reason: "invalid offer id" };
|
|
526
|
+
const fill = fillInputs(opts);
|
|
527
|
+
if (typeof fill === "string") return { ok: false, trust: "unverified", reason: fill };
|
|
419
528
|
let incl;
|
|
420
529
|
try {
|
|
421
530
|
incl = await opts.light.verifyTxInclusion(id);
|
|
@@ -457,52 +566,17 @@ async function preverifyOffer(opts) {
|
|
|
457
566
|
const w = rec.want ?? {};
|
|
458
567
|
const payto = w.payto && ADDR.test(String(w.payto).toLowerCase()) ? String(w.payto).toLowerCase() : seller;
|
|
459
568
|
const terms = provenOfferTerms(offerRec, blockHeight);
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
let planBound = false;
|
|
464
|
-
const has = (v) => v !== void 0 && v !== null;
|
|
465
|
-
const servedStatus = opts.servedOffer?.status;
|
|
466
|
-
if (!has(opts.pay)) planReason = "no `pay` was given, so no output plan was sized";
|
|
467
|
-
else if (isTokenWant(offerRec.want)) planReason = "the offer is token-priced: the payment is a token transfer, not CSD outputs";
|
|
468
|
-
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)";
|
|
469
|
-
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)";
|
|
470
|
-
else {
|
|
471
|
-
const provenState = {
|
|
472
|
-
id,
|
|
473
|
-
seller,
|
|
474
|
-
give: offerRec.give,
|
|
475
|
-
want: { ...offerRec.want, payto },
|
|
476
|
-
taker: offerRec.taker,
|
|
477
|
-
bid: offerRec.bid,
|
|
478
|
-
status: servedStatus,
|
|
479
|
-
expiresEpoch: 0,
|
|
480
|
-
height: blockHeight,
|
|
481
|
-
feeBps: terms.feeBps
|
|
482
|
-
};
|
|
483
|
-
try {
|
|
484
|
-
const plan = fillOutputPlan(provenState, opts.pay);
|
|
485
|
-
if (plan.kind === "csd-outputs") {
|
|
486
|
-
outputPlan = plan.outputs;
|
|
487
|
-
if (has(opts.plannedOutputs)) {
|
|
488
|
-
sumsMismatch = bindPlannedSums(opts.plannedOutputs, plan.outputs);
|
|
489
|
-
planBound = true;
|
|
490
|
-
}
|
|
491
|
-
} else if (plan.kind === "undeliverable") {
|
|
492
|
-
sumsMismatch = `this pay would not deliver against the proven offer (${plan.reason}) - refusing (the CSD would be lost)`;
|
|
493
|
-
} else {
|
|
494
|
-
planReason = `the proven offer settles without CSD outputs (${plan.kind})`;
|
|
495
|
-
}
|
|
496
|
-
} catch (e) {
|
|
497
|
-
sumsMismatch = `couldn't size the fill against the proven offer (${e?.message ?? e}) - refusing rather than sizing from served fields`;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
569
|
+
const expiresEpoch = Number(ee);
|
|
570
|
+
const expiresAt = offerExpiryHeightOf(expiresEpoch, blockHeight);
|
|
571
|
+
const j = judgeFill(offerRec, { id, seller, payto, terms, expiresEpoch, expiresAt }, opts.servedOffer, fill);
|
|
500
572
|
const planFields = {
|
|
501
|
-
...outputPlan ? { outputPlan } : {},
|
|
502
|
-
...has(opts.
|
|
503
|
-
|
|
573
|
+
...j.outputPlan ? { outputPlan: j.outputPlan } : {},
|
|
574
|
+
...has(opts.pay) ? { sized: j.outputPlan !== void 0 } : {},
|
|
575
|
+
...has(opts.plannedOutputs) ? { planBound: j.planBound } : {},
|
|
576
|
+
...!j.outputPlan && j.planReason && (has(opts.pay) || has(opts.plannedOutputs)) ? { planReason: j.planReason } : {},
|
|
577
|
+
...j.endorsement ? { endorsement: j.endorsement, fillTarget: j.fillTarget } : {}
|
|
504
578
|
};
|
|
505
|
-
const base = { payto, seller, terms, blockHeight, ...planFields };
|
|
579
|
+
const base = { payto, seller, terms, blockHeight, expiresEpoch, expiresAt, ...planFields };
|
|
506
580
|
if (opts.servedOffer !== void 0 && opts.servedOffer !== null) {
|
|
507
581
|
const so = opts.servedOffer;
|
|
508
582
|
if (so?.want?.payto === void 0 || so?.want?.payto === null)
|
|
@@ -516,7 +590,7 @@ async function preverifyOffer(opts) {
|
|
|
516
590
|
if (bindOfferTerms(opts.servedOffer, terms, { give: true, wantType: true }))
|
|
517
591
|
return { ok: false, trust: "verified", reason: "the served terms don't match the offer's on-chain record (fee, rebate, partial sizing, the give, or the want's type or token price - a lying resolver could mis-size, re-price or re-route the fill, redirecting or burning your payment)", ...base };
|
|
518
592
|
}
|
|
519
|
-
if (
|
|
593
|
+
if (j.refusal) return { ok: false, trust: "verified", reason: j.refusal, ...base };
|
|
520
594
|
return { ok: true, trust: "verified", ...base };
|
|
521
595
|
}
|
|
522
596
|
|
|
@@ -537,6 +611,20 @@ var spvBackoffMs = (attempt) => Math.min(4e3, 700 * (attempt + 1));
|
|
|
537
611
|
var SPV_RETRY_WAIT_BUDGET_MS = 16e3;
|
|
538
612
|
var SPV_RECHECK_DEPTH = 12;
|
|
539
613
|
var spvTipOf = (lc) => lc.baseHeight + lc.chain.length - 1;
|
|
614
|
+
var SPV_BEYOND_TIP = /^tx at \d+ is \d+ blocks beyond tip; sync\(\d+\) first$/;
|
|
615
|
+
var SpvLightClient = class extends LightClient {
|
|
616
|
+
syncInFlight = null;
|
|
617
|
+
async sync(to, from) {
|
|
618
|
+
while (this.syncInFlight) await this.syncInFlight;
|
|
619
|
+
const run = super.sync(to, from);
|
|
620
|
+
this.syncInFlight = run;
|
|
621
|
+
try {
|
|
622
|
+
return await run;
|
|
623
|
+
} finally {
|
|
624
|
+
if (this.syncInFlight === run) this.syncInFlight = null;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
};
|
|
540
628
|
var Cairn = class {
|
|
541
629
|
config;
|
|
542
630
|
chain;
|
|
@@ -552,6 +640,9 @@ var Cairn = class {
|
|
|
552
640
|
// syncFromCheckpoint calls onto the same client (the loser would hit "seedTrusted must be called on a
|
|
553
641
|
// fresh client"). Cleared when the seed settles; a failure leaves no cached state, so the next call retries.
|
|
554
642
|
_spvSeedInflight = null;
|
|
643
|
+
// The in-flight reseed of withSpvReseed, shared by callers whose structural failures overlap. Cleared when it
|
|
644
|
+
// settles; each caller still commits the fresh client only after its own retry succeeds.
|
|
645
|
+
_spvReseedInflight = null;
|
|
555
646
|
_spvCp;
|
|
556
647
|
// W12: the cairn base the SPV header-batch provider fetches /api/headers from (same base the rest of the SDK uses).
|
|
557
648
|
_cairnBase;
|
|
@@ -607,14 +698,18 @@ var Cairn = class {
|
|
|
607
698
|
// (rate limit / gateway / timeout / non-dense / CF 200-HTML) keeps the cache and rethrows: wiping
|
|
608
699
|
// on those turns one /api/headers 429 into a reseed storm (DOS-HDR-3). A second structural
|
|
609
700
|
// failure propagates fail-closed; there is no loop. recheckCachedHeader feeds this path: a near-tip
|
|
610
|
-
// cached header that the served chain no longer carries is a structural failure.
|
|
701
|
+
// cached header that the served chain no longer carries is a structural failure. Callers whose failures overlap
|
|
702
|
+
// (waiters on one shared sync) share one reseed.
|
|
611
703
|
async withSpvReseed(op) {
|
|
612
704
|
const lc = await this.seededSpvLight();
|
|
613
705
|
try {
|
|
614
706
|
return await op(lc);
|
|
615
707
|
} catch (e) {
|
|
616
708
|
if (spvIsTransient(e)) throw e;
|
|
617
|
-
|
|
709
|
+
this._spvReseedInflight ??= this.seedFreshSpvLight().finally(() => {
|
|
710
|
+
this._spvReseedInflight = null;
|
|
711
|
+
});
|
|
712
|
+
const fresh = await this._spvReseedInflight;
|
|
618
713
|
const out = await op(fresh);
|
|
619
714
|
this._spvLight = fresh;
|
|
620
715
|
return out;
|
|
@@ -634,7 +729,7 @@ var Cairn = class {
|
|
|
634
729
|
// the caller commits the result only on success (MF-14; the wallet G8 posture - never null or
|
|
635
730
|
// dirty the shared field while an attempt is in flight).
|
|
636
731
|
async seedFreshSpvLight() {
|
|
637
|
-
const lc = new
|
|
732
|
+
const lc = new SpvLightClient({
|
|
638
733
|
client: this.chain.client,
|
|
639
734
|
checkpoints: { [this._spvCp.height]: this._spvCp.hash },
|
|
640
735
|
headersBatchProvider: (from, count) => this.fetchHeadersBatch(from, count)
|
|
@@ -676,11 +771,7 @@ var Cairn = class {
|
|
|
676
771
|
* data-availability hiccup; back off (honouring Retry-After) a bounded number of times, then THROW so
|
|
677
772
|
* `sync()` fails closed. A non-dense range is rejected terminally (never retried).
|
|
678
773
|
*
|
|
679
|
-
*
|
|
680
|
-
* mounted on `/explorer/api`, `/trade/api` and `/api/rpc`, not here). A SAME-ORIGIN dApp (hosted on the
|
|
681
|
-
* cairn base) reaches it now; a CROSS-ORIGIN third-party dApp cannot until B7d adds the CORS mount and
|
|
682
|
-
* restarts the cairn service. This code is correct as written; it becomes reachable cross-origin only
|
|
683
|
-
* once B7d ships. This is a recorded dependency, not a blocker for authoring/testing.
|
|
774
|
+
* Cross-origin reach depends on the server's CORS mount on `/api/headers` (the cairn server mounts one).
|
|
684
775
|
*/
|
|
685
776
|
async fetchHeadersBatch(from, count) {
|
|
686
777
|
const g = this.config.fetch ?? globalThis.fetch;
|
|
@@ -728,7 +819,8 @@ var Cairn = class {
|
|
|
728
819
|
* commitment, derives the payment recipient + seller from the offer's on-chain AUTHOR (the funding input's
|
|
729
820
|
* prevout owner, txid-committed, NOT the malleable scriptSig), and, if you pass the resolver-SERVED `offer`
|
|
730
821
|
* object, binds its payto/seller + fee/rebate/partial terms to the proven ones. Returns a TRUST-LABELED result
|
|
731
|
-
* (fail-closed on a positive mismatch
|
|
822
|
+
* (fail-closed on a positive mismatch; `transient` on a lagging chain view; final, naming the anchor, for an
|
|
823
|
+
* offer mined below the SPV checkpoint).
|
|
732
824
|
*
|
|
733
825
|
* This is best-effort corroboration, NOT the payment-grade boundary: the Cairn Wallet's OWN on-device
|
|
734
826
|
* fill-SPV (0.2.60+, fails-closed before signing) is that. A dApp settling real value should still clear this
|
|
@@ -736,19 +828,44 @@ var Cairn = class {
|
|
|
736
828
|
*
|
|
737
829
|
* Pass opts.pay (the CSD base units you intend to pay) to receive the PROVEN outputPlan for a whole fill of a
|
|
738
830
|
* non-partial offer, and opts.plannedOutputs to bind your planned per-address sums to it before signing; build
|
|
739
|
-
* EXACTLY the outputPlan, never resolver-served outputs.
|
|
831
|
+
* EXACTLY the outputPlan, never resolver-served outputs. Pass opts.me (your address) and opts.tip (the chain tip
|
|
832
|
+
* height) with pay to have cairnx-core's fillEndorsement and offerFillTooLate judge the fill for you at that tip; without them `ok`
|
|
833
|
+
* says the offer record is proven, not that you can fill it now.
|
|
740
834
|
*/
|
|
741
835
|
async verifyOfferForFill(offerId, servedOffer, opts) {
|
|
742
836
|
const light = {
|
|
743
837
|
verifyTxInclusion: (txidHex) => this.withSpvReseed(async (lc) => {
|
|
744
838
|
const haveTo = spvTipOf(lc);
|
|
745
|
-
|
|
746
|
-
if (r.trustLevel === "
|
|
747
|
-
|
|
748
|
-
|
|
839
|
+
let r = await lc.verifyTxInclusion(txidHex);
|
|
840
|
+
if (r.trustLevel === "rpc-trusted" && SPV_BEYOND_TIP.test(r.reason ?? "")) {
|
|
841
|
+
let tipHeight;
|
|
842
|
+
try {
|
|
843
|
+
tipHeight = Number((await this.chain.tip()).height);
|
|
844
|
+
} catch (e) {
|
|
845
|
+
return { ...r, reason: `${r.reason} (the node tip read failed: ${e?.message ?? e})` };
|
|
846
|
+
}
|
|
847
|
+
if (Number.isSafeInteger(tipHeight) && tipHeight > spvTipOf(lc)) await lc.sync(tipHeight);
|
|
848
|
+
r = await lc.verifyTxInclusion(txidHex);
|
|
849
|
+
}
|
|
850
|
+
if (r.trustLevel !== "verified-inclusion") return r;
|
|
851
|
+
const height = Number(r.blockHeight);
|
|
852
|
+
if (!Number.isSafeInteger(height))
|
|
853
|
+
return { trustLevel: "rpc-trusted", included: false, reason: `the node's containing height for this tx is not a block height (${String(r.blockHeight)})` };
|
|
854
|
+
if (height <= haveTo) await this.recheckCachedHeader(lc, height, haveTo);
|
|
855
|
+
return { ...r, blockHeight: height };
|
|
749
856
|
})
|
|
750
857
|
};
|
|
751
|
-
return preverifyOffer({
|
|
858
|
+
return preverifyOffer({
|
|
859
|
+
light,
|
|
860
|
+
client: this.chain.client,
|
|
861
|
+
offerId,
|
|
862
|
+
servedOffer,
|
|
863
|
+
pay: opts?.pay,
|
|
864
|
+
plannedOutputs: opts?.plannedOutputs,
|
|
865
|
+
me: opts?.me,
|
|
866
|
+
tip: opts?.tip,
|
|
867
|
+
fillTargetId: opts?.fillTargetId
|
|
868
|
+
});
|
|
752
869
|
}
|
|
753
870
|
/**
|
|
754
871
|
* Detect + connect the Cairn Wallet (browser only) and attach it as `cairn.wallet`.
|
package/dist/indexer.cjs
CHANGED
|
@@ -24,8 +24,81 @@ __export(indexer_exports, {
|
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(indexer_exports);
|
|
26
26
|
var import_csd_codec = require("@inversealtruism/csd-codec");
|
|
27
|
+
|
|
28
|
+
// src/errors.ts
|
|
29
|
+
var SDK_VERSION = "0.8.2";
|
|
30
|
+
var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
|
|
31
|
+
var CairnError = class extends Error {
|
|
32
|
+
/** Stable machine code for programmatic branching. */
|
|
33
|
+
code;
|
|
34
|
+
/** The human message without the appended docs/version footer. */
|
|
35
|
+
shortMessage;
|
|
36
|
+
/** Relative docs path (under the SDK docs base), if any. */
|
|
37
|
+
docsPath;
|
|
38
|
+
/** The cairn-sdk version that produced this error. */
|
|
39
|
+
version = SDK_VERSION;
|
|
40
|
+
/**
|
|
41
|
+
* Whether a dApp may safely AUTO-retry this failure. `false` for fund-safety refusals and user-intent
|
|
42
|
+
* declines (the default); `true` only for provably transient infra faults where nothing was signed.
|
|
43
|
+
* A `SubmitInFlightError` is `false` too: reconcile its `txid`, never blind-retry.
|
|
44
|
+
*/
|
|
45
|
+
retryable;
|
|
46
|
+
constructor(shortMessage, opts = {}) {
|
|
47
|
+
const docs = opts.docsPath;
|
|
48
|
+
super(
|
|
49
|
+
[shortMessage, docs ? `Docs: ${DOCS_BASE}${docs}` : "", `Version: cairn-sdk@${SDK_VERSION}`].filter(Boolean).join("\n"),
|
|
50
|
+
{ cause: opts.cause }
|
|
51
|
+
);
|
|
52
|
+
this.name = new.target.name;
|
|
53
|
+
this.code = opts.code ?? "UNKNOWN";
|
|
54
|
+
this.shortMessage = shortMessage;
|
|
55
|
+
this.docsPath = docs;
|
|
56
|
+
this.retryable = opts.retryable ?? false;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Walk the `cause` chain. With a predicate, returns the FIRST matching error (or null). Without one,
|
|
60
|
+
* returns the DEEPEST cause (the root). Mirrors viem's `BaseError.walk()`.
|
|
61
|
+
*/
|
|
62
|
+
walk(fn) {
|
|
63
|
+
let current = this;
|
|
64
|
+
let last = this;
|
|
65
|
+
while (current != null) {
|
|
66
|
+
if (fn && fn(current)) return current;
|
|
67
|
+
last = current;
|
|
68
|
+
current = current?.cause;
|
|
69
|
+
}
|
|
70
|
+
return fn ? null : last;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var HttpError = class extends CairnError {
|
|
74
|
+
status;
|
|
75
|
+
url;
|
|
76
|
+
body;
|
|
77
|
+
constructor(status, url, body) {
|
|
78
|
+
super(`HTTP ${status} for ${url}${body ? `: ${String(body).slice(0, 200)}` : ""}`, { code: "HTTP_ERROR" });
|
|
79
|
+
this.status = status;
|
|
80
|
+
this.url = url;
|
|
81
|
+
this.body = body;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// src/indexer.ts
|
|
27
86
|
var HEX64 = /^0x[0-9a-fA-F]{64}$/;
|
|
28
87
|
var MAX_SSE_BUF = 1024 * 1024;
|
|
88
|
+
var UTXO_PAGE = 1e3;
|
|
89
|
+
var UTXO_MAX_PAGES = 100;
|
|
90
|
+
var UTXO_CURSOR = /^\d{1,16}:0x[0-9a-fA-F]{64}:\d{1,10}$/;
|
|
91
|
+
function utxoCursor(row) {
|
|
92
|
+
const r = row;
|
|
93
|
+
const c = `${String(r?.status?.block_height)}:${String(r?.txid)}:${String(r?.vout)}`;
|
|
94
|
+
return UTXO_CURSOR.test(c) ? c : null;
|
|
95
|
+
}
|
|
96
|
+
function cursorAfter(a, b) {
|
|
97
|
+
const [ha, ta, va] = a.split(":"), [hb, tb, vb] = b.split(":");
|
|
98
|
+
if (Number(ha) !== Number(hb)) return Number(ha) > Number(hb);
|
|
99
|
+
if (ta.toLowerCase() !== tb.toLowerCase()) return ta.toLowerCase() > tb.toLowerCase();
|
|
100
|
+
return Number(va) > Number(vb);
|
|
101
|
+
}
|
|
29
102
|
function isValidMerkleProof(p) {
|
|
30
103
|
if (!p || typeof p !== "object") return false;
|
|
31
104
|
const m = p;
|
|
@@ -75,8 +148,29 @@ var IndexerClient = class {
|
|
|
75
148
|
addressTxs(a) {
|
|
76
149
|
return this.http.getJson(`/address/${encodeURIComponent(a)}/txs`);
|
|
77
150
|
}
|
|
78
|
-
|
|
79
|
-
|
|
151
|
+
/** Every unspent output of an address. The indexer answers in pages of at most 1,000 rows; this follows its
|
|
152
|
+
* `?after=` cursor until a short page, reading at most 100 pages: when the 100th is full (100,000 or more
|
|
153
|
+
* outputs) it throws HttpError instead. An indexer that predates paging answers the whole set at once. */
|
|
154
|
+
async addressUtxo(a) {
|
|
155
|
+
const path = `/address/${encodeURIComponent(a)}/utxo`;
|
|
156
|
+
const first = await this.http.getJson(path);
|
|
157
|
+
if (!Array.isArray(first) || first.length !== UTXO_PAGE) return first;
|
|
158
|
+
const out = [...first];
|
|
159
|
+
let page = first;
|
|
160
|
+
let prev = null;
|
|
161
|
+
for (let pages = 1; page.length === UTXO_PAGE; pages++) {
|
|
162
|
+
if (pages === UTXO_MAX_PAGES)
|
|
163
|
+
throw new HttpError(200, this.http.url(path), `too many unspent outputs: addressUtxo reads at most ${UTXO_MAX_PAGES} pages of ${UTXO_PAGE} rows, and page ${UTXO_MAX_PAGES} is full`);
|
|
164
|
+
const after = utxoCursor(page[UTXO_PAGE - 1]);
|
|
165
|
+
if (!after) throw new HttpError(200, this.http.url(path), "malformed response: a utxo row has no block_height, txid and vout to page from");
|
|
166
|
+
if (prev !== null && !cursorAfter(after, prev)) throw new HttpError(200, this.http.url(path, { after }), "malformed response: the utxo pages do not advance");
|
|
167
|
+
page = await this.http.getJson(path, { after });
|
|
168
|
+
if (!Array.isArray(page)) throw new HttpError(200, this.http.url(path, { after }), "malformed response: a utxo page is not an array");
|
|
169
|
+
if (prev === null && page.length > 0 && utxoCursor(page[0]) === utxoCursor(first[0])) return first;
|
|
170
|
+
prev = after;
|
|
171
|
+
out.push(...page);
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
80
174
|
}
|
|
81
175
|
reputation(a) {
|
|
82
176
|
return this.http.getJson(`/address/${encodeURIComponent(a)}/reputation`);
|
|
@@ -95,7 +189,8 @@ var IndexerClient = class {
|
|
|
95
189
|
}
|
|
96
190
|
// ---- trust-minimized inclusion ----------------------------------------
|
|
97
191
|
/**
|
|
98
|
-
* Verify
|
|
192
|
+
* Verify that `txid` is a node of a block's merkle tree (an inner node or the root folds too, so pass an id you
|
|
193
|
+
* hashed from a transaction body you hold) by folding its merkle branch to the root
|
|
99
194
|
* with csd-codec.verifyMerkleProof. If `headerMerkleAt` was provided, the
|
|
100
195
|
* proof's root is cross-checked against the header merkle at that height →
|
|
101
196
|
* trustLevel "verified-inclusion" (only as trustworthy as that header source —
|
package/dist/indexer.d.cts
CHANGED
|
@@ -62,6 +62,9 @@ declare class IndexerClient {
|
|
|
62
62
|
txMerkleProof(id: string): Promise<MerkleProof>;
|
|
63
63
|
address(a: string): Promise<unknown>;
|
|
64
64
|
addressTxs(a: string): Promise<unknown[]>;
|
|
65
|
+
/** Every unspent output of an address. The indexer answers in pages of at most 1,000 rows; this follows its
|
|
66
|
+
* `?after=` cursor until a short page, reading at most 100 pages: when the 100th is full (100,000 or more
|
|
67
|
+
* outputs) it throws HttpError instead. An indexer that predates paging answers the whole set at once. */
|
|
65
68
|
addressUtxo(a: string): Promise<unknown[]>;
|
|
66
69
|
reputation(a: string): Promise<unknown>;
|
|
67
70
|
domains(): Promise<unknown[]>;
|
|
@@ -69,7 +72,8 @@ declare class IndexerClient {
|
|
|
69
72
|
proposal(id: string): Promise<unknown>;
|
|
70
73
|
attestations(id: string): Promise<unknown[]>;
|
|
71
74
|
/**
|
|
72
|
-
* Verify
|
|
75
|
+
* Verify that `txid` is a node of a block's merkle tree (an inner node or the root folds too, so pass an id you
|
|
76
|
+
* hashed from a transaction body you hold) by folding its merkle branch to the root
|
|
73
77
|
* with csd-codec.verifyMerkleProof. If `headerMerkleAt` was provided, the
|
|
74
78
|
* proof's root is cross-checked against the header merkle at that height →
|
|
75
79
|
* trustLevel "verified-inclusion" (only as trustworthy as that header source —
|
package/dist/indexer.d.ts
CHANGED
|
@@ -62,6 +62,9 @@ declare class IndexerClient {
|
|
|
62
62
|
txMerkleProof(id: string): Promise<MerkleProof>;
|
|
63
63
|
address(a: string): Promise<unknown>;
|
|
64
64
|
addressTxs(a: string): Promise<unknown[]>;
|
|
65
|
+
/** Every unspent output of an address. The indexer answers in pages of at most 1,000 rows; this follows its
|
|
66
|
+
* `?after=` cursor until a short page, reading at most 100 pages: when the 100th is full (100,000 or more
|
|
67
|
+
* outputs) it throws HttpError instead. An indexer that predates paging answers the whole set at once. */
|
|
65
68
|
addressUtxo(a: string): Promise<unknown[]>;
|
|
66
69
|
reputation(a: string): Promise<unknown>;
|
|
67
70
|
domains(): Promise<unknown[]>;
|
|
@@ -69,7 +72,8 @@ declare class IndexerClient {
|
|
|
69
72
|
proposal(id: string): Promise<unknown>;
|
|
70
73
|
attestations(id: string): Promise<unknown[]>;
|
|
71
74
|
/**
|
|
72
|
-
* Verify
|
|
75
|
+
* Verify that `txid` is a node of a block's merkle tree (an inner node or the root folds too, so pass an id you
|
|
76
|
+
* hashed from a transaction body you hold) by folding its merkle branch to the root
|
|
73
77
|
* with csd-codec.verifyMerkleProof. If `headerMerkleAt` was provided, the
|
|
74
78
|
* proof's root is cross-checked against the header merkle at that height →
|
|
75
79
|
* trustLevel "verified-inclusion" (only as trustworthy as that header source —
|