@inversealtruism/cairn-sdk 0.2.3 → 0.3.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.
@@ -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.3";
28
+ var SDK_VERSION = "0.3.2";
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-72YSLTLO.js";
4
- import "./chunk-TKZXUBDX.js";
5
- import "./chunk-M6OSWJVC.js";
3
+ } from "./chunk-AGU6K5EZ.js";
4
+ import "./chunk-BN3PFJDY.js";
5
+ import "./chunk-FMXNJQWL.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: () => import_cairnx_core2.bindOfferTerms,
42
44
  connect: () => connect,
43
45
  detectProvider: () => detectProvider,
44
46
  discoverProviders: () => discoverProviders,
45
47
  errorCode: () => errorCode,
48
+ feeBpsAt: () => import_cairnx_core2.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.3";
58
+ var SDK_VERSION = "0.3.2";
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
  }
@@ -246,8 +298,9 @@ var Http = class {
246
298
  try {
247
299
  const res = await this.raw("GET", path, { query });
248
300
  if (!res.ok) {
249
- const err = new HttpError(res.status, url, await safeText(res));
250
- if (!(res.status >= 500 && attempt < this.retries)) throw err;
301
+ if (!(res.status >= 500 && attempt < this.retries)) throw new HttpError(res.status, url, await safeText(res));
302
+ res.body?.cancel().catch(() => {
303
+ });
251
304
  } else {
252
305
  return this.parseJson(res, new TextDecoder().decode(await readCapped(res, this.maxBytes)), url);
253
306
  }
@@ -442,23 +495,23 @@ var BoardClient = class {
442
495
  const expiresEpoch = Math.floor(height / import_csd_codec3.EPOCH_LEN) + lifetime;
443
496
  const fee = input.fee ?? import_csd_codec3.MIN_FEE_PROPOSE;
444
497
  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 };
498
+ const txid3 = String(r.txid ?? "");
499
+ if (!txid3) throw new CairnError("Propose submitted but no txid was returned by the wallet.");
500
+ const reg = await this.tryRegister(content, txid3);
501
+ return { txid: txid3, payloadHash: ph, content, registered: reg.ok, registerError: reg.error };
449
502
  }
450
503
  /**
451
504
  * Register off-chain content for an already-submitted proposal txid. Idempotent.
452
505
  * Returns { ok } — the cairn server verifies sha256(canonical) == on-chain payload_hash.
453
506
  */
454
- async registerContent(content, txid2) {
507
+ async registerContent(content, txid3) {
455
508
  try {
456
509
  const res = await this.http.postJson("/api/content", {
457
510
  domain: content.domain,
458
511
  title: content.title,
459
512
  body: content.body,
460
513
  links: content.links,
461
- txid: txid2
514
+ txid: txid3
462
515
  });
463
516
  return { ok: !!res.ok, error: res.ok ? void 0 : res.error };
464
517
  } catch (e) {
@@ -482,12 +535,12 @@ var BoardClient = class {
482
535
  if (!Number.isFinite(h)) throw new CairnError("Could not read chain tip height for expiry computation.");
483
536
  return h;
484
537
  }
485
- async tryRegister(content, txid2) {
538
+ async tryRegister(content, txid3) {
486
539
  const delays = [0, 1500, 3e3, 5e3];
487
540
  let last = { ok: false, error: "not attempted" };
488
541
  for (const d of delays) {
489
542
  if (d) await sleep(d);
490
- last = await this.registerContent(content, txid2);
543
+ last = await this.registerContent(content, txid3);
491
544
  if (last.ok) return last;
492
545
  if (!/not found yet|not found/i.test(last.error ?? "")) return last;
493
546
  }
@@ -594,10 +647,10 @@ var IndexerClient = class {
594
647
  * same-origin RPC safe is the forward PoW re-verification, not an origin gate (which no longer exists).
595
648
  * Fails closed: a malformed proof or a chain-disagreement returns "not-found" (the latter with equivocation:true).
596
649
  */
597
- async verifyInclusion(txid2) {
650
+ async verifyInclusion(txid3) {
598
651
  let proof;
599
652
  try {
600
- proof = await this.txMerkleProof(txid2);
653
+ proof = await this.txMerkleProof(txid3);
601
654
  } catch {
602
655
  return { included: false, trustLevel: "not-found", reason: "no merkle proof (tx not indexed)" };
603
656
  }
@@ -606,7 +659,7 @@ var IndexerClient = class {
606
659
  }
607
660
  let folds = false;
608
661
  try {
609
- folds = (0, import_csd_codec4.verifyMerkleProof)(txid2, proof.pos, proof.merkle, proof.merkle_root);
662
+ folds = (0, import_csd_codec4.verifyMerkleProof)(txid3, proof.pos, proof.merkle, proof.merkle_root);
610
663
  } catch {
611
664
  return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "malformed merkle proof" };
612
665
  }
@@ -883,7 +936,7 @@ var NamesClient = class {
883
936
  async resolve(name) {
884
937
  if (!nameOk(name)) return null;
885
938
  try {
886
- return await this.http.getJson(`/trade/api/cairnx/resolve/${name}`);
939
+ return await this.http.getJson(`/trade/api/cairnx/resolve/${encodeURIComponent(name)}`);
887
940
  } catch (e) {
888
941
  if (e?.status === 404) return null;
889
942
  throw e;
@@ -893,7 +946,7 @@ var NamesClient = class {
893
946
  async name(name) {
894
947
  if (!nameOk(name)) return null;
895
948
  try {
896
- return await this.http.getJson(`/trade/api/cairnx/name/${name}`);
949
+ return await this.http.getJson(`/trade/api/cairnx/name/${encodeURIComponent(name)}`);
897
950
  } catch (e) {
898
951
  if (e?.status === 404) return null;
899
952
  throw e;
@@ -903,7 +956,7 @@ var NamesClient = class {
903
956
  async nameHistory(name) {
904
957
  if (!nameOk(name)) return null;
905
958
  try {
906
- return await this.http.getJson(`/trade/api/cairnx/name-history/${name}`);
959
+ return await this.http.getJson(`/trade/api/cairnx/name-history/${encodeURIComponent(name)}`);
907
960
  } catch (e) {
908
961
  if (e?.status === 404) return null;
909
962
  throw e;
@@ -1031,6 +1084,13 @@ function unwrap(reply) {
1031
1084
  if (reply && reply.ok) return reply.result;
1032
1085
  throw mapProviderError(reply?.error ?? "Unknown wallet error", reply?.code);
1033
1086
  }
1087
+ function unwrapWrite(reply) {
1088
+ const result = unwrap(reply);
1089
+ if (result && typeof result === "object" && result.ok === false) {
1090
+ throw mapSubmitResultError(result);
1091
+ }
1092
+ return result;
1093
+ }
1034
1094
  var WalletConnection = class {
1035
1095
  provider;
1036
1096
  _addr = null;
@@ -1097,12 +1157,18 @@ var WalletConnection = class {
1097
1157
  return null;
1098
1158
  }
1099
1159
  }
1100
- /** Atomic fill (CairnX DvP): pay + attest in ONE tx. Always prompts with clear-signing. */
1160
+ /**
1161
+ * CairnX fill: pay + attest in ONE tx (atomic SETTLEMENT SHAPE, both revert together). Always prompts with
1162
+ * clear-signing. The payment recipient in `params.outputs` is RESOLVER-TRUSTED here: this SDK does NOT
1163
+ * SPV-verify it, so a lying resolver can redirect the payment. A dApp settling real value MUST corroborate
1164
+ * the offer on-chain before building `outputs` (F13). The wallet's own fill-SPV boundary (cairn-wallet
1165
+ * 0.2.60+, which binds payment/rebate/fee to the merkle-proven offer) is the payment-grade check; see FillParams.
1166
+ */
1101
1167
  fillOffer(params) {
1102
1168
  if (typeof this.provider.fillOffer !== "function") {
1103
1169
  return Promise.reject(new UnsupportedMethodError("this wallet predates fillOffer() \u2014 ask the user to update the Cairn Wallet"));
1104
1170
  }
1105
- return Promise.resolve(this.provider.fillOffer(params)).then(unwrap);
1171
+ return Promise.resolve(this.provider.fillOffer(params)).then(unwrapWrite);
1106
1172
  }
1107
1173
  /** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
1108
1174
  async getPermissions() {
@@ -1135,26 +1201,26 @@ var WalletConnection = class {
1135
1201
  }
1136
1202
  /** Send CSD (always prompts with clear-signing; wallet picks inputs + returns change to itself). */
1137
1203
  send(params) {
1138
- return Promise.resolve(this.provider.send(params)).then(unwrap);
1204
+ return Promise.resolve(this.provider.send(params)).then(unwrapWrite);
1139
1205
  }
1140
1206
  /** Low-level Propose (always prompts). Prefer `cairn.board.propose()` which computes the hash/uri/expiry. */
1141
1207
  propose(params) {
1142
- return Promise.resolve(this.provider.propose(params)).then(unwrap);
1208
+ return Promise.resolve(this.provider.propose(params)).then(unwrapWrite);
1143
1209
  }
1144
1210
  /** Attest/support a proposal (always prompts). */
1145
1211
  attest(params) {
1146
- return Promise.resolve(this.provider.attest(params)).then(unwrap);
1212
+ return Promise.resolve(this.provider.attest(params)).then(unwrapWrite);
1147
1213
  }
1148
1214
  /** Seal a commit-reveal claim (always prompts; salt stays inside the wallet). */
1149
1215
  sealClaim(params) {
1150
- return Promise.resolve(this.provider.sealClaim(params)).then(unwrap);
1216
+ return Promise.resolve(this.provider.sealClaim(params)).then(unwrapWrite);
1151
1217
  }
1152
1218
  /** Reveal a previously sealed claim by its sealing txid (always prompts). */
1153
- revealClaim(txid2) {
1154
- if (typeof txid2 !== "string" || !txid2) {
1219
+ revealClaim(txid3) {
1220
+ if (typeof txid3 !== "string" || !txid3) {
1155
1221
  return Promise.reject(new UnsupportedMethodError("revealClaim requires the sealing txid"));
1156
1222
  }
1157
- return Promise.resolve(this.provider.revealClaim(txid2)).then(unwrap);
1223
+ return Promise.resolve(this.provider.revealClaim(txid3)).then(unwrapWrite);
1158
1224
  }
1159
1225
  };
1160
1226
  async function getWallet(opts = {}) {
@@ -1166,6 +1232,92 @@ async function connect(opts = {}) {
1166
1232
  return { wallet, address };
1167
1233
  }
1168
1234
 
1235
+ // src/fillverify.ts
1236
+ var import_csd_client3 = require("@inversealtruism/csd-client");
1237
+ var import_csd_codec6 = require("@inversealtruism/csd-codec");
1238
+ var import_cairnx_core2 = require("@inversealtruism/cairnx-core");
1239
+ var ADDR = /^0x[0-9a-f]{40}$/;
1240
+ var HASH = /^0x[0-9a-f]{64}$/;
1241
+ var COINBASE = "0x" + "00".repeat(32);
1242
+ async function provenAuthor(client, provenTx) {
1243
+ const in0 = provenTx.inputs?.[0];
1244
+ if (!in0 || String(in0.prevTxid).toLowerCase() === COINBASE && Number(in0.vout) === 4294967295) return null;
1245
+ let info;
1246
+ try {
1247
+ info = await client.tx(String(in0.prevTxid));
1248
+ } catch {
1249
+ return null;
1250
+ }
1251
+ const body = info?.tx ?? info;
1252
+ if (!body || !Array.isArray(body.outputs) || !Array.isArray(body.inputs)) return null;
1253
+ let src;
1254
+ try {
1255
+ src = (0, import_csd_client3.rpcTxToTx)(body);
1256
+ } catch {
1257
+ return null;
1258
+ }
1259
+ if (String((0, import_csd_codec6.txid)(src)).toLowerCase() !== String(in0.prevTxid).toLowerCase()) return null;
1260
+ const out = src.outputs?.[Number(in0.vout)];
1261
+ const spk = out?.scriptPubkey ? String(out.scriptPubkey).toLowerCase() : null;
1262
+ return spk && ADDR.test(spk) ? spk : null;
1263
+ }
1264
+ async function preverifyOffer(opts) {
1265
+ const id = String(opts.offerId).toLowerCase();
1266
+ if (!HASH.test(id)) return { ok: false, trust: "unverified", reason: "invalid offer id" };
1267
+ let incl;
1268
+ try {
1269
+ incl = await opts.light.verifyTxInclusion(id);
1270
+ } catch (e) {
1271
+ return { ok: false, trust: "unverified", transient: true, reason: `couldn't reach the chain to verify the offer (${e?.message ?? e})` };
1272
+ }
1273
+ if (!incl.included || incl.trustLevel !== "verified-inclusion" || !incl.tx || !incl.appPayloadHash)
1274
+ return { ok: false, trust: "unverified", transient: true, blockHeight: incl.blockHeight, reason: incl.reason ?? `offer not merkle-proven on-chain (${incl.trustLevel})` };
1275
+ const blockHeight = Number(incl.blockHeight);
1276
+ let provenTx, provenId;
1277
+ try {
1278
+ provenTx = (0, import_csd_client3.rpcTxToTx)(incl.tx);
1279
+ provenId = (0, import_csd_codec6.txid)(provenTx);
1280
+ } catch {
1281
+ return { ok: false, trust: "unverified", reason: "the on-chain offer tx couldn't be re-derived" };
1282
+ }
1283
+ if (String(provenId).toLowerCase() !== id) return { ok: false, trust: "unverified", reason: "the merkle-proven tx doesn't match the offer id" };
1284
+ const app = incl.tx.app;
1285
+ if (!app || app.type !== "Propose") return { ok: false, trust: "unverified", reason: "the offer tx is not a Propose" };
1286
+ let rec;
1287
+ try {
1288
+ rec = (0, import_cairnx_core2.parseRecord)(app.uri, incl.appPayloadHash);
1289
+ } catch {
1290
+ rec = null;
1291
+ }
1292
+ if (!rec || rec.t !== "offer") return { ok: false, trust: "unverified", reason: "the offer record doesn't bind to its on-chain commitment" };
1293
+ if (String((0, import_csd_codec6.payloadHash)(rec)).toLowerCase() !== String(incl.appPayloadHash).toLowerCase())
1294
+ return { ok: false, trust: "unverified", reason: "the offer record doesn't match the on-chain (merkle-proven) commitment" };
1295
+ const seller = await provenAuthor(opts.client, provenTx);
1296
+ 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" };
1297
+ const w = rec.want ?? {};
1298
+ const payto = w.payto && ADDR.test(String(w.payto).toLowerCase()) ? String(w.payto).toLowerCase() : seller;
1299
+ const terms = {
1300
+ height: blockHeight,
1301
+ feeBps: (0, import_cairnx_core2.feeBpsAt)(blockHeight),
1302
+ value: w.value !== void 0 ? String(w.value) : void 0,
1303
+ taker: rec.taker !== void 0 ? String(rec.taker).toLowerCase() : void 0,
1304
+ bid: rec.bid !== void 0 ? String(rec.bid).toLowerCase() : void 0,
1305
+ min: rec.min !== void 0 ? String(rec.min) : void 0
1306
+ };
1307
+ const base = { payto, seller, terms, blockHeight };
1308
+ if (opts.servedOffer !== void 0 && opts.servedOffer !== null) {
1309
+ const so = opts.servedOffer;
1310
+ const servedPayto = so?.want?.payto !== void 0 && so?.want?.payto !== null ? String(so.want.payto).toLowerCase() : "";
1311
+ if (servedPayto && servedPayto !== payto)
1312
+ return { ok: false, trust: "verified", reason: "the served payment recipient doesn't match the offer's on-chain author/record", ...base };
1313
+ if (so?.seller !== void 0 && so?.seller !== null && String(so.seller).toLowerCase() !== seller)
1314
+ 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 };
1315
+ if ((0, import_cairnx_core2.bindOfferTerms)(opts.servedOffer, terms))
1316
+ 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 };
1317
+ }
1318
+ return { ok: true, trust: "verified", ...base };
1319
+ }
1320
+
1169
1321
  // src/controller.ts
1170
1322
  var DISCONNECTED = Object.freeze({ status: "disconnected", account: null, error: null });
1171
1323
  var CairnController = class {
@@ -1307,18 +1459,39 @@ var Cairn = class {
1307
1459
  async verifiedHeaderMerkleAt(height) {
1308
1460
  if (!Number.isInteger(height) || height < this._spvCp.height)
1309
1461
  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
- }
1462
+ const lc = await this.seededSpvLight();
1316
1463
  const haveTo = lc.baseHeight + lc.chain.length - 1;
1317
1464
  if (haveTo < height) await lc.sync(height);
1318
1465
  const vh = lc.chain[height - lc.baseHeight];
1319
1466
  if (!vh) throw new Error(`light client has no verified header at ${height}`);
1320
1467
  return String(vh.header.merkle);
1321
1468
  }
1469
+ /** The PoW-verifying SPV light client, seeded ONCE from the pinned checkpoint (shared by the M3 header-merkle
1470
+ * bind and the F13 offer pre-verify). Reused across calls; the first call near tip syncs the post-checkpoint span. */
1471
+ async seededSpvLight() {
1472
+ if (!this._spvLight) this._spvLight = new import_csd_light.LightClient({ client: this.chain.client, checkpoints: { [this._spvCp.height]: this._spvCp.hash } });
1473
+ if (!this._spvSeeded) {
1474
+ await this._spvLight.syncFromCheckpoint(this._spvCp.height, this._spvCp.hash);
1475
+ this._spvSeeded = true;
1476
+ }
1477
+ return this._spvLight;
1478
+ }
1479
+ /**
1480
+ * F13 — corroborate an offer ON-CHAIN before building a `fillOffer` payment's `outputs`. Merkle-proves the
1481
+ * offer into the PoW-verified header chain (from the pinned SPV checkpoint), binds the record to its on-chain
1482
+ * commitment, derives the payment recipient + seller from the offer's on-chain AUTHOR (the funding input's
1483
+ * prevout owner, txid-committed, NOT the malleable scriptSig), and, if you pass the resolver-SERVED `offer`
1484
+ * object, binds its payto/seller + fee/rebate/partial terms to the proven ones. Returns a TRUST-LABELED result
1485
+ * (fail-closed on a positive mismatch, fail-soft `transient` on a lagging / below-checkpoint chain view).
1486
+ *
1487
+ * This is best-effort corroboration, NOT the payment-grade boundary: the Cairn Wallet's OWN on-device
1488
+ * fill-SPV (0.2.60+, fails-closed before signing) is that. A dApp settling real value should still clear this
1489
+ * before building `outputs`, so it never hands the wallet a payment a lying resolver would redirect.
1490
+ */
1491
+ async verifyOfferForFill(offerId, servedOffer) {
1492
+ const light = await this.seededSpvLight();
1493
+ return preverifyOffer({ light, client: this.chain.client, offerId, servedOffer });
1494
+ }
1322
1495
  /**
1323
1496
  * Detect + connect the Cairn Wallet (browser only). Wires the wallet into
1324
1497
  * `cairn.board` for signed writes. Resolves to the connected address.
@@ -1355,15 +1528,20 @@ var Cairn = class {
1355
1528
  NotInstalledError,
1356
1529
  RegistryClient,
1357
1530
  SDK_VERSION,
1531
+ SubmitInFlightError,
1358
1532
  UnsupportedMethodError,
1359
1533
  UserRejectedError,
1360
1534
  WalletConnection,
1361
1535
  WalletLockedError,
1536
+ bindOfferTerms,
1362
1537
  connect,
1363
1538
  detectProvider,
1364
1539
  discoverProviders,
1365
1540
  errorCode,
1541
+ feeBpsAt,
1366
1542
  getWallet,
1367
1543
  isInstalled,
1368
- mapProviderError
1544
+ mapProviderError,
1545
+ mapSubmitResultError,
1546
+ preverifyOffer
1369
1547
  });