@inversealtruism/cairn-sdk 0.1.1 → 0.1.3

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
@@ -22,6 +22,7 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  BoardClient: () => BoardClient,
24
24
  Cairn: () => Cairn,
25
+ CairnController: () => CairnController,
25
26
  CairnError: () => CairnError,
26
27
  Chain: () => Chain,
27
28
  ContentClient: () => ContentClient,
@@ -31,42 +32,77 @@ __export(index_exports, {
31
32
  IndexerClient: () => IndexerClient,
32
33
  NotInstalledError: () => NotInstalledError,
33
34
  RegistryClient: () => RegistryClient,
35
+ SDK_VERSION: () => SDK_VERSION,
34
36
  UnsupportedMethodError: () => UnsupportedMethodError,
35
37
  UserRejectedError: () => UserRejectedError,
36
38
  WalletConnection: () => WalletConnection,
37
39
  WalletLockedError: () => WalletLockedError,
38
40
  connect: () => connect,
39
41
  detectProvider: () => detectProvider,
42
+ discoverProviders: () => discoverProviders,
43
+ errorCode: () => errorCode,
40
44
  getWallet: () => getWallet,
41
- isInstalled: () => isInstalled
45
+ isInstalled: () => isInstalled,
46
+ mapProviderError: () => mapProviderError
42
47
  });
43
48
  module.exports = __toCommonJS(index_exports);
44
49
 
45
50
  // src/errors.ts
51
+ var SDK_VERSION = "0.1.2";
52
+ var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
46
53
  var CairnError = class extends Error {
47
- constructor(message) {
48
- super(message);
54
+ /** Stable machine code for programmatic branching. */
55
+ code;
56
+ /** The human message without the appended docs/version footer. */
57
+ shortMessage;
58
+ /** Relative docs path (under the SDK docs base), if any. */
59
+ docsPath;
60
+ /** The cairn-sdk version that produced this error. */
61
+ version = SDK_VERSION;
62
+ constructor(shortMessage, opts = {}) {
63
+ const docs = opts.docsPath;
64
+ super(
65
+ [shortMessage, docs ? `Docs: ${DOCS_BASE}${docs}` : "", `Version: cairn-sdk@${SDK_VERSION}`].filter(Boolean).join("\n"),
66
+ { cause: opts.cause }
67
+ );
49
68
  this.name = new.target.name;
69
+ this.code = opts.code ?? "UNKNOWN";
70
+ this.shortMessage = shortMessage;
71
+ this.docsPath = docs;
72
+ }
73
+ /**
74
+ * Walk the `cause` chain. With a predicate, returns the FIRST matching error (or null). Without one,
75
+ * returns the DEEPEST cause (the root). Mirrors viem's `BaseError.walk()`.
76
+ */
77
+ walk(fn) {
78
+ let current = this;
79
+ let last = this;
80
+ while (current != null) {
81
+ if (fn && fn(current)) return current;
82
+ last = current;
83
+ current = current?.cause;
84
+ }
85
+ return fn ? null : last;
50
86
  }
51
87
  };
52
88
  var NotInstalledError = class extends CairnError {
53
89
  constructor(message = "Cairn Wallet not detected. Install it from cairn-substrate.com.") {
54
- super(message);
90
+ super(message, { code: "NOT_INSTALLED", docsPath: "/install" });
55
91
  }
56
92
  };
57
93
  var UserRejectedError = class extends CairnError {
58
94
  constructor(message = "Request rejected by user.") {
59
- super(message);
95
+ super(message, { code: "USER_REJECTED", docsPath: "/errors#user-rejected" });
60
96
  }
61
97
  };
62
98
  var WalletLockedError = class extends CairnError {
63
99
  constructor(message = "Wallet is locked.") {
64
- super(message);
100
+ super(message, { code: "WALLET_LOCKED", docsPath: "/errors#wallet-locked" });
65
101
  }
66
102
  };
67
103
  var UnsupportedMethodError = class extends CairnError {
68
104
  constructor(message = "Method not supported by the wallet.") {
69
- super(message);
105
+ super(message, { code: "UNSUPPORTED_METHOD", docsPath: "/errors#unsupported-method" });
70
106
  }
71
107
  };
72
108
  var HttpError = class extends CairnError {
@@ -74,7 +110,7 @@ var HttpError = class extends CairnError {
74
110
  url;
75
111
  body;
76
112
  constructor(status, url, body) {
77
- super(`HTTP ${status} for ${url}${body ? `: ${String(body).slice(0, 200)}` : ""}`);
113
+ super(`HTTP ${status} for ${url}${body ? `: ${String(body).slice(0, 200)}` : ""}`, { code: "HTTP_ERROR" });
78
114
  this.status = status;
79
115
  this.url = url;
80
116
  this.body = body;
@@ -83,7 +119,7 @@ var HttpError = class extends CairnError {
83
119
  var ContentVerificationError = class extends CairnError {
84
120
  expected;
85
121
  constructor(expected) {
86
- super(`Content does not match its payload hash ${expected} \u2014 bytes were tampered with in transit.`);
122
+ super(`Content does not match its payload hash ${expected} \u2014 bytes were tampered with in transit.`, { code: "CONTENT_VERIFICATION", docsPath: "/content#verification" });
87
123
  this.expected = expected;
88
124
  }
89
125
  };
@@ -91,9 +127,12 @@ function mapProviderError(error) {
91
127
  const e = String(error || "").toLowerCase();
92
128
  if (e.includes("rejected by user")) return new UserRejectedError(error);
93
129
  if (e.includes("wallet locked")) return new WalletLockedError(error);
94
- if (e.includes("unsupported dapp method")) return new UnsupportedMethodError(error);
130
+ if (e.includes("unsupported dapp method") || e.includes("predates") || e.includes("too old")) return new UnsupportedMethodError(error);
95
131
  return new CairnError(error || "Unknown wallet error");
96
132
  }
133
+ function errorCode(err) {
134
+ return err instanceof CairnError ? err.code : "UNKNOWN";
135
+ }
97
136
 
98
137
  // src/http.ts
99
138
  var Http = class {
@@ -175,6 +214,8 @@ var import_csd_crypto = require("@inversealtruism/csd-crypto");
175
214
  var import_csd_light = require("@inversealtruism/csd-light");
176
215
  var import_csd_client2 = require("@inversealtruism/csd-client");
177
216
  var import_csd_light2 = require("@inversealtruism/csd-light");
217
+ var import_csd_tx2 = require("@inversealtruism/csd-tx");
218
+ var import_csd_crypto2 = require("@inversealtruism/csd-crypto");
178
219
  var Chain = class {
179
220
  client;
180
221
  opts;
@@ -195,6 +236,29 @@ var Chain = class {
195
236
  submit(nodeJsonTx) {
196
237
  return this.client.submit(nodeJsonTx);
197
238
  }
239
+ /**
240
+ * Build + sign (+ optionally broadcast) a transfer with INPUT-VALUE VERIFICATION (the H2-safe send).
241
+ * Fetches the signer's UTXOs, selects, then re-verifies each selected input's REAL value against the
242
+ * chain (`verifyInputValues` recomputes each source tx's txid, so a lying RPC can't under-report) and
243
+ * computes change from the VERIFIED total — closing the UTXO-VALUE-1 implicit-fee-burn. Fail-closed:
244
+ * if any source can't be verified it signs nothing. Prefer this over the pure `buildSend`. Pass
245
+ * `broadcast:false` to get the signed tx back without submitting.
246
+ */
247
+ async send(p) {
248
+ const from = (0, import_csd_crypto2.addrFromPriv)(p.priv);
249
+ const u = await this.client.utxos(from);
250
+ const built = await (0, import_csd_tx2.buildSendVerified)({
251
+ outputs: p.outputs,
252
+ fee: p.fee,
253
+ utxos: u.utxos,
254
+ priv: p.priv,
255
+ maxFee: p.maxFee,
256
+ verify: (inputs) => (0, import_csd_client2.verifyInputValues)(this.client, inputs)
257
+ });
258
+ if (!built.ok || p.broadcast === false) return built;
259
+ const submitResult = await this.client.submit(built.nodeJson);
260
+ return { ...built, submitResult };
261
+ }
198
262
  /** A verifying light client over the same RPC (built once, reused). */
199
263
  light() {
200
264
  if (!this._light) {
@@ -437,9 +501,12 @@ var IndexerClient = class {
437
501
  /**
438
502
  * Verify a tx is included in a block by folding its merkle branch to the root
439
503
  * with csd-codec.verifyMerkleProof. If `headerMerkleAt` was provided, the
440
- * proof's root is cross-checked against the ON-CHAIN header merkle at that
441
- * height → trustLevel "verified-inclusion". Otherwise the proof is only
442
- * internally consistent → "proof-consistent" (still useful, but the indexer is trusted).
504
+ * proof's root is cross-checked against the header merkle at that height →
505
+ * trustLevel "verified-inclusion" (only as trustworthy as that header source —
506
+ * see headerMerkleAt). Otherwise the proof is only internally consistent →
507
+ * "proof-consistent" (still useful, but the indexer is trusted). The Cairn
508
+ * facade wires headerMerkleAt ONLY when the node RPC is an independent origin
509
+ * from the indexer, so the default same-origin setup stays at "proof-consistent".
443
510
  */
444
511
  async verifyInclusion(txid2) {
445
512
  let proof;
@@ -689,6 +756,26 @@ var RegistryClient = class {
689
756
  };
690
757
 
691
758
  // src/connect.ts
759
+ function discoverProviders(opts = {}) {
760
+ const timeoutMs = opts.timeoutMs ?? 1e3;
761
+ return new Promise((resolve) => {
762
+ if (typeof window === "undefined") {
763
+ resolve([]);
764
+ return;
765
+ }
766
+ const found = /* @__PURE__ */ new Map();
767
+ const onAnnounce = (ev) => {
768
+ const d = ev?.detail;
769
+ if (d?.info?.uuid && d?.provider?.isCairn) found.set(d.info.uuid, d);
770
+ };
771
+ window.addEventListener("csd:announceProvider", onAnnounce);
772
+ window.dispatchEvent(new CustomEvent("csd:requestProvider"));
773
+ setTimeout(() => {
774
+ window.removeEventListener("csd:announceProvider", onAnnounce);
775
+ resolve([...found.values()]);
776
+ }, timeoutMs);
777
+ });
778
+ }
692
779
  function detectProvider(opts = {}) {
693
780
  const timeoutMs = opts.timeoutMs ?? 3e3;
694
781
  return new Promise((resolve, reject) => {
@@ -696,18 +783,19 @@ function detectProvider(opts = {}) {
696
783
  reject(new NotInstalledError("Not running in a browser \u2014 `window.cairn` is unavailable."));
697
784
  return;
698
785
  }
699
- if (window.cairn?.isCairn) {
700
- resolve(window.cairn);
701
- return;
702
- }
703
786
  let done = false;
704
- const onInit = () => {
787
+ const finish = (p) => {
705
788
  if (done) return;
706
- if (window.cairn?.isCairn) {
707
- done = true;
708
- cleanup();
709
- resolve(window.cairn);
710
- }
789
+ done = true;
790
+ cleanup();
791
+ resolve(p);
792
+ };
793
+ const onInit = () => {
794
+ if (!done && window.cairn?.isCairn) finish(window.cairn);
795
+ };
796
+ const onAnnounce = (ev) => {
797
+ const p = ev?.detail?.provider;
798
+ if (p?.isCairn) finish(p);
711
799
  };
712
800
  const timer = setTimeout(() => {
713
801
  if (done) return;
@@ -718,8 +806,15 @@ function detectProvider(opts = {}) {
718
806
  const cleanup = () => {
719
807
  clearTimeout(timer);
720
808
  window.removeEventListener("cairn#initialized", onInit);
809
+ window.removeEventListener("csd:announceProvider", onAnnounce);
721
810
  };
722
811
  window.addEventListener("cairn#initialized", onInit);
812
+ window.addEventListener("csd:announceProvider", onAnnounce);
813
+ if (window.cairn?.isCairn) {
814
+ finish(window.cairn);
815
+ return;
816
+ }
817
+ window.dispatchEvent(new CustomEvent("csd:requestProvider"));
723
818
  onInit();
724
819
  });
725
820
  }
@@ -756,10 +851,76 @@ var WalletConnection = class {
756
851
  this._addr = addr;
757
852
  return addr;
758
853
  }
759
- /** Passwordless sign-in: a signature over a login nonce (always prompts; structurally cannot sign a tx). */
854
+ /**
855
+ * @deprecated First-party-only legacy login (it authenticates against the wallet's own configured
856
+ * server and is rejected from third-party origins). Use {@link signInWithCsd} for any site.
857
+ * Passwordless sign-in: a signature over a login nonce (always prompts; structurally cannot sign a tx).
858
+ */
760
859
  signIn() {
761
860
  return Promise.resolve(this.provider.signIn()).then(unwrap);
762
861
  }
862
+ /**
863
+ * Audience-bound "Sign in with CSD" (SIWC) — the secure, replay-resistant third-party login.
864
+ * The wallet binds the REAL requesting origin + your single-use server `nonce` into the signed
865
+ * message and returns the artifact; it never mints a session. VERIFY the result SERVER-SIDE with
866
+ * `verifySiwc` from `@inversealtruism/csd-siwc` (domain/nonce/chain/time/signature/account), then
867
+ * issue your OWN session. The signature is proof-of-control, NOT a bearer token. Always prompts.
868
+ */
869
+ signInWithCsd(params) {
870
+ if (!params || typeof params.nonce !== "string" || !params.nonce) {
871
+ return Promise.reject(new UnsupportedMethodError("signInWithCsd requires a server-issued { nonce }"));
872
+ }
873
+ if (typeof this.provider.signInWithCsd !== "function") {
874
+ return Promise.reject(new UnsupportedMethodError("this wallet predates Sign-in-with-CSD \u2014 ask the user to update the Cairn Wallet"));
875
+ }
876
+ return Promise.resolve(this.provider.signInWithCsd(params)).then(unwrap);
877
+ }
878
+ /** True if this wallet supports audience-bound SIWC (safe to call `signInWithCsd`). */
879
+ get supportsSiwc() {
880
+ return typeof this.provider.signInWithCsd === "function";
881
+ }
882
+ /** Feature/capability detection. Returns null if the wallet predates `getCapabilities`. */
883
+ async getCapabilities() {
884
+ if (typeof this.provider.getCapabilities !== "function") return null;
885
+ try {
886
+ return await this.provider.getCapabilities();
887
+ } catch {
888
+ return null;
889
+ }
890
+ }
891
+ /** Atomic fill (CairnX DvP): pay + attest in ONE tx. Always prompts with clear-signing. */
892
+ fillOffer(params) {
893
+ return Promise.resolve(this.provider.fillOffer(params)).then(unwrap);
894
+ }
895
+ /** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
896
+ async getPermissions() {
897
+ if (typeof this.provider.getPermissions !== "function") return [];
898
+ return unwrap(await this.provider.getPermissions());
899
+ }
900
+ /** Explicitly request permission to see the address (prompts, like connect). */
901
+ requestPermissions() {
902
+ if (typeof this.provider.requestPermissions !== "function") {
903
+ return Promise.reject(new UnsupportedMethodError("this wallet predates requestPermissions \u2014 use connect()"));
904
+ }
905
+ return Promise.resolve(this.provider.requestPermissions()).then(unwrap);
906
+ }
907
+ /** Revoke THIS origin's own access (silent). Returns { revoked }. Safe no-op on older wallets. */
908
+ async revokePermissions() {
909
+ if (typeof this.provider.revokePermissions !== "function") return { revoked: false };
910
+ return unwrap(await this.provider.revokePermissions());
911
+ }
912
+ /**
913
+ * Subscribe to provider events. `accountsChanged` fires with `[]` when this site loses access
914
+ * (wallet lock / account switch / revoke) — re-call connect() to (re)share; `disconnect` likewise.
915
+ * No-op if the wallet predates events. Use the SAME handler reference with off() to unsubscribe.
916
+ */
917
+ on(event, handler) {
918
+ this.provider.on?.(event, handler);
919
+ }
920
+ /** Remove a previously-added event handler (must be the same reference passed to on()). */
921
+ off(event, handler) {
922
+ this.provider.removeListener?.(event, handler);
923
+ }
763
924
  /** Send CSD (always prompts with clear-signing; wallet picks inputs + returns change to itself). */
764
925
  send(params) {
765
926
  return Promise.resolve(this.provider.send(params)).then(unwrap);
@@ -793,7 +954,79 @@ async function connect(opts = {}) {
793
954
  return { wallet, address };
794
955
  }
795
956
 
957
+ // src/controller.ts
958
+ var DISCONNECTED = Object.freeze({ status: "disconnected", account: null, error: null });
959
+ var CairnController = class {
960
+ state = DISCONNECTED;
961
+ conn = null;
962
+ listeners = /* @__PURE__ */ new Set();
963
+ getWalletFn;
964
+ detectOpts;
965
+ constructor(opts = {}) {
966
+ const { getWallet: gw, ...detect } = opts;
967
+ this.getWalletFn = gw ?? getWallet;
968
+ this.detectOpts = detect;
969
+ }
970
+ /** useSyncExternalStore-compatible. Returns an unsubscribe fn. */
971
+ subscribe = (cb) => {
972
+ this.listeners.add(cb);
973
+ return () => {
974
+ this.listeners.delete(cb);
975
+ };
976
+ };
977
+ /** useSyncExternalStore-compatible snapshot — stable reference until the state actually changes. */
978
+ getSnapshot = () => this.state;
979
+ /** The underlying WalletConnection (for advanced use: send/propose/permissions/…). Null until connect(). */
980
+ get connection() {
981
+ return this.conn;
982
+ }
983
+ setState(patch) {
984
+ const next = { ...this.state, ...patch };
985
+ if (next.status === this.state.status && next.account === this.state.account && next.error === this.state.error) return;
986
+ this.state = Object.freeze(next);
987
+ for (const l of [...this.listeners]) l();
988
+ }
989
+ onAccounts = (accounts) => {
990
+ if (!Array.isArray(accounts) || accounts.length === 0) this.setState({ status: "disconnected", account: null });
991
+ else this.setState({ status: "connected", account: String(accounts[0]), error: null });
992
+ };
993
+ onDisconnect = () => {
994
+ this.setState({ status: "disconnected", account: null });
995
+ };
996
+ /** Detect + connect (prompts the user the first time per origin). Resolves the connected address. */
997
+ connect = async () => {
998
+ this.setState({ status: "connecting", error: null });
999
+ try {
1000
+ if (!this.conn) {
1001
+ this.conn = await this.getWalletFn(this.detectOpts);
1002
+ this.conn.on("accountsChanged", this.onAccounts);
1003
+ this.conn.on("disconnect", this.onDisconnect);
1004
+ }
1005
+ const addr = await this.conn.connect();
1006
+ this.setState({ status: "connected", account: addr, error: null });
1007
+ return addr;
1008
+ } catch (e) {
1009
+ this.setState({ status: "disconnected", account: null, error: e?.message ?? String(e) });
1010
+ throw e;
1011
+ }
1012
+ };
1013
+ /** Forget the connection locally AND revoke this origin's wallet-side permission (best-effort). */
1014
+ disconnect = async () => {
1015
+ try {
1016
+ await this.conn?.revokePermissions();
1017
+ } catch {
1018
+ }
1019
+ this.setState({ status: "disconnected", account: null, error: null });
1020
+ };
1021
+ /** Audience-bound sign-in (must connect() first). Returns the signed artifact to verify server-side. */
1022
+ signInWithCsd = (params) => {
1023
+ if (!this.conn) return Promise.reject(new Error("call connect() before signInWithCsd()"));
1024
+ return this.conn.signInWithCsd(params);
1025
+ };
1026
+ };
1027
+
796
1028
  // src/index.ts
1029
+ var DEFAULT_SPV_CHECKPOINT = { height: 31310, hash: "0x000000000000138bd3eee4b8d2fbacb8d5433ac3040ddbf1c45a5e3e0cc9e814" };
797
1030
  var DEFAULT_CAIRN = "https://cairn-substrate.com";
798
1031
  var Cairn = class {
799
1032
  config;
@@ -804,6 +1037,10 @@ var Cairn = class {
804
1037
  board;
805
1038
  wallet;
806
1039
  cairnHttp;
1040
+ // Lazily-seeded SPV light client + its checkpoint (audit M3 — PoW-verified header source).
1041
+ _spvLight = null;
1042
+ _spvSeeded = false;
1043
+ _spvCp;
807
1044
  constructor(config = {}) {
808
1045
  this.config = config;
809
1046
  const fetch = config.fetch;
@@ -814,22 +1051,41 @@ var Cairn = class {
814
1051
  this.cairnHttp = new Http({ baseUrl: cairn, fetch, timeoutMs });
815
1052
  const indexerHttp = new Http({ baseUrl: indexer, fetch, timeoutMs });
816
1053
  const swarmHttp = config.baseUrls?.swarm ? new Http({ baseUrl: config.baseUrls.swarm.replace(/\/+$/, ""), fetch, timeoutMs }) : void 0;
1054
+ this._spvCp = config.spvCheckpoint ?? DEFAULT_SPV_CHECKPOINT;
817
1055
  this.chain = new Chain({ rpcUrl: rpc, fetch, timeoutMs });
818
1056
  this.wallet = config.wallet;
819
1057
  this.board = new BoardClient(this.cairnHttp, this.wallet);
820
1058
  this.index = new IndexerClient(indexerHttp, {
821
1059
  fetch,
822
1060
  WebSocketImpl: config.WebSocketImpl,
823
- // Upgrade inclusion proofs to fully trust-minimized by cross-checking the
824
- // proof root against the on-chain header merkle at that height.
825
- headerMerkleAt: async (h) => {
826
- const block = await this.chain.client.blockByHeight(h);
827
- return String(block.header.merkle);
828
- }
1061
+ headerMerkleAt: (h) => this.verifiedHeaderMerkleAt(h)
829
1062
  });
830
1063
  this.content = new ContentClient({ swarm: swarmHttp, indexer: indexerHttp, cairn: this.cairnHttp });
831
1064
  this.registry = new RegistryClient({ baseUrl: indexer, fetch });
832
1065
  }
1066
+ /**
1067
+ * PoW-VERIFIED header merkle at a height (audit M3 — the no-PoW half). Lazily seeds a light
1068
+ * client from the pinned SPV checkpoint and verifies the header chain FORWARD (PoW + LWMA +
1069
+ * chainwork) to `height`, then returns that header's merkle root. THROWS for a height below the
1070
+ * checkpoint (can't verify backward) or if verification fails — the IndexerClient then degrades
1071
+ * to the honest "proof-consistent" instead of over-claiming "verified-inclusion". The verified
1072
+ * header chain is cached across calls; the first call near tip syncs the post-checkpoint span.
1073
+ */
1074
+ async verifiedHeaderMerkleAt(height) {
1075
+ if (!Number.isInteger(height) || height < this._spvCp.height)
1076
+ throw new Error(`height ${height} is below the SPV checkpoint ${this._spvCp.height} (cannot PoW-verify backward)`);
1077
+ if (!this._spvLight) this._spvLight = new import_csd_light.LightClient({ client: this.chain.client, checkpoints: { [this._spvCp.height]: this._spvCp.hash } });
1078
+ const lc = this._spvLight;
1079
+ if (!this._spvSeeded) {
1080
+ await lc.syncFromCheckpoint(this._spvCp.height, this._spvCp.hash);
1081
+ this._spvSeeded = true;
1082
+ }
1083
+ const haveTo = lc.baseHeight + lc.chain.length - 1;
1084
+ if (haveTo < height) await lc.sync(height);
1085
+ const vh = lc.chain[height - lc.baseHeight];
1086
+ if (!vh) throw new Error(`light client has no verified header at ${height}`);
1087
+ return String(vh.header.merkle);
1088
+ }
833
1089
  /**
834
1090
  * Detect + connect the Cairn Wallet (browser only). Wires the wallet into
835
1091
  * `cairn.board` for signed writes. Resolves to the connected address.
@@ -853,6 +1109,7 @@ var Cairn = class {
853
1109
  0 && (module.exports = {
854
1110
  BoardClient,
855
1111
  Cairn,
1112
+ CairnController,
856
1113
  CairnError,
857
1114
  Chain,
858
1115
  ContentClient,
@@ -862,12 +1119,16 @@ var Cairn = class {
862
1119
  IndexerClient,
863
1120
  NotInstalledError,
864
1121
  RegistryClient,
1122
+ SDK_VERSION,
865
1123
  UnsupportedMethodError,
866
1124
  UserRejectedError,
867
1125
  WalletConnection,
868
1126
  WalletLockedError,
869
1127
  connect,
870
1128
  detectProvider,
1129
+ discoverProviders,
1130
+ errorCode,
871
1131
  getWallet,
872
- isInstalled
1132
+ isInstalled,
1133
+ mapProviderError
873
1134
  });
package/dist/index.d.cts CHANGED
@@ -9,7 +9,8 @@ import { ContentClient } from './content.cjs';
9
9
  export { ContentClientOptions, PreparedContent } from './content.cjs';
10
10
  import { RegistryClient } from './registry.cjs';
11
11
  import { WalletConnection, DetectOptions } from './connect.cjs';
12
- export { AttestParams, CairnProvider, ConnectResult, ProposeParams, SealClaimParams, SendParams, SignInResult, TxResult, connect, detectProvider, getWallet, isInstalled } from './connect.cjs';
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
+ export { CairnController, CairnControllerOptions, CairnState } from './controller.cjs';
13
14
  import '@inversealtruism/csd-client';
14
15
  import '@inversealtruism/csd-tx';
15
16
  import '@inversealtruism/csd-codec';
@@ -17,8 +18,29 @@ import '@inversealtruism/csd-crypto';
17
18
  import '@inversealtruism/csd-light';
18
19
  import '@inversealtruism/csd-registry';
19
20
 
21
+ /** Stable machine-readable error codes (branch on these, not the message text). */
22
+ type CairnErrorCode = "NOT_INSTALLED" | "USER_REJECTED" | "WALLET_LOCKED" | "UNSUPPORTED_METHOD" | "HTTP_ERROR" | "CONTENT_VERIFICATION" | "UNKNOWN";
23
+ declare const SDK_VERSION = "0.1.2";
24
+ interface CairnErrorOptions {
25
+ code?: CairnErrorCode;
26
+ docsPath?: string;
27
+ cause?: unknown;
28
+ }
20
29
  declare class CairnError extends Error {
21
- constructor(message: string);
30
+ /** Stable machine code for programmatic branching. */
31
+ readonly code: CairnErrorCode;
32
+ /** The human message without the appended docs/version footer. */
33
+ readonly shortMessage: string;
34
+ /** Relative docs path (under the SDK docs base), if any. */
35
+ readonly docsPath?: string;
36
+ /** The cairn-sdk version that produced this error. */
37
+ readonly version = "0.1.2";
38
+ constructor(shortMessage: string, opts?: CairnErrorOptions);
39
+ /**
40
+ * Walk the `cause` chain. With a predicate, returns the FIRST matching error (or null). Without one,
41
+ * returns the DEEPEST cause (the root). Mirrors viem's `BaseError.walk()`.
42
+ */
43
+ walk(fn?: (err: unknown) => boolean): unknown;
22
44
  }
23
45
  /** The Cairn Wallet extension is not installed (no `window.cairn` appeared). */
24
46
  declare class NotInstalledError extends CairnError {
@@ -32,7 +54,7 @@ declare class UserRejectedError extends CairnError {
32
54
  declare class WalletLockedError extends CairnError {
33
55
  constructor(message?: string);
34
56
  }
35
- /** The method is not exposed to dApps by the wallet boundary. */
57
+ /** The method is not exposed to dApps by the wallet boundary (or the wallet is too old for it). */
36
58
  declare class UnsupportedMethodError extends CairnError {
37
59
  constructor(message?: string);
38
60
  }
@@ -48,6 +70,13 @@ declare class ContentVerificationError extends CairnError {
48
70
  expected: string;
49
71
  constructor(expected: string);
50
72
  }
73
+ /**
74
+ * Map a provider error string (from the wallet's `{ ok:false, error }`) onto a typed error. The
75
+ * wallet's strings are stable: "rejected by user", "wallet locked", "unsupported dApp method: …".
76
+ */
77
+ declare function mapProviderError(error: string): CairnError;
78
+ /** Convenience: the stable code of any error (or "UNKNOWN" for a non-CairnError). */
79
+ declare function errorCode(err: unknown): CairnErrorCode;
51
80
 
52
81
  interface CairnBaseUrls {
53
82
  /** Cairn board server (board + /api/rpc proxy + /content origin). Default https://cairn-substrate.com */
@@ -80,6 +109,18 @@ interface CairnConfig {
80
109
  wallet?: WalletConnection;
81
110
  /** WebSocket implementation for indexer subscriptions (defaults to global). */
82
111
  WebSocketImpl?: typeof WebSocket;
112
+ /**
113
+ * SPV trust anchor for genuine "verified-inclusion" (audit M3). A merkle inclusion proof is only
114
+ * "verified-inclusion" when its block header is PoW-verified — otherwise a lying node can serve a
115
+ * fake header whose merkle matches a fake proof. The light client verifies the header chain
116
+ * (PoW + LWMA + chainwork) forward from this pinned checkpoint; inclusion at a height ≥ the
117
+ * checkpoint is then trust-minimized. Defaults to the ecosystem checkpoint (same hash swapguard
118
+ * ships). Pin your own for full control. Txs BELOW the checkpoint degrade to "proof-consistent".
119
+ */
120
+ spvCheckpoint?: {
121
+ height: number;
122
+ hash: string;
123
+ };
83
124
  }
84
125
  declare class Cairn {
85
126
  readonly config: CairnConfig;
@@ -90,7 +131,19 @@ declare class Cairn {
90
131
  board: BoardClient;
91
132
  wallet?: WalletConnection;
92
133
  private readonly cairnHttp;
134
+ private _spvLight;
135
+ private _spvSeeded;
136
+ private readonly _spvCp;
93
137
  constructor(config?: CairnConfig);
138
+ /**
139
+ * PoW-VERIFIED header merkle at a height (audit M3 — the no-PoW half). Lazily seeds a light
140
+ * client from the pinned SPV checkpoint and verifies the header chain FORWARD (PoW + LWMA +
141
+ * chainwork) to `height`, then returns that header's merkle root. THROWS for a height below the
142
+ * checkpoint (can't verify backward) or if verification fails — the IndexerClient then degrades
143
+ * to the honest "proof-consistent" instead of over-claiming "verified-inclusion". The verified
144
+ * header chain is cached across calls; the first call near tip syncs the post-checkpoint span.
145
+ */
146
+ private verifiedHeaderMerkleAt;
94
147
  /**
95
148
  * Detect + connect the Cairn Wallet (browser only). Wires the wallet into
96
149
  * `cairn.board` for signed writes. Resolves to the connected address.
@@ -102,4 +155,4 @@ declare class Cairn {
102
155
  get connected(): boolean;
103
156
  }
104
157
 
105
- export { BoardClient, Cairn, type CairnBaseUrls, type CairnConfig, CairnError, Chain, ContentClient, ContentVerificationError, DetectOptions, FetchLike, HttpError, IndexerClient, NotInstalledError, RegistryClient, UnsupportedMethodError, UserRejectedError, WalletConnection, WalletLockedError };
158
+ export { BoardClient, Cairn, type CairnBaseUrls, type CairnConfig, CairnError, type CairnErrorCode, type CairnErrorOptions, Chain, ContentClient, ContentVerificationError, DetectOptions, FetchLike, HttpError, IndexerClient, NotInstalledError, RegistryClient, SDK_VERSION, UnsupportedMethodError, UserRejectedError, WalletConnection, WalletLockedError, errorCode, mapProviderError };