@inversealtruism/cairn-sdk 0.1.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/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/board.cjs +193 -0
- package/dist/board.d.cts +151 -0
- package/dist/board.d.ts +151 -0
- package/dist/board.js +7 -0
- package/dist/chain.cjs +144 -0
- package/dist/chain.d.cts +36 -0
- package/dist/chain.d.ts +36 -0
- package/dist/chain.js +86 -0
- package/dist/chunk-6VWLWF7R.js +121 -0
- package/dist/chunk-BZVJVL4Q.js +76 -0
- package/dist/chunk-ETZYBDFC.js +232 -0
- package/dist/chunk-G7FABIF4.js +60 -0
- package/dist/chunk-OGP2W7DL.js +163 -0
- package/dist/chunk-PHMU3CG5.js +63 -0
- package/dist/chunk-SMAPCKVG.js +118 -0
- package/dist/connect.cjs +177 -0
- package/dist/connect.d.cts +127 -0
- package/dist/connect.d.ts +127 -0
- package/dist/connect.js +15 -0
- package/dist/content.cjs +113 -0
- package/dist/content.d.cts +44 -0
- package/dist/content.d.ts +44 -0
- package/dist/content.js +7 -0
- package/dist/http-D2CzfOsm.d.cts +30 -0
- package/dist/http-D2CzfOsm.d.ts +30 -0
- package/dist/index.cjs +873 -0
- package/dist/index.d.cts +105 -0
- package/dist/index.d.ts +105 -0
- package/dist/index.js +181 -0
- package/dist/indexer.cjs +256 -0
- package/dist/indexer.d.cts +103 -0
- package/dist/indexer.d.ts +103 -0
- package/dist/indexer.js +6 -0
- package/dist/registry.cjs +77 -0
- package/dist/registry.d.cts +30 -0
- package/dist/registry.d.ts +30 -0
- package/dist/registry.js +26 -0
- package/package.json +97 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// src/indexer.ts
|
|
2
|
+
import { verifyMerkleProof } from "@inversealtruism/csd-codec";
|
|
3
|
+
var IndexerClient = class {
|
|
4
|
+
http;
|
|
5
|
+
opts;
|
|
6
|
+
constructor(http, opts = {}) {
|
|
7
|
+
this.http = http;
|
|
8
|
+
this.opts = opts;
|
|
9
|
+
}
|
|
10
|
+
// ---- REST --------------------------------------------------------------
|
|
11
|
+
health() {
|
|
12
|
+
return this.http.getJson("/health");
|
|
13
|
+
}
|
|
14
|
+
tipHeight() {
|
|
15
|
+
return this.http.getJson("/blocks/tip/height");
|
|
16
|
+
}
|
|
17
|
+
tipHash() {
|
|
18
|
+
return this.http.getJson("/blocks/tip/hash");
|
|
19
|
+
}
|
|
20
|
+
blockHashAtHeight(h) {
|
|
21
|
+
return this.http.getJson(`/block-height/${h}`);
|
|
22
|
+
}
|
|
23
|
+
block(hash) {
|
|
24
|
+
return this.http.getJson(`/block/${hash}`);
|
|
25
|
+
}
|
|
26
|
+
blockTxids(hash) {
|
|
27
|
+
return this.http.getJson(`/block/${hash}/txids`);
|
|
28
|
+
}
|
|
29
|
+
tx(id) {
|
|
30
|
+
return this.http.getJson(`/tx/${id}`);
|
|
31
|
+
}
|
|
32
|
+
txStatus(id) {
|
|
33
|
+
return this.http.getJson(`/tx/${id}/status`);
|
|
34
|
+
}
|
|
35
|
+
txMerkleProof(id) {
|
|
36
|
+
return this.http.getJson(`/tx/${id}/merkle-proof`);
|
|
37
|
+
}
|
|
38
|
+
address(a) {
|
|
39
|
+
return this.http.getJson(`/address/${a}`);
|
|
40
|
+
}
|
|
41
|
+
addressTxs(a) {
|
|
42
|
+
return this.http.getJson(`/address/${a}/txs`);
|
|
43
|
+
}
|
|
44
|
+
addressUtxo(a) {
|
|
45
|
+
return this.http.getJson(`/address/${a}/utxo`);
|
|
46
|
+
}
|
|
47
|
+
reputation(a) {
|
|
48
|
+
return this.http.getJson(`/address/${a}/reputation`);
|
|
49
|
+
}
|
|
50
|
+
domains() {
|
|
51
|
+
return this.http.getJson("/domains");
|
|
52
|
+
}
|
|
53
|
+
domainProposals(d) {
|
|
54
|
+
return this.http.getJson(`/domain/${encodeURIComponent(d)}/proposals`);
|
|
55
|
+
}
|
|
56
|
+
proposal(id) {
|
|
57
|
+
return this.http.getJson(`/proposal/${id}`);
|
|
58
|
+
}
|
|
59
|
+
attestations(id) {
|
|
60
|
+
return this.http.getJson(`/proposal/${id}/attestations`);
|
|
61
|
+
}
|
|
62
|
+
registryPeers() {
|
|
63
|
+
return this.http.getJson("/registry/peers");
|
|
64
|
+
}
|
|
65
|
+
registryGateways() {
|
|
66
|
+
return this.http.getJson("/registry/gateways");
|
|
67
|
+
}
|
|
68
|
+
identity(handle) {
|
|
69
|
+
return this.http.getJson(`/identity/${encodeURIComponent(handle)}`);
|
|
70
|
+
}
|
|
71
|
+
reverseIdentity(addr) {
|
|
72
|
+
return this.http.getJson(`/address/${addr}/identity`);
|
|
73
|
+
}
|
|
74
|
+
// ---- trust-minimized inclusion ----------------------------------------
|
|
75
|
+
/**
|
|
76
|
+
* Verify a tx is included in a block by folding its merkle branch to the root
|
|
77
|
+
* with csd-codec.verifyMerkleProof. If `headerMerkleAt` was provided, the
|
|
78
|
+
* proof's root is cross-checked against the ON-CHAIN header merkle at that
|
|
79
|
+
* height → trustLevel "verified-inclusion". Otherwise the proof is only
|
|
80
|
+
* internally consistent → "proof-consistent" (still useful, but the indexer is trusted).
|
|
81
|
+
*/
|
|
82
|
+
async verifyInclusion(txid) {
|
|
83
|
+
let proof;
|
|
84
|
+
try {
|
|
85
|
+
proof = await this.txMerkleProof(txid);
|
|
86
|
+
} catch {
|
|
87
|
+
return { included: false, trustLevel: "not-found", reason: "no merkle proof (tx not indexed)" };
|
|
88
|
+
}
|
|
89
|
+
const folds = verifyMerkleProof(txid, proof.pos, proof.merkle, proof.merkle_root);
|
|
90
|
+
if (!folds) {
|
|
91
|
+
return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "merkle branch does not fold to the claimed root" };
|
|
92
|
+
}
|
|
93
|
+
if (this.opts.headerMerkleAt) {
|
|
94
|
+
try {
|
|
95
|
+
const onchain = await this.opts.headerMerkleAt(proof.block_height);
|
|
96
|
+
if (norm(onchain) !== norm(proof.merkle_root)) {
|
|
97
|
+
return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "proof root != on-chain header merkle (indexer disagrees with chain)" };
|
|
98
|
+
}
|
|
99
|
+
return { included: true, trustLevel: "verified-inclusion", blockHeight: proof.block_height, pos: proof.pos, merkleRoot: proof.merkle_root };
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { included: true, trustLevel: "proof-consistent", blockHeight: proof.block_height, pos: proof.pos, merkleRoot: proof.merkle_root };
|
|
104
|
+
}
|
|
105
|
+
// ---- live streams ------------------------------------------------------
|
|
106
|
+
/** Stream all events (blocks, proposals, attestations, reorgs) via SSE. */
|
|
107
|
+
streamAll(handlers) {
|
|
108
|
+
return this.sse("/stream/all", handlers);
|
|
109
|
+
}
|
|
110
|
+
/** Stream blocks + reorgs only. */
|
|
111
|
+
streamBlocks(handlers) {
|
|
112
|
+
return this.sse("/stream/blocks", handlers);
|
|
113
|
+
}
|
|
114
|
+
/** Stream proposals (+ reorgs) for a domain. */
|
|
115
|
+
streamDomain(domain, handlers) {
|
|
116
|
+
return this.sse(`/stream/domain/${encodeURIComponent(domain)}`, handlers);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Subscribe over WebSocket with selective tracking. Returns a handle with
|
|
120
|
+
* `.close()`. `onEvent` receives every matching IndexEvent (incl. reorgs).
|
|
121
|
+
*/
|
|
122
|
+
subscribe(sub, onEvent, onError) {
|
|
123
|
+
const WS = this.opts.WebSocketImpl ?? globalThis.WebSocket;
|
|
124
|
+
if (!WS) {
|
|
125
|
+
throw new Error("No WebSocket available \u2014 pass WebSocketImpl in the indexer options (e.g. in older Node).");
|
|
126
|
+
}
|
|
127
|
+
const wsUrl = this.http.url("/ws").replace(/^http/, "ws");
|
|
128
|
+
let closed = false;
|
|
129
|
+
const ws = new WS(wsUrl);
|
|
130
|
+
ws.addEventListener("open", () => {
|
|
131
|
+
const msg = {};
|
|
132
|
+
if (sub.all) msg["track-all"] = true;
|
|
133
|
+
if (sub.domains?.length) msg["track-domain"] = sub.domains;
|
|
134
|
+
if (sub.addresses?.length) msg["track-address"] = sub.addresses;
|
|
135
|
+
if (sub.proposals?.length) msg["track-proposal"] = sub.proposals;
|
|
136
|
+
ws.send(JSON.stringify(Object.keys(msg).length ? msg : { "track-all": true }));
|
|
137
|
+
});
|
|
138
|
+
ws.addEventListener("message", (ev) => {
|
|
139
|
+
try {
|
|
140
|
+
const data = JSON.parse(typeof ev.data === "string" ? ev.data : String(ev.data));
|
|
141
|
+
if (data && data.kind) onEvent(data);
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
if (onError) ws.addEventListener("error", (e) => onError(e));
|
|
146
|
+
return {
|
|
147
|
+
close() {
|
|
148
|
+
if (closed) return;
|
|
149
|
+
closed = true;
|
|
150
|
+
try {
|
|
151
|
+
ws.close();
|
|
152
|
+
} catch {
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// ---- internal SSE over fetch (portable + auto-reconnect) ---------------
|
|
158
|
+
sse(path, handlers) {
|
|
159
|
+
const fetchImpl = this.opts.fetch ?? globalThis.fetch;
|
|
160
|
+
const url = this.http.url(path);
|
|
161
|
+
let closed = false;
|
|
162
|
+
let ctrl = null;
|
|
163
|
+
const run = async () => {
|
|
164
|
+
while (!closed) {
|
|
165
|
+
ctrl = new AbortController();
|
|
166
|
+
try {
|
|
167
|
+
const res = await fetchImpl(url, { signal: ctrl.signal, headers: { accept: "text/event-stream" } });
|
|
168
|
+
if (!res.ok || !res.body) throw new Error(`SSE ${res.status}`);
|
|
169
|
+
handlers.onOpen?.();
|
|
170
|
+
const reader = res.body.getReader();
|
|
171
|
+
const decoder = new TextDecoder();
|
|
172
|
+
let buf = "";
|
|
173
|
+
for (; ; ) {
|
|
174
|
+
const { value, done } = await reader.read();
|
|
175
|
+
if (done) break;
|
|
176
|
+
buf += decoder.decode(value, { stream: true });
|
|
177
|
+
let idx;
|
|
178
|
+
while ((idx = buf.indexOf("\n\n")) >= 0) {
|
|
179
|
+
const frame = buf.slice(0, idx);
|
|
180
|
+
buf = buf.slice(idx + 2);
|
|
181
|
+
dispatchFrame(frame, handlers);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
if (closed) return;
|
|
186
|
+
handlers.onError?.(err);
|
|
187
|
+
}
|
|
188
|
+
if (closed) return;
|
|
189
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
void run();
|
|
193
|
+
return {
|
|
194
|
+
close() {
|
|
195
|
+
if (closed) return;
|
|
196
|
+
closed = true;
|
|
197
|
+
try {
|
|
198
|
+
ctrl?.abort();
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
function dispatchFrame(frame, h) {
|
|
206
|
+
let event = "message";
|
|
207
|
+
const dataLines = [];
|
|
208
|
+
for (const line of frame.split("\n")) {
|
|
209
|
+
if (line.startsWith(":")) continue;
|
|
210
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
211
|
+
else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim());
|
|
212
|
+
}
|
|
213
|
+
if (event === "hello" || dataLines.length === 0) return;
|
|
214
|
+
let parsed;
|
|
215
|
+
try {
|
|
216
|
+
parsed = JSON.parse(dataLines.join("\n"));
|
|
217
|
+
} catch {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
h.onEvent?.(parsed);
|
|
221
|
+
if (parsed.kind === "block") h.onBlock?.(parsed);
|
|
222
|
+
else if (parsed.kind === "proposal") h.onProposal?.(parsed);
|
|
223
|
+
else if (parsed.kind === "attestation") h.onAttestation?.(parsed);
|
|
224
|
+
else if (parsed.kind === "reorg") h.onReorg?.(parsed);
|
|
225
|
+
}
|
|
226
|
+
function norm(h) {
|
|
227
|
+
return String(h).toLowerCase().replace(/^0x/, "");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export {
|
|
231
|
+
IndexerClient
|
|
232
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/registry.ts
|
|
2
|
+
import {
|
|
3
|
+
discoverPeers,
|
|
4
|
+
discoverGateways,
|
|
5
|
+
resolveName,
|
|
6
|
+
reverseName,
|
|
7
|
+
fromRecords
|
|
8
|
+
} from "@inversealtruism/csd-registry";
|
|
9
|
+
import {
|
|
10
|
+
fromRecords as fromRecords2,
|
|
11
|
+
verifyPeer as verifyPeer2,
|
|
12
|
+
verifyGateway as verifyGateway2,
|
|
13
|
+
verifyIdentitySig as verifyIdentitySig2,
|
|
14
|
+
DOMAINS as DOMAINS2,
|
|
15
|
+
buildPeerRecord,
|
|
16
|
+
buildGatewayRecord,
|
|
17
|
+
buildIdentityCommit,
|
|
18
|
+
buildIdentityReveal,
|
|
19
|
+
epochOf
|
|
20
|
+
} from "@inversealtruism/csd-registry";
|
|
21
|
+
var RegistryClient = class {
|
|
22
|
+
src;
|
|
23
|
+
constructor(opts) {
|
|
24
|
+
this.src = { baseUrl: opts.baseUrl.replace(/\/+$/, ""), fetch: opts.fetch };
|
|
25
|
+
}
|
|
26
|
+
/** Discover ranked content/pin gateways (GET /registry/gateways). */
|
|
27
|
+
gateways() {
|
|
28
|
+
return discoverGateways(this.src);
|
|
29
|
+
}
|
|
30
|
+
/** Discover ranked libp2p peers (GET /registry/peers). */
|
|
31
|
+
peers() {
|
|
32
|
+
return discoverPeers(this.src);
|
|
33
|
+
}
|
|
34
|
+
/** Resolve a handle → identity (GET /identity/:handle). Returns null if unresolved. */
|
|
35
|
+
resolveName(handle) {
|
|
36
|
+
return resolveName(this.src, handle);
|
|
37
|
+
}
|
|
38
|
+
/** Reverse-resolve an address → identity (GET /address/:addr/identity). */
|
|
39
|
+
reverseName(address) {
|
|
40
|
+
return reverseName(this.src, address);
|
|
41
|
+
}
|
|
42
|
+
/** The deterministic, client-side resolvers (recompute from raw ChainRecord[]). */
|
|
43
|
+
get fromRecords() {
|
|
44
|
+
return fromRecords;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
RegistryClient,
|
|
50
|
+
fromRecords2 as fromRecords,
|
|
51
|
+
verifyPeer2 as verifyPeer,
|
|
52
|
+
verifyGateway2 as verifyGateway,
|
|
53
|
+
verifyIdentitySig2 as verifyIdentitySig,
|
|
54
|
+
DOMAINS2 as DOMAINS,
|
|
55
|
+
buildPeerRecord,
|
|
56
|
+
buildGatewayRecord,
|
|
57
|
+
buildIdentityCommit,
|
|
58
|
+
buildIdentityReveal,
|
|
59
|
+
epochOf
|
|
60
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CairnError
|
|
3
|
+
} from "./chunk-PHMU3CG5.js";
|
|
4
|
+
|
|
5
|
+
// src/board.ts
|
|
6
|
+
import { payloadHash } from "@inversealtruism/csd-codec";
|
|
7
|
+
import { MIN_FEE_PROPOSE, MIN_FEE_ATTEST, EPOCH_LEN } from "@inversealtruism/csd-codec";
|
|
8
|
+
var BoardClient = class {
|
|
9
|
+
http;
|
|
10
|
+
wallet;
|
|
11
|
+
constructor(http, wallet) {
|
|
12
|
+
this.http = http;
|
|
13
|
+
this.wallet = wallet;
|
|
14
|
+
}
|
|
15
|
+
requireWallet() {
|
|
16
|
+
if (!this.wallet) {
|
|
17
|
+
throw new CairnError("This action needs a connected wallet. Construct Cairn with a wallet, or call cairn.connect() first.");
|
|
18
|
+
}
|
|
19
|
+
return this.wallet;
|
|
20
|
+
}
|
|
21
|
+
// ---- reads (public) ----------------------------------------------------
|
|
22
|
+
/** Ranked board for a domain + time window. */
|
|
23
|
+
board(opts = {}) {
|
|
24
|
+
return this.http.getJson("/api/board", { domain: opts.domain ?? "all", window: opts.window ?? "all" });
|
|
25
|
+
}
|
|
26
|
+
/** Alias for `board()` — the top items. */
|
|
27
|
+
top(opts = {}) {
|
|
28
|
+
return this.board(opts);
|
|
29
|
+
}
|
|
30
|
+
/** A single item with its supports + pledges + integrity flag. */
|
|
31
|
+
item(id) {
|
|
32
|
+
return this.http.getJson(`/api/item/${encodeURIComponent(id)}`);
|
|
33
|
+
}
|
|
34
|
+
/** Known + discovered domains. */
|
|
35
|
+
domains() {
|
|
36
|
+
return this.http.getJson("/api/domains");
|
|
37
|
+
}
|
|
38
|
+
/** All quests (work-graph items). */
|
|
39
|
+
quests() {
|
|
40
|
+
return this.http.getJson("/api/quests");
|
|
41
|
+
}
|
|
42
|
+
/** A single quest with claims/submissions/payouts. */
|
|
43
|
+
quest(id) {
|
|
44
|
+
return this.http.getJson(`/api/quest/${encodeURIComponent(id)}`);
|
|
45
|
+
}
|
|
46
|
+
/** An address's on-chain-derived profile + identity + reputation. */
|
|
47
|
+
profile(addr, opts = {}) {
|
|
48
|
+
return this.http.getJson(`/api/profile/${encodeURIComponent(addr)}`, opts.live ? { live: 1 } : void 0);
|
|
49
|
+
}
|
|
50
|
+
/** Reputation leaderboard (top 50). */
|
|
51
|
+
leaderboard() {
|
|
52
|
+
return this.http.getJson("/api/leaderboard");
|
|
53
|
+
}
|
|
54
|
+
/** Network dashboard snapshot. */
|
|
55
|
+
network() {
|
|
56
|
+
return this.http.getJson("/api/network");
|
|
57
|
+
}
|
|
58
|
+
/** Network time-series for charts. */
|
|
59
|
+
networkSeries(range = "24h") {
|
|
60
|
+
return this.http.getJson("/api/network/series", { range });
|
|
61
|
+
}
|
|
62
|
+
/** Recent blocks with miner/reward. */
|
|
63
|
+
networkBlocks(limit = 15) {
|
|
64
|
+
return this.http.getJson("/api/network/blocks", { limit });
|
|
65
|
+
}
|
|
66
|
+
/** Per-miner hashrate estimate. */
|
|
67
|
+
miner(addr) {
|
|
68
|
+
return this.http.getJson(`/api/miner/${encodeURIComponent(addr)}`);
|
|
69
|
+
}
|
|
70
|
+
/** Recent activity feed. */
|
|
71
|
+
activity() {
|
|
72
|
+
return this.http.getJson("/api/activity");
|
|
73
|
+
}
|
|
74
|
+
/** The gamified cairn wall. */
|
|
75
|
+
wall() {
|
|
76
|
+
return this.http.getJson("/api/wall");
|
|
77
|
+
}
|
|
78
|
+
/** Server health + chain reachability. */
|
|
79
|
+
health() {
|
|
80
|
+
return this.http.getJson("/api/health");
|
|
81
|
+
}
|
|
82
|
+
// ---- writes (wallet-signed) -------------------------------------------
|
|
83
|
+
/**
|
|
84
|
+
* Publish a board item: compute the canonical payload hash, have the WALLET
|
|
85
|
+
* build/sign/submit the Propose (clear-signed approval), then register the
|
|
86
|
+
* off-chain content. The payload hash is byte-identical to what the cairn
|
|
87
|
+
* server verifies (csd-codec.payloadHash over { v:1, domain, title, body, links }).
|
|
88
|
+
*/
|
|
89
|
+
async propose(input) {
|
|
90
|
+
const wallet = this.requireWallet();
|
|
91
|
+
const content = {
|
|
92
|
+
v: 1,
|
|
93
|
+
domain: input.domain,
|
|
94
|
+
title: input.title,
|
|
95
|
+
body: input.body ?? "",
|
|
96
|
+
links: input.links ?? []
|
|
97
|
+
};
|
|
98
|
+
const ph = payloadHash(content);
|
|
99
|
+
const uri = `cairn:v1:${ph.slice(2, 14)}`;
|
|
100
|
+
const lifetime = input.lifetimeEpochs ?? 720;
|
|
101
|
+
const height = await this.tipHeight();
|
|
102
|
+
const expiresEpoch = Math.floor(height / EPOCH_LEN) + lifetime;
|
|
103
|
+
const fee = input.fee ?? MIN_FEE_PROPOSE;
|
|
104
|
+
const r = await wallet.propose({ domain: content.domain, payloadHash: ph, uri, expiresEpoch, fee });
|
|
105
|
+
const txid = String(r.txid ?? "");
|
|
106
|
+
if (!txid) throw new CairnError("Propose submitted but no txid was returned by the wallet.");
|
|
107
|
+
const reg = await this.tryRegister(content, txid);
|
|
108
|
+
return { txid, payloadHash: ph, content, registered: reg.ok, registerError: reg.error };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Register off-chain content for an already-submitted proposal txid. Idempotent.
|
|
112
|
+
* Returns { ok } — the cairn server verifies sha256(canonical) == on-chain payload_hash.
|
|
113
|
+
*/
|
|
114
|
+
async registerContent(content, txid) {
|
|
115
|
+
try {
|
|
116
|
+
const res = await this.http.postJson("/api/content", {
|
|
117
|
+
domain: content.domain,
|
|
118
|
+
title: content.title,
|
|
119
|
+
body: content.body,
|
|
120
|
+
links: content.links,
|
|
121
|
+
txid
|
|
122
|
+
});
|
|
123
|
+
return { ok: !!res.ok, error: res.ok ? void 0 : res.error };
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** Support/attest a proposal (wallet-signed; always clear-signed). */
|
|
129
|
+
async support(proposalId, opts = {}) {
|
|
130
|
+
const wallet = this.requireWallet();
|
|
131
|
+
return wallet.attest({
|
|
132
|
+
proposalId,
|
|
133
|
+
score: opts.score ?? 80,
|
|
134
|
+
confidence: opts.confidence ?? 70,
|
|
135
|
+
fee: opts.fee ?? MIN_FEE_ATTEST
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// ---- internal ----------------------------------------------------------
|
|
139
|
+
async tipHeight() {
|
|
140
|
+
const tip = await this.http.getJson("/api/rpc/tip");
|
|
141
|
+
const h = Number(tip?.height);
|
|
142
|
+
if (!Number.isFinite(h)) throw new CairnError("Could not read chain tip height for expiry computation.");
|
|
143
|
+
return h;
|
|
144
|
+
}
|
|
145
|
+
async tryRegister(content, txid) {
|
|
146
|
+
const delays = [0, 1500, 3e3, 5e3];
|
|
147
|
+
let last = { ok: false, error: "not attempted" };
|
|
148
|
+
for (const d of delays) {
|
|
149
|
+
if (d) await sleep(d);
|
|
150
|
+
last = await this.registerContent(content, txid);
|
|
151
|
+
if (last.ok) return last;
|
|
152
|
+
if (!/not found yet|not found/i.test(last.error ?? "")) return last;
|
|
153
|
+
}
|
|
154
|
+
return last;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
function sleep(ms) {
|
|
158
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export {
|
|
162
|
+
BoardClient
|
|
163
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var CairnError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = new.target.name;
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var NotInstalledError = class extends CairnError {
|
|
9
|
+
constructor(message = "Cairn Wallet not detected. Install it from cairn-substrate.com.") {
|
|
10
|
+
super(message);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var UserRejectedError = class extends CairnError {
|
|
14
|
+
constructor(message = "Request rejected by user.") {
|
|
15
|
+
super(message);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var WalletLockedError = class extends CairnError {
|
|
19
|
+
constructor(message = "Wallet is locked.") {
|
|
20
|
+
super(message);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var UnsupportedMethodError = class extends CairnError {
|
|
24
|
+
constructor(message = "Method not supported by the wallet.") {
|
|
25
|
+
super(message);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var HttpError = class extends CairnError {
|
|
29
|
+
status;
|
|
30
|
+
url;
|
|
31
|
+
body;
|
|
32
|
+
constructor(status, url, body) {
|
|
33
|
+
super(`HTTP ${status} for ${url}${body ? `: ${String(body).slice(0, 200)}` : ""}`);
|
|
34
|
+
this.status = status;
|
|
35
|
+
this.url = url;
|
|
36
|
+
this.body = body;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var ContentVerificationError = class extends CairnError {
|
|
40
|
+
expected;
|
|
41
|
+
constructor(expected) {
|
|
42
|
+
super(`Content does not match its payload hash ${expected} \u2014 bytes were tampered with in transit.`);
|
|
43
|
+
this.expected = expected;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
function mapProviderError(error) {
|
|
47
|
+
const e = String(error || "").toLowerCase();
|
|
48
|
+
if (e.includes("rejected by user")) return new UserRejectedError(error);
|
|
49
|
+
if (e.includes("wallet locked")) return new WalletLockedError(error);
|
|
50
|
+
if (e.includes("unsupported dapp method")) return new UnsupportedMethodError(error);
|
|
51
|
+
return new CairnError(error || "Unknown wallet error");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
CairnError,
|
|
56
|
+
NotInstalledError,
|
|
57
|
+
UserRejectedError,
|
|
58
|
+
WalletLockedError,
|
|
59
|
+
UnsupportedMethodError,
|
|
60
|
+
HttpError,
|
|
61
|
+
ContentVerificationError,
|
|
62
|
+
mapProviderError
|
|
63
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NotInstalledError,
|
|
3
|
+
UnsupportedMethodError,
|
|
4
|
+
mapProviderError
|
|
5
|
+
} from "./chunk-PHMU3CG5.js";
|
|
6
|
+
|
|
7
|
+
// src/connect.ts
|
|
8
|
+
function detectProvider(opts = {}) {
|
|
9
|
+
const timeoutMs = opts.timeoutMs ?? 3e3;
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
if (typeof window === "undefined") {
|
|
12
|
+
reject(new NotInstalledError("Not running in a browser \u2014 `window.cairn` is unavailable."));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (window.cairn?.isCairn) {
|
|
16
|
+
resolve(window.cairn);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
let done = false;
|
|
20
|
+
const onInit = () => {
|
|
21
|
+
if (done) return;
|
|
22
|
+
if (window.cairn?.isCairn) {
|
|
23
|
+
done = true;
|
|
24
|
+
cleanup();
|
|
25
|
+
resolve(window.cairn);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const timer = setTimeout(() => {
|
|
29
|
+
if (done) return;
|
|
30
|
+
done = true;
|
|
31
|
+
cleanup();
|
|
32
|
+
reject(new NotInstalledError());
|
|
33
|
+
}, timeoutMs);
|
|
34
|
+
const cleanup = () => {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
window.removeEventListener("cairn#initialized", onInit);
|
|
37
|
+
};
|
|
38
|
+
window.addEventListener("cairn#initialized", onInit);
|
|
39
|
+
onInit();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function isInstalled() {
|
|
43
|
+
return typeof window !== "undefined" && !!window.cairn?.isCairn;
|
|
44
|
+
}
|
|
45
|
+
function unwrap(reply) {
|
|
46
|
+
if (reply && reply.ok) return reply.result;
|
|
47
|
+
throw mapProviderError(reply?.error ?? "Unknown wallet error");
|
|
48
|
+
}
|
|
49
|
+
var WalletConnection = class {
|
|
50
|
+
provider;
|
|
51
|
+
_addr = null;
|
|
52
|
+
constructor(provider) {
|
|
53
|
+
this.provider = provider;
|
|
54
|
+
}
|
|
55
|
+
/** The wallet's version string (e.g. "0.2.x"). */
|
|
56
|
+
get version() {
|
|
57
|
+
return this.provider.version;
|
|
58
|
+
}
|
|
59
|
+
/** The connected address, if `connect()`/`getAddress()` has resolved. */
|
|
60
|
+
get address() {
|
|
61
|
+
return this._addr;
|
|
62
|
+
}
|
|
63
|
+
/** Request connection. First time per origin prompts the user; afterwards may be silent. */
|
|
64
|
+
async connect() {
|
|
65
|
+
const { addr } = unwrap(await this.provider.connect());
|
|
66
|
+
this._addr = addr;
|
|
67
|
+
return addr;
|
|
68
|
+
}
|
|
69
|
+
/** Get the connected address (does not force a new connection prompt if already consented). */
|
|
70
|
+
async getAddress() {
|
|
71
|
+
const { addr } = unwrap(await this.provider.getAddress());
|
|
72
|
+
this._addr = addr;
|
|
73
|
+
return addr;
|
|
74
|
+
}
|
|
75
|
+
/** Passwordless sign-in: a signature over a login nonce (always prompts; structurally cannot sign a tx). */
|
|
76
|
+
signIn() {
|
|
77
|
+
return Promise.resolve(this.provider.signIn()).then(unwrap);
|
|
78
|
+
}
|
|
79
|
+
/** Send CSD (always prompts with clear-signing; wallet picks inputs + returns change to itself). */
|
|
80
|
+
send(params) {
|
|
81
|
+
return Promise.resolve(this.provider.send(params)).then(unwrap);
|
|
82
|
+
}
|
|
83
|
+
/** Low-level Propose (always prompts). Prefer `cairn.board.propose()` which computes the hash/uri/expiry. */
|
|
84
|
+
propose(params) {
|
|
85
|
+
return Promise.resolve(this.provider.propose(params)).then(unwrap);
|
|
86
|
+
}
|
|
87
|
+
/** Attest/support a proposal (always prompts). */
|
|
88
|
+
attest(params) {
|
|
89
|
+
return Promise.resolve(this.provider.attest(params)).then(unwrap);
|
|
90
|
+
}
|
|
91
|
+
/** Seal a commit-reveal claim (always prompts; salt stays inside the wallet). */
|
|
92
|
+
sealClaim(params) {
|
|
93
|
+
return Promise.resolve(this.provider.sealClaim(params)).then(unwrap);
|
|
94
|
+
}
|
|
95
|
+
/** Reveal a previously sealed claim by its sealing txid (always prompts). */
|
|
96
|
+
revealClaim(txid) {
|
|
97
|
+
if (typeof txid !== "string" || !txid) {
|
|
98
|
+
return Promise.reject(new UnsupportedMethodError("revealClaim requires the sealing txid"));
|
|
99
|
+
}
|
|
100
|
+
return Promise.resolve(this.provider.revealClaim(txid)).then(unwrap);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
async function getWallet(opts = {}) {
|
|
104
|
+
return new WalletConnection(await detectProvider(opts));
|
|
105
|
+
}
|
|
106
|
+
async function connect(opts = {}) {
|
|
107
|
+
const wallet = await getWallet(opts);
|
|
108
|
+
const address = await wallet.connect();
|
|
109
|
+
return { wallet, address };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export {
|
|
113
|
+
detectProvider,
|
|
114
|
+
isInstalled,
|
|
115
|
+
WalletConnection,
|
|
116
|
+
getWallet,
|
|
117
|
+
connect
|
|
118
|
+
};
|