@inversealtruism/cairn-sdk 0.3.2 → 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
@@ -46,16 +46,21 @@ __export(index_exports, {
46
46
  discoverProviders: () => discoverProviders,
47
47
  errorCode: () => errorCode,
48
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.2";
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",
@@ -1015,6 +1020,7 @@ var NamesClient = class {
1015
1020
  };
1016
1021
 
1017
1022
  // src/connect.ts
1023
+ var DEFAULT_PROVIDER_TIMEOUT_MS = 10 * 6e4;
1018
1024
  function discoverProviders(opts = {}) {
1019
1025
  const timeoutMs = opts.timeoutMs ?? 1e3;
1020
1026
  return new Promise((resolve) => {
@@ -1094,8 +1100,24 @@ function unwrapWrite(reply) {
1094
1100
  var WalletConnection = class {
1095
1101
  provider;
1096
1102
  _addr = null;
1097
- 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 = {}) {
1098
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));
1099
1121
  }
1100
1122
  /** The wallet's version string (e.g. "0.2.x"). */
1101
1123
  get version() {
@@ -1107,13 +1129,13 @@ var WalletConnection = class {
1107
1129
  }
1108
1130
  /** Request connection. First time per origin prompts the user; afterwards may be silent. */
1109
1131
  async connect() {
1110
- const { addr } = unwrap(await this.provider.connect());
1132
+ const { addr } = unwrap(await this.call(() => this.provider.connect(), "connect"));
1111
1133
  this._addr = addr;
1112
1134
  return addr;
1113
1135
  }
1114
1136
  /** Get the connected address (does not force a new connection prompt if already consented). */
1115
1137
  async getAddress() {
1116
- const { addr } = unwrap(await this.provider.getAddress());
1138
+ const { addr } = unwrap(await this.call(() => this.provider.getAddress(), "getAddress"));
1117
1139
  this._addr = addr;
1118
1140
  return addr;
1119
1141
  }
@@ -1126,7 +1148,7 @@ var WalletConnection = class {
1126
1148
  if (typeof this.provider.signIn !== "function") {
1127
1149
  return Promise.reject(new UnsupportedMethodError("this wallet does not expose signIn()"));
1128
1150
  }
1129
- return Promise.resolve(this.provider.signIn()).then(unwrap);
1151
+ return this.call(() => this.provider.signIn(), "signIn").then(unwrap);
1130
1152
  }
1131
1153
  /**
1132
1154
  * Audience-bound "Sign in with CSD" (SIWC) — the secure, replay-resistant third-party login.
@@ -1142,7 +1164,7 @@ var WalletConnection = class {
1142
1164
  if (typeof this.provider.signInWithCsd !== "function") {
1143
1165
  return Promise.reject(new UnsupportedMethodError("this wallet predates Sign-in-with-CSD \u2014 ask the user to update the Cairn Wallet"));
1144
1166
  }
1145
- return Promise.resolve(this.provider.signInWithCsd(params)).then(unwrap);
1167
+ return this.call(() => this.provider.signInWithCsd(params), "signInWithCsd").then(unwrap);
1146
1168
  }
1147
1169
  /** True if this wallet supports audience-bound SIWC (safe to call `signInWithCsd`). */
1148
1170
  get supportsSiwc() {
@@ -1152,7 +1174,7 @@ var WalletConnection = class {
1152
1174
  async getCapabilities() {
1153
1175
  if (typeof this.provider.getCapabilities !== "function") return null;
1154
1176
  try {
1155
- return await this.provider.getCapabilities();
1177
+ return await this.call(() => this.provider.getCapabilities(), "getCapabilities");
1156
1178
  } catch {
1157
1179
  return null;
1158
1180
  }
@@ -1168,24 +1190,24 @@ var WalletConnection = class {
1168
1190
  if (typeof this.provider.fillOffer !== "function") {
1169
1191
  return Promise.reject(new UnsupportedMethodError("this wallet predates fillOffer() \u2014 ask the user to update the Cairn Wallet"));
1170
1192
  }
1171
- return Promise.resolve(this.provider.fillOffer(params)).then(unwrapWrite);
1193
+ return this.call(() => this.provider.fillOffer(params), "fillOffer").then(unwrapWrite);
1172
1194
  }
1173
1195
  /** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
1174
1196
  async getPermissions() {
1175
1197
  if (typeof this.provider.getPermissions !== "function") return [];
1176
- return unwrap(await this.provider.getPermissions());
1198
+ return unwrap(await this.call(() => this.provider.getPermissions(), "getPermissions"));
1177
1199
  }
1178
1200
  /** Explicitly request permission to see the address (prompts, like connect). */
1179
1201
  requestPermissions() {
1180
1202
  if (typeof this.provider.requestPermissions !== "function") {
1181
1203
  return Promise.reject(new UnsupportedMethodError("this wallet predates requestPermissions \u2014 use connect()"));
1182
1204
  }
1183
- return Promise.resolve(this.provider.requestPermissions()).then(unwrap);
1205
+ return this.call(() => this.provider.requestPermissions(), "requestPermissions").then(unwrap);
1184
1206
  }
1185
1207
  /** Revoke THIS origin's own access (silent). Returns { revoked }. Safe no-op on older wallets. */
1186
1208
  async revokePermissions() {
1187
1209
  if (typeof this.provider.revokePermissions !== "function") return { revoked: false };
1188
- return unwrap(await this.provider.revokePermissions());
1210
+ return unwrap(await this.call(() => this.provider.revokePermissions(), "revokePermissions"));
1189
1211
  }
1190
1212
  /**
1191
1213
  * Subscribe to provider events. `accountsChanged` fires with `[]` when this site loses access
@@ -1201,26 +1223,26 @@ var WalletConnection = class {
1201
1223
  }
1202
1224
  /** Send CSD (always prompts with clear-signing; wallet picks inputs + returns change to itself). */
1203
1225
  send(params) {
1204
- return Promise.resolve(this.provider.send(params)).then(unwrapWrite);
1226
+ return this.call(() => this.provider.send(params), "send").then(unwrapWrite);
1205
1227
  }
1206
1228
  /** Low-level Propose (always prompts). Prefer `cairn.board.propose()` which computes the hash/uri/expiry. */
1207
1229
  propose(params) {
1208
- return Promise.resolve(this.provider.propose(params)).then(unwrapWrite);
1230
+ return this.call(() => this.provider.propose(params), "propose").then(unwrapWrite);
1209
1231
  }
1210
1232
  /** Attest/support a proposal (always prompts). */
1211
1233
  attest(params) {
1212
- return Promise.resolve(this.provider.attest(params)).then(unwrapWrite);
1234
+ return this.call(() => this.provider.attest(params), "attest").then(unwrapWrite);
1213
1235
  }
1214
1236
  /** Seal a commit-reveal claim (always prompts; salt stays inside the wallet). */
1215
1237
  sealClaim(params) {
1216
- return Promise.resolve(this.provider.sealClaim(params)).then(unwrapWrite);
1238
+ return this.call(() => this.provider.sealClaim(params), "sealClaim").then(unwrapWrite);
1217
1239
  }
1218
1240
  /** Reveal a previously sealed claim by its sealing txid (always prompts). */
1219
1241
  revealClaim(txid3) {
1220
1242
  if (typeof txid3 !== "string" || !txid3) {
1221
1243
  return Promise.reject(new UnsupportedMethodError("revealClaim requires the sealing txid"));
1222
1244
  }
1223
- return Promise.resolve(this.provider.revealClaim(txid3)).then(unwrapWrite);
1245
+ return this.call(() => this.provider.revealClaim(txid3), "revealClaim").then(unwrapWrite);
1224
1246
  }
1225
1247
  };
1226
1248
  async function getWallet(opts = {}) {
@@ -1239,6 +1261,23 @@ var import_cairnx_core2 = require("@inversealtruism/cairnx-core");
1239
1261
  var ADDR = /^0x[0-9a-f]{40}$/;
1240
1262
  var HASH = /^0x[0-9a-f]{64}$/;
1241
1263
  var COINBASE = "0x" + "00".repeat(32);
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;
1280
+ }
1242
1281
  async function provenAuthor(client, provenTx) {
1243
1282
  const in0 = provenTx.inputs?.[0];
1244
1283
  if (!in0 || String(in0.prevTxid).toLowerCase() === COINBASE && Number(in0.vout) === 4294967295) return null;
@@ -1294,17 +1333,35 @@ async function preverifyOffer(opts) {
1294
1333
  return { ok: false, trust: "unverified", reason: "the offer record doesn't match the on-chain (merkle-proven) commitment" };
1295
1334
  const seller = await provenAuthor(opts.client, provenTx);
1296
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;
1297
1337
  const w = rec.want ?? {};
1298
1338
  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 };
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 } : {} };
1308
1365
  if (opts.servedOffer !== void 0 && opts.servedOffer !== null) {
1309
1366
  const so = opts.servedOffer;
1310
1367
  const servedPayto = so?.want?.payto !== void 0 && so?.want?.payto !== null ? String(so.want.payto).toLowerCase() : "";
@@ -1312,9 +1369,10 @@ async function preverifyOffer(opts) {
1312
1369
  return { ok: false, trust: "verified", reason: "the served payment recipient doesn't match the offer's on-chain author/record", ...base };
1313
1370
  if (so?.seller !== void 0 && so?.seller !== null && String(so.seller).toLowerCase() !== seller)
1314
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 };
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 };
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 };
1317
1374
  }
1375
+ if (sumsMismatch) return { ok: false, trust: "verified", reason: sumsMismatch, ...base };
1318
1376
  return { ok: true, trust: "verified", ...base };
1319
1377
  }
1320
1378
 
@@ -1410,6 +1468,10 @@ var CairnController = class {
1410
1468
  // src/index.ts
1411
1469
  var DEFAULT_SPV_CHECKPOINT = { height: 38142, hash: "0x00000000000140f023cc0ee1457a40833f2fcb4de44291b9d373e50f17b97232" };
1412
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));
1413
1475
  var Cairn = class {
1414
1476
  config;
1415
1477
  chain;
@@ -1425,6 +1487,8 @@ var Cairn = class {
1425
1487
  _spvLight = null;
1426
1488
  _spvSeeded = false;
1427
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;
1428
1492
  constructor(config = {}) {
1429
1493
  this.config = config;
1430
1494
  const fetch = config.fetch;
@@ -1433,6 +1497,7 @@ var Cairn = class {
1433
1497
  const rpc = config.baseUrls?.rpc ?? `${cairn}/api/rpc`;
1434
1498
  const indexer = (config.baseUrls?.indexer ?? `${cairn}/explorer/api`).replace(/\/+$/, "");
1435
1499
  this.cairnHttp = new Http({ baseUrl: cairn, fetch, timeoutMs });
1500
+ this._cairnBase = cairn;
1436
1501
  const indexerHttp = new Http({ baseUrl: indexer, fetch, timeoutMs });
1437
1502
  const swarmHttp = config.baseUrls?.swarm ? new Http({ baseUrl: config.baseUrls.swarm.replace(/\/+$/, ""), fetch, timeoutMs }) : void 0;
1438
1503
  this._spvCp = config.spvCheckpoint ?? DEFAULT_SPV_CHECKPOINT;
@@ -1469,13 +1534,69 @@ var Cairn = class {
1469
1534
  /** The PoW-verifying SPV light client, seeded ONCE from the pinned checkpoint (shared by the M3 header-merkle
1470
1535
  * bind and the F13 offer pre-verify). Reused across calls; the first call near tip syncs the post-checkpoint span. */
1471
1536
  async seededSpvLight() {
1472
- 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
+ });
1473
1542
  if (!this._spvSeeded) {
1474
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
+ }
1475
1550
  this._spvSeeded = true;
1476
1551
  }
1477
1552
  return this._spvLight;
1478
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
+ }
1479
1600
  /**
1480
1601
  * F13 — corroborate an offer ON-CHAIN before building a `fillOffer` payment's `outputs`. Merkle-proves the
1481
1602
  * offer into the PoW-verified header chain (from the pinned SPV checkpoint), binds the record to its on-chain
@@ -1539,9 +1660,14 @@ var Cairn = class {
1539
1660
  discoverProviders,
1540
1661
  errorCode,
1541
1662
  feeBpsAt,
1663
+ fillEndorsement,
1664
+ fillIsSafe,
1665
+ fillOutputPlan,
1542
1666
  getWallet,
1543
1667
  isInstalled,
1544
1668
  mapProviderError,
1545
1669
  mapSubmitResultError,
1546
- preverifyOffer
1670
+ preverifyOffer,
1671
+ previewFill,
1672
+ requiredFillOutputs
1547
1673
  });
package/dist/index.d.cts CHANGED
@@ -10,9 +10,10 @@ 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
- export { bindOfferTerms, feeBpsAt } from '@inversealtruism/cairnx-core';
16
17
  import '@inversealtruism/csd-client';
17
18
  import '@inversealtruism/csd-tx';
18
19
  import '@inversealtruism/csd-codec';
@@ -105,15 +106,6 @@ declare class NamesClient {
105
106
  state(): Promise<unknown>;
106
107
  }
107
108
 
108
- /** The fee/rebate-relevant fields of an offer, derived from the MERKLE-PROVEN offer (never a served object). */
109
- interface ProvenOfferTerms {
110
- height: number;
111
- feeBps: number;
112
- value?: string;
113
- taker?: string;
114
- bid?: string;
115
- min?: string;
116
- }
117
109
  /** The trust-labeled verdict. `ok:false` + `transient:true` = a retryable chain-catching-up soft-fail (NOT a
118
110
  * proven mismatch). `payto`/`seller`/`terms` are populated whenever they could be merkle-derived. */
119
111
  interface OfferFillCheck {
@@ -127,6 +119,15 @@ interface OfferFillCheck {
127
119
  seller?: string;
128
120
  terms?: ProvenOfferTerms;
129
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
+ }[];
130
131
  }
131
132
 
132
133
  /**
@@ -144,11 +145,20 @@ declare function preverifyOffer(opts: {
144
145
  };
145
146
  offerId: string;
146
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>;
147
157
  }): Promise<OfferFillCheck>;
148
158
 
149
159
  /** Stable machine-readable error codes (branch on these, not the message text). */
150
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";
151
- declare const SDK_VERSION = "0.3.2";
161
+ declare const SDK_VERSION = "0.4.0";
152
162
  interface CairnErrorOptions {
153
163
  code?: CairnErrorCode;
154
164
  docsPath?: string;
@@ -168,7 +178,7 @@ declare class CairnError extends Error {
168
178
  /** Relative docs path (under the SDK docs base), if any. */
169
179
  readonly docsPath?: string;
170
180
  /** The cairn-sdk version that produced this error. */
171
- readonly version = "0.3.2";
181
+ readonly version = "0.4.0";
172
182
  /**
173
183
  * Whether a dApp may safely AUTO-retry this failure. `false` for fund-safety refusals and user-intent
174
184
  * declines (the default); `true` only for provably transient infra faults where nothing was signed.
@@ -309,6 +319,7 @@ declare class Cairn {
309
319
  private _spvLight;
310
320
  private _spvSeeded;
311
321
  private readonly _spvCp;
322
+ private readonly _cairnBase;
312
323
  constructor(config?: CairnConfig);
313
324
  /**
314
325
  * PoW-VERIFIED header merkle at a height (audit M3 — the no-PoW half). Lazily seeds a light
@@ -322,6 +333,21 @@ declare class Cairn {
322
333
  /** The PoW-verifying SPV light client, seeded ONCE from the pinned checkpoint (shared by the M3 header-merkle
323
334
  * bind and the F13 offer pre-verify). Reused across calls; the first call near tip syncs the post-checkpoint span. */
324
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;
325
351
  /**
326
352
  * F13 — corroborate an offer ON-CHAIN before building a `fillOffer` payment's `outputs`. Merkle-proves the
327
353
  * offer into the PoW-verified header chain (from the pinned SPV checkpoint), binds the record to its on-chain
@@ -346,4 +372,4 @@ declare class Cairn {
346
372
  get connected(): boolean;
347
373
  }
348
374
 
349
- 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, errorCode, 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 };
package/dist/index.d.ts CHANGED
@@ -10,9 +10,10 @@ export { ContentClientOptions, PreparedContent } from './content.js';
10
10
  import { RegistryClient } from './registry.js';
11
11
  import { WalletConnection, DetectOptions } from './connect.js';
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.js';
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.js';
15
- export { bindOfferTerms, feeBpsAt } from '@inversealtruism/cairnx-core';
16
17
  import '@inversealtruism/csd-client';
17
18
  import '@inversealtruism/csd-tx';
18
19
  import '@inversealtruism/csd-codec';
@@ -105,15 +106,6 @@ declare class NamesClient {
105
106
  state(): Promise<unknown>;
106
107
  }
107
108
 
108
- /** The fee/rebate-relevant fields of an offer, derived from the MERKLE-PROVEN offer (never a served object). */
109
- interface ProvenOfferTerms {
110
- height: number;
111
- feeBps: number;
112
- value?: string;
113
- taker?: string;
114
- bid?: string;
115
- min?: string;
116
- }
117
109
  /** The trust-labeled verdict. `ok:false` + `transient:true` = a retryable chain-catching-up soft-fail (NOT a
118
110
  * proven mismatch). `payto`/`seller`/`terms` are populated whenever they could be merkle-derived. */
119
111
  interface OfferFillCheck {
@@ -127,6 +119,15 @@ interface OfferFillCheck {
127
119
  seller?: string;
128
120
  terms?: ProvenOfferTerms;
129
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
+ }[];
130
131
  }
131
132
 
132
133
  /**
@@ -144,11 +145,20 @@ declare function preverifyOffer(opts: {
144
145
  };
145
146
  offerId: string;
146
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>;
147
157
  }): Promise<OfferFillCheck>;
148
158
 
149
159
  /** Stable machine-readable error codes (branch on these, not the message text). */
150
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";
151
- declare const SDK_VERSION = "0.3.2";
161
+ declare const SDK_VERSION = "0.4.0";
152
162
  interface CairnErrorOptions {
153
163
  code?: CairnErrorCode;
154
164
  docsPath?: string;
@@ -168,7 +178,7 @@ declare class CairnError extends Error {
168
178
  /** Relative docs path (under the SDK docs base), if any. */
169
179
  readonly docsPath?: string;
170
180
  /** The cairn-sdk version that produced this error. */
171
- readonly version = "0.3.2";
181
+ readonly version = "0.4.0";
172
182
  /**
173
183
  * Whether a dApp may safely AUTO-retry this failure. `false` for fund-safety refusals and user-intent
174
184
  * declines (the default); `true` only for provably transient infra faults where nothing was signed.
@@ -309,6 +319,7 @@ declare class Cairn {
309
319
  private _spvLight;
310
320
  private _spvSeeded;
311
321
  private readonly _spvCp;
322
+ private readonly _cairnBase;
312
323
  constructor(config?: CairnConfig);
313
324
  /**
314
325
  * PoW-VERIFIED header merkle at a height (audit M3 — the no-PoW half). Lazily seeds a light
@@ -322,6 +333,21 @@ declare class Cairn {
322
333
  /** The PoW-verifying SPV light client, seeded ONCE from the pinned checkpoint (shared by the M3 header-merkle
323
334
  * bind and the F13 offer pre-verify). Reused across calls; the first call near tip syncs the post-checkpoint span. */
324
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;
325
351
  /**
326
352
  * F13 — corroborate an offer ON-CHAIN before building a `fillOffer` payment's `outputs`. Merkle-proves the
327
353
  * offer into the PoW-verified header chain (from the pinned SPV checkpoint), binds the record to its on-chain
@@ -346,4 +372,4 @@ declare class Cairn {
346
372
  get connected(): boolean;
347
373
  }
348
374
 
349
- 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, errorCode, 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 };