@inversealtruism/cairn-sdk 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -40,22 +40,27 @@ __export(index_exports, {
40
40
  UserRejectedError: () => UserRejectedError,
41
41
  WalletConnection: () => WalletConnection,
42
42
  WalletLockedError: () => WalletLockedError,
43
- bindOfferTerms: () => bindOfferTerms,
43
+ bindOfferTerms: () => import_cairnx_core2.bindOfferTerms,
44
44
  connect: () => connect,
45
45
  detectProvider: () => detectProvider,
46
46
  discoverProviders: () => discoverProviders,
47
47
  errorCode: () => errorCode,
48
- feeBpsAt: () => feeBpsAt,
48
+ feeBpsAt: () => import_cairnx_core2.feeBpsAt,
49
+ fillEndorsement: () => import_cairnx_core2.fillEndorsement,
50
+ fillIsSafe: () => import_cairnx_core2.fillIsSafe,
51
+ fillOutputPlan: () => import_cairnx_core2.fillOutputPlan,
49
52
  getWallet: () => getWallet,
50
53
  isInstalled: () => isInstalled,
51
54
  mapProviderError: () => mapProviderError,
52
55
  mapSubmitResultError: () => mapSubmitResultError,
53
- preverifyOffer: () => preverifyOffer
56
+ preverifyOffer: () => preverifyOffer,
57
+ previewFill: () => import_cairnx_core2.previewFill,
58
+ requiredFillOutputs: () => import_cairnx_core2.requiredFillOutputs
54
59
  });
55
60
  module.exports = __toCommonJS(index_exports);
56
61
 
57
62
  // src/errors.ts
58
- var SDK_VERSION = "0.3.0";
63
+ var SDK_VERSION = "0.4.0";
59
64
  var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
60
65
  var CairnError = class extends Error {
61
66
  /** Stable machine code for programmatic branching. */
@@ -147,7 +152,7 @@ var SubmitInFlightError = class extends CairnError {
147
152
  this.txid = txid3;
148
153
  }
149
154
  };
150
- var NATIVE_CODES = /* @__PURE__ */ new Set(["ACCOUNT_CHANGED", "FIRST_PARTY_ONLY", "RATE_LIMITED", "APPROVAL_CLOSED", "FORBIDDEN", "INTERNAL"]);
155
+ var NATIVE_CODES = /* @__PURE__ */ new Set(["FIRST_PARTY_ONLY", "RATE_LIMITED", "FORBIDDEN", "INTERNAL"]);
151
156
  var NESTED_TRANSIENT = /* @__PURE__ */ new Set(["VERIFY_UNAVAILABLE", "OFFER_UNKNOWN"]);
152
157
  var NESTED_TERMINAL = /* @__PURE__ */ new Set([
153
158
  "FILL_UNSAFE",
@@ -298,8 +303,9 @@ var Http = class {
298
303
  try {
299
304
  const res = await this.raw("GET", path, { query });
300
305
  if (!res.ok) {
301
- const err = new HttpError(res.status, url, await safeText(res));
302
- if (!(res.status >= 500 && attempt < this.retries)) throw err;
306
+ if (!(res.status >= 500 && attempt < this.retries)) throw new HttpError(res.status, url, await safeText(res));
307
+ res.body?.cancel().catch(() => {
308
+ });
303
309
  } else {
304
310
  return this.parseJson(res, new TextDecoder().decode(await readCapped(res, this.maxBytes)), url);
305
311
  }
@@ -935,7 +941,7 @@ var NamesClient = class {
935
941
  async resolve(name) {
936
942
  if (!nameOk(name)) return null;
937
943
  try {
938
- return await this.http.getJson(`/trade/api/cairnx/resolve/${name}`);
944
+ return await this.http.getJson(`/trade/api/cairnx/resolve/${encodeURIComponent(name)}`);
939
945
  } catch (e) {
940
946
  if (e?.status === 404) return null;
941
947
  throw e;
@@ -945,7 +951,7 @@ var NamesClient = class {
945
951
  async name(name) {
946
952
  if (!nameOk(name)) return null;
947
953
  try {
948
- return await this.http.getJson(`/trade/api/cairnx/name/${name}`);
954
+ return await this.http.getJson(`/trade/api/cairnx/name/${encodeURIComponent(name)}`);
949
955
  } catch (e) {
950
956
  if (e?.status === 404) return null;
951
957
  throw e;
@@ -955,7 +961,7 @@ var NamesClient = class {
955
961
  async nameHistory(name) {
956
962
  if (!nameOk(name)) return null;
957
963
  try {
958
- return await this.http.getJson(`/trade/api/cairnx/name-history/${name}`);
964
+ return await this.http.getJson(`/trade/api/cairnx/name-history/${encodeURIComponent(name)}`);
959
965
  } catch (e) {
960
966
  if (e?.status === 404) return null;
961
967
  throw e;
@@ -1014,6 +1020,7 @@ var NamesClient = class {
1014
1020
  };
1015
1021
 
1016
1022
  // src/connect.ts
1023
+ var DEFAULT_PROVIDER_TIMEOUT_MS = 10 * 6e4;
1017
1024
  function discoverProviders(opts = {}) {
1018
1025
  const timeoutMs = opts.timeoutMs ?? 1e3;
1019
1026
  return new Promise((resolve) => {
@@ -1093,8 +1100,24 @@ function unwrapWrite(reply) {
1093
1100
  var WalletConnection = class {
1094
1101
  provider;
1095
1102
  _addr = null;
1096
- constructor(provider) {
1103
+ /** B7c: per-call provider timeout (ms). GENEROUS by default so it never rejects an in-flight human
1104
+ * approval; 0 disables it. Every provider call (writes especially) races against it. */
1105
+ _timeoutMs;
1106
+ constructor(provider, opts = {}) {
1097
1107
  this.provider = provider;
1108
+ this._timeoutMs = opts.timeoutMs ?? DEFAULT_PROVIDER_TIMEOUT_MS;
1109
+ }
1110
+ // B7c: race a provider call against the per-call timeout. On timeout, throw a fail-closed CairnError
1111
+ // (retryable:false: a write may already have reached the wallet, so never invite a blind auto-retry). The
1112
+ // genuine wallet resolves quickly once the user acts; this only fires on a truly hung/absent response.
1113
+ call(op, label) {
1114
+ const ms = this._timeoutMs;
1115
+ if (!Number.isFinite(ms) || ms <= 0) return Promise.resolve(op());
1116
+ let timer;
1117
+ const timeout = new Promise((_, reject) => {
1118
+ timer = setTimeout(() => reject(new CairnError(`the wallet did not respond to ${label} within ${ms}ms`, { retryable: false })), ms);
1119
+ });
1120
+ return Promise.race([Promise.resolve(op()), timeout]).finally(() => clearTimeout(timer));
1098
1121
  }
1099
1122
  /** The wallet's version string (e.g. "0.2.x"). */
1100
1123
  get version() {
@@ -1106,13 +1129,13 @@ var WalletConnection = class {
1106
1129
  }
1107
1130
  /** Request connection. First time per origin prompts the user; afterwards may be silent. */
1108
1131
  async connect() {
1109
- const { addr } = unwrap(await this.provider.connect());
1132
+ const { addr } = unwrap(await this.call(() => this.provider.connect(), "connect"));
1110
1133
  this._addr = addr;
1111
1134
  return addr;
1112
1135
  }
1113
1136
  /** Get the connected address (does not force a new connection prompt if already consented). */
1114
1137
  async getAddress() {
1115
- const { addr } = unwrap(await this.provider.getAddress());
1138
+ const { addr } = unwrap(await this.call(() => this.provider.getAddress(), "getAddress"));
1116
1139
  this._addr = addr;
1117
1140
  return addr;
1118
1141
  }
@@ -1125,7 +1148,7 @@ var WalletConnection = class {
1125
1148
  if (typeof this.provider.signIn !== "function") {
1126
1149
  return Promise.reject(new UnsupportedMethodError("this wallet does not expose signIn()"));
1127
1150
  }
1128
- return Promise.resolve(this.provider.signIn()).then(unwrap);
1151
+ return this.call(() => this.provider.signIn(), "signIn").then(unwrap);
1129
1152
  }
1130
1153
  /**
1131
1154
  * Audience-bound "Sign in with CSD" (SIWC) — the secure, replay-resistant third-party login.
@@ -1141,7 +1164,7 @@ var WalletConnection = class {
1141
1164
  if (typeof this.provider.signInWithCsd !== "function") {
1142
1165
  return Promise.reject(new UnsupportedMethodError("this wallet predates Sign-in-with-CSD \u2014 ask the user to update the Cairn Wallet"));
1143
1166
  }
1144
- return Promise.resolve(this.provider.signInWithCsd(params)).then(unwrap);
1167
+ return this.call(() => this.provider.signInWithCsd(params), "signInWithCsd").then(unwrap);
1145
1168
  }
1146
1169
  /** True if this wallet supports audience-bound SIWC (safe to call `signInWithCsd`). */
1147
1170
  get supportsSiwc() {
@@ -1151,7 +1174,7 @@ var WalletConnection = class {
1151
1174
  async getCapabilities() {
1152
1175
  if (typeof this.provider.getCapabilities !== "function") return null;
1153
1176
  try {
1154
- return await this.provider.getCapabilities();
1177
+ return await this.call(() => this.provider.getCapabilities(), "getCapabilities");
1155
1178
  } catch {
1156
1179
  return null;
1157
1180
  }
@@ -1167,24 +1190,24 @@ var WalletConnection = class {
1167
1190
  if (typeof this.provider.fillOffer !== "function") {
1168
1191
  return Promise.reject(new UnsupportedMethodError("this wallet predates fillOffer() \u2014 ask the user to update the Cairn Wallet"));
1169
1192
  }
1170
- return Promise.resolve(this.provider.fillOffer(params)).then(unwrapWrite);
1193
+ return this.call(() => this.provider.fillOffer(params), "fillOffer").then(unwrapWrite);
1171
1194
  }
1172
1195
  /** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
1173
1196
  async getPermissions() {
1174
1197
  if (typeof this.provider.getPermissions !== "function") return [];
1175
- return unwrap(await this.provider.getPermissions());
1198
+ return unwrap(await this.call(() => this.provider.getPermissions(), "getPermissions"));
1176
1199
  }
1177
1200
  /** Explicitly request permission to see the address (prompts, like connect). */
1178
1201
  requestPermissions() {
1179
1202
  if (typeof this.provider.requestPermissions !== "function") {
1180
1203
  return Promise.reject(new UnsupportedMethodError("this wallet predates requestPermissions \u2014 use connect()"));
1181
1204
  }
1182
- return Promise.resolve(this.provider.requestPermissions()).then(unwrap);
1205
+ return this.call(() => this.provider.requestPermissions(), "requestPermissions").then(unwrap);
1183
1206
  }
1184
1207
  /** Revoke THIS origin's own access (silent). Returns { revoked }. Safe no-op on older wallets. */
1185
1208
  async revokePermissions() {
1186
1209
  if (typeof this.provider.revokePermissions !== "function") return { revoked: false };
1187
- return unwrap(await this.provider.revokePermissions());
1210
+ return unwrap(await this.call(() => this.provider.revokePermissions(), "revokePermissions"));
1188
1211
  }
1189
1212
  /**
1190
1213
  * Subscribe to provider events. `accountsChanged` fires with `[]` when this site loses access
@@ -1200,26 +1223,26 @@ var WalletConnection = class {
1200
1223
  }
1201
1224
  /** Send CSD (always prompts with clear-signing; wallet picks inputs + returns change to itself). */
1202
1225
  send(params) {
1203
- return Promise.resolve(this.provider.send(params)).then(unwrapWrite);
1226
+ return this.call(() => this.provider.send(params), "send").then(unwrapWrite);
1204
1227
  }
1205
1228
  /** Low-level Propose (always prompts). Prefer `cairn.board.propose()` which computes the hash/uri/expiry. */
1206
1229
  propose(params) {
1207
- return Promise.resolve(this.provider.propose(params)).then(unwrapWrite);
1230
+ return this.call(() => this.provider.propose(params), "propose").then(unwrapWrite);
1208
1231
  }
1209
1232
  /** Attest/support a proposal (always prompts). */
1210
1233
  attest(params) {
1211
- return Promise.resolve(this.provider.attest(params)).then(unwrapWrite);
1234
+ return this.call(() => this.provider.attest(params), "attest").then(unwrapWrite);
1212
1235
  }
1213
1236
  /** Seal a commit-reveal claim (always prompts; salt stays inside the wallet). */
1214
1237
  sealClaim(params) {
1215
- return Promise.resolve(this.provider.sealClaim(params)).then(unwrapWrite);
1238
+ return this.call(() => this.provider.sealClaim(params), "sealClaim").then(unwrapWrite);
1216
1239
  }
1217
1240
  /** Reveal a previously sealed claim by its sealing txid (always prompts). */
1218
1241
  revealClaim(txid3) {
1219
1242
  if (typeof txid3 !== "string" || !txid3) {
1220
1243
  return Promise.reject(new UnsupportedMethodError("revealClaim requires the sealing txid"));
1221
1244
  }
1222
- return Promise.resolve(this.provider.revealClaim(txid3)).then(unwrapWrite);
1245
+ return this.call(() => this.provider.revealClaim(txid3), "revealClaim").then(unwrapWrite);
1223
1246
  }
1224
1247
  };
1225
1248
  async function getWallet(opts = {}) {
@@ -1238,19 +1261,22 @@ var import_cairnx_core2 = require("@inversealtruism/cairnx-core");
1238
1261
  var ADDR = /^0x[0-9a-f]{40}$/;
1239
1262
  var HASH = /^0x[0-9a-f]{64}$/;
1240
1263
  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;
1264
+ function bindPlannedSums(planned, proven) {
1265
+ const provenMap = new Map(proven.map((o) => [o.to.toLowerCase(), o.value]));
1266
+ const plannedMap = /* @__PURE__ */ new Map();
1267
+ try {
1268
+ for (const [a, v] of Object.entries(planned)) {
1269
+ const k = String(a).toLowerCase();
1270
+ plannedMap.set(k, (plannedMap.get(k) ?? 0n) + BigInt(v));
1271
+ }
1272
+ } catch {
1273
+ return "invalid plannedOutputs (amounts must be base-unit integers)";
1274
+ }
1275
+ for (const k of /* @__PURE__ */ new Set([...provenMap.keys(), ...plannedMap.keys()])) {
1276
+ if ((provenMap.get(k) ?? 0n) !== (plannedMap.get(k) ?? 0n))
1277
+ return `your planned payment doesn't match the proven fill outputs (${k}: planned ${(plannedMap.get(k) ?? 0n).toString()}, proven ${(provenMap.get(k) ?? 0n).toString()}) - refusing (an overpay past the proven amount is unrecoverable, and a missing or low leg is a doomed underpay)`;
1278
+ }
1279
+ return void 0;
1254
1280
  }
1255
1281
  async function provenAuthor(client, provenTx) {
1256
1282
  const in0 = provenTx.inputs?.[0];
@@ -1307,17 +1333,35 @@ async function preverifyOffer(opts) {
1307
1333
  return { ok: false, trust: "unverified", reason: "the offer record doesn't match the on-chain (merkle-proven) commitment" };
1308
1334
  const seller = await provenAuthor(opts.client, provenTx);
1309
1335
  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" };
1336
+ const offerRec = rec;
1310
1337
  const w = rec.want ?? {};
1311
1338
  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 };
1339
+ const terms = (0, import_cairnx_core2.provenOfferTerms)(offerRec, blockHeight);
1340
+ let outputPlan;
1341
+ let sumsMismatch;
1342
+ if (opts.pay !== void 0 && opts.pay !== null && !(0, import_cairnx_core2.isTokenWant)(offerRec.want) && offerRec.min === void 0) {
1343
+ const provenState = {
1344
+ id,
1345
+ seller,
1346
+ give: offerRec.give,
1347
+ want: offerRec.want,
1348
+ taker: offerRec.taker,
1349
+ bid: offerRec.bid,
1350
+ status: "open",
1351
+ expiresEpoch: 0,
1352
+ height: blockHeight,
1353
+ feeBps: terms.feeBps
1354
+ };
1355
+ const plan = (0, import_cairnx_core2.fillOutputPlan)(provenState, opts.pay);
1356
+ if (plan.kind === "csd-outputs") {
1357
+ outputPlan = plan.outputs;
1358
+ if (opts.plannedOutputs !== void 0 && opts.plannedOutputs !== null)
1359
+ sumsMismatch = bindPlannedSums(opts.plannedOutputs, plan.outputs);
1360
+ } else if (plan.kind === "undeliverable") {
1361
+ sumsMismatch = `this pay would not deliver against the proven offer (${plan.reason}) - refusing (the CSD would be lost)`;
1362
+ }
1363
+ }
1364
+ const base = { payto, seller, terms, blockHeight, ...outputPlan ? { outputPlan } : {} };
1321
1365
  if (opts.servedOffer !== void 0 && opts.servedOffer !== null) {
1322
1366
  const so = opts.servedOffer;
1323
1367
  const servedPayto = so?.want?.payto !== void 0 && so?.want?.payto !== null ? String(so.want.payto).toLowerCase() : "";
@@ -1325,9 +1369,10 @@ async function preverifyOffer(opts) {
1325
1369
  return { ok: false, trust: "verified", reason: "the served payment recipient doesn't match the offer's on-chain author/record", ...base };
1326
1370
  if (so?.seller !== void 0 && so?.seller !== null && String(so.seller).toLowerCase() !== seller)
1327
1371
  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 };
1372
+ if ((0, import_cairnx_core2.bindOfferTerms)(opts.servedOffer, terms, { give: true, wantType: true }))
1373
+ 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 type - a lying resolver could mis-size or re-route the fill, redirecting or burning your payment)", ...base };
1330
1374
  }
1375
+ if (sumsMismatch) return { ok: false, trust: "verified", reason: sumsMismatch, ...base };
1331
1376
  return { ok: true, trust: "verified", ...base };
1332
1377
  }
1333
1378
 
@@ -1423,6 +1468,10 @@ var CairnController = class {
1423
1468
  // src/index.ts
1424
1469
  var DEFAULT_SPV_CHECKPOINT = { height: 38142, hash: "0x00000000000140f023cc0ee1457a40833f2fcb4de44291b9d373e50f17b97232" };
1425
1470
  var DEFAULT_CAIRN = "https://cairn-substrate.com";
1471
+ var SPV_HTTP_TIMEOUT_MS = 15e3;
1472
+ var SPV_HTTP_MAX_RETRIES = 4;
1473
+ var spvSleep = (ms) => new Promise((r) => setTimeout(r, ms));
1474
+ var spvRetryDelayMs = (attempt, retryAfterSecs) => Math.min(4e3, Math.max(700 * (attempt + 1), retryAfterSecs * 1e3));
1426
1475
  var Cairn = class {
1427
1476
  config;
1428
1477
  chain;
@@ -1438,6 +1487,8 @@ var Cairn = class {
1438
1487
  _spvLight = null;
1439
1488
  _spvSeeded = false;
1440
1489
  _spvCp;
1490
+ // W12: the cairn base the SPV header-batch provider fetches /api/headers from (same base the rest of the SDK uses).
1491
+ _cairnBase;
1441
1492
  constructor(config = {}) {
1442
1493
  this.config = config;
1443
1494
  const fetch = config.fetch;
@@ -1446,6 +1497,7 @@ var Cairn = class {
1446
1497
  const rpc = config.baseUrls?.rpc ?? `${cairn}/api/rpc`;
1447
1498
  const indexer = (config.baseUrls?.indexer ?? `${cairn}/explorer/api`).replace(/\/+$/, "");
1448
1499
  this.cairnHttp = new Http({ baseUrl: cairn, fetch, timeoutMs });
1500
+ this._cairnBase = cairn;
1449
1501
  const indexerHttp = new Http({ baseUrl: indexer, fetch, timeoutMs });
1450
1502
  const swarmHttp = config.baseUrls?.swarm ? new Http({ baseUrl: config.baseUrls.swarm.replace(/\/+$/, ""), fetch, timeoutMs }) : void 0;
1451
1503
  this._spvCp = config.spvCheckpoint ?? DEFAULT_SPV_CHECKPOINT;
@@ -1482,13 +1534,69 @@ var Cairn = class {
1482
1534
  /** The PoW-verifying SPV light client, seeded ONCE from the pinned checkpoint (shared by the M3 header-merkle
1483
1535
  * bind and the F13 offer pre-verify). Reused across calls; the first call near tip syncs the post-checkpoint span. */
1484
1536
  async seededSpvLight() {
1485
- if (!this._spvLight) this._spvLight = new import_csd_light.LightClient({ client: this.chain.client, checkpoints: { [this._spvCp.height]: this._spvCp.hash } });
1537
+ if (!this._spvLight) this._spvLight = new import_csd_light.LightClient({
1538
+ client: this.chain.client,
1539
+ checkpoints: { [this._spvCp.height]: this._spvCp.hash },
1540
+ headersBatchProvider: (from, count) => this.fetchHeadersBatch(from, count)
1541
+ });
1486
1542
  if (!this._spvSeeded) {
1487
1543
  await this._spvLight.syncFromCheckpoint(this._spvCp.height, this._spvCp.hash);
1544
+ try {
1545
+ const tipHeight = Number((await this.chain.tip()).height);
1546
+ const have = this._spvLight.baseHeight + this._spvLight.chain.length - 1;
1547
+ if (Number.isFinite(tipHeight) && tipHeight > have) await this._spvLight.sync(tipHeight);
1548
+ } catch {
1549
+ }
1488
1550
  this._spvSeeded = true;
1489
1551
  }
1490
1552
  return this._spvLight;
1491
1553
  }
1554
+ /**
1555
+ * W12 batch header transport for the checkpoint..tip forward sync, backed by the cairn server's
1556
+ * `/api/headers/:from/:count`. Carries ZERO trust: the LightClient re-verifies every header's PoW /
1557
+ * prev-link / LWMA, so a forged or stale row fails closed and retrying transport proves nothing new.
1558
+ * Retry budget: a 429 (per-IP header budget) or 502/503 (a height past the lagging indexer, or the
1559
+ * server's 10s deadline) is a transient data-availability hiccup; back off (honouring Retry-After) a
1560
+ * bounded number of times, then THROW so `sync()` fails closed. A non-dense range is rejected.
1561
+ *
1562
+ * B7d / F6 DEPENDENCY: `/api/headers` serves no Access-Control-Allow-Origin today (`corsPublic` is
1563
+ * mounted on `/explorer/api`, `/trade/api` and `/api/rpc`, not here). A SAME-ORIGIN dApp (hosted on the
1564
+ * cairn base) reaches it now; a CROSS-ORIGIN third-party dApp cannot until B7d adds the CORS mount and
1565
+ * restarts the cairn service. This code is correct as written; it becomes reachable cross-origin only
1566
+ * once B7d ships. This is a recorded dependency, not a blocker for authoring/testing.
1567
+ */
1568
+ async fetchHeadersBatch(from, count) {
1569
+ const g = this.config.fetch ?? globalThis.fetch;
1570
+ if (!g) throw new Error("No fetch available for SPV header sync - pass `fetch` in the SDK options.");
1571
+ const doFetch = this.config.fetch ? this.config.fetch : g.bind(globalThis);
1572
+ const url = `${this._cairnBase}/api/headers/${from}/${count}`;
1573
+ for (let attempt = 0; ; attempt++) {
1574
+ let res;
1575
+ const ctrl = new AbortController();
1576
+ const timer = setTimeout(() => ctrl.abort(), SPV_HTTP_TIMEOUT_MS);
1577
+ try {
1578
+ res = await doFetch(url, { signal: ctrl.signal });
1579
+ } catch (e) {
1580
+ if (attempt >= SPV_HTTP_MAX_RETRIES) throw e;
1581
+ await spvSleep(spvRetryDelayMs(attempt, 0));
1582
+ continue;
1583
+ } finally {
1584
+ clearTimeout(timer);
1585
+ }
1586
+ if (res.ok) {
1587
+ const rows = (await res.json())?.headers;
1588
+ if (!Array.isArray(rows) || rows.length !== count) throw new Error(`/api/headers: non-dense range (${from}/${count})`);
1589
+ return rows.map((r) => ({ header: r.header, hash: r.hash }));
1590
+ }
1591
+ if ((res.status === 429 || res.status === 502 || res.status === 503) && attempt < SPV_HTTP_MAX_RETRIES) {
1592
+ res.body?.cancel?.().catch(() => {
1593
+ });
1594
+ await spvSleep(spvRetryDelayMs(attempt, Number(res.headers.get("retry-after")) || 0));
1595
+ continue;
1596
+ }
1597
+ throw new Error(`/api/headers ${res.status}`);
1598
+ }
1599
+ }
1492
1600
  /**
1493
1601
  * F13 — corroborate an offer ON-CHAIN before building a `fillOffer` payment's `outputs`. Merkle-proves the
1494
1602
  * offer into the PoW-verified header chain (from the pinned SPV checkpoint), binds the record to its on-chain
@@ -1552,9 +1660,14 @@ var Cairn = class {
1552
1660
  discoverProviders,
1553
1661
  errorCode,
1554
1662
  feeBpsAt,
1663
+ fillEndorsement,
1664
+ fillIsSafe,
1665
+ fillOutputPlan,
1555
1666
  getWallet,
1556
1667
  isInstalled,
1557
1668
  mapProviderError,
1558
1669
  mapSubmitResultError,
1559
- preverifyOffer
1670
+ preverifyOffer,
1671
+ previewFill,
1672
+ requiredFillOutputs
1560
1673
  });
package/dist/index.d.cts CHANGED
@@ -10,6 +10,8 @@ export { ContentClientOptions, PreparedContent } from './content.cjs';
10
10
  import { RegistryClient } from './registry.cjs';
11
11
  import { WalletConnection, DetectOptions } from './connect.cjs';
12
12
  export { AttestParams, CairnProvider, ConnectResult, FillParams, ProposeParams, ProviderDetail, ProviderInfo, SealClaimParams, SendParams, SignInResult, SiwcParams, SiwcResult, TxResult, WalletCapabilities, WalletPermission, connect, detectProvider, discoverProviders, getWallet, isInstalled } from './connect.cjs';
13
+ import { ProvenOfferTerms } from '@inversealtruism/cairnx-core';
14
+ export { FillEndorsement, FillOutputPlan, FillPreview, FillSafety, ProvenOfferTerms, bindOfferTerms, feeBpsAt, fillEndorsement, fillIsSafe, fillOutputPlan, previewFill, requiredFillOutputs } from '@inversealtruism/cairnx-core';
13
15
  import { InclusionResult } from '@inversealtruism/csd-light';
14
16
  export { CairnController, CairnControllerOptions, CairnState } from './controller.cjs';
15
17
  import '@inversealtruism/csd-client';
@@ -104,15 +106,6 @@ declare class NamesClient {
104
106
  state(): Promise<unknown>;
105
107
  }
106
108
 
107
- /** The fee/rebate-relevant fields of an offer, derived from the MERKLE-PROVEN offer (never a served object). */
108
- interface ProvenOfferTerms {
109
- height: number;
110
- feeBps: number;
111
- value?: string;
112
- taker?: string;
113
- bid?: string;
114
- min?: string;
115
- }
116
109
  /** The trust-labeled verdict. `ok:false` + `transient:true` = a retryable chain-catching-up soft-fail (NOT a
117
110
  * proven mismatch). `payto`/`seller`/`terms` are populated whenever they could be merkle-derived. */
118
111
  interface OfferFillCheck {
@@ -126,12 +119,17 @@ interface OfferFillCheck {
126
119
  seller?: string;
127
120
  terms?: ProvenOfferTerms;
128
121
  blockHeight?: number;
122
+ /** B7b (REBIND W3/M1): the PROVEN CSD outputs to build for `pay` (a WHOLE fill of a non-partial offer),
123
+ * sized from the merkle-proven offer by the single-sourced `fillOutputPlan`. Present only when `pay` was
124
+ * passed and the offer could be sized. Build EXACTLY these; never the resolver-served outputs a lying
125
+ * resolver could redirect. A partial (min-bearing) offer is left unsized (its running paid/delivered is
126
+ * not provable at this corroboration layer; the wallet's fill-SPV is the payment-grade sizer). */
127
+ outputPlan?: {
128
+ to: string;
129
+ value: bigint;
130
+ }[];
129
131
  }
130
- /** The treasury fee rate stamped on an offer at its creation height (resolve.ts: v11 ? (v16 ? 150 : 100) : 0). */
131
- declare const feeBpsAt: (height: number) => number;
132
- /** true iff any fee/rebate/partial-sizing field of the SERVED offer diverges from the merkle-proven terms `t`
133
- * (fail-closed: any divergence => the caller refuses). Byte-identical to cairnx-core `bindOfferTerms`. */
134
- declare function bindOfferTerms(servedOffer: unknown, t: ProvenOfferTerms): boolean;
132
+
135
133
  /**
136
134
  * Pre-verify an offer on-chain before building a `fillOffer` payment. `light` is a PoW-verifying light client
137
135
  * (its `verifyTxInclusion` surfaces the merkle-proven tx + committed appPayloadHash); `client` reads source txs
@@ -147,11 +145,20 @@ declare function preverifyOffer(opts: {
147
145
  };
148
146
  offerId: string;
149
147
  servedOffer?: unknown;
148
+ /** B7b (REBIND W3/M1): the CSD base units you intend to pay. When given, the result carries `outputPlan`
149
+ * (the PROVEN CSD outputs to build for a WHOLE fill of a non-partial offer, sized by the single-sourced
150
+ * `fillOutputPlan`) and, if you also pass `plannedOutputs`, binds them EXACTLY to that proven plan.
151
+ * A partial (min-bearing) offer is left unsized here; the wallet's fill-SPV is its payment-grade sizer. */
152
+ pay?: bigint | string | number;
153
+ /** B7b: your planned per-address CSD sums (the outputs you WILL build), bound both ways to `outputPlan`.
154
+ * Requires `pay`. A missing/low leg is a doomed underpay; an extra/high leg is a W3 overpay or an N26
155
+ * smuggled output - both refused. */
156
+ plannedOutputs?: Record<string, bigint | string | number>;
150
157
  }): Promise<OfferFillCheck>;
151
158
 
152
159
  /** Stable machine-readable error codes (branch on these, not the message text). */
153
160
  type CairnErrorCode = "NOT_INSTALLED" | "USER_REJECTED" | "WALLET_LOCKED" | "UNSUPPORTED_METHOD" | "HTTP_ERROR" | "CONTENT_VERIFICATION" | "ACCOUNT_CHANGED" | "FIRST_PARTY_ONLY" | "RATE_LIMITED" | "APPROVAL_CLOSED" | "FORBIDDEN" | "INTERNAL" | "GHOST_INPUTS_SKIPPED" | "VERIFY_UNAVAILABLE" | "VERIFY_TAMPER" | "INSUFFICIENT" | "TOO_MANY_INPUTS" | "FEE_TOO_LOW" | "FEE_CAP" | "BAD_FEE" | "ZERO_ADDR_REFUSED" | "BAD_OUTPUTS" | "NO_OUTPUTS" | "BAD_REQUEST" | "OFFER_UNKNOWN" | "FILL_UNSAFE" | "FILL_WRONG_TARGET" | "SOURCE_DIVERGENCE" | "SUBMIT_REJECTED" | "SUBMIT_MAYBE_INFLIGHT" | "SUBMIT_DUPLICATE" | "NOTHING_TO_CONSOLIDATE" | "UNKNOWN";
154
- declare const SDK_VERSION = "0.3.0";
161
+ declare const SDK_VERSION = "0.4.0";
155
162
  interface CairnErrorOptions {
156
163
  code?: CairnErrorCode;
157
164
  docsPath?: string;
@@ -171,7 +178,7 @@ declare class CairnError extends Error {
171
178
  /** Relative docs path (under the SDK docs base), if any. */
172
179
  readonly docsPath?: string;
173
180
  /** The cairn-sdk version that produced this error. */
174
- readonly version = "0.3.0";
181
+ readonly version = "0.4.0";
175
182
  /**
176
183
  * Whether a dApp may safely AUTO-retry this failure. `false` for fund-safety refusals and user-intent
177
184
  * declines (the default); `true` only for provably transient infra faults where nothing was signed.
@@ -312,6 +319,7 @@ declare class Cairn {
312
319
  private _spvLight;
313
320
  private _spvSeeded;
314
321
  private readonly _spvCp;
322
+ private readonly _cairnBase;
315
323
  constructor(config?: CairnConfig);
316
324
  /**
317
325
  * PoW-VERIFIED header merkle at a height (audit M3 — the no-PoW half). Lazily seeds a light
@@ -325,6 +333,21 @@ declare class Cairn {
325
333
  /** The PoW-verifying SPV light client, seeded ONCE from the pinned checkpoint (shared by the M3 header-merkle
326
334
  * bind and the F13 offer pre-verify). Reused across calls; the first call near tip syncs the post-checkpoint span. */
327
335
  private seededSpvLight;
336
+ /**
337
+ * W12 batch header transport for the checkpoint..tip forward sync, backed by the cairn server's
338
+ * `/api/headers/:from/:count`. Carries ZERO trust: the LightClient re-verifies every header's PoW /
339
+ * prev-link / LWMA, so a forged or stale row fails closed and retrying transport proves nothing new.
340
+ * Retry budget: a 429 (per-IP header budget) or 502/503 (a height past the lagging indexer, or the
341
+ * server's 10s deadline) is a transient data-availability hiccup; back off (honouring Retry-After) a
342
+ * bounded number of times, then THROW so `sync()` fails closed. A non-dense range is rejected.
343
+ *
344
+ * B7d / F6 DEPENDENCY: `/api/headers` serves no Access-Control-Allow-Origin today (`corsPublic` is
345
+ * mounted on `/explorer/api`, `/trade/api` and `/api/rpc`, not here). A SAME-ORIGIN dApp (hosted on the
346
+ * cairn base) reaches it now; a CROSS-ORIGIN third-party dApp cannot until B7d adds the CORS mount and
347
+ * restarts the cairn service. This code is correct as written; it becomes reachable cross-origin only
348
+ * once B7d ships. This is a recorded dependency, not a blocker for authoring/testing.
349
+ */
350
+ private fetchHeadersBatch;
328
351
  /**
329
352
  * F13 — corroborate an offer ON-CHAIN before building a `fillOffer` payment's `outputs`. Merkle-proves the
330
353
  * offer into the PoW-verified header chain (from the pinned SPV checkpoint), binds the record to its on-chain
@@ -349,4 +372,4 @@ declare class Cairn {
349
372
  get connected(): boolean;
350
373
  }
351
374
 
352
- export { BoardClient, Cairn, type CairnBaseUrls, type CairnConfig, CairnError, type CairnErrorCode, type CairnErrorOptions, Chain, ContentClient, ContentVerificationError, DEFAULT_SPV_CHECKPOINT, DetectOptions, FetchLike, Http, HttpError, IndexerClient, type NameDetail, type NameResolution, NamesClient, NotInstalledError, type OfferFillCheck, type PrimaryName, type ProvenOfferTerms, RegistryClient, SDK_VERSION, SubmitInFlightError, type TokenInfo, UnsupportedMethodError, UserRejectedError, WalletConnection, WalletLockedError, bindOfferTerms, errorCode, feeBpsAt, mapProviderError, mapSubmitResultError, preverifyOffer };
375
+ export { BoardClient, Cairn, type CairnBaseUrls, type CairnConfig, CairnError, type CairnErrorCode, type CairnErrorOptions, Chain, ContentClient, ContentVerificationError, DEFAULT_SPV_CHECKPOINT, DetectOptions, FetchLike, Http, HttpError, IndexerClient, type NameDetail, type NameResolution, NamesClient, NotInstalledError, type OfferFillCheck, type PrimaryName, RegistryClient, SDK_VERSION, SubmitInFlightError, type TokenInfo, UnsupportedMethodError, UserRejectedError, WalletConnection, WalletLockedError, errorCode, mapProviderError, mapSubmitResultError, preverifyOffer };