@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.d.ts CHANGED
@@ -9,7 +9,8 @@ import { ContentClient } from './content.js';
9
9
  export { ContentClientOptions, PreparedContent } from './content.js';
10
10
  import { RegistryClient } from './registry.js';
11
11
  import { WalletConnection, DetectOptions } from './connect.js';
12
- export { AttestParams, CairnProvider, ConnectResult, ProposeParams, SealClaimParams, SendParams, SignInResult, TxResult, connect, detectProvider, getWallet, isInstalled } from './connect.js';
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
+ export { CairnController, CairnControllerOptions, CairnState } from './controller.js';
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 };
package/dist/index.js CHANGED
@@ -1,34 +1,42 @@
1
+ import {
2
+ RegistryClient
3
+ } from "./chunk-G7FABIF4.js";
1
4
  import {
2
5
  BoardClient
3
- } from "./chunk-OGP2W7DL.js";
6
+ } from "./chunk-645CYBVN.js";
7
+ import {
8
+ Chain,
9
+ LightClient
10
+ } from "./chunk-5KAUQH72.js";
11
+ import {
12
+ ContentClient
13
+ } from "./chunk-OSXS26RA.js";
14
+ import {
15
+ IndexerClient
16
+ } from "./chunk-FNBE2FPE.js";
4
17
  import {
5
- Chain
6
- } from "./chunk-6VWLWF7R.js";
18
+ CairnController
19
+ } from "./chunk-ZAE2BIH5.js";
7
20
  import {
8
21
  WalletConnection,
9
22
  connect,
10
23
  detectProvider,
24
+ discoverProviders,
11
25
  getWallet,
12
26
  isInstalled
13
- } from "./chunk-SMAPCKVG.js";
14
- import {
15
- ContentClient
16
- } from "./chunk-BZVJVL4Q.js";
27
+ } from "./chunk-G4OZKZSJ.js";
17
28
  import {
18
29
  CairnError,
19
30
  ContentVerificationError,
20
31
  HttpError,
21
32
  NotInstalledError,
33
+ SDK_VERSION,
22
34
  UnsupportedMethodError,
23
35
  UserRejectedError,
24
- WalletLockedError
25
- } from "./chunk-PHMU3CG5.js";
26
- import {
27
- IndexerClient
28
- } from "./chunk-ETZYBDFC.js";
29
- import {
30
- RegistryClient
31
- } from "./chunk-G7FABIF4.js";
36
+ WalletLockedError,
37
+ errorCode,
38
+ mapProviderError
39
+ } from "./chunk-UMGWVOOT.js";
32
40
 
33
41
  // src/http.ts
34
42
  var Http = class {
@@ -103,6 +111,7 @@ async function safeText(res) {
103
111
  }
104
112
 
105
113
  // src/index.ts
114
+ var DEFAULT_SPV_CHECKPOINT = { height: 31310, hash: "0x000000000000138bd3eee4b8d2fbacb8d5433ac3040ddbf1c45a5e3e0cc9e814" };
106
115
  var DEFAULT_CAIRN = "https://cairn-substrate.com";
107
116
  var Cairn = class {
108
117
  config;
@@ -113,6 +122,10 @@ var Cairn = class {
113
122
  board;
114
123
  wallet;
115
124
  cairnHttp;
125
+ // Lazily-seeded SPV light client + its checkpoint (audit M3 — PoW-verified header source).
126
+ _spvLight = null;
127
+ _spvSeeded = false;
128
+ _spvCp;
116
129
  constructor(config = {}) {
117
130
  this.config = config;
118
131
  const fetch = config.fetch;
@@ -123,22 +136,41 @@ var Cairn = class {
123
136
  this.cairnHttp = new Http({ baseUrl: cairn, fetch, timeoutMs });
124
137
  const indexerHttp = new Http({ baseUrl: indexer, fetch, timeoutMs });
125
138
  const swarmHttp = config.baseUrls?.swarm ? new Http({ baseUrl: config.baseUrls.swarm.replace(/\/+$/, ""), fetch, timeoutMs }) : void 0;
139
+ this._spvCp = config.spvCheckpoint ?? DEFAULT_SPV_CHECKPOINT;
126
140
  this.chain = new Chain({ rpcUrl: rpc, fetch, timeoutMs });
127
141
  this.wallet = config.wallet;
128
142
  this.board = new BoardClient(this.cairnHttp, this.wallet);
129
143
  this.index = new IndexerClient(indexerHttp, {
130
144
  fetch,
131
145
  WebSocketImpl: config.WebSocketImpl,
132
- // Upgrade inclusion proofs to fully trust-minimized by cross-checking the
133
- // proof root against the on-chain header merkle at that height.
134
- headerMerkleAt: async (h) => {
135
- const block = await this.chain.client.blockByHeight(h);
136
- return String(block.header.merkle);
137
- }
146
+ headerMerkleAt: (h) => this.verifiedHeaderMerkleAt(h)
138
147
  });
139
148
  this.content = new ContentClient({ swarm: swarmHttp, indexer: indexerHttp, cairn: this.cairnHttp });
140
149
  this.registry = new RegistryClient({ baseUrl: indexer, fetch });
141
150
  }
151
+ /**
152
+ * PoW-VERIFIED header merkle at a height (audit M3 — the no-PoW half). Lazily seeds a light
153
+ * client from the pinned SPV checkpoint and verifies the header chain FORWARD (PoW + LWMA +
154
+ * chainwork) to `height`, then returns that header's merkle root. THROWS for a height below the
155
+ * checkpoint (can't verify backward) or if verification fails — the IndexerClient then degrades
156
+ * to the honest "proof-consistent" instead of over-claiming "verified-inclusion". The verified
157
+ * header chain is cached across calls; the first call near tip syncs the post-checkpoint span.
158
+ */
159
+ async verifiedHeaderMerkleAt(height) {
160
+ if (!Number.isInteger(height) || height < this._spvCp.height)
161
+ throw new Error(`height ${height} is below the SPV checkpoint ${this._spvCp.height} (cannot PoW-verify backward)`);
162
+ if (!this._spvLight) this._spvLight = new LightClient({ client: this.chain.client, checkpoints: { [this._spvCp.height]: this._spvCp.hash } });
163
+ const lc = this._spvLight;
164
+ if (!this._spvSeeded) {
165
+ await lc.syncFromCheckpoint(this._spvCp.height, this._spvCp.hash);
166
+ this._spvSeeded = true;
167
+ }
168
+ const haveTo = lc.baseHeight + lc.chain.length - 1;
169
+ if (haveTo < height) await lc.sync(height);
170
+ const vh = lc.chain[height - lc.baseHeight];
171
+ if (!vh) throw new Error(`light client has no verified header at ${height}`);
172
+ return String(vh.header.merkle);
173
+ }
142
174
  /**
143
175
  * Detect + connect the Cairn Wallet (browser only). Wires the wallet into
144
176
  * `cairn.board` for signed writes. Resolves to the connected address.
@@ -161,6 +193,7 @@ var Cairn = class {
161
193
  export {
162
194
  BoardClient,
163
195
  Cairn,
196
+ CairnController,
164
197
  CairnError,
165
198
  Chain,
166
199
  ContentClient,
@@ -170,12 +203,16 @@ export {
170
203
  IndexerClient,
171
204
  NotInstalledError,
172
205
  RegistryClient,
206
+ SDK_VERSION,
173
207
  UnsupportedMethodError,
174
208
  UserRejectedError,
175
209
  WalletConnection,
176
210
  WalletLockedError,
177
211
  connect,
178
212
  detectProvider,
213
+ discoverProviders,
214
+ errorCode,
179
215
  getWallet,
180
- isInstalled
216
+ isInstalled,
217
+ mapProviderError
181
218
  };
package/dist/indexer.cjs CHANGED
@@ -99,9 +99,12 @@ var IndexerClient = class {
99
99
  /**
100
100
  * Verify a tx is included in a block by folding its merkle branch to the root
101
101
  * with csd-codec.verifyMerkleProof. If `headerMerkleAt` was provided, the
102
- * proof's root is cross-checked against the ON-CHAIN header merkle at that
103
- * height → trustLevel "verified-inclusion". Otherwise the proof is only
104
- * internally consistent → "proof-consistent" (still useful, but the indexer is trusted).
102
+ * proof's root is cross-checked against the header merkle at that height →
103
+ * trustLevel "verified-inclusion" (only as trustworthy as that header source —
104
+ * see headerMerkleAt). Otherwise the proof is only internally consistent →
105
+ * "proof-consistent" (still useful, but the indexer is trusted). The Cairn
106
+ * facade wires headerMerkleAt ONLY when the node RPC is an independent origin
107
+ * from the indexer, so the default same-origin setup stays at "proof-consistent".
105
108
  */
106
109
  async verifyInclusion(txid) {
107
110
  let proof;
@@ -25,9 +25,13 @@ interface IndexerClientOptions {
25
25
  /** A WebSocket implementation (defaults to globalThis.WebSocket). */
26
26
  WebSocketImpl?: typeof WebSocket;
27
27
  /**
28
- * Optional: given a block height, return its on-chain header merkle root. Wire
29
- * this from the chain/light client to upgrade verifyInclusion() from
30
- * "proof-consistent" to fully trust-minimized "verified-inclusion".
28
+ * Optional: given a block height, return its on-chain header merkle root. verifyInclusion()
29
+ * cross-checks the indexer's proof root against this and, on a match, labels the result
30
+ * "verified-inclusion". IMPORTANT: that label is only as strong as THIS source — it is fully
31
+ * trust-minimized ONLY if the header is independently verified (e.g. a PoW-checking light client,
32
+ * or at least a node on a DIFFERENT trust domain than the indexer). A header fetched by raw RPC
33
+ * from the SAME server that serves the proof proves nothing (one compromised server controls
34
+ * both); for that case prefer no headerMerkleAt → an honest "proof-consistent".
31
35
  */
32
36
  headerMerkleAt?: (height: number) => Promise<string>;
33
37
  }
@@ -64,9 +68,12 @@ declare class IndexerClient {
64
68
  /**
65
69
  * Verify a tx is included in a block by folding its merkle branch to the root
66
70
  * with csd-codec.verifyMerkleProof. If `headerMerkleAt` was provided, the
67
- * proof's root is cross-checked against the ON-CHAIN header merkle at that
68
- * height → trustLevel "verified-inclusion". Otherwise the proof is only
69
- * internally consistent → "proof-consistent" (still useful, but the indexer is trusted).
71
+ * proof's root is cross-checked against the header merkle at that height →
72
+ * trustLevel "verified-inclusion" (only as trustworthy as that header source —
73
+ * see headerMerkleAt). Otherwise the proof is only internally consistent →
74
+ * "proof-consistent" (still useful, but the indexer is trusted). The Cairn
75
+ * facade wires headerMerkleAt ONLY when the node RPC is an independent origin
76
+ * from the indexer, so the default same-origin setup stays at "proof-consistent".
70
77
  */
71
78
  verifyInclusion(txid: string): Promise<InclusionResult>;
72
79
  /** Stream all events (blocks, proposals, attestations, reorgs) via SSE. */
package/dist/indexer.d.ts CHANGED
@@ -25,9 +25,13 @@ interface IndexerClientOptions {
25
25
  /** A WebSocket implementation (defaults to globalThis.WebSocket). */
26
26
  WebSocketImpl?: typeof WebSocket;
27
27
  /**
28
- * Optional: given a block height, return its on-chain header merkle root. Wire
29
- * this from the chain/light client to upgrade verifyInclusion() from
30
- * "proof-consistent" to fully trust-minimized "verified-inclusion".
28
+ * Optional: given a block height, return its on-chain header merkle root. verifyInclusion()
29
+ * cross-checks the indexer's proof root against this and, on a match, labels the result
30
+ * "verified-inclusion". IMPORTANT: that label is only as strong as THIS source — it is fully
31
+ * trust-minimized ONLY if the header is independently verified (e.g. a PoW-checking light client,
32
+ * or at least a node on a DIFFERENT trust domain than the indexer). A header fetched by raw RPC
33
+ * from the SAME server that serves the proof proves nothing (one compromised server controls
34
+ * both); for that case prefer no headerMerkleAt → an honest "proof-consistent".
31
35
  */
32
36
  headerMerkleAt?: (height: number) => Promise<string>;
33
37
  }
@@ -64,9 +68,12 @@ declare class IndexerClient {
64
68
  /**
65
69
  * Verify a tx is included in a block by folding its merkle branch to the root
66
70
  * with csd-codec.verifyMerkleProof. If `headerMerkleAt` was provided, the
67
- * proof's root is cross-checked against the ON-CHAIN header merkle at that
68
- * height → trustLevel "verified-inclusion". Otherwise the proof is only
69
- * internally consistent → "proof-consistent" (still useful, but the indexer is trusted).
71
+ * proof's root is cross-checked against the header merkle at that height →
72
+ * trustLevel "verified-inclusion" (only as trustworthy as that header source —
73
+ * see headerMerkleAt). Otherwise the proof is only internally consistent →
74
+ * "proof-consistent" (still useful, but the indexer is trusted). The Cairn
75
+ * facade wires headerMerkleAt ONLY when the node RPC is an independent origin
76
+ * from the indexer, so the default same-origin setup stays at "proof-consistent".
70
77
  */
71
78
  verifyInclusion(txid: string): Promise<InclusionResult>;
72
79
  /** Stream all events (blocks, proposals, attestations, reorgs) via SSE. */
package/dist/indexer.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  IndexerClient
3
- } from "./chunk-ETZYBDFC.js";
3
+ } from "./chunk-FNBE2FPE.js";
4
4
  export {
5
5
  IndexerClient
6
6
  };