@inversealtruism/cairn-sdk 0.2.2 → 0.3.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.
@@ -25,7 +25,7 @@ __export(controller_exports, {
25
25
  module.exports = __toCommonJS(controller_exports);
26
26
 
27
27
  // src/errors.ts
28
- var SDK_VERSION = "0.2.2";
28
+ var SDK_VERSION = "0.3.0";
29
29
  var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
30
30
  var CairnError = class extends Error {
31
31
  /** Stable machine code for programmatic branching. */
@@ -36,6 +36,12 @@ var CairnError = class extends Error {
36
36
  docsPath;
37
37
  /** The cairn-sdk version that produced this error. */
38
38
  version = SDK_VERSION;
39
+ /**
40
+ * Whether a dApp may safely AUTO-retry this failure. `false` for fund-safety refusals and user-intent
41
+ * declines (the default); `true` only for provably transient infra faults where nothing was signed.
42
+ * A `SubmitInFlightError` is `false` too: reconcile its `txid`, never blind-retry.
43
+ */
44
+ retryable;
39
45
  constructor(shortMessage, opts = {}) {
40
46
  const docs = opts.docsPath;
41
47
  super(
@@ -46,6 +52,7 @@ var CairnError = class extends Error {
46
52
  this.code = opts.code ?? "UNKNOWN";
47
53
  this.shortMessage = shortMessage;
48
54
  this.docsPath = docs;
55
+ this.retryable = opts.retryable ?? false;
49
56
  }
50
57
  /**
51
58
  * Walk the `cause` chain. With a predicate, returns the FIRST matching error (or null). Without one,
@@ -82,11 +89,43 @@ var UnsupportedMethodError = class extends CairnError {
82
89
  super(message, { code: "UNSUPPORTED_METHOD", docsPath: "/errors#unsupported-method" });
83
90
  }
84
91
  };
92
+ var SubmitInFlightError = class extends CairnError {
93
+ /** The locally-computed consensus txid to reconcile against the chain (may be undefined if the wallet omitted it). */
94
+ txid;
95
+ /** Always true: the tx MAY be in the mempool. Reconcile via `txid`; never blind-retry. */
96
+ maybeSent = true;
97
+ constructor(message, code, txid) {
98
+ super(message, { code, docsPath: "/errors#submit-inflight", retryable: false });
99
+ this.txid = txid;
100
+ }
101
+ };
85
102
  var NATIVE_CODES = /* @__PURE__ */ new Set(["ACCOUNT_CHANGED", "FIRST_PARTY_ONLY", "RATE_LIMITED", "APPROVAL_CLOSED", "FORBIDDEN", "INTERNAL"]);
103
+ var NESTED_TRANSIENT = /* @__PURE__ */ new Set(["VERIFY_UNAVAILABLE", "OFFER_UNKNOWN"]);
104
+ var NESTED_TERMINAL = /* @__PURE__ */ new Set([
105
+ "FILL_UNSAFE",
106
+ "FILL_WRONG_TARGET",
107
+ "SOURCE_DIVERGENCE",
108
+ "VERIFY_TAMPER",
109
+ "SUBMIT_REJECTED",
110
+ "BAD_REQUEST",
111
+ "BAD_FEE",
112
+ "BAD_OUTPUTS",
113
+ "NO_OUTPUTS",
114
+ "ZERO_ADDR_REFUSED",
115
+ "TOO_MANY_INPUTS",
116
+ "INSUFFICIENT",
117
+ "FEE_CAP",
118
+ "FEE_TOO_LOW",
119
+ "ACCOUNT_CHANGED",
120
+ "GHOST_INPUTS_SKIPPED",
121
+ "NOTHING_TO_CONSOLIDATE"
122
+ ]);
86
123
  function mapProviderError(error, code) {
87
124
  if (code === "USER_REJECTED" || code === "APPROVAL_CLOSED") return new UserRejectedError(error);
88
125
  if (code === "WALLET_LOCKED") return new WalletLockedError(error);
89
126
  if (code === "UNSUPPORTED_METHOD" || code === "UNKNOWN_KIND") return new UnsupportedMethodError(error);
127
+ if (code && NESTED_TRANSIENT.has(code)) return new CairnError(error || code, { code, retryable: true });
128
+ if (code && NESTED_TERMINAL.has(code)) return new CairnError(error || code, { code, retryable: false });
90
129
  if (code && NATIVE_CODES.has(code)) return new CairnError(error || code, { code });
91
130
  const e = String(error || "").toLowerCase();
92
131
  if (e.includes("rejected by user")) return new UserRejectedError(error);
@@ -94,6 +133,14 @@ function mapProviderError(error, code) {
94
133
  if (e.includes("unsupported dapp method") || e.includes("predates") || e.includes("too old")) return new UnsupportedMethodError(error);
95
134
  return new CairnError(error || "Unknown wallet error");
96
135
  }
136
+ function mapSubmitResultError(result) {
137
+ const code = result?.code;
138
+ const msg = typeof result?.error === "string" && result.error || code || "The wallet refused the transaction.";
139
+ if (code === "SUBMIT_MAYBE_INFLIGHT" || code === "SUBMIT_DUPLICATE") {
140
+ return new SubmitInFlightError(msg, code, typeof result?.txid === "string" ? result.txid : void 0);
141
+ }
142
+ return mapProviderError(msg, code);
143
+ }
97
144
 
98
145
  // src/connect.ts
99
146
  function detectProvider(opts = {}) {
@@ -142,6 +189,13 @@ function unwrap(reply) {
142
189
  if (reply && reply.ok) return reply.result;
143
190
  throw mapProviderError(reply?.error ?? "Unknown wallet error", reply?.code);
144
191
  }
192
+ function unwrapWrite(reply) {
193
+ const result = unwrap(reply);
194
+ if (result && typeof result === "object" && result.ok === false) {
195
+ throw mapSubmitResultError(result);
196
+ }
197
+ return result;
198
+ }
145
199
  var WalletConnection = class {
146
200
  provider;
147
201
  _addr = null;
@@ -208,12 +262,18 @@ var WalletConnection = class {
208
262
  return null;
209
263
  }
210
264
  }
211
- /** Atomic fill (CairnX DvP): pay + attest in ONE tx. Always prompts with clear-signing. */
265
+ /**
266
+ * CairnX fill: pay + attest in ONE tx (atomic SETTLEMENT SHAPE, both revert together). Always prompts with
267
+ * clear-signing. The payment recipient in `params.outputs` is RESOLVER-TRUSTED here: this SDK does NOT
268
+ * SPV-verify it, so a lying resolver can redirect the payment. A dApp settling real value MUST corroborate
269
+ * the offer on-chain before building `outputs` (F13). The wallet's own fill-SPV boundary (cairn-wallet
270
+ * 0.2.60+, which binds payment/rebate/fee to the merkle-proven offer) is the payment-grade check; see FillParams.
271
+ */
212
272
  fillOffer(params) {
213
273
  if (typeof this.provider.fillOffer !== "function") {
214
274
  return Promise.reject(new UnsupportedMethodError("this wallet predates fillOffer() \u2014 ask the user to update the Cairn Wallet"));
215
275
  }
216
- return Promise.resolve(this.provider.fillOffer(params)).then(unwrap);
276
+ return Promise.resolve(this.provider.fillOffer(params)).then(unwrapWrite);
217
277
  }
218
278
  /** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
219
279
  async getPermissions() {
@@ -246,26 +306,26 @@ var WalletConnection = class {
246
306
  }
247
307
  /** Send CSD (always prompts with clear-signing; wallet picks inputs + returns change to itself). */
248
308
  send(params) {
249
- return Promise.resolve(this.provider.send(params)).then(unwrap);
309
+ return Promise.resolve(this.provider.send(params)).then(unwrapWrite);
250
310
  }
251
311
  /** Low-level Propose (always prompts). Prefer `cairn.board.propose()` which computes the hash/uri/expiry. */
252
312
  propose(params) {
253
- return Promise.resolve(this.provider.propose(params)).then(unwrap);
313
+ return Promise.resolve(this.provider.propose(params)).then(unwrapWrite);
254
314
  }
255
315
  /** Attest/support a proposal (always prompts). */
256
316
  attest(params) {
257
- return Promise.resolve(this.provider.attest(params)).then(unwrap);
317
+ return Promise.resolve(this.provider.attest(params)).then(unwrapWrite);
258
318
  }
259
319
  /** Seal a commit-reveal claim (always prompts; salt stays inside the wallet). */
260
320
  sealClaim(params) {
261
- return Promise.resolve(this.provider.sealClaim(params)).then(unwrap);
321
+ return Promise.resolve(this.provider.sealClaim(params)).then(unwrapWrite);
262
322
  }
263
323
  /** Reveal a previously sealed claim by its sealing txid (always prompts). */
264
324
  revealClaim(txid) {
265
325
  if (typeof txid !== "string" || !txid) {
266
326
  return Promise.reject(new UnsupportedMethodError("revealClaim requires the sealing txid"));
267
327
  }
268
- return Promise.resolve(this.provider.revealClaim(txid)).then(unwrap);
328
+ return Promise.resolve(this.provider.revealClaim(txid)).then(unwrapWrite);
269
329
  }
270
330
  };
271
331
  async function getWallet(opts = {}) {
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  CairnController
3
- } from "./chunk-LCE4IWMW.js";
4
- import "./chunk-QRVMYGSL.js";
5
- import "./chunk-ZJURNM5P.js";
3
+ } from "./chunk-UMXLWUEF.js";
4
+ import "./chunk-QV2GIXO7.js";
5
+ import "./chunk-E2Z3XSCW.js";
6
6
  export {
7
7
  CairnController
8
8
  };
package/dist/index.cjs CHANGED
@@ -35,22 +35,27 @@ __export(index_exports, {
35
35
  NotInstalledError: () => NotInstalledError,
36
36
  RegistryClient: () => RegistryClient,
37
37
  SDK_VERSION: () => SDK_VERSION,
38
+ SubmitInFlightError: () => SubmitInFlightError,
38
39
  UnsupportedMethodError: () => UnsupportedMethodError,
39
40
  UserRejectedError: () => UserRejectedError,
40
41
  WalletConnection: () => WalletConnection,
41
42
  WalletLockedError: () => WalletLockedError,
43
+ bindOfferTerms: () => bindOfferTerms,
42
44
  connect: () => connect,
43
45
  detectProvider: () => detectProvider,
44
46
  discoverProviders: () => discoverProviders,
45
47
  errorCode: () => errorCode,
48
+ feeBpsAt: () => feeBpsAt,
46
49
  getWallet: () => getWallet,
47
50
  isInstalled: () => isInstalled,
48
- mapProviderError: () => mapProviderError
51
+ mapProviderError: () => mapProviderError,
52
+ mapSubmitResultError: () => mapSubmitResultError,
53
+ preverifyOffer: () => preverifyOffer
49
54
  });
50
55
  module.exports = __toCommonJS(index_exports);
51
56
 
52
57
  // src/errors.ts
53
- var SDK_VERSION = "0.2.2";
58
+ var SDK_VERSION = "0.3.0";
54
59
  var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
55
60
  var CairnError = class extends Error {
56
61
  /** Stable machine code for programmatic branching. */
@@ -61,6 +66,12 @@ var CairnError = class extends Error {
61
66
  docsPath;
62
67
  /** The cairn-sdk version that produced this error. */
63
68
  version = SDK_VERSION;
69
+ /**
70
+ * Whether a dApp may safely AUTO-retry this failure. `false` for fund-safety refusals and user-intent
71
+ * declines (the default); `true` only for provably transient infra faults where nothing was signed.
72
+ * A `SubmitInFlightError` is `false` too: reconcile its `txid`, never blind-retry.
73
+ */
74
+ retryable;
64
75
  constructor(shortMessage, opts = {}) {
65
76
  const docs = opts.docsPath;
66
77
  super(
@@ -71,6 +82,7 @@ var CairnError = class extends Error {
71
82
  this.code = opts.code ?? "UNKNOWN";
72
83
  this.shortMessage = shortMessage;
73
84
  this.docsPath = docs;
85
+ this.retryable = opts.retryable ?? false;
74
86
  }
75
87
  /**
76
88
  * Walk the `cause` chain. With a predicate, returns the FIRST matching error (or null). Without one,
@@ -125,11 +137,43 @@ var ContentVerificationError = class extends CairnError {
125
137
  this.expected = expected;
126
138
  }
127
139
  };
140
+ var SubmitInFlightError = class extends CairnError {
141
+ /** The locally-computed consensus txid to reconcile against the chain (may be undefined if the wallet omitted it). */
142
+ txid;
143
+ /** Always true: the tx MAY be in the mempool. Reconcile via `txid`; never blind-retry. */
144
+ maybeSent = true;
145
+ constructor(message, code, txid3) {
146
+ super(message, { code, docsPath: "/errors#submit-inflight", retryable: false });
147
+ this.txid = txid3;
148
+ }
149
+ };
128
150
  var NATIVE_CODES = /* @__PURE__ */ new Set(["ACCOUNT_CHANGED", "FIRST_PARTY_ONLY", "RATE_LIMITED", "APPROVAL_CLOSED", "FORBIDDEN", "INTERNAL"]);
151
+ var NESTED_TRANSIENT = /* @__PURE__ */ new Set(["VERIFY_UNAVAILABLE", "OFFER_UNKNOWN"]);
152
+ var NESTED_TERMINAL = /* @__PURE__ */ new Set([
153
+ "FILL_UNSAFE",
154
+ "FILL_WRONG_TARGET",
155
+ "SOURCE_DIVERGENCE",
156
+ "VERIFY_TAMPER",
157
+ "SUBMIT_REJECTED",
158
+ "BAD_REQUEST",
159
+ "BAD_FEE",
160
+ "BAD_OUTPUTS",
161
+ "NO_OUTPUTS",
162
+ "ZERO_ADDR_REFUSED",
163
+ "TOO_MANY_INPUTS",
164
+ "INSUFFICIENT",
165
+ "FEE_CAP",
166
+ "FEE_TOO_LOW",
167
+ "ACCOUNT_CHANGED",
168
+ "GHOST_INPUTS_SKIPPED",
169
+ "NOTHING_TO_CONSOLIDATE"
170
+ ]);
129
171
  function mapProviderError(error, code) {
130
172
  if (code === "USER_REJECTED" || code === "APPROVAL_CLOSED") return new UserRejectedError(error);
131
173
  if (code === "WALLET_LOCKED") return new WalletLockedError(error);
132
174
  if (code === "UNSUPPORTED_METHOD" || code === "UNKNOWN_KIND") return new UnsupportedMethodError(error);
175
+ if (code && NESTED_TRANSIENT.has(code)) return new CairnError(error || code, { code, retryable: true });
176
+ if (code && NESTED_TERMINAL.has(code)) return new CairnError(error || code, { code, retryable: false });
133
177
  if (code && NATIVE_CODES.has(code)) return new CairnError(error || code, { code });
134
178
  const e = String(error || "").toLowerCase();
135
179
  if (e.includes("rejected by user")) return new UserRejectedError(error);
@@ -137,6 +181,14 @@ function mapProviderError(error, code) {
137
181
  if (e.includes("unsupported dapp method") || e.includes("predates") || e.includes("too old")) return new UnsupportedMethodError(error);
138
182
  return new CairnError(error || "Unknown wallet error");
139
183
  }
184
+ function mapSubmitResultError(result) {
185
+ const code = result?.code;
186
+ const msg = typeof result?.error === "string" && result.error || code || "The wallet refused the transaction.";
187
+ if (code === "SUBMIT_MAYBE_INFLIGHT" || code === "SUBMIT_DUPLICATE") {
188
+ return new SubmitInFlightError(msg, code, typeof result?.txid === "string" ? result.txid : void 0);
189
+ }
190
+ return mapProviderError(msg, code);
191
+ }
140
192
  function errorCode(err) {
141
193
  return err instanceof CairnError ? err.code : "UNKNOWN";
142
194
  }
@@ -442,23 +494,23 @@ var BoardClient = class {
442
494
  const expiresEpoch = Math.floor(height / import_csd_codec3.EPOCH_LEN) + lifetime;
443
495
  const fee = input.fee ?? import_csd_codec3.MIN_FEE_PROPOSE;
444
496
  const r = await wallet.propose({ domain: content.domain, payloadHash: ph, uri, expiresEpoch, fee });
445
- const txid2 = String(r.txid ?? "");
446
- if (!txid2) throw new CairnError("Propose submitted but no txid was returned by the wallet.");
447
- const reg = await this.tryRegister(content, txid2);
448
- return { txid: txid2, payloadHash: ph, content, registered: reg.ok, registerError: reg.error };
497
+ const txid3 = String(r.txid ?? "");
498
+ if (!txid3) throw new CairnError("Propose submitted but no txid was returned by the wallet.");
499
+ const reg = await this.tryRegister(content, txid3);
500
+ return { txid: txid3, payloadHash: ph, content, registered: reg.ok, registerError: reg.error };
449
501
  }
450
502
  /**
451
503
  * Register off-chain content for an already-submitted proposal txid. Idempotent.
452
504
  * Returns { ok } — the cairn server verifies sha256(canonical) == on-chain payload_hash.
453
505
  */
454
- async registerContent(content, txid2) {
506
+ async registerContent(content, txid3) {
455
507
  try {
456
508
  const res = await this.http.postJson("/api/content", {
457
509
  domain: content.domain,
458
510
  title: content.title,
459
511
  body: content.body,
460
512
  links: content.links,
461
- txid: txid2
513
+ txid: txid3
462
514
  });
463
515
  return { ok: !!res.ok, error: res.ok ? void 0 : res.error };
464
516
  } catch (e) {
@@ -482,12 +534,12 @@ var BoardClient = class {
482
534
  if (!Number.isFinite(h)) throw new CairnError("Could not read chain tip height for expiry computation.");
483
535
  return h;
484
536
  }
485
- async tryRegister(content, txid2) {
537
+ async tryRegister(content, txid3) {
486
538
  const delays = [0, 1500, 3e3, 5e3];
487
539
  let last = { ok: false, error: "not attempted" };
488
540
  for (const d of delays) {
489
541
  if (d) await sleep(d);
490
- last = await this.registerContent(content, txid2);
542
+ last = await this.registerContent(content, txid3);
491
543
  if (last.ok) return last;
492
544
  if (!/not found yet|not found/i.test(last.error ?? "")) return last;
493
545
  }
@@ -594,10 +646,10 @@ var IndexerClient = class {
594
646
  * same-origin RPC safe is the forward PoW re-verification, not an origin gate (which no longer exists).
595
647
  * Fails closed: a malformed proof or a chain-disagreement returns "not-found" (the latter with equivocation:true).
596
648
  */
597
- async verifyInclusion(txid2) {
649
+ async verifyInclusion(txid3) {
598
650
  let proof;
599
651
  try {
600
- proof = await this.txMerkleProof(txid2);
652
+ proof = await this.txMerkleProof(txid3);
601
653
  } catch {
602
654
  return { included: false, trustLevel: "not-found", reason: "no merkle proof (tx not indexed)" };
603
655
  }
@@ -606,7 +658,7 @@ var IndexerClient = class {
606
658
  }
607
659
  let folds = false;
608
660
  try {
609
- folds = (0, import_csd_codec4.verifyMerkleProof)(txid2, proof.pos, proof.merkle, proof.merkle_root);
661
+ folds = (0, import_csd_codec4.verifyMerkleProof)(txid3, proof.pos, proof.merkle, proof.merkle_root);
610
662
  } catch {
611
663
  return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "malformed merkle proof" };
612
664
  }
@@ -1031,6 +1083,13 @@ function unwrap(reply) {
1031
1083
  if (reply && reply.ok) return reply.result;
1032
1084
  throw mapProviderError(reply?.error ?? "Unknown wallet error", reply?.code);
1033
1085
  }
1086
+ function unwrapWrite(reply) {
1087
+ const result = unwrap(reply);
1088
+ if (result && typeof result === "object" && result.ok === false) {
1089
+ throw mapSubmitResultError(result);
1090
+ }
1091
+ return result;
1092
+ }
1034
1093
  var WalletConnection = class {
1035
1094
  provider;
1036
1095
  _addr = null;
@@ -1097,12 +1156,18 @@ var WalletConnection = class {
1097
1156
  return null;
1098
1157
  }
1099
1158
  }
1100
- /** Atomic fill (CairnX DvP): pay + attest in ONE tx. Always prompts with clear-signing. */
1159
+ /**
1160
+ * CairnX fill: pay + attest in ONE tx (atomic SETTLEMENT SHAPE, both revert together). Always prompts with
1161
+ * clear-signing. The payment recipient in `params.outputs` is RESOLVER-TRUSTED here: this SDK does NOT
1162
+ * SPV-verify it, so a lying resolver can redirect the payment. A dApp settling real value MUST corroborate
1163
+ * the offer on-chain before building `outputs` (F13). The wallet's own fill-SPV boundary (cairn-wallet
1164
+ * 0.2.60+, which binds payment/rebate/fee to the merkle-proven offer) is the payment-grade check; see FillParams.
1165
+ */
1101
1166
  fillOffer(params) {
1102
1167
  if (typeof this.provider.fillOffer !== "function") {
1103
1168
  return Promise.reject(new UnsupportedMethodError("this wallet predates fillOffer() \u2014 ask the user to update the Cairn Wallet"));
1104
1169
  }
1105
- return Promise.resolve(this.provider.fillOffer(params)).then(unwrap);
1170
+ return Promise.resolve(this.provider.fillOffer(params)).then(unwrapWrite);
1106
1171
  }
1107
1172
  /** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
1108
1173
  async getPermissions() {
@@ -1135,26 +1200,26 @@ var WalletConnection = class {
1135
1200
  }
1136
1201
  /** Send CSD (always prompts with clear-signing; wallet picks inputs + returns change to itself). */
1137
1202
  send(params) {
1138
- return Promise.resolve(this.provider.send(params)).then(unwrap);
1203
+ return Promise.resolve(this.provider.send(params)).then(unwrapWrite);
1139
1204
  }
1140
1205
  /** Low-level Propose (always prompts). Prefer `cairn.board.propose()` which computes the hash/uri/expiry. */
1141
1206
  propose(params) {
1142
- return Promise.resolve(this.provider.propose(params)).then(unwrap);
1207
+ return Promise.resolve(this.provider.propose(params)).then(unwrapWrite);
1143
1208
  }
1144
1209
  /** Attest/support a proposal (always prompts). */
1145
1210
  attest(params) {
1146
- return Promise.resolve(this.provider.attest(params)).then(unwrap);
1211
+ return Promise.resolve(this.provider.attest(params)).then(unwrapWrite);
1147
1212
  }
1148
1213
  /** Seal a commit-reveal claim (always prompts; salt stays inside the wallet). */
1149
1214
  sealClaim(params) {
1150
- return Promise.resolve(this.provider.sealClaim(params)).then(unwrap);
1215
+ return Promise.resolve(this.provider.sealClaim(params)).then(unwrapWrite);
1151
1216
  }
1152
1217
  /** Reveal a previously sealed claim by its sealing txid (always prompts). */
1153
- revealClaim(txid2) {
1154
- if (typeof txid2 !== "string" || !txid2) {
1218
+ revealClaim(txid3) {
1219
+ if (typeof txid3 !== "string" || !txid3) {
1155
1220
  return Promise.reject(new UnsupportedMethodError("revealClaim requires the sealing txid"));
1156
1221
  }
1157
- return Promise.resolve(this.provider.revealClaim(txid2)).then(unwrap);
1222
+ return Promise.resolve(this.provider.revealClaim(txid3)).then(unwrapWrite);
1158
1223
  }
1159
1224
  };
1160
1225
  async function getWallet(opts = {}) {
@@ -1166,6 +1231,106 @@ async function connect(opts = {}) {
1166
1231
  return { wallet, address };
1167
1232
  }
1168
1233
 
1234
+ // src/fillverify.ts
1235
+ var import_csd_client3 = require("@inversealtruism/csd-client");
1236
+ var import_csd_codec6 = require("@inversealtruism/csd-codec");
1237
+ var import_cairnx_core2 = require("@inversealtruism/cairnx-core");
1238
+ var ADDR = /^0x[0-9a-f]{40}$/;
1239
+ var HASH = /^0x[0-9a-f]{64}$/;
1240
+ var COINBASE = "0x" + "00".repeat(32);
1241
+ var feeBpsAt = (height) => height >= import_cairnx_core2.V11_HEIGHT ? height >= import_cairnx_core2.V16_HEIGHT ? import_cairnx_core2.FEE_BPS_V16 : import_cairnx_core2.FEE_BPS : 0;
1242
+ function bindOfferTerms(servedOffer, t) {
1243
+ const o = servedOffer;
1244
+ const s = (v) => v === void 0 || v === null ? "" : String(v).toLowerCase();
1245
+ if (Number(o?.height) !== t.height) return true;
1246
+ if (Number(o?.feeBps) !== t.feeBps) return true;
1247
+ if (t.value !== void 0 && String(o?.want?.value) !== t.value) return true;
1248
+ if (s(o?.taker) !== s(t.taker)) return true;
1249
+ if (s(o?.bid) !== s(t.bid)) return true;
1250
+ const om = o?.min;
1251
+ if ((om !== void 0 && om !== null) !== (t.min !== void 0)) return true;
1252
+ if (t.min !== void 0 && String(om) !== t.min) return true;
1253
+ return false;
1254
+ }
1255
+ async function provenAuthor(client, provenTx) {
1256
+ const in0 = provenTx.inputs?.[0];
1257
+ if (!in0 || String(in0.prevTxid).toLowerCase() === COINBASE && Number(in0.vout) === 4294967295) return null;
1258
+ let info;
1259
+ try {
1260
+ info = await client.tx(String(in0.prevTxid));
1261
+ } catch {
1262
+ return null;
1263
+ }
1264
+ const body = info?.tx ?? info;
1265
+ if (!body || !Array.isArray(body.outputs) || !Array.isArray(body.inputs)) return null;
1266
+ let src;
1267
+ try {
1268
+ src = (0, import_csd_client3.rpcTxToTx)(body);
1269
+ } catch {
1270
+ return null;
1271
+ }
1272
+ if (String((0, import_csd_codec6.txid)(src)).toLowerCase() !== String(in0.prevTxid).toLowerCase()) return null;
1273
+ const out = src.outputs?.[Number(in0.vout)];
1274
+ const spk = out?.scriptPubkey ? String(out.scriptPubkey).toLowerCase() : null;
1275
+ return spk && ADDR.test(spk) ? spk : null;
1276
+ }
1277
+ async function preverifyOffer(opts) {
1278
+ const id = String(opts.offerId).toLowerCase();
1279
+ if (!HASH.test(id)) return { ok: false, trust: "unverified", reason: "invalid offer id" };
1280
+ let incl;
1281
+ try {
1282
+ incl = await opts.light.verifyTxInclusion(id);
1283
+ } catch (e) {
1284
+ return { ok: false, trust: "unverified", transient: true, reason: `couldn't reach the chain to verify the offer (${e?.message ?? e})` };
1285
+ }
1286
+ if (!incl.included || incl.trustLevel !== "verified-inclusion" || !incl.tx || !incl.appPayloadHash)
1287
+ return { ok: false, trust: "unverified", transient: true, blockHeight: incl.blockHeight, reason: incl.reason ?? `offer not merkle-proven on-chain (${incl.trustLevel})` };
1288
+ const blockHeight = Number(incl.blockHeight);
1289
+ let provenTx, provenId;
1290
+ try {
1291
+ provenTx = (0, import_csd_client3.rpcTxToTx)(incl.tx);
1292
+ provenId = (0, import_csd_codec6.txid)(provenTx);
1293
+ } catch {
1294
+ return { ok: false, trust: "unverified", reason: "the on-chain offer tx couldn't be re-derived" };
1295
+ }
1296
+ if (String(provenId).toLowerCase() !== id) return { ok: false, trust: "unverified", reason: "the merkle-proven tx doesn't match the offer id" };
1297
+ const app = incl.tx.app;
1298
+ if (!app || app.type !== "Propose") return { ok: false, trust: "unverified", reason: "the offer tx is not a Propose" };
1299
+ let rec;
1300
+ try {
1301
+ rec = (0, import_cairnx_core2.parseRecord)(app.uri, incl.appPayloadHash);
1302
+ } catch {
1303
+ rec = null;
1304
+ }
1305
+ if (!rec || rec.t !== "offer") return { ok: false, trust: "unverified", reason: "the offer record doesn't bind to its on-chain commitment" };
1306
+ if (String((0, import_csd_codec6.payloadHash)(rec)).toLowerCase() !== String(incl.appPayloadHash).toLowerCase())
1307
+ return { ok: false, trust: "unverified", reason: "the offer record doesn't match the on-chain (merkle-proven) commitment" };
1308
+ const seller = await provenAuthor(opts.client, provenTx);
1309
+ if (!seller) return { ok: false, trust: "verified", transient: true, blockHeight, reason: "couldn't prove the offer's on-chain author yet; the chain view may be catching up, try again" };
1310
+ const w = rec.want ?? {};
1311
+ const payto = w.payto && ADDR.test(String(w.payto).toLowerCase()) ? String(w.payto).toLowerCase() : seller;
1312
+ const terms = {
1313
+ height: blockHeight,
1314
+ feeBps: feeBpsAt(blockHeight),
1315
+ value: w.value !== void 0 ? String(w.value) : void 0,
1316
+ taker: rec.taker !== void 0 ? String(rec.taker).toLowerCase() : void 0,
1317
+ bid: rec.bid !== void 0 ? String(rec.bid).toLowerCase() : void 0,
1318
+ min: rec.min !== void 0 ? String(rec.min) : void 0
1319
+ };
1320
+ const base = { payto, seller, terms, blockHeight };
1321
+ if (opts.servedOffer !== void 0 && opts.servedOffer !== null) {
1322
+ const so = opts.servedOffer;
1323
+ const servedPayto = so?.want?.payto !== void 0 && so?.want?.payto !== null ? String(so.want.payto).toLowerCase() : "";
1324
+ if (servedPayto && servedPayto !== payto)
1325
+ return { ok: false, trust: "verified", reason: "the served payment recipient doesn't match the offer's on-chain author/record", ...base };
1326
+ if (so?.seller !== void 0 && so?.seller !== null && String(so.seller).toLowerCase() !== seller)
1327
+ return { ok: false, trust: "verified", reason: "the served seller doesn't match the offer's on-chain author (a lying resolver may be redirecting your payment)", ...base };
1328
+ if (bindOfferTerms(opts.servedOffer, terms))
1329
+ return { ok: false, trust: "verified", reason: "the served fee/rebate/partial terms don't match the offer's on-chain record (a lying resolver could mis-size the fill, causing a rejected fill after your payment)", ...base };
1330
+ }
1331
+ return { ok: true, trust: "verified", ...base };
1332
+ }
1333
+
1169
1334
  // src/controller.ts
1170
1335
  var DISCONNECTED = Object.freeze({ status: "disconnected", account: null, error: null });
1171
1336
  var CairnController = class {
@@ -1307,18 +1472,39 @@ var Cairn = class {
1307
1472
  async verifiedHeaderMerkleAt(height) {
1308
1473
  if (!Number.isInteger(height) || height < this._spvCp.height)
1309
1474
  throw new Error(`height ${height} is below the SPV checkpoint ${this._spvCp.height} (cannot PoW-verify backward)`);
1310
- if (!this._spvLight) this._spvLight = new import_csd_light.LightClient({ client: this.chain.client, checkpoints: { [this._spvCp.height]: this._spvCp.hash } });
1311
- const lc = this._spvLight;
1312
- if (!this._spvSeeded) {
1313
- await lc.syncFromCheckpoint(this._spvCp.height, this._spvCp.hash);
1314
- this._spvSeeded = true;
1315
- }
1475
+ const lc = await this.seededSpvLight();
1316
1476
  const haveTo = lc.baseHeight + lc.chain.length - 1;
1317
1477
  if (haveTo < height) await lc.sync(height);
1318
1478
  const vh = lc.chain[height - lc.baseHeight];
1319
1479
  if (!vh) throw new Error(`light client has no verified header at ${height}`);
1320
1480
  return String(vh.header.merkle);
1321
1481
  }
1482
+ /** The PoW-verifying SPV light client, seeded ONCE from the pinned checkpoint (shared by the M3 header-merkle
1483
+ * bind and the F13 offer pre-verify). Reused across calls; the first call near tip syncs the post-checkpoint span. */
1484
+ async seededSpvLight() {
1485
+ if (!this._spvLight) this._spvLight = new import_csd_light.LightClient({ client: this.chain.client, checkpoints: { [this._spvCp.height]: this._spvCp.hash } });
1486
+ if (!this._spvSeeded) {
1487
+ await this._spvLight.syncFromCheckpoint(this._spvCp.height, this._spvCp.hash);
1488
+ this._spvSeeded = true;
1489
+ }
1490
+ return this._spvLight;
1491
+ }
1492
+ /**
1493
+ * F13 — corroborate an offer ON-CHAIN before building a `fillOffer` payment's `outputs`. Merkle-proves the
1494
+ * offer into the PoW-verified header chain (from the pinned SPV checkpoint), binds the record to its on-chain
1495
+ * commitment, derives the payment recipient + seller from the offer's on-chain AUTHOR (the funding input's
1496
+ * prevout owner, txid-committed, NOT the malleable scriptSig), and, if you pass the resolver-SERVED `offer`
1497
+ * object, binds its payto/seller + fee/rebate/partial terms to the proven ones. Returns a TRUST-LABELED result
1498
+ * (fail-closed on a positive mismatch, fail-soft `transient` on a lagging / below-checkpoint chain view).
1499
+ *
1500
+ * This is best-effort corroboration, NOT the payment-grade boundary: the Cairn Wallet's OWN on-device
1501
+ * fill-SPV (0.2.60+, fails-closed before signing) is that. A dApp settling real value should still clear this
1502
+ * before building `outputs`, so it never hands the wallet a payment a lying resolver would redirect.
1503
+ */
1504
+ async verifyOfferForFill(offerId, servedOffer) {
1505
+ const light = await this.seededSpvLight();
1506
+ return preverifyOffer({ light, client: this.chain.client, offerId, servedOffer });
1507
+ }
1322
1508
  /**
1323
1509
  * Detect + connect the Cairn Wallet (browser only). Wires the wallet into
1324
1510
  * `cairn.board` for signed writes. Resolves to the connected address.
@@ -1355,15 +1541,20 @@ var Cairn = class {
1355
1541
  NotInstalledError,
1356
1542
  RegistryClient,
1357
1543
  SDK_VERSION,
1544
+ SubmitInFlightError,
1358
1545
  UnsupportedMethodError,
1359
1546
  UserRejectedError,
1360
1547
  WalletConnection,
1361
1548
  WalletLockedError,
1549
+ bindOfferTerms,
1362
1550
  connect,
1363
1551
  detectProvider,
1364
1552
  discoverProviders,
1365
1553
  errorCode,
1554
+ feeBpsAt,
1366
1555
  getWallet,
1367
1556
  isInstalled,
1368
- mapProviderError
1557
+ mapProviderError,
1558
+ mapSubmitResultError,
1559
+ preverifyOffer
1369
1560
  });