@inversealtruism/cairn-sdk 0.1.3 → 0.2.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/README.md +16 -16
- package/dist/board.cjs +1 -1
- package/dist/board.d.cts +1 -1
- package/dist/board.d.ts +1 -1
- package/dist/board.js +2 -2
- package/dist/chain.cjs +1 -1
- package/dist/chain.d.cts +1 -1
- package/dist/chain.d.ts +1 -1
- package/dist/chain.js +1 -1
- package/dist/{chunk-G7FABIF4.js → chunk-BGJZF7S2.js} +11 -2
- package/dist/{chunk-5KAUQH72.js → chunk-C452UODV.js} +1 -1
- package/dist/{chunk-FNBE2FPE.js → chunk-J5J4JEJ6.js} +50 -18
- package/dist/{chunk-UMGWVOOT.js → chunk-JGXD3WIC.js} +7 -2
- package/dist/{chunk-ZAE2BIH5.js → chunk-PYREGAB3.js} +21 -3
- package/dist/{chunk-645CYBVN.js → chunk-RYZJDZLJ.js} +1 -1
- package/dist/{chunk-OSXS26RA.js → chunk-SIUUY6FZ.js} +4 -3
- package/dist/{chunk-G4OZKZSJ.js → chunk-UIEOBVFI.js} +8 -2
- package/dist/connect.cjs +14 -3
- package/dist/connect.d.cts +7 -0
- package/dist/connect.d.ts +7 -0
- package/dist/connect.js +2 -2
- package/dist/content.cjs +4 -3
- package/dist/content.d.cts +1 -1
- package/dist/content.d.ts +1 -1
- package/dist/content.js +2 -2
- package/dist/controller.cjs +34 -5
- package/dist/controller.d.cts +1 -0
- package/dist/controller.d.ts +1 -0
- package/dist/controller.js +3 -3
- package/dist/{http-D2CzfOsm.d.cts → http-C9C4L4hz.d.cts} +11 -0
- package/dist/{http-D2CzfOsm.d.ts → http-C9C4L4hz.d.ts} +11 -0
- package/dist/index.cjs +272 -37
- package/dist/index.d.cts +99 -11
- package/dist/index.d.ts +99 -11
- package/dist/index.js +179 -17
- package/dist/indexer.cjs +50 -18
- package/dist/indexer.d.cts +15 -4
- package/dist/indexer.d.ts +15 -4
- package/dist/indexer.js +1 -1
- package/dist/react.cjs +34 -5
- package/dist/react.js +3 -3
- package/dist/registry.cjs +11 -2
- package/dist/registry.d.cts +12 -3
- package/dist/registry.d.ts +12 -3
- package/dist/registry.js +1 -1
- package/package.json +32 -23
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @inversealtruism/cairn-sdk
|
|
2
2
|
|
|
3
|
-
The **Compute Substrate dApp kit
|
|
3
|
+
The **Compute Substrate dApp kit:** one cohesive toolkit for building apps on CSD. It composes everything in the Cairn ecosystem behind a single `Cairn` object:
|
|
4
4
|
|
|
5
5
|
| | |
|
|
6
6
|
|---|---|
|
|
@@ -8,7 +8,7 @@ The **Compute Substrate dApp kit** — one cohesive toolkit for building apps on
|
|
|
8
8
|
| `cairn.chain` | Node RPC + transaction builders + a verifying **light client** (re-exports the `csd-*` primitives). |
|
|
9
9
|
| `cairn.board` | The signal **board** + work graph. Reads are public; writes are wallet-signed. |
|
|
10
10
|
| `cairn.index` | The L2 **explorer/indexer**: REST + merkle proofs + live SSE/WS feeds. |
|
|
11
|
-
| `cairn.content` | **Self-certifying content
|
|
11
|
+
| `cairn.content` | **Self-certifying content:** publish via the board, fetch with client-side verification. |
|
|
12
12
|
| `cairn.registry` | L3 **peer / gateway / identity** resolution. |
|
|
13
13
|
|
|
14
14
|
```bash
|
|
@@ -26,7 +26,7 @@ const cairn = new Cairn({ network: "mainnet" }); // defaults to cairn-substrat
|
|
|
26
26
|
const top = await cairn.board.top({ domain: "csd:apps" });
|
|
27
27
|
const tip = await cairn.chain.tip();
|
|
28
28
|
|
|
29
|
-
// connect the user's wallet (browser)
|
|
29
|
+
// connect the user's wallet (browser): prompts once, then "connected"
|
|
30
30
|
await cairn.connect();
|
|
31
31
|
|
|
32
32
|
// publish a board item, signed by the user's wallet (clear-signed approval)
|
|
@@ -42,17 +42,17 @@ cairn.index.streamAll({ onProposal: (e) => console.log("new proposal", e) });
|
|
|
42
42
|
|
|
43
43
|
## How wallet connection works (and why it's safe)
|
|
44
44
|
|
|
45
|
-
The SDK follows the exact model MetaMask uses
|
|
45
|
+
The SDK follows the exact model MetaMask uses, there is **no allowlist you maintain**:
|
|
46
46
|
|
|
47
47
|
- **`connect()` / `getAddress()`** grant *address visibility*. The first time a site connects, the user approves once; after that the wallet answers silently (the site shows up under **Settings → Connected sites**, revocable anytime). This is the only "silent" call.
|
|
48
|
-
- **Every signing / fund-moving call
|
|
48
|
+
- **Every signing / fund-moving call:** `send`, `propose`, `attest`, `signIn`, `sealClaim`, `revealClaim`, **always** opens the wallet's clear-signing approval window, **every time**, no matter what. Being "connected" never pre-approves a signature. A connected site cannot move a satoshi without an explicit, fully-disclosed approval click.
|
|
49
49
|
|
|
50
50
|
```ts
|
|
51
51
|
import { connect, UserRejectedError, NotInstalledError } from "@inversealtruism/cairn-sdk";
|
|
52
52
|
|
|
53
53
|
try {
|
|
54
54
|
const { wallet, address } = await connect();
|
|
55
|
-
await wallet.send({ to: "0x…", amount: 100_000_000 }); // 1 CSD
|
|
55
|
+
await wallet.send({ to: "0x…", amount: 100_000_000 }); // 1 CSD, still prompts
|
|
56
56
|
} catch (e) {
|
|
57
57
|
if (e instanceof NotInstalledError) { /* tell the user to install the wallet */ }
|
|
58
58
|
if (e instanceof UserRejectedError) { /* they declined */ }
|
|
@@ -61,29 +61,29 @@ try {
|
|
|
61
61
|
|
|
62
62
|
## API by namespace
|
|
63
63
|
|
|
64
|
-
**`cairn.board
|
|
64
|
+
**`cairn.board`:** `top()/board({domain,window})`, `item(id)`, `domains()`, `quests()/quest(id)`, `profile(addr)`, `leaderboard()`, `network()/networkSeries()/miner(addr)`, `activity()`, `wall()`; writes `propose({domain,title,body,links?,fee?})`, `support(id,{score?,confidence?,fee?})`.
|
|
65
65
|
|
|
66
|
-
**`cairn.index
|
|
66
|
+
**`cairn.index`:** `tipHeight()`, `block()/blockTxids()`, `tx()/txStatus()/txMerkleProof()`, `address()/addressTxs()/addressUtxo()`, `proposal()/attestations()`, `reputation()`, `registryPeers()/registryGateways()/identity()`, **`verifyInclusion(txid)`** (trust-minimized), `streamAll()/streamBlocks()/streamDomain()` (SSE), `subscribe()` (WS).
|
|
67
67
|
|
|
68
|
-
**`cairn.content
|
|
68
|
+
**`cairn.content`:** `prepare(obj)`/`put(obj)` → `{ payloadHash, canonical, bytes }`; `get(hash)` / `getBytes(hash)` (fetch **and verify** `sha256(bytes) === hash`, throws `ContentVerificationError` on tamper); `hash(obj)`.
|
|
69
69
|
|
|
70
|
-
**`cairn.chain
|
|
70
|
+
**`cairn.chain`:** `tip()`, `utxos(addr)`, `submit(nodeJsonTx)`, `.client` (full `CsdClient`), `.light()` (verifying light client). The package also re-exports the primitives under the `/chain` subpath: `buildSend/buildPropose/buildAttest`, `signTx`, `keygen`, `payloadHash`, `verifyMerkleProof`, `LightClient`, etc.
|
|
71
71
|
|
|
72
|
-
**`cairn.registry
|
|
72
|
+
**`cairn.registry`:** `gateways()`, `peers()`, `resolveName(handle)`, `reverseName(addr)`, `.fromRecords` (deterministic local resolvers).
|
|
73
73
|
|
|
74
74
|
Subpath imports are available for tree-shaking: `@inversealtruism/cairn-sdk/{connect,board,indexer,content,registry,chain}`.
|
|
75
75
|
|
|
76
76
|
## Examples
|
|
77
77
|
|
|
78
|
-
- **`examples/read-only.mjs
|
|
79
|
-
- **`examples/hello-csd
|
|
78
|
+
- **`examples/read-only.mjs`:** runs today against live mainnet, no wallet, no build: `npm run test:live`. Demonstrates chain/board/indexer/registry reads + verified merkle inclusion + verified content fetch.
|
|
79
|
+
- **`examples/hello-csd/`:** a no-framework browser dApp (connect → read board → publish → live feed). Build the bundle with `npm run build:example`, then serve the folder from **localhost** (the wallet injects there) with the Cairn Wallet installed.
|
|
80
80
|
|
|
81
81
|
## Trust model & honest limits
|
|
82
82
|
|
|
83
|
-
- **Content is always verified client-side.** `cairn.content.get()` rejects bytes that don't hash to the requested `payloadHash
|
|
84
|
-
- **Merkle inclusion is trust-minimized
|
|
83
|
+
- **Content is always verified client-side.** `cairn.content.get()` rejects bytes that don't hash to the requested `payloadHash`, an untrusted gateway can't serve tampered content.
|
|
84
|
+
- **Merkle inclusion is trust-minimized, to the strength of your header source.** `verifyInclusion()` folds the proof and, when a header-merkle source is wired, cross-checks the root against it → `trustLevel: "verified-inclusion"`; otherwise `"proof-consistent"` (indexer trusted). That cross-check only means something against an *independent* header, so the default `Cairn` wiring enables it ONLY when your node RPC is a different origin than the indexer (the default same-origin proxy stays at `"proof-consistent"`, a single compromised server can't be cross-checked against itself). For full trust-minimization against a lying node too, supply a PoW-verifying `headerMerkleAt` (e.g. backed by `cairn.chain.light()`).
|
|
85
85
|
- **Balances are RPC-trusted.** Headers don't commit to the UTXO set; balance reads trust the node/proxy (same as every light client).
|
|
86
|
-
- **Indexer/swarm run behind the cairn proxy.** By default `cairn.index` and content resolution use the cairn server's hardened, read-only, rate-limited `/explorer/api` reverse proxy (no separate public indexer needed). **SSE works through it; `subscribe()` (WebSocket) needs a direct indexer endpoint
|
|
86
|
+
- **Indexer/swarm run behind the cairn proxy.** By default `cairn.index` and content resolution use the cairn server's hardened, read-only, rate-limited `/explorer/api` reverse proxy (no separate public indexer needed). **SSE works through it; `subscribe()` (WebSocket) needs a direct indexer endpoint:** set `baseUrls.indexer` to a private/localhost indexer for WS.
|
|
87
87
|
- **Today the wallet injects only on `cairn-substrate.com` + localhost.** Connecting from an arbitrary third-party origin requires the wallet's "connect anywhere" release (broadened injection), which ships after the per-origin consent layer is proven.
|
|
88
88
|
|
|
89
89
|
## Configuration
|
package/dist/board.cjs
CHANGED
|
@@ -27,7 +27,7 @@ var import_csd_codec = require("@inversealtruism/csd-codec");
|
|
|
27
27
|
var import_csd_codec2 = require("@inversealtruism/csd-codec");
|
|
28
28
|
|
|
29
29
|
// src/errors.ts
|
|
30
|
-
var SDK_VERSION = "0.
|
|
30
|
+
var SDK_VERSION = "0.2.0";
|
|
31
31
|
var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
|
|
32
32
|
var CairnError = class extends Error {
|
|
33
33
|
/** Stable machine code for programmatic branching. */
|
package/dist/board.d.cts
CHANGED
package/dist/board.d.ts
CHANGED
package/dist/board.js
CHANGED
package/dist/chain.cjs
CHANGED
|
@@ -115,7 +115,7 @@ var Chain = class {
|
|
|
115
115
|
});
|
|
116
116
|
if (!built.ok || p.broadcast === false) return built;
|
|
117
117
|
const submitResult = await this.client.submit(built.nodeJson);
|
|
118
|
-
return { ...built, submitResult };
|
|
118
|
+
return { ...built, ok: built.ok && submitResult.ok !== false, submitResult };
|
|
119
119
|
}
|
|
120
120
|
/** A verifying light client over the same RPC (built once, reused). */
|
|
121
121
|
light() {
|
package/dist/chain.d.cts
CHANGED
|
@@ -7,7 +7,7 @@ export { App, BlockHeader, Tx, TxInput, TxOutput, canonicalJson, deserialize, hb
|
|
|
7
7
|
export { ADDR_RE, addrFromPriv, addrFromPub, hash160, isValidAddr, isValidPriv, keygen, pubFromPriv, randomNonce, signDigest, verifyDigest } from '@inversealtruism/csd-crypto';
|
|
8
8
|
import { LightClient } from '@inversealtruism/csd-light';
|
|
9
9
|
export { InclusionResult, LightClient, ReorgResult, TrustLevel, VerifiedHeader, expectedBits, expectedBitsFromWindow } from '@inversealtruism/csd-light';
|
|
10
|
-
import { F as FetchLike } from './http-
|
|
10
|
+
import { F as FetchLike } from './http-C9C4L4hz.cjs';
|
|
11
11
|
|
|
12
12
|
interface ChainOptions {
|
|
13
13
|
/** Node RPC base URL — defaults to the public Cairn proxy (no local node needed). */
|
package/dist/chain.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { App, BlockHeader, Tx, TxInput, TxOutput, canonicalJson, deserialize, hb
|
|
|
7
7
|
export { ADDR_RE, addrFromPriv, addrFromPub, hash160, isValidAddr, isValidPriv, keygen, pubFromPriv, randomNonce, signDigest, verifyDigest } from '@inversealtruism/csd-crypto';
|
|
8
8
|
import { LightClient } from '@inversealtruism/csd-light';
|
|
9
9
|
export { InclusionResult, LightClient, ReorgResult, TrustLevel, VerifiedHeader, expectedBits, expectedBitsFromWindow } from '@inversealtruism/csd-light';
|
|
10
|
-
import { F as FetchLike } from './http-
|
|
10
|
+
import { F as FetchLike } from './http-C9C4L4hz.js';
|
|
11
11
|
|
|
12
12
|
interface ChainOptions {
|
|
13
13
|
/** Node RPC base URL — defaults to the public Cairn proxy (no local node needed). */
|
package/dist/chain.js
CHANGED
|
@@ -31,11 +31,20 @@ var RegistryClient = class {
|
|
|
31
31
|
peers() {
|
|
32
32
|
return discoverPeers(this.src);
|
|
33
33
|
}
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a handle → identity (GET /identity/:handle). Returns null if unresolved.
|
|
36
|
+
*
|
|
37
|
+
* ⚠ TRUST (CAIRN-SDK-RESOLVE-VERIFIED-FOOTGUN): the returned `verified` flag is the INDEXER's assertion,
|
|
38
|
+
* NOT a client-side proof. A hostile/compromised indexer (or a MITM of the read proxy) can return
|
|
39
|
+
* `verified:true` for a wrong address. Do NOT use this directly as a payee/login target without either (a)
|
|
40
|
+
* recomputing from raw records via `fromRecords.name(...)`, or (b) cross-checking a second independent
|
|
41
|
+
* source (the wallet's namespv union cure). Treat `verified` here as a hint, not a guarantee.
|
|
42
|
+
*/
|
|
35
43
|
resolveName(handle) {
|
|
36
44
|
return resolveName(this.src, handle);
|
|
37
45
|
}
|
|
38
|
-
/** Reverse-resolve an address → identity (GET /address/:addr/identity).
|
|
46
|
+
/** Reverse-resolve an address → identity (GET /address/:addr/identity).
|
|
47
|
+
* ⚠ Same indexer-trust caveat as resolveName — `verified` is the indexer's claim, not a client proof. */
|
|
39
48
|
reverseName(address) {
|
|
40
49
|
return reverseName(this.src, address);
|
|
41
50
|
}
|
|
@@ -92,7 +92,7 @@ var Chain = class {
|
|
|
92
92
|
});
|
|
93
93
|
if (!built.ok || p.broadcast === false) return built;
|
|
94
94
|
const submitResult = await this.client.submit(built.nodeJson);
|
|
95
|
-
return { ...built, submitResult };
|
|
95
|
+
return { ...built, ok: built.ok && submitResult.ok !== false, submitResult };
|
|
96
96
|
}
|
|
97
97
|
/** A verifying light client over the same RPC (built once, reused). */
|
|
98
98
|
light() {
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
// src/indexer.ts
|
|
2
2
|
import { verifyMerkleProof } from "@inversealtruism/csd-codec";
|
|
3
|
+
var HEX64 = /^0x[0-9a-fA-F]{64}$/;
|
|
4
|
+
var MAX_SSE_BUF = 1024 * 1024;
|
|
5
|
+
function isValidMerkleProof(p) {
|
|
6
|
+
if (!p || typeof p !== "object") return false;
|
|
7
|
+
const m = p;
|
|
8
|
+
return Number.isInteger(m.block_height) && m.block_height >= 0 && Number.isInteger(m.pos) && m.pos >= 0 && typeof m.merkle_root === "string" && HEX64.test(m.merkle_root) && Array.isArray(m.merkle) && m.merkle.every((x) => typeof x === "string" && HEX64.test(x));
|
|
9
|
+
}
|
|
3
10
|
var IndexerClient = class {
|
|
4
11
|
http;
|
|
5
12
|
opts;
|
|
@@ -20,32 +27,35 @@ var IndexerClient = class {
|
|
|
20
27
|
blockHashAtHeight(h) {
|
|
21
28
|
return this.http.getJson(`/block-height/${h}`);
|
|
22
29
|
}
|
|
30
|
+
// CAIRNSDK-CONTENT-HASH-PATH-1: encode every caller-supplied id segment. Raw interpolation let an id like
|
|
31
|
+
// `../../api/rpc/tip` walk `new URL` to a DIFFERENT same-origin endpoint whose body was then returned
|
|
32
|
+
// verbatim as the requested object (worse than the content case, which discards the walked-to response).
|
|
23
33
|
block(hash) {
|
|
24
|
-
return this.http.getJson(`/block/${hash}`);
|
|
34
|
+
return this.http.getJson(`/block/${encodeURIComponent(hash)}`);
|
|
25
35
|
}
|
|
26
36
|
blockTxids(hash) {
|
|
27
|
-
return this.http.getJson(`/block/${hash}/txids`);
|
|
37
|
+
return this.http.getJson(`/block/${encodeURIComponent(hash)}/txids`);
|
|
28
38
|
}
|
|
29
39
|
tx(id) {
|
|
30
|
-
return this.http.getJson(`/tx/${id}`);
|
|
40
|
+
return this.http.getJson(`/tx/${encodeURIComponent(id)}`);
|
|
31
41
|
}
|
|
32
42
|
txStatus(id) {
|
|
33
|
-
return this.http.getJson(`/tx/${id}/status`);
|
|
43
|
+
return this.http.getJson(`/tx/${encodeURIComponent(id)}/status`);
|
|
34
44
|
}
|
|
35
45
|
txMerkleProof(id) {
|
|
36
|
-
return this.http.getJson(`/tx/${id}/merkle-proof`);
|
|
46
|
+
return this.http.getJson(`/tx/${encodeURIComponent(id)}/merkle-proof`);
|
|
37
47
|
}
|
|
38
48
|
address(a) {
|
|
39
|
-
return this.http.getJson(`/address/${a}`);
|
|
49
|
+
return this.http.getJson(`/address/${encodeURIComponent(a)}`);
|
|
40
50
|
}
|
|
41
51
|
addressTxs(a) {
|
|
42
|
-
return this.http.getJson(`/address/${a}/txs`);
|
|
52
|
+
return this.http.getJson(`/address/${encodeURIComponent(a)}/txs`);
|
|
43
53
|
}
|
|
44
54
|
addressUtxo(a) {
|
|
45
|
-
return this.http.getJson(`/address/${a}/utxo`);
|
|
55
|
+
return this.http.getJson(`/address/${encodeURIComponent(a)}/utxo`);
|
|
46
56
|
}
|
|
47
57
|
reputation(a) {
|
|
48
|
-
return this.http.getJson(`/address/${a}/reputation`);
|
|
58
|
+
return this.http.getJson(`/address/${encodeURIComponent(a)}/reputation`);
|
|
49
59
|
}
|
|
50
60
|
domains() {
|
|
51
61
|
return this.http.getJson("/domains");
|
|
@@ -54,10 +64,10 @@ var IndexerClient = class {
|
|
|
54
64
|
return this.http.getJson(`/domain/${encodeURIComponent(d)}/proposals`);
|
|
55
65
|
}
|
|
56
66
|
proposal(id) {
|
|
57
|
-
return this.http.getJson(`/proposal/${id}`);
|
|
67
|
+
return this.http.getJson(`/proposal/${encodeURIComponent(id)}`);
|
|
58
68
|
}
|
|
59
69
|
attestations(id) {
|
|
60
|
-
return this.http.getJson(`/proposal/${id}/attestations`);
|
|
70
|
+
return this.http.getJson(`/proposal/${encodeURIComponent(id)}/attestations`);
|
|
61
71
|
}
|
|
62
72
|
registryPeers() {
|
|
63
73
|
return this.http.getJson("/registry/peers");
|
|
@@ -69,7 +79,7 @@ var IndexerClient = class {
|
|
|
69
79
|
return this.http.getJson(`/identity/${encodeURIComponent(handle)}`);
|
|
70
80
|
}
|
|
71
81
|
reverseIdentity(addr) {
|
|
72
|
-
return this.http.getJson(`/address/${addr}/identity`);
|
|
82
|
+
return this.http.getJson(`/address/${encodeURIComponent(addr)}/identity`);
|
|
73
83
|
}
|
|
74
84
|
// ---- trust-minimized inclusion ----------------------------------------
|
|
75
85
|
/**
|
|
@@ -78,9 +88,11 @@ var IndexerClient = class {
|
|
|
78
88
|
* proof's root is cross-checked against the header merkle at that height →
|
|
79
89
|
* trustLevel "verified-inclusion" (only as trustworthy as that header source —
|
|
80
90
|
* see headerMerkleAt). Otherwise the proof is only internally consistent →
|
|
81
|
-
* "proof-consistent" (still useful, but the indexer is trusted).
|
|
82
|
-
*
|
|
83
|
-
*
|
|
91
|
+
* "proof-consistent" (still useful, but the indexer is trusted). INDEXER-DOC-STALE-1: the Cairn facade now
|
|
92
|
+
* wires headerMerkleAt UNCONDITIONALLY via a PoW-verifying checkpoint light client (index.ts), so the
|
|
93
|
+
* default reaches "verified-inclusion" when the proof root matches the PoW-verified header — what makes a
|
|
94
|
+
* same-origin RPC safe is the forward PoW re-verification, not an origin gate (which no longer exists).
|
|
95
|
+
* Fails closed: a malformed proof or a chain-disagreement returns "not-found" (the latter with equivocation:true).
|
|
84
96
|
*/
|
|
85
97
|
async verifyInclusion(txid) {
|
|
86
98
|
let proof;
|
|
@@ -89,7 +101,15 @@ var IndexerClient = class {
|
|
|
89
101
|
} catch {
|
|
90
102
|
return { included: false, trustLevel: "not-found", reason: "no merkle proof (tx not indexed)" };
|
|
91
103
|
}
|
|
92
|
-
|
|
104
|
+
if (!isValidMerkleProof(proof)) {
|
|
105
|
+
return { included: false, trustLevel: "not-found", reason: "malformed merkle proof" };
|
|
106
|
+
}
|
|
107
|
+
let folds = false;
|
|
108
|
+
try {
|
|
109
|
+
folds = verifyMerkleProof(txid, proof.pos, proof.merkle, proof.merkle_root);
|
|
110
|
+
} catch {
|
|
111
|
+
return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "malformed merkle proof" };
|
|
112
|
+
}
|
|
93
113
|
if (!folds) {
|
|
94
114
|
return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "merkle branch does not fold to the claimed root" };
|
|
95
115
|
}
|
|
@@ -97,7 +117,7 @@ var IndexerClient = class {
|
|
|
97
117
|
try {
|
|
98
118
|
const onchain = await this.opts.headerMerkleAt(proof.block_height);
|
|
99
119
|
if (norm(onchain) !== norm(proof.merkle_root)) {
|
|
100
|
-
return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "proof root != on-chain header merkle (indexer disagrees with chain)" };
|
|
120
|
+
return { included: false, trustLevel: "not-found", equivocation: true, blockHeight: proof.block_height, reason: "proof root != on-chain header merkle (indexer disagrees with chain)" };
|
|
101
121
|
}
|
|
102
122
|
return { included: true, trustLevel: "verified-inclusion", blockHeight: proof.block_height, pos: proof.pos, merkleRoot: proof.merkle_root };
|
|
103
123
|
} catch {
|
|
@@ -121,6 +141,11 @@ var IndexerClient = class {
|
|
|
121
141
|
/**
|
|
122
142
|
* Subscribe over WebSocket with selective tracking. Returns a handle with
|
|
123
143
|
* `.close()`. `onEvent` receives every matching IndexEvent (incl. reorgs).
|
|
144
|
+
*
|
|
145
|
+
* WS-NORECONNECT-1: unlike the SSE feeds (streamAll/streamBlocks/streamDomain, which auto-reconnect with
|
|
146
|
+
* backoff), this WebSocket subscription does NOT auto-reconnect — it surfaces a terminal error via
|
|
147
|
+
* `onError` (when provided) and stops. A consumer tracking reorgs over WS MUST re-`subscribe()` on error
|
|
148
|
+
* to avoid a silently-stale view; for built-in reconnect prefer the SSE `stream*` methods.
|
|
124
149
|
*/
|
|
125
150
|
subscribe(sub, onEvent, onError) {
|
|
126
151
|
const WS = this.opts.WebSocketImpl ?? globalThis.WebSocket;
|
|
@@ -183,13 +208,20 @@ var IndexerClient = class {
|
|
|
183
208
|
buf = buf.slice(idx + 2);
|
|
184
209
|
dispatchFrame(frame, handlers);
|
|
185
210
|
}
|
|
211
|
+
if (buf.length > MAX_SSE_BUF) {
|
|
212
|
+
try {
|
|
213
|
+
await reader.cancel();
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
throw new Error(`SSE buffer exceeded ${MAX_SSE_BUF} bytes with no frame delimiter \u2014 aborting`);
|
|
217
|
+
}
|
|
186
218
|
}
|
|
187
219
|
} catch (err) {
|
|
188
220
|
if (closed) return;
|
|
189
221
|
handlers.onError?.(err);
|
|
190
222
|
}
|
|
191
223
|
if (closed) return;
|
|
192
|
-
await new Promise((r) => setTimeout(r, 2e3));
|
|
224
|
+
await new Promise((r) => setTimeout(r, 1e3 + Math.floor(Math.random() * 2e3)));
|
|
193
225
|
}
|
|
194
226
|
};
|
|
195
227
|
void run();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/errors.ts
|
|
2
|
-
var SDK_VERSION = "0.
|
|
2
|
+
var SDK_VERSION = "0.2.0";
|
|
3
3
|
var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
|
|
4
4
|
var CairnError = class extends Error {
|
|
5
5
|
/** Stable machine code for programmatic branching. */
|
|
@@ -74,7 +74,12 @@ var ContentVerificationError = class extends CairnError {
|
|
|
74
74
|
this.expected = expected;
|
|
75
75
|
}
|
|
76
76
|
};
|
|
77
|
-
|
|
77
|
+
var NATIVE_CODES = /* @__PURE__ */ new Set(["ACCOUNT_CHANGED", "FIRST_PARTY_ONLY", "RATE_LIMITED", "APPROVAL_CLOSED", "FORBIDDEN", "INTERNAL"]);
|
|
78
|
+
function mapProviderError(error, code) {
|
|
79
|
+
if (code === "USER_REJECTED" || code === "APPROVAL_CLOSED") return new UserRejectedError(error);
|
|
80
|
+
if (code === "WALLET_LOCKED") return new WalletLockedError(error);
|
|
81
|
+
if (code === "UNSUPPORTED_METHOD" || code === "UNKNOWN_KIND") return new UnsupportedMethodError(error);
|
|
82
|
+
if (code && NATIVE_CODES.has(code)) return new CairnError(error || code, { code });
|
|
78
83
|
const e = String(error || "").toLowerCase();
|
|
79
84
|
if (e.includes("rejected by user")) return new UserRejectedError(error);
|
|
80
85
|
if (e.includes("wallet locked")) return new WalletLockedError(error);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getWallet
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-UIEOBVFI.js";
|
|
4
4
|
|
|
5
5
|
// src/controller.ts
|
|
6
6
|
var DISCONNECTED = Object.freeze({ status: "disconnected", account: null, error: null });
|
|
@@ -35,12 +35,28 @@ var CairnController = class {
|
|
|
35
35
|
for (const l of [...this.listeners]) l();
|
|
36
36
|
}
|
|
37
37
|
onAccounts = (accounts) => {
|
|
38
|
-
if (!Array.isArray(accounts) || accounts.length === 0)
|
|
39
|
-
|
|
38
|
+
if (!Array.isArray(accounts) || accounts.length === 0) {
|
|
39
|
+
this.setState({ status: "disconnected", account: null });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const next = String(accounts[0]);
|
|
43
|
+
if (this.state.status === "connected" && this.state.account && next.toLowerCase() === this.state.account.toLowerCase()) {
|
|
44
|
+
this.setState({ status: "connected", account: next, error: null });
|
|
45
|
+
} else {
|
|
46
|
+
this.setState({ status: "disconnected", account: null });
|
|
47
|
+
}
|
|
40
48
|
};
|
|
41
49
|
onDisconnect = () => {
|
|
42
50
|
this.setState({ status: "disconnected", account: null });
|
|
43
51
|
};
|
|
52
|
+
// Detach the provider listeners (ghost-reconnect fix). Idempotent + best-effort.
|
|
53
|
+
detach() {
|
|
54
|
+
try {
|
|
55
|
+
this.conn?.off?.("accountsChanged", this.onAccounts);
|
|
56
|
+
this.conn?.off?.("disconnect", this.onDisconnect);
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
}
|
|
44
60
|
/** Detect + connect (prompts the user the first time per origin). Resolves the connected address. */
|
|
45
61
|
connect = async () => {
|
|
46
62
|
this.setState({ status: "connecting", error: null });
|
|
@@ -64,6 +80,8 @@ var CairnController = class {
|
|
|
64
80
|
await this.conn?.revokePermissions();
|
|
65
81
|
} catch {
|
|
66
82
|
}
|
|
83
|
+
this.detach();
|
|
84
|
+
this.conn = null;
|
|
67
85
|
this.setState({ status: "disconnected", account: null, error: null });
|
|
68
86
|
};
|
|
69
87
|
/** Audience-bound sign-in (must connect() first). Returns the signed artifact to verify server-side. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ContentVerificationError
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-JGXD3WIC.js";
|
|
4
4
|
|
|
5
5
|
// src/content.ts
|
|
6
6
|
import { payloadHash, canonicalJson, verifyContentBytes } from "@inversealtruism/csd-codec";
|
|
@@ -67,8 +67,9 @@ var ContentClient = class {
|
|
|
67
67
|
}
|
|
68
68
|
};
|
|
69
69
|
function normalizeHash(hash) {
|
|
70
|
-
const h = hash.startsWith("0x") || hash.startsWith("0X") ? hash : `0x${hash}
|
|
71
|
-
|
|
70
|
+
const h = (hash.startsWith("0x") || hash.startsWith("0X") ? hash : `0x${hash}`).toLowerCase();
|
|
71
|
+
if (!/^0x[0-9a-f]{64}$/.test(h)) throw new Error(`invalid content hash: ${hash}`);
|
|
72
|
+
return h;
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
export {
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
NotInstalledError,
|
|
3
3
|
UnsupportedMethodError,
|
|
4
4
|
mapProviderError
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-JGXD3WIC.js";
|
|
6
6
|
|
|
7
7
|
// src/connect.ts
|
|
8
8
|
function discoverProviders(opts = {}) {
|
|
@@ -72,7 +72,7 @@ function isInstalled() {
|
|
|
72
72
|
}
|
|
73
73
|
function unwrap(reply) {
|
|
74
74
|
if (reply && reply.ok) return reply.result;
|
|
75
|
-
throw mapProviderError(reply?.error ?? "Unknown wallet error");
|
|
75
|
+
throw mapProviderError(reply?.error ?? "Unknown wallet error", reply?.code);
|
|
76
76
|
}
|
|
77
77
|
var WalletConnection = class {
|
|
78
78
|
provider;
|
|
@@ -106,6 +106,9 @@ var WalletConnection = class {
|
|
|
106
106
|
* Passwordless sign-in: a signature over a login nonce (always prompts; structurally cannot sign a tx).
|
|
107
107
|
*/
|
|
108
108
|
signIn() {
|
|
109
|
+
if (typeof this.provider.signIn !== "function") {
|
|
110
|
+
return Promise.reject(new UnsupportedMethodError("this wallet does not expose signIn()"));
|
|
111
|
+
}
|
|
109
112
|
return Promise.resolve(this.provider.signIn()).then(unwrap);
|
|
110
113
|
}
|
|
111
114
|
/**
|
|
@@ -139,6 +142,9 @@ var WalletConnection = class {
|
|
|
139
142
|
}
|
|
140
143
|
/** Atomic fill (CairnX DvP): pay + attest in ONE tx. Always prompts with clear-signing. */
|
|
141
144
|
fillOffer(params) {
|
|
145
|
+
if (typeof this.provider.fillOffer !== "function") {
|
|
146
|
+
return Promise.reject(new UnsupportedMethodError("this wallet predates fillOffer() \u2014 ask the user to update the Cairn Wallet"));
|
|
147
|
+
}
|
|
142
148
|
return Promise.resolve(this.provider.fillOffer(params)).then(unwrap);
|
|
143
149
|
}
|
|
144
150
|
/** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
|
package/dist/connect.cjs
CHANGED
|
@@ -30,7 +30,7 @@ __export(connect_exports, {
|
|
|
30
30
|
module.exports = __toCommonJS(connect_exports);
|
|
31
31
|
|
|
32
32
|
// src/errors.ts
|
|
33
|
-
var SDK_VERSION = "0.
|
|
33
|
+
var SDK_VERSION = "0.2.0";
|
|
34
34
|
var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
|
|
35
35
|
var CairnError = class extends Error {
|
|
36
36
|
/** Stable machine code for programmatic branching. */
|
|
@@ -87,7 +87,12 @@ var UnsupportedMethodError = class extends CairnError {
|
|
|
87
87
|
super(message, { code: "UNSUPPORTED_METHOD", docsPath: "/errors#unsupported-method" });
|
|
88
88
|
}
|
|
89
89
|
};
|
|
90
|
-
|
|
90
|
+
var NATIVE_CODES = /* @__PURE__ */ new Set(["ACCOUNT_CHANGED", "FIRST_PARTY_ONLY", "RATE_LIMITED", "APPROVAL_CLOSED", "FORBIDDEN", "INTERNAL"]);
|
|
91
|
+
function mapProviderError(error, code) {
|
|
92
|
+
if (code === "USER_REJECTED" || code === "APPROVAL_CLOSED") return new UserRejectedError(error);
|
|
93
|
+
if (code === "WALLET_LOCKED") return new WalletLockedError(error);
|
|
94
|
+
if (code === "UNSUPPORTED_METHOD" || code === "UNKNOWN_KIND") return new UnsupportedMethodError(error);
|
|
95
|
+
if (code && NATIVE_CODES.has(code)) return new CairnError(error || code, { code });
|
|
91
96
|
const e = String(error || "").toLowerCase();
|
|
92
97
|
if (e.includes("rejected by user")) return new UserRejectedError(error);
|
|
93
98
|
if (e.includes("wallet locked")) return new WalletLockedError(error);
|
|
@@ -163,7 +168,7 @@ function isInstalled() {
|
|
|
163
168
|
}
|
|
164
169
|
function unwrap(reply) {
|
|
165
170
|
if (reply && reply.ok) return reply.result;
|
|
166
|
-
throw mapProviderError(reply?.error ?? "Unknown wallet error");
|
|
171
|
+
throw mapProviderError(reply?.error ?? "Unknown wallet error", reply?.code);
|
|
167
172
|
}
|
|
168
173
|
var WalletConnection = class {
|
|
169
174
|
provider;
|
|
@@ -197,6 +202,9 @@ var WalletConnection = class {
|
|
|
197
202
|
* Passwordless sign-in: a signature over a login nonce (always prompts; structurally cannot sign a tx).
|
|
198
203
|
*/
|
|
199
204
|
signIn() {
|
|
205
|
+
if (typeof this.provider.signIn !== "function") {
|
|
206
|
+
return Promise.reject(new UnsupportedMethodError("this wallet does not expose signIn()"));
|
|
207
|
+
}
|
|
200
208
|
return Promise.resolve(this.provider.signIn()).then(unwrap);
|
|
201
209
|
}
|
|
202
210
|
/**
|
|
@@ -230,6 +238,9 @@ var WalletConnection = class {
|
|
|
230
238
|
}
|
|
231
239
|
/** Atomic fill (CairnX DvP): pay + attest in ONE tx. Always prompts with clear-signing. */
|
|
232
240
|
fillOffer(params) {
|
|
241
|
+
if (typeof this.provider.fillOffer !== "function") {
|
|
242
|
+
return Promise.reject(new UnsupportedMethodError("this wallet predates fillOffer() \u2014 ask the user to update the Cairn Wallet"));
|
|
243
|
+
}
|
|
233
244
|
return Promise.resolve(this.provider.fillOffer(params)).then(unwrap);
|
|
234
245
|
}
|
|
235
246
|
/** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
|
package/dist/connect.d.cts
CHANGED
|
@@ -145,6 +145,7 @@ type ProviderReply<T> = {
|
|
|
145
145
|
} | {
|
|
146
146
|
ok: false;
|
|
147
147
|
error: string;
|
|
148
|
+
code?: string;
|
|
148
149
|
};
|
|
149
150
|
declare global {
|
|
150
151
|
interface Window {
|
|
@@ -188,6 +189,12 @@ declare function isInstalled(): boolean;
|
|
|
188
189
|
/**
|
|
189
190
|
* High-level, typed wallet handle. Construct via `connect()` or `new WalletConnection(provider)`.
|
|
190
191
|
* Methods throw `UserRejectedError` / `WalletLockedError` / `UnsupportedMethodError` on failure.
|
|
192
|
+
*
|
|
193
|
+
* Note (typed-error contract): the genuine Cairn Wallet always RESOLVES a `{ok,result|error}` reply, so a
|
|
194
|
+
* failure is mapped to a typed `CairnError` via `unwrap`. A non-conformant or SPOOFED provider that instead
|
|
195
|
+
* REJECTS its promise will surface that raw rejection un-mapped (it bypasses `unwrap`), so a dApp's
|
|
196
|
+
* `catch (e) { errorCode(e) }` may see `UNKNOWN` from such a provider. The real auth boundary is server-side
|
|
197
|
+
* SIWC verification, never a client-side error code.
|
|
191
198
|
*/
|
|
192
199
|
declare class WalletConnection {
|
|
193
200
|
readonly provider: CairnProvider;
|
package/dist/connect.d.ts
CHANGED
|
@@ -145,6 +145,7 @@ type ProviderReply<T> = {
|
|
|
145
145
|
} | {
|
|
146
146
|
ok: false;
|
|
147
147
|
error: string;
|
|
148
|
+
code?: string;
|
|
148
149
|
};
|
|
149
150
|
declare global {
|
|
150
151
|
interface Window {
|
|
@@ -188,6 +189,12 @@ declare function isInstalled(): boolean;
|
|
|
188
189
|
/**
|
|
189
190
|
* High-level, typed wallet handle. Construct via `connect()` or `new WalletConnection(provider)`.
|
|
190
191
|
* Methods throw `UserRejectedError` / `WalletLockedError` / `UnsupportedMethodError` on failure.
|
|
192
|
+
*
|
|
193
|
+
* Note (typed-error contract): the genuine Cairn Wallet always RESOLVES a `{ok,result|error}` reply, so a
|
|
194
|
+
* failure is mapped to a typed `CairnError` via `unwrap`. A non-conformant or SPOOFED provider that instead
|
|
195
|
+
* REJECTS its promise will surface that raw rejection un-mapped (it bypasses `unwrap`), so a dApp's
|
|
196
|
+
* `catch (e) { errorCode(e) }` may see `UNKNOWN` from such a provider. The real auth boundary is server-side
|
|
197
|
+
* SIWC verification, never a client-side error code.
|
|
191
198
|
*/
|
|
192
199
|
declare class WalletConnection {
|
|
193
200
|
readonly provider: CairnProvider;
|
package/dist/connect.js
CHANGED
package/dist/content.cjs
CHANGED
|
@@ -26,7 +26,7 @@ module.exports = __toCommonJS(content_exports);
|
|
|
26
26
|
var import_csd_codec = require("@inversealtruism/csd-codec");
|
|
27
27
|
|
|
28
28
|
// src/errors.ts
|
|
29
|
-
var SDK_VERSION = "0.
|
|
29
|
+
var SDK_VERSION = "0.2.0";
|
|
30
30
|
var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
|
|
31
31
|
var CairnError = class extends Error {
|
|
32
32
|
/** Stable machine code for programmatic branching. */
|
|
@@ -135,8 +135,9 @@ var ContentClient = class {
|
|
|
135
135
|
}
|
|
136
136
|
};
|
|
137
137
|
function normalizeHash(hash) {
|
|
138
|
-
const h = hash.startsWith("0x") || hash.startsWith("0X") ? hash : `0x${hash}
|
|
139
|
-
|
|
138
|
+
const h = (hash.startsWith("0x") || hash.startsWith("0X") ? hash : `0x${hash}`).toLowerCase();
|
|
139
|
+
if (!/^0x[0-9a-f]{64}$/.test(h)) throw new Error(`invalid content hash: ${hash}`);
|
|
140
|
+
return h;
|
|
140
141
|
}
|
|
141
142
|
// Annotate the CommonJS export names for ESM import in node:
|
|
142
143
|
0 && (module.exports = {
|
package/dist/content.d.cts
CHANGED
package/dist/content.d.ts
CHANGED