@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.cjs
CHANGED
|
@@ -58,7 +58,7 @@ __export(index_exports, {
|
|
|
58
58
|
module.exports = __toCommonJS(index_exports);
|
|
59
59
|
|
|
60
60
|
// src/errors.ts
|
|
61
|
-
var SDK_VERSION = "0.8.
|
|
61
|
+
var SDK_VERSION = "0.8.2";
|
|
62
62
|
var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
|
|
63
63
|
var CairnError = class extends Error {
|
|
64
64
|
/** Stable machine code for programmatic branching. */
|
|
@@ -141,17 +141,22 @@ var ContentVerificationError = class extends CairnError {
|
|
|
141
141
|
}
|
|
142
142
|
};
|
|
143
143
|
var SubmitInFlightError = class extends CairnError {
|
|
144
|
-
/** The locally-computed consensus txid to reconcile against the chain (
|
|
144
|
+
/** The locally-computed consensus txid to reconcile against the chain (undefined when it is not known). */
|
|
145
145
|
txid;
|
|
146
|
-
/**
|
|
146
|
+
/** The signed transaction, set only when `Chain.send` threw this (a wallet write keeps it in the wallet).
|
|
147
|
+
* `chain.submit(nodeJson)` resends it unchanged: the same coins under the same txid, so it cannot pay twice,
|
|
148
|
+
* where a new send may select other coins. */
|
|
149
|
+
nodeJson;
|
|
150
|
+
/** Always true: the tx MAY be in the mempool. Reconcile via `txid` or the balance; never blind-retry. */
|
|
147
151
|
maybeSent = true;
|
|
148
|
-
constructor(message, code, txid3) {
|
|
149
|
-
super(message, { code, docsPath: "/errors#submit-inflight", retryable: false });
|
|
152
|
+
constructor(message, code, txid3, cause, nodeJson) {
|
|
153
|
+
super(message, { code, docsPath: "/errors#submit-inflight", retryable: false, cause });
|
|
150
154
|
this.txid = txid3;
|
|
155
|
+
this.nodeJson = nodeJson;
|
|
151
156
|
}
|
|
152
157
|
};
|
|
153
158
|
var NATIVE_CODES = /* @__PURE__ */ new Set(["FIRST_PARTY_ONLY", "RATE_LIMITED", "FORBIDDEN", "INTERNAL"]);
|
|
154
|
-
var NESTED_TRANSIENT = /* @__PURE__ */ new Set(["VERIFY_UNAVAILABLE", "OFFER_UNKNOWN", "WALLET_UNAVAILABLE", "REVIEW_REQUIRED"]);
|
|
159
|
+
var NESTED_TRANSIENT = /* @__PURE__ */ new Set(["VERIFY_UNAVAILABLE", "OFFER_UNKNOWN", "WALLET_UNAVAILABLE", "REVIEW_REQUIRED", "SEAL_NOT_MINED"]);
|
|
155
160
|
var NESTED_TERMINAL = /* @__PURE__ */ new Set([
|
|
156
161
|
"FILL_UNSAFE",
|
|
157
162
|
"FILL_WRONG_TARGET",
|
|
@@ -173,7 +178,9 @@ var NESTED_TERMINAL = /* @__PURE__ */ new Set([
|
|
|
173
178
|
"OFFER_SOURCE_CONFLICT",
|
|
174
179
|
"FILL_LEGACY_SUNSET",
|
|
175
180
|
"VERIFY_HORIZON",
|
|
176
|
-
"VERIFY_UNREPRESENTABLE"
|
|
181
|
+
"VERIFY_UNREPRESENTABLE",
|
|
182
|
+
"RESERVATION_DENIED",
|
|
183
|
+
"RPC_BLOCK_MISMATCH"
|
|
177
184
|
]);
|
|
178
185
|
function mapProviderError(error, code) {
|
|
179
186
|
if (code === "USER_REJECTED" || code === "APPROVAL_CLOSED") return new UserRejectedError(error);
|
|
@@ -191,11 +198,15 @@ function mapProviderError(error, code) {
|
|
|
191
198
|
function mapSubmitResultError(result) {
|
|
192
199
|
const code = result?.code;
|
|
193
200
|
const msg = typeof result?.error === "string" && result.error || code || "The wallet refused the transaction.";
|
|
194
|
-
if (code === "SUBMIT_MAYBE_INFLIGHT" || code === "SUBMIT_DUPLICATE") {
|
|
201
|
+
if (code === "SUBMIT_MAYBE_INFLIGHT" || code === "SUBMIT_DUPLICATE" || code === "WALLET_UNAVAILABLE") {
|
|
195
202
|
return new SubmitInFlightError(msg, code, typeof result?.txid === "string" ? result.txid : void 0);
|
|
196
203
|
}
|
|
197
204
|
return mapProviderError(msg, code);
|
|
198
205
|
}
|
|
206
|
+
function mapWriteProviderError(error, code) {
|
|
207
|
+
if (code === "WALLET_UNAVAILABLE") return new SubmitInFlightError(error || code, code);
|
|
208
|
+
return mapProviderError(error, code);
|
|
209
|
+
}
|
|
199
210
|
function errorCode(err) {
|
|
200
211
|
return err instanceof CairnError ? err.code : "UNKNOWN";
|
|
201
212
|
}
|
|
@@ -404,7 +415,11 @@ var Chain = class {
|
|
|
404
415
|
* chain (`verifyInputValues` recomputes each source tx's txid, so a lying RPC can't under-report) and
|
|
405
416
|
* computes change from the VERIFIED total — closing the UTXO-VALUE-1 implicit-fee-burn. Fail-closed:
|
|
406
417
|
* if any source can't be verified it signs nothing. Prefer this over the pure `buildSend`. Pass
|
|
407
|
-
* `broadcast:false` to get the signed tx back without submitting.
|
|
418
|
+
* `broadcast:false` to get the signed tx back without submitting. A submit request that fails after signing
|
|
419
|
+
* throws a `SubmitInFlightError` carrying the signed `txid` and `nodeJson`: the node may have taken it, so check
|
|
420
|
+
* before resending, and resend that `nodeJson` with `submit` rather than a new send. A 4xx answer (a proxy's
|
|
421
|
+
* body cap or rate limit, or the node refusing the request) broadcast nothing and throws a final `CairnError`
|
|
422
|
+
* with code `SUBMIT_REJECTED`.
|
|
408
423
|
*/
|
|
409
424
|
async send(p) {
|
|
410
425
|
const from = (0, import_csd_crypto2.addrFromPriv)(p.priv);
|
|
@@ -418,10 +433,19 @@ var Chain = class {
|
|
|
418
433
|
verify: (inputs) => (0, import_csd_client2.verifyInputValues)(this.client, inputs)
|
|
419
434
|
});
|
|
420
435
|
if (!built.ok || p.broadcast === false) return built;
|
|
421
|
-
|
|
436
|
+
let submitResult;
|
|
437
|
+
try {
|
|
438
|
+
submitResult = await this.client.submit(built.nodeJson);
|
|
439
|
+
} catch (e) {
|
|
440
|
+
const what = e?.message ?? String(e);
|
|
441
|
+
if (e?.terminal === true)
|
|
442
|
+
throw new CairnError(`the submit request was refused (${what}), so nothing was broadcast; a 400 or 413 from a proxy is usually its request body cap (a transaction with many inputs), a 429 its rate limit`, { code: "SUBMIT_REJECTED", cause: e });
|
|
443
|
+
throw new SubmitInFlightError(`the submit request failed after signing (${what}); the node may have taken the transaction, so check ${built.txid} before sending again`, "SUBMIT_MAYBE_INFLIGHT", built.txid, e, built.nodeJson);
|
|
444
|
+
}
|
|
422
445
|
return { ...built, ok: built.ok && submitResult.ok !== false, submitResult };
|
|
423
446
|
}
|
|
424
|
-
/** A
|
|
447
|
+
/** A light client over the same RPC (built once, reused), with no checkpoint and no batch header provider: it proves
|
|
448
|
+
* nothing until seeded. The facade's seeded client backs `cairn.index.verifyInclusion` and `verifyOfferForFill`. */
|
|
425
449
|
light() {
|
|
426
450
|
if (!this._light) {
|
|
427
451
|
this._light = new import_csd_light2.LightClient({ client: this.client });
|
|
@@ -434,6 +458,20 @@ var Chain = class {
|
|
|
434
458
|
var import_csd_codec2 = require("@inversealtruism/csd-codec");
|
|
435
459
|
var HEX64 = /^0x[0-9a-fA-F]{64}$/;
|
|
436
460
|
var MAX_SSE_BUF = 1024 * 1024;
|
|
461
|
+
var UTXO_PAGE = 1e3;
|
|
462
|
+
var UTXO_MAX_PAGES = 100;
|
|
463
|
+
var UTXO_CURSOR = /^\d{1,16}:0x[0-9a-fA-F]{64}:\d{1,10}$/;
|
|
464
|
+
function utxoCursor(row) {
|
|
465
|
+
const r = row;
|
|
466
|
+
const c = `${String(r?.status?.block_height)}:${String(r?.txid)}:${String(r?.vout)}`;
|
|
467
|
+
return UTXO_CURSOR.test(c) ? c : null;
|
|
468
|
+
}
|
|
469
|
+
function cursorAfter(a, b) {
|
|
470
|
+
const [ha, ta, va] = a.split(":"), [hb2, tb, vb] = b.split(":");
|
|
471
|
+
if (Number(ha) !== Number(hb2)) return Number(ha) > Number(hb2);
|
|
472
|
+
if (ta.toLowerCase() !== tb.toLowerCase()) return ta.toLowerCase() > tb.toLowerCase();
|
|
473
|
+
return Number(va) > Number(vb);
|
|
474
|
+
}
|
|
437
475
|
function isValidMerkleProof(p) {
|
|
438
476
|
if (!p || typeof p !== "object") return false;
|
|
439
477
|
const m = p;
|
|
@@ -483,8 +521,29 @@ var IndexerClient = class {
|
|
|
483
521
|
addressTxs(a) {
|
|
484
522
|
return this.http.getJson(`/address/${encodeURIComponent(a)}/txs`);
|
|
485
523
|
}
|
|
486
|
-
|
|
487
|
-
|
|
524
|
+
/** Every unspent output of an address. The indexer answers in pages of at most 1,000 rows; this follows its
|
|
525
|
+
* `?after=` cursor until a short page, reading at most 100 pages: when the 100th is full (100,000 or more
|
|
526
|
+
* outputs) it throws HttpError instead. An indexer that predates paging answers the whole set at once. */
|
|
527
|
+
async addressUtxo(a) {
|
|
528
|
+
const path = `/address/${encodeURIComponent(a)}/utxo`;
|
|
529
|
+
const first = await this.http.getJson(path);
|
|
530
|
+
if (!Array.isArray(first) || first.length !== UTXO_PAGE) return first;
|
|
531
|
+
const out = [...first];
|
|
532
|
+
let page = first;
|
|
533
|
+
let prev = null;
|
|
534
|
+
for (let pages = 1; page.length === UTXO_PAGE; pages++) {
|
|
535
|
+
if (pages === UTXO_MAX_PAGES)
|
|
536
|
+
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`);
|
|
537
|
+
const after = utxoCursor(page[UTXO_PAGE - 1]);
|
|
538
|
+
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");
|
|
539
|
+
if (prev !== null && !cursorAfter(after, prev)) throw new HttpError(200, this.http.url(path, { after }), "malformed response: the utxo pages do not advance");
|
|
540
|
+
page = await this.http.getJson(path, { after });
|
|
541
|
+
if (!Array.isArray(page)) throw new HttpError(200, this.http.url(path, { after }), "malformed response: a utxo page is not an array");
|
|
542
|
+
if (prev === null && page.length > 0 && utxoCursor(page[0]) === utxoCursor(first[0])) return first;
|
|
543
|
+
prev = after;
|
|
544
|
+
out.push(...page);
|
|
545
|
+
}
|
|
546
|
+
return out;
|
|
488
547
|
}
|
|
489
548
|
reputation(a) {
|
|
490
549
|
return this.http.getJson(`/address/${encodeURIComponent(a)}/reputation`);
|
|
@@ -503,7 +562,8 @@ var IndexerClient = class {
|
|
|
503
562
|
}
|
|
504
563
|
// ---- trust-minimized inclusion ----------------------------------------
|
|
505
564
|
/**
|
|
506
|
-
* Verify
|
|
565
|
+
* 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
|
|
566
|
+
* hashed from a transaction body you hold) by folding its merkle branch to the root
|
|
507
567
|
* with csd-codec.verifyMerkleProof. If `headerMerkleAt` was provided, the
|
|
508
568
|
* proof's root is cross-checked against the header merkle at that height →
|
|
509
569
|
* trustLevel "verified-inclusion" (only as trustworthy as that header source —
|
|
@@ -987,7 +1047,8 @@ function unwrap(reply) {
|
|
|
987
1047
|
throw mapProviderError(reply?.error ?? "Unknown wallet error", reply?.code);
|
|
988
1048
|
}
|
|
989
1049
|
function unwrapWrite(reply) {
|
|
990
|
-
|
|
1050
|
+
if (!reply || !reply.ok) throw mapWriteProviderError(reply?.error ?? "Unknown wallet error", reply?.code);
|
|
1051
|
+
const result = reply.result;
|
|
991
1052
|
if (result && typeof result === "object" && result.ok === false) {
|
|
992
1053
|
throw mapSubmitResultError(result);
|
|
993
1054
|
}
|
|
@@ -1003,10 +1064,12 @@ var WalletConnection = class {
|
|
|
1003
1064
|
this.provider = provider;
|
|
1004
1065
|
this._timeoutMs = opts.timeoutMs ?? DEFAULT_PROVIDER_TIMEOUT_MS;
|
|
1005
1066
|
}
|
|
1006
|
-
// B7c: race a provider call against the per-call timeout. On timeout, throw a fail-closed
|
|
1007
|
-
// (retryable:false
|
|
1008
|
-
//
|
|
1009
|
-
|
|
1067
|
+
// B7c: race a provider call against the per-call timeout. On timeout, throw a fail-closed error
|
|
1068
|
+
// (retryable:false). A write's request stays open in the wallet and may still be approved and sent, so its
|
|
1069
|
+
// timeout is a maybe-sent SubmitInFlightError (WALLET_TIMEOUT, no txid); while that request stays open, a resend
|
|
1070
|
+
// opens a second one, and approving both sends twice. The genuine wallet resolves quickly
|
|
1071
|
+
// once the user acts; this only fires on a truly hung/absent response.
|
|
1072
|
+
call(op, label, write = false) {
|
|
1010
1073
|
const ms = this._timeoutMs;
|
|
1011
1074
|
const invoke = () => new Promise((resolve) => {
|
|
1012
1075
|
resolve(op());
|
|
@@ -1014,7 +1077,7 @@ var WalletConnection = class {
|
|
|
1014
1077
|
if (!Number.isFinite(ms) || ms <= 0) return invoke();
|
|
1015
1078
|
let timer;
|
|
1016
1079
|
const timeout = new Promise((_, reject) => {
|
|
1017
|
-
timer = setTimeout(() => reject(new CairnError(`the wallet did not respond to ${label} within ${ms}ms`, { retryable: false })), ms);
|
|
1080
|
+
timer = setTimeout(() => reject(write ? new SubmitInFlightError(`the wallet did not respond to ${label} within ${ms}ms; the request may still be open in the wallet and be sent if approved, so reject it in the wallet and check the chain before sending it again`, "WALLET_TIMEOUT") : new CairnError(`the wallet did not respond to ${label} within ${ms}ms`, { retryable: false })), ms);
|
|
1018
1081
|
});
|
|
1019
1082
|
return Promise.race([invoke(), timeout]).finally(() => clearTimeout(timer));
|
|
1020
1083
|
}
|
|
@@ -1090,7 +1153,7 @@ var WalletConnection = class {
|
|
|
1090
1153
|
return Promise.reject(new UnsupportedMethodError("this wallet predates fillOffer() \u2014 ask the user to update the Cairn Wallet"));
|
|
1091
1154
|
}
|
|
1092
1155
|
this.adviseFillSpvCapability();
|
|
1093
|
-
return this.call(() => this.provider.fillOffer(params), "fillOffer").then(unwrapWrite);
|
|
1156
|
+
return this.call(() => this.provider.fillOffer(params), "fillOffer", true).then(unwrapWrite);
|
|
1094
1157
|
}
|
|
1095
1158
|
// P75-5 MF-10: ONE capability advisory per connection. The wallet's on-device fill-SPV (0.2.60+) is the
|
|
1096
1159
|
// payment-grade fill boundary; when the provider SELF-REPORTS an older version, warn the integrator once
|
|
@@ -1138,26 +1201,26 @@ var WalletConnection = class {
|
|
|
1138
1201
|
}
|
|
1139
1202
|
/** Send CSD (always prompts with clear-signing; wallet picks inputs + returns change to itself). */
|
|
1140
1203
|
send(params) {
|
|
1141
|
-
return this.call(() => this.provider.send(params), "send").then(unwrapWrite);
|
|
1204
|
+
return this.call(() => this.provider.send(params), "send", true).then(unwrapWrite);
|
|
1142
1205
|
}
|
|
1143
1206
|
/** Low-level Propose (always prompts). The caller computes the payload hash, uri and expiry epoch. */
|
|
1144
1207
|
propose(params) {
|
|
1145
|
-
return this.call(() => this.provider.propose(params), "propose").then(unwrapWrite);
|
|
1208
|
+
return this.call(() => this.provider.propose(params), "propose", true).then(unwrapWrite);
|
|
1146
1209
|
}
|
|
1147
1210
|
/** Attest/support a proposal (always prompts). */
|
|
1148
1211
|
attest(params) {
|
|
1149
|
-
return this.call(() => this.provider.attest(params), "attest").then(unwrapWrite);
|
|
1212
|
+
return this.call(() => this.provider.attest(params), "attest", true).then(unwrapWrite);
|
|
1150
1213
|
}
|
|
1151
1214
|
/** Seal a commit-reveal claim (always prompts; salt stays inside the wallet). */
|
|
1152
1215
|
sealClaim(params) {
|
|
1153
|
-
return this.call(() => this.provider.sealClaim(params), "sealClaim").then(unwrapWrite);
|
|
1216
|
+
return this.call(() => this.provider.sealClaim(params), "sealClaim", true).then(unwrapWrite);
|
|
1154
1217
|
}
|
|
1155
1218
|
/** Reveal a previously sealed claim by its sealing txid (always prompts). */
|
|
1156
1219
|
revealClaim(txid3) {
|
|
1157
1220
|
if (typeof txid3 !== "string" || !txid3) {
|
|
1158
1221
|
return Promise.reject(new UnsupportedMethodError("revealClaim requires the sealing txid"));
|
|
1159
1222
|
}
|
|
1160
|
-
return this.call(() => this.provider.revealClaim(txid3), "revealClaim").then(unwrapWrite);
|
|
1223
|
+
return this.call(() => this.provider.revealClaim(txid3), "revealClaim", true).then(unwrapWrite);
|
|
1161
1224
|
}
|
|
1162
1225
|
};
|
|
1163
1226
|
async function getWallet(opts = {}) {
|
|
@@ -1215,9 +1278,114 @@ async function provenAuthor(client, provenTx) {
|
|
|
1215
1278
|
if (String((0, import_csd_codec4.txid)(src)).toLowerCase() !== String(in0.prevTxid).toLowerCase()) return null;
|
|
1216
1279
|
return (0, import_csd_crypto3.authorOf)(provenTx, src.outputs?.[Number(in0.vout)]?.scriptPubkey);
|
|
1217
1280
|
}
|
|
1281
|
+
var has = (v) => v !== void 0 && v !== null;
|
|
1282
|
+
function fillInputs(o) {
|
|
1283
|
+
const base = { pay: o.pay, plannedOutputs: o.plannedOutputs };
|
|
1284
|
+
if (!has(o.me) && !has(o.tip) && !has(o.fillTargetId)) return base;
|
|
1285
|
+
const me = typeof o.me === "string" ? o.me.toLowerCase() : "";
|
|
1286
|
+
const target = has(o.fillTargetId) ? String(o.fillTargetId).toLowerCase() : void 0;
|
|
1287
|
+
if (!ADDR.test(me) || !Number.isSafeInteger(o.tip) || o.tip < 0 || target !== void 0 && !HASH.test(target) || !has(o.pay))
|
|
1288
|
+
return "invalid fill inputs: pass me (your 0x address), tip (the chain tip height) and pay together; fillTargetId, when given, is a 0x txid";
|
|
1289
|
+
return { ...base, me, tip: o.tip, fillTargetId: target };
|
|
1290
|
+
}
|
|
1291
|
+
function judgeFill(rec, proven, served, f) {
|
|
1292
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
1293
|
+
const status = served?.status;
|
|
1294
|
+
const terms = proven.terms;
|
|
1295
|
+
const state = {
|
|
1296
|
+
id: proven.id,
|
|
1297
|
+
seller: proven.seller,
|
|
1298
|
+
give: rec.give,
|
|
1299
|
+
want: { ...rec.want, payto: proven.payto },
|
|
1300
|
+
taker: rec.taker,
|
|
1301
|
+
bid: rec.bid,
|
|
1302
|
+
min: rec.min,
|
|
1303
|
+
status,
|
|
1304
|
+
expiresEpoch: proven.expiresEpoch,
|
|
1305
|
+
height: terms.height,
|
|
1306
|
+
feeBps: terms.feeBps,
|
|
1307
|
+
paid: str(served?.paid),
|
|
1308
|
+
delivered: str(served?.delivered),
|
|
1309
|
+
claimedBy: str(served?.claimedBy),
|
|
1310
|
+
claimUntilHeight: typeof served?.claimUntilHeight === "number" ? served.claimUntilHeight : void 0,
|
|
1311
|
+
claimTxid: str(served?.claimTxid)
|
|
1312
|
+
};
|
|
1313
|
+
const j = { planBound: false };
|
|
1314
|
+
const err = (e) => e?.message ?? String(e);
|
|
1315
|
+
const token = (0, import_cairnx_core2.isTokenWant)(rec.want);
|
|
1316
|
+
const progress = state.paid !== void 0 && state.delivered !== void 0;
|
|
1317
|
+
if (typeof status === "string" && status !== "open")
|
|
1318
|
+
j.refusal = `the resolver serves this offer as ${status}, not open - refusing (CairnX does not apply a fill of it)`;
|
|
1319
|
+
if (token && has(f.plannedOutputs)) {
|
|
1320
|
+
const legs = bindPlannedSums(f.plannedOutputs, []);
|
|
1321
|
+
if (legs) j.refusal ??= `the offer is token-priced, so a fill moves no CSD: ${legs}`;
|
|
1322
|
+
}
|
|
1323
|
+
if (!has(f.pay)) j.planReason = "no `pay` was given, so no output plan was sized";
|
|
1324
|
+
else if (j.refusal) j.planReason = "the fill is refused, so no output plan was sized";
|
|
1325
|
+
else if (token) j.planReason = "the offer is token-priced: the payment is a token transfer, not CSD outputs";
|
|
1326
|
+
else if (rec.min !== void 0) {
|
|
1327
|
+
j.planReason = "the offer is partially fillable: its running paid/delivered is not provable here (the wallet's fill-SPV sizes it)";
|
|
1328
|
+
if (typeof status === "string" && progress) {
|
|
1329
|
+
try {
|
|
1330
|
+
const p = (0, import_cairnx_core2.previewFill)(state, f.pay);
|
|
1331
|
+
if (!p.deliverable) j.refusal = `this pay would not deliver against the offer's served progress (${p.reason}) - refusing (the CSD would be lost)`;
|
|
1332
|
+
} catch (e) {
|
|
1333
|
+
j.refusal = `couldn't size the fill against the offer (${err(e)}) - refusing`;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
} 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)";
|
|
1337
|
+
else {
|
|
1338
|
+
try {
|
|
1339
|
+
const plan = (0, import_cairnx_core2.fillOutputPlan)(state, f.pay);
|
|
1340
|
+
if (plan.kind === "csd-outputs") {
|
|
1341
|
+
j.outputPlan = plan.outputs;
|
|
1342
|
+
if (has(f.plannedOutputs)) {
|
|
1343
|
+
j.refusal = bindPlannedSums(f.plannedOutputs, plan.outputs);
|
|
1344
|
+
j.planBound = true;
|
|
1345
|
+
}
|
|
1346
|
+
} else if (plan.kind === "undeliverable") {
|
|
1347
|
+
j.refusal = `this pay would not deliver against the proven offer (${plan.reason}) - refusing (the CSD would be lost)`;
|
|
1348
|
+
} else {
|
|
1349
|
+
j.planReason = `the proven offer settles without CSD outputs (${plan.kind})`;
|
|
1350
|
+
}
|
|
1351
|
+
} catch (e) {
|
|
1352
|
+
j.refusal = `couldn't size the fill against the proven offer (${err(e)}) - refusing rather than sizing from served fields`;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
if (f.me !== void 0 && f.tip !== void 0 && has(f.pay)) {
|
|
1356
|
+
const unplan = () => {
|
|
1357
|
+
delete j.outputPlan;
|
|
1358
|
+
j.planBound = false;
|
|
1359
|
+
j.planReason = "the fill is refused, so no output plan was sized";
|
|
1360
|
+
};
|
|
1361
|
+
if ((0, import_cairnx_core2.offerFillTooLate)(proven.expiresAt, f.tip)) {
|
|
1362
|
+
j.refusal ??= `the offer's last live block ${proven.expiresAt - 1} leaves less than the ${import_cairnx_core2.FILL_TIP_MARGIN}-block cushion above tip ${f.tip}, so the fill is too close to the offer's expiry - do not sign it`;
|
|
1363
|
+
unplan();
|
|
1364
|
+
}
|
|
1365
|
+
if (typeof status !== "string") j.refusal ??= "the fill endorsement needs the resolver's offer object (its status, claim and progress) - pass servedOffer";
|
|
1366
|
+
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";
|
|
1367
|
+
else {
|
|
1368
|
+
try {
|
|
1369
|
+
const e = (0, import_cairnx_core2.fillEndorsement)(state, f.me, f.pay, f.tip, f.fillTargetId !== void 0 ? { fillTargetId: f.fillTargetId } : void 0);
|
|
1370
|
+
j.endorsement = { verdict: e.verdict, reason: e.reason };
|
|
1371
|
+
j.fillTarget = (0, import_cairnx_core2.fillTargetId)(state, f.tip);
|
|
1372
|
+
if (e.verdict === "refused") {
|
|
1373
|
+
j.refusal ??= `CairnX would refuse this fill (${e.reason}) - do not sign it`;
|
|
1374
|
+
unplan();
|
|
1375
|
+
}
|
|
1376
|
+
} catch (e) {
|
|
1377
|
+
j.refusal ??= `couldn't judge the fill against the offer (${err(e)}) - refusing`;
|
|
1378
|
+
unplan();
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return j;
|
|
1383
|
+
}
|
|
1218
1384
|
async function preverifyOffer(opts) {
|
|
1219
1385
|
const id = String(opts.offerId).toLowerCase();
|
|
1220
1386
|
if (!HASH.test(id)) return { ok: false, trust: "unverified", reason: "invalid offer id" };
|
|
1387
|
+
const fill = fillInputs(opts);
|
|
1388
|
+
if (typeof fill === "string") return { ok: false, trust: "unverified", reason: fill };
|
|
1221
1389
|
let incl;
|
|
1222
1390
|
try {
|
|
1223
1391
|
incl = await opts.light.verifyTxInclusion(id);
|
|
@@ -1259,52 +1427,17 @@ async function preverifyOffer(opts) {
|
|
|
1259
1427
|
const w = rec.want ?? {};
|
|
1260
1428
|
const payto = w.payto && ADDR.test(String(w.payto).toLowerCase()) ? String(w.payto).toLowerCase() : seller;
|
|
1261
1429
|
const terms = (0, import_cairnx_core2.provenOfferTerms)(offerRec, blockHeight);
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
let planBound = false;
|
|
1266
|
-
const has = (v) => v !== void 0 && v !== null;
|
|
1267
|
-
const servedStatus = opts.servedOffer?.status;
|
|
1268
|
-
if (!has(opts.pay)) planReason = "no `pay` was given, so no output plan was sized";
|
|
1269
|
-
else if ((0, import_cairnx_core2.isTokenWant)(offerRec.want)) planReason = "the offer is token-priced: the payment is a token transfer, not CSD outputs";
|
|
1270
|
-
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)";
|
|
1271
|
-
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)";
|
|
1272
|
-
else {
|
|
1273
|
-
const provenState = {
|
|
1274
|
-
id,
|
|
1275
|
-
seller,
|
|
1276
|
-
give: offerRec.give,
|
|
1277
|
-
want: { ...offerRec.want, payto },
|
|
1278
|
-
taker: offerRec.taker,
|
|
1279
|
-
bid: offerRec.bid,
|
|
1280
|
-
status: servedStatus,
|
|
1281
|
-
expiresEpoch: 0,
|
|
1282
|
-
height: blockHeight,
|
|
1283
|
-
feeBps: terms.feeBps
|
|
1284
|
-
};
|
|
1285
|
-
try {
|
|
1286
|
-
const plan = (0, import_cairnx_core2.fillOutputPlan)(provenState, opts.pay);
|
|
1287
|
-
if (plan.kind === "csd-outputs") {
|
|
1288
|
-
outputPlan = plan.outputs;
|
|
1289
|
-
if (has(opts.plannedOutputs)) {
|
|
1290
|
-
sumsMismatch = bindPlannedSums(opts.plannedOutputs, plan.outputs);
|
|
1291
|
-
planBound = true;
|
|
1292
|
-
}
|
|
1293
|
-
} else if (plan.kind === "undeliverable") {
|
|
1294
|
-
sumsMismatch = `this pay would not deliver against the proven offer (${plan.reason}) - refusing (the CSD would be lost)`;
|
|
1295
|
-
} else {
|
|
1296
|
-
planReason = `the proven offer settles without CSD outputs (${plan.kind})`;
|
|
1297
|
-
}
|
|
1298
|
-
} catch (e) {
|
|
1299
|
-
sumsMismatch = `couldn't size the fill against the proven offer (${e?.message ?? e}) - refusing rather than sizing from served fields`;
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1430
|
+
const expiresEpoch = Number(ee);
|
|
1431
|
+
const expiresAt = (0, import_cairnx_core2.offerExpiryHeightOf)(expiresEpoch, blockHeight);
|
|
1432
|
+
const j = judgeFill(offerRec, { id, seller, payto, terms, expiresEpoch, expiresAt }, opts.servedOffer, fill);
|
|
1302
1433
|
const planFields = {
|
|
1303
|
-
...outputPlan ? { outputPlan } : {},
|
|
1304
|
-
...has(opts.
|
|
1305
|
-
|
|
1434
|
+
...j.outputPlan ? { outputPlan: j.outputPlan } : {},
|
|
1435
|
+
...has(opts.pay) ? { sized: j.outputPlan !== void 0 } : {},
|
|
1436
|
+
...has(opts.plannedOutputs) ? { planBound: j.planBound } : {},
|
|
1437
|
+
...!j.outputPlan && j.planReason && (has(opts.pay) || has(opts.plannedOutputs)) ? { planReason: j.planReason } : {},
|
|
1438
|
+
...j.endorsement ? { endorsement: j.endorsement, fillTarget: j.fillTarget } : {}
|
|
1306
1439
|
};
|
|
1307
|
-
const base = { payto, seller, terms, blockHeight, ...planFields };
|
|
1440
|
+
const base = { payto, seller, terms, blockHeight, expiresEpoch, expiresAt, ...planFields };
|
|
1308
1441
|
if (opts.servedOffer !== void 0 && opts.servedOffer !== null) {
|
|
1309
1442
|
const so = opts.servedOffer;
|
|
1310
1443
|
if (so?.want?.payto === void 0 || so?.want?.payto === null)
|
|
@@ -1318,7 +1451,7 @@ async function preverifyOffer(opts) {
|
|
|
1318
1451
|
if ((0, import_cairnx_core2.bindOfferTerms)(opts.servedOffer, terms, { give: true, wantType: true }))
|
|
1319
1452
|
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 };
|
|
1320
1453
|
}
|
|
1321
|
-
if (
|
|
1454
|
+
if (j.refusal) return { ok: false, trust: "verified", reason: j.refusal, ...base };
|
|
1322
1455
|
return { ok: true, trust: "verified", ...base };
|
|
1323
1456
|
}
|
|
1324
1457
|
|
|
@@ -1443,6 +1576,20 @@ var spvBackoffMs = (attempt) => Math.min(4e3, 700 * (attempt + 1));
|
|
|
1443
1576
|
var SPV_RETRY_WAIT_BUDGET_MS = 16e3;
|
|
1444
1577
|
var SPV_RECHECK_DEPTH = 12;
|
|
1445
1578
|
var spvTipOf = (lc) => lc.baseHeight + lc.chain.length - 1;
|
|
1579
|
+
var SPV_BEYOND_TIP = /^tx at \d+ is \d+ blocks beyond tip; sync\(\d+\) first$/;
|
|
1580
|
+
var SpvLightClient = class extends import_csd_light.LightClient {
|
|
1581
|
+
syncInFlight = null;
|
|
1582
|
+
async sync(to, from) {
|
|
1583
|
+
while (this.syncInFlight) await this.syncInFlight;
|
|
1584
|
+
const run = super.sync(to, from);
|
|
1585
|
+
this.syncInFlight = run;
|
|
1586
|
+
try {
|
|
1587
|
+
return await run;
|
|
1588
|
+
} finally {
|
|
1589
|
+
if (this.syncInFlight === run) this.syncInFlight = null;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1446
1593
|
var Cairn = class {
|
|
1447
1594
|
config;
|
|
1448
1595
|
chain;
|
|
@@ -1458,6 +1605,9 @@ var Cairn = class {
|
|
|
1458
1605
|
// syncFromCheckpoint calls onto the same client (the loser would hit "seedTrusted must be called on a
|
|
1459
1606
|
// fresh client"). Cleared when the seed settles; a failure leaves no cached state, so the next call retries.
|
|
1460
1607
|
_spvSeedInflight = null;
|
|
1608
|
+
// The in-flight reseed of withSpvReseed, shared by callers whose structural failures overlap. Cleared when it
|
|
1609
|
+
// settles; each caller still commits the fresh client only after its own retry succeeds.
|
|
1610
|
+
_spvReseedInflight = null;
|
|
1461
1611
|
_spvCp;
|
|
1462
1612
|
// W12: the cairn base the SPV header-batch provider fetches /api/headers from (same base the rest of the SDK uses).
|
|
1463
1613
|
_cairnBase;
|
|
@@ -1513,14 +1663,18 @@ var Cairn = class {
|
|
|
1513
1663
|
// (rate limit / gateway / timeout / non-dense / CF 200-HTML) keeps the cache and rethrows: wiping
|
|
1514
1664
|
// on those turns one /api/headers 429 into a reseed storm (DOS-HDR-3). A second structural
|
|
1515
1665
|
// failure propagates fail-closed; there is no loop. recheckCachedHeader feeds this path: a near-tip
|
|
1516
|
-
// cached header that the served chain no longer carries is a structural failure.
|
|
1666
|
+
// cached header that the served chain no longer carries is a structural failure. Callers whose failures overlap
|
|
1667
|
+
// (waiters on one shared sync) share one reseed.
|
|
1517
1668
|
async withSpvReseed(op) {
|
|
1518
1669
|
const lc = await this.seededSpvLight();
|
|
1519
1670
|
try {
|
|
1520
1671
|
return await op(lc);
|
|
1521
1672
|
} catch (e) {
|
|
1522
1673
|
if (spvIsTransient(e)) throw e;
|
|
1523
|
-
|
|
1674
|
+
this._spvReseedInflight ??= this.seedFreshSpvLight().finally(() => {
|
|
1675
|
+
this._spvReseedInflight = null;
|
|
1676
|
+
});
|
|
1677
|
+
const fresh = await this._spvReseedInflight;
|
|
1524
1678
|
const out = await op(fresh);
|
|
1525
1679
|
this._spvLight = fresh;
|
|
1526
1680
|
return out;
|
|
@@ -1540,7 +1694,7 @@ var Cairn = class {
|
|
|
1540
1694
|
// the caller commits the result only on success (MF-14; the wallet G8 posture - never null or
|
|
1541
1695
|
// dirty the shared field while an attempt is in flight).
|
|
1542
1696
|
async seedFreshSpvLight() {
|
|
1543
|
-
const lc = new
|
|
1697
|
+
const lc = new SpvLightClient({
|
|
1544
1698
|
client: this.chain.client,
|
|
1545
1699
|
checkpoints: { [this._spvCp.height]: this._spvCp.hash },
|
|
1546
1700
|
headersBatchProvider: (from, count) => this.fetchHeadersBatch(from, count)
|
|
@@ -1582,11 +1736,7 @@ var Cairn = class {
|
|
|
1582
1736
|
* data-availability hiccup; back off (honouring Retry-After) a bounded number of times, then THROW so
|
|
1583
1737
|
* `sync()` fails closed. A non-dense range is rejected terminally (never retried).
|
|
1584
1738
|
*
|
|
1585
|
-
*
|
|
1586
|
-
* mounted on `/explorer/api`, `/trade/api` and `/api/rpc`, not here). A SAME-ORIGIN dApp (hosted on the
|
|
1587
|
-
* cairn base) reaches it now; a CROSS-ORIGIN third-party dApp cannot until B7d adds the CORS mount and
|
|
1588
|
-
* restarts the cairn service. This code is correct as written; it becomes reachable cross-origin only
|
|
1589
|
-
* once B7d ships. This is a recorded dependency, not a blocker for authoring/testing.
|
|
1739
|
+
* Cross-origin reach depends on the server's CORS mount on `/api/headers` (the cairn server mounts one).
|
|
1590
1740
|
*/
|
|
1591
1741
|
async fetchHeadersBatch(from, count) {
|
|
1592
1742
|
const g = this.config.fetch ?? globalThis.fetch;
|
|
@@ -1634,7 +1784,8 @@ var Cairn = class {
|
|
|
1634
1784
|
* commitment, derives the payment recipient + seller from the offer's on-chain AUTHOR (the funding input's
|
|
1635
1785
|
* prevout owner, txid-committed, NOT the malleable scriptSig), and, if you pass the resolver-SERVED `offer`
|
|
1636
1786
|
* object, binds its payto/seller + fee/rebate/partial terms to the proven ones. Returns a TRUST-LABELED result
|
|
1637
|
-
* (fail-closed on a positive mismatch
|
|
1787
|
+
* (fail-closed on a positive mismatch; `transient` on a lagging chain view; final, naming the anchor, for an
|
|
1788
|
+
* offer mined below the SPV checkpoint).
|
|
1638
1789
|
*
|
|
1639
1790
|
* This is best-effort corroboration, NOT the payment-grade boundary: the Cairn Wallet's OWN on-device
|
|
1640
1791
|
* fill-SPV (0.2.60+, fails-closed before signing) is that. A dApp settling real value should still clear this
|
|
@@ -1642,19 +1793,44 @@ var Cairn = class {
|
|
|
1642
1793
|
*
|
|
1643
1794
|
* Pass opts.pay (the CSD base units you intend to pay) to receive the PROVEN outputPlan for a whole fill of a
|
|
1644
1795
|
* non-partial offer, and opts.plannedOutputs to bind your planned per-address sums to it before signing; build
|
|
1645
|
-
* EXACTLY the outputPlan, never resolver-served outputs.
|
|
1796
|
+
* EXACTLY the outputPlan, never resolver-served outputs. Pass opts.me (your address) and opts.tip (the chain tip
|
|
1797
|
+
* height) with pay to have cairnx-core's fillEndorsement and offerFillTooLate judge the fill for you at that tip; without them `ok`
|
|
1798
|
+
* says the offer record is proven, not that you can fill it now.
|
|
1646
1799
|
*/
|
|
1647
1800
|
async verifyOfferForFill(offerId, servedOffer, opts) {
|
|
1648
1801
|
const light = {
|
|
1649
1802
|
verifyTxInclusion: (txidHex) => this.withSpvReseed(async (lc) => {
|
|
1650
1803
|
const haveTo = spvTipOf(lc);
|
|
1651
|
-
|
|
1652
|
-
if (r.trustLevel === "
|
|
1653
|
-
|
|
1654
|
-
|
|
1804
|
+
let r = await lc.verifyTxInclusion(txidHex);
|
|
1805
|
+
if (r.trustLevel === "rpc-trusted" && SPV_BEYOND_TIP.test(r.reason ?? "")) {
|
|
1806
|
+
let tipHeight;
|
|
1807
|
+
try {
|
|
1808
|
+
tipHeight = Number((await this.chain.tip()).height);
|
|
1809
|
+
} catch (e) {
|
|
1810
|
+
return { ...r, reason: `${r.reason} (the node tip read failed: ${e?.message ?? e})` };
|
|
1811
|
+
}
|
|
1812
|
+
if (Number.isSafeInteger(tipHeight) && tipHeight > spvTipOf(lc)) await lc.sync(tipHeight);
|
|
1813
|
+
r = await lc.verifyTxInclusion(txidHex);
|
|
1814
|
+
}
|
|
1815
|
+
if (r.trustLevel !== "verified-inclusion") return r;
|
|
1816
|
+
const height = Number(r.blockHeight);
|
|
1817
|
+
if (!Number.isSafeInteger(height))
|
|
1818
|
+
return { trustLevel: "rpc-trusted", included: false, reason: `the node's containing height for this tx is not a block height (${String(r.blockHeight)})` };
|
|
1819
|
+
if (height <= haveTo) await this.recheckCachedHeader(lc, height, haveTo);
|
|
1820
|
+
return { ...r, blockHeight: height };
|
|
1655
1821
|
})
|
|
1656
1822
|
};
|
|
1657
|
-
return preverifyOffer({
|
|
1823
|
+
return preverifyOffer({
|
|
1824
|
+
light,
|
|
1825
|
+
client: this.chain.client,
|
|
1826
|
+
offerId,
|
|
1827
|
+
servedOffer,
|
|
1828
|
+
pay: opts?.pay,
|
|
1829
|
+
plannedOutputs: opts?.plannedOutputs,
|
|
1830
|
+
me: opts?.me,
|
|
1831
|
+
tip: opts?.tip,
|
|
1832
|
+
fillTargetId: opts?.fillTargetId
|
|
1833
|
+
});
|
|
1658
1834
|
}
|
|
1659
1835
|
/**
|
|
1660
1836
|
* Detect + connect the Cairn Wallet (browser only) and attach it as `cairn.wallet`.
|