@inversealtruism/cairn-sdk 0.8.0 → 0.8.2
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 +3 -3
- package/dist/chain.cjs +83 -3
- package/dist/chain.d.cts +9 -4
- package/dist/chain.d.ts +9 -4
- package/dist/chain.js +6 -1
- package/dist/{chunk-DTPADO7N.js → chunk-2RR4ZJPQ.js} +1 -1
- package/dist/{chunk-UAPWZZQK.js → chunk-37OHCH7R.js} +20 -8
- package/dist/{chunk-JXBO4LV6.js → chunk-AHPXOYCC.js} +1 -1
- package/dist/{chunk-WWO3FXHG.js → chunk-EVEBQSDK.js} +19 -14
- package/dist/{chunk-C452UODV.js → chunk-IPBMMJ3Z.js} +26 -4
- package/dist/{chunk-CI7YODPP.js → chunk-NRCTL7OI.js} +43 -3
- package/dist/connect.cjs +34 -20
- package/dist/connect.js +2 -2
- package/dist/content.cjs +1 -1
- package/dist/content.js +2 -2
- package/dist/controller.cjs +34 -20
- package/dist/controller.js +3 -3
- package/dist/index.cjs +268 -99
- package/dist/index.d.cts +77 -32
- package/dist/index.d.ts +77 -32
- package/dist/index.js +190 -82
- package/dist/indexer.cjs +98 -3
- package/dist/indexer.d.cts +5 -1
- package/dist/indexer.d.ts +5 -1
- package/dist/indexer.js +2 -1
- package/dist/react.cjs +34 -20
- package/dist/react.js +3 -3
- package/package.json +10 -7
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ The **Compute Substrate dApp kit:** one cohesive toolkit for building apps on CS
|
|
|
5
5
|
| | |
|
|
6
6
|
|---|---|
|
|
7
7
|
| `cairn.wallet` | Connect the **Cairn Wallet**, sign things (clear-signed approvals). The key never leaves the extension. |
|
|
8
|
-
| `cairn.chain` | Node RPC + transaction builders +
|
|
8
|
+
| `cairn.chain` | Node RPC + transaction builders + the **light client** primitives (re-exports the `csd-*` primitives). |
|
|
9
9
|
| `cairn.index` | The L2 **explorer/indexer**: REST + merkle proofs + live SSE/WS feeds. |
|
|
10
10
|
| `cairn.content` | **Self-certifying content:** hash for an on-chain Propose, fetch with client-side verification. |
|
|
11
11
|
| `cairn.names` | **.csd names + CairnX reads:** name resolution, reverse/primary lookup, tokens, offers, bids, canonical state. |
|
|
@@ -56,13 +56,13 @@ try {
|
|
|
56
56
|
|
|
57
57
|
## API by namespace
|
|
58
58
|
|
|
59
|
-
**`cairn.index`:** `tipHeight()`, `block()/blockTxids()`, `tx()/txStatus()/txMerkleProof()`, `address()/addressTxs()/addressUtxo()
|
|
59
|
+
**`cairn.index`:** `tipHeight()`, `block()/blockTxids()`, `tx()/txStatus()/txMerkleProof()`, `address()/addressTxs()/addressUtxo()` (every unspent output, read in pages of 1,000 up to 100 pages; a full 100th page throws `HttpError`), `domains()/domainProposals(domain)`, `proposal()/attestations()`, `reputation()`, **`verifyInclusion(txid)`** (proves `txid` is a node of a PoW-verified block's merkle tree; an inner node or a block's root folds too, so hash the transaction body you hold to get the id, see the guide), `streamAll()/streamBlocks()/streamDomain()` (SSE), `subscribe()` (WS).
|
|
60
60
|
|
|
61
61
|
**`cairn.names`:** `resolve(name)` (display-path name to address), `name(name)`/`nameHistory(name)`, `primary(addr)` (reverse lookup)/`namesOf(addr)`, `names()`, `tokens()/token(ticker)`, `offers()`/`bids()` (open rows by default; pass `{ status }` for another status, or `{ status: "all" }` for every row), `state()` (the canonical CairnX state). Reads go through the public `/trade/api/cairnx/*` proxy; for payment-grade name resolution verify through the wallet or a replaying resolver, not a bare read (see `src/names.ts` header).
|
|
62
62
|
|
|
63
63
|
**`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)`.
|
|
64
64
|
|
|
65
|
-
**`cairn.chain`:** `tip()`, `utxos(addr)`, `submit(nodeJsonTx)`, `.client` (full `CsdClient`), `.light()` (
|
|
65
|
+
**`cairn.chain`:** `tip()`, `utxos(addr)`, `submit(nodeJsonTx)`, `.client` (full `CsdClient`), `.light()` (a `LightClient` over the same RPC with no checkpoint and no batch header source: it proves nothing until seeded, and a sync from it reads one block per height through the RPC; for verified reads use `cairn.index.verifyInclusion()` and `cairn.verifyOfferForFill()`, which run the facade's light client seeded from `DEFAULT_SPV_CHECKPOINT`, or build your own `new LightClient({ client, checkpoints, headersBatchProvider })` from a pinned checkpoint). The package also re-exports the primitives under the `/chain` subpath: `buildSendVerified/buildProposeVerified/buildAttestVerified` (they check each input's value against its source transaction; use them over the bare `buildSend/buildPropose/buildAttest` with any RPC you do not run), `signTx`, `keygen`, `payloadHash`, `verifyMerkleProof`, `LightClient`, etc.
|
|
66
66
|
|
|
67
67
|
Subpath imports are available for tree-shaking: `@inversealtruism/cairn-sdk/{connect,indexer,content,chain,controller,react}`.
|
|
68
68
|
|
package/dist/chain.cjs
CHANGED
|
@@ -27,7 +27,9 @@ __export(chain_exports, {
|
|
|
27
27
|
addrFromPriv: () => import_csd_crypto.addrFromPriv,
|
|
28
28
|
addrFromPub: () => import_csd_crypto.addrFromPub,
|
|
29
29
|
buildAttest: () => import_csd_tx.buildAttest,
|
|
30
|
+
buildAttestVerified: () => import_csd_tx.buildAttestVerified,
|
|
30
31
|
buildPropose: () => import_csd_tx.buildPropose,
|
|
32
|
+
buildProposeVerified: () => import_csd_tx.buildProposeVerified,
|
|
31
33
|
buildSend: () => import_csd_tx.buildSend,
|
|
32
34
|
buildSendVerified: () => import_csd_tx.buildSendVerified,
|
|
33
35
|
canonicalJson: () => import_csd_codec.canonicalJson,
|
|
@@ -74,6 +76,69 @@ var import_csd_client2 = require("@inversealtruism/csd-client");
|
|
|
74
76
|
var import_csd_light2 = require("@inversealtruism/csd-light");
|
|
75
77
|
var import_csd_tx2 = require("@inversealtruism/csd-tx");
|
|
76
78
|
var import_csd_crypto2 = require("@inversealtruism/csd-crypto");
|
|
79
|
+
|
|
80
|
+
// src/errors.ts
|
|
81
|
+
var SDK_VERSION = "0.8.2";
|
|
82
|
+
var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
|
|
83
|
+
var CairnError = class extends Error {
|
|
84
|
+
/** Stable machine code for programmatic branching. */
|
|
85
|
+
code;
|
|
86
|
+
/** The human message without the appended docs/version footer. */
|
|
87
|
+
shortMessage;
|
|
88
|
+
/** Relative docs path (under the SDK docs base), if any. */
|
|
89
|
+
docsPath;
|
|
90
|
+
/** The cairn-sdk version that produced this error. */
|
|
91
|
+
version = SDK_VERSION;
|
|
92
|
+
/**
|
|
93
|
+
* Whether a dApp may safely AUTO-retry this failure. `false` for fund-safety refusals and user-intent
|
|
94
|
+
* declines (the default); `true` only for provably transient infra faults where nothing was signed.
|
|
95
|
+
* A `SubmitInFlightError` is `false` too: reconcile its `txid`, never blind-retry.
|
|
96
|
+
*/
|
|
97
|
+
retryable;
|
|
98
|
+
constructor(shortMessage, opts = {}) {
|
|
99
|
+
const docs = opts.docsPath;
|
|
100
|
+
super(
|
|
101
|
+
[shortMessage, docs ? `Docs: ${DOCS_BASE}${docs}` : "", `Version: cairn-sdk@${SDK_VERSION}`].filter(Boolean).join("\n"),
|
|
102
|
+
{ cause: opts.cause }
|
|
103
|
+
);
|
|
104
|
+
this.name = new.target.name;
|
|
105
|
+
this.code = opts.code ?? "UNKNOWN";
|
|
106
|
+
this.shortMessage = shortMessage;
|
|
107
|
+
this.docsPath = docs;
|
|
108
|
+
this.retryable = opts.retryable ?? false;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Walk the `cause` chain. With a predicate, returns the FIRST matching error (or null). Without one,
|
|
112
|
+
* returns the DEEPEST cause (the root). Mirrors viem's `BaseError.walk()`.
|
|
113
|
+
*/
|
|
114
|
+
walk(fn) {
|
|
115
|
+
let current = this;
|
|
116
|
+
let last = this;
|
|
117
|
+
while (current != null) {
|
|
118
|
+
if (fn && fn(current)) return current;
|
|
119
|
+
last = current;
|
|
120
|
+
current = current?.cause;
|
|
121
|
+
}
|
|
122
|
+
return fn ? null : last;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
var SubmitInFlightError = class extends CairnError {
|
|
126
|
+
/** The locally-computed consensus txid to reconcile against the chain (undefined when it is not known). */
|
|
127
|
+
txid;
|
|
128
|
+
/** The signed transaction, set only when `Chain.send` threw this (a wallet write keeps it in the wallet).
|
|
129
|
+
* `chain.submit(nodeJson)` resends it unchanged: the same coins under the same txid, so it cannot pay twice,
|
|
130
|
+
* where a new send may select other coins. */
|
|
131
|
+
nodeJson;
|
|
132
|
+
/** Always true: the tx MAY be in the mempool. Reconcile via `txid` or the balance; never blind-retry. */
|
|
133
|
+
maybeSent = true;
|
|
134
|
+
constructor(message, code, txid2, cause, nodeJson) {
|
|
135
|
+
super(message, { code, docsPath: "/errors#submit-inflight", retryable: false, cause });
|
|
136
|
+
this.txid = txid2;
|
|
137
|
+
this.nodeJson = nodeJson;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// src/chain.ts
|
|
77
142
|
var Chain = class {
|
|
78
143
|
client;
|
|
79
144
|
opts;
|
|
@@ -100,7 +165,11 @@ var Chain = class {
|
|
|
100
165
|
* chain (`verifyInputValues` recomputes each source tx's txid, so a lying RPC can't under-report) and
|
|
101
166
|
* computes change from the VERIFIED total — closing the UTXO-VALUE-1 implicit-fee-burn. Fail-closed:
|
|
102
167
|
* if any source can't be verified it signs nothing. Prefer this over the pure `buildSend`. Pass
|
|
103
|
-
* `broadcast:false` to get the signed tx back without submitting.
|
|
168
|
+
* `broadcast:false` to get the signed tx back without submitting. A submit request that fails after signing
|
|
169
|
+
* throws a `SubmitInFlightError` carrying the signed `txid` and `nodeJson`: the node may have taken it, so check
|
|
170
|
+
* before resending, and resend that `nodeJson` with `submit` rather than a new send. A 4xx answer (a proxy's
|
|
171
|
+
* body cap or rate limit, or the node refusing the request) broadcast nothing and throws a final `CairnError`
|
|
172
|
+
* with code `SUBMIT_REJECTED`.
|
|
104
173
|
*/
|
|
105
174
|
async send(p) {
|
|
106
175
|
const from = (0, import_csd_crypto2.addrFromPriv)(p.priv);
|
|
@@ -114,10 +183,19 @@ var Chain = class {
|
|
|
114
183
|
verify: (inputs) => (0, import_csd_client2.verifyInputValues)(this.client, inputs)
|
|
115
184
|
});
|
|
116
185
|
if (!built.ok || p.broadcast === false) return built;
|
|
117
|
-
|
|
186
|
+
let submitResult;
|
|
187
|
+
try {
|
|
188
|
+
submitResult = await this.client.submit(built.nodeJson);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
const what = e?.message ?? String(e);
|
|
191
|
+
if (e?.terminal === true)
|
|
192
|
+
throw new CairnError(`the submit request was refused (${what}), so nothing was broadcast; a 400 or 413 from a proxy is usually its request body cap (a transaction with many inputs), a 429 its rate limit`, { code: "SUBMIT_REJECTED", cause: e });
|
|
193
|
+
throw new SubmitInFlightError(`the submit request failed after signing (${what}); the node may have taken the transaction, so check ${built.txid} before sending again`, "SUBMIT_MAYBE_INFLIGHT", built.txid, e, built.nodeJson);
|
|
194
|
+
}
|
|
118
195
|
return { ...built, ok: built.ok && submitResult.ok !== false, submitResult };
|
|
119
196
|
}
|
|
120
|
-
/** A
|
|
197
|
+
/** A light client over the same RPC (built once, reused), with no checkpoint and no batch header provider: it proves
|
|
198
|
+
* nothing until seeded. The facade's seeded client backs `cairn.index.verifyInclusion` and `verifyOfferForFill`. */
|
|
121
199
|
light() {
|
|
122
200
|
if (!this._light) {
|
|
123
201
|
this._light = new import_csd_light2.LightClient({ client: this.client });
|
|
@@ -134,7 +212,9 @@ var Chain = class {
|
|
|
134
212
|
addrFromPriv,
|
|
135
213
|
addrFromPub,
|
|
136
214
|
buildAttest,
|
|
215
|
+
buildAttestVerified,
|
|
137
216
|
buildPropose,
|
|
217
|
+
buildProposeVerified,
|
|
138
218
|
buildSend,
|
|
139
219
|
buildSendVerified,
|
|
140
220
|
canonicalJson,
|
package/dist/chain.d.cts
CHANGED
|
@@ -2,7 +2,7 @@ import * as _inversealtruism_csd_client from '@inversealtruism/csd-client';
|
|
|
2
2
|
import { CsdClient } from '@inversealtruism/csd-client';
|
|
3
3
|
export { ClientOptions, CsdClient, RpcBlock, RpcHeaderJson, RpcSubmit, RpcTip, RpcTxJson, RpcUtxo, RpcUtxos, rpcHeaderToHeader, rpcTxToTx, verifyInputValues } from '@inversealtruism/csd-client';
|
|
4
4
|
import { BuildResult } from '@inversealtruism/csd-tx';
|
|
5
|
-
export { BuildResult, InputVerifier, Selection, Signed, Utxo, buildAttest, buildPropose, buildSend, buildSendVerified, selectInputs, signTx, txSize, txToNodeJson } from '@inversealtruism/csd-tx';
|
|
5
|
+
export { BuildResult, InputVerifier, Selection, Signed, Utxo, buildAttest, buildAttestVerified, buildPropose, buildProposeVerified, buildSend, buildSendVerified, selectInputs, signTx, txSize, txToNodeJson } from '@inversealtruism/csd-tx';
|
|
6
6
|
export { App, BlockHeader, Tx, TxInput, TxOutput, canonicalJson, deserialize, hb, headerHash, hx, merkleBranch, merkleRoot, payloadHash, serialize, sha256d, sighash, strip0x, strippedTx, txid, verifyContentBytes, verifyMerkleProof } from '@inversealtruism/csd-codec';
|
|
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';
|
|
@@ -17,7 +17,7 @@ interface ChainOptions {
|
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* Convenience handle over a CSD node RPC. `client` is the full typed RPC client;
|
|
20
|
-
* `light()` lazily builds
|
|
20
|
+
* `light()` lazily builds an unseeded light client over the same endpoint.
|
|
21
21
|
*/
|
|
22
22
|
declare class Chain {
|
|
23
23
|
readonly client: CsdClient;
|
|
@@ -36,7 +36,11 @@ declare class Chain {
|
|
|
36
36
|
* chain (`verifyInputValues` recomputes each source tx's txid, so a lying RPC can't under-report) and
|
|
37
37
|
* computes change from the VERIFIED total — closing the UTXO-VALUE-1 implicit-fee-burn. Fail-closed:
|
|
38
38
|
* if any source can't be verified it signs nothing. Prefer this over the pure `buildSend`. Pass
|
|
39
|
-
* `broadcast:false` to get the signed tx back without submitting.
|
|
39
|
+
* `broadcast:false` to get the signed tx back without submitting. A submit request that fails after signing
|
|
40
|
+
* throws a `SubmitInFlightError` carrying the signed `txid` and `nodeJson`: the node may have taken it, so check
|
|
41
|
+
* before resending, and resend that `nodeJson` with `submit` rather than a new send. A 4xx answer (a proxy's
|
|
42
|
+
* body cap or rate limit, or the node refusing the request) broadcast nothing and throws a final `CairnError`
|
|
43
|
+
* with code `SUBMIT_REJECTED`.
|
|
40
44
|
*/
|
|
41
45
|
send(p: {
|
|
42
46
|
priv: string;
|
|
@@ -50,7 +54,8 @@ declare class Chain {
|
|
|
50
54
|
}): Promise<BuildResult & {
|
|
51
55
|
submitResult?: _inversealtruism_csd_client.RpcSubmit;
|
|
52
56
|
}>;
|
|
53
|
-
/** A
|
|
57
|
+
/** A light client over the same RPC (built once, reused), with no checkpoint and no batch header provider: it proves
|
|
58
|
+
* nothing until seeded. The facade's seeded client backs `cairn.index.verifyInclusion` and `verifyOfferForFill`. */
|
|
54
59
|
light(): LightClient;
|
|
55
60
|
}
|
|
56
61
|
|
package/dist/chain.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as _inversealtruism_csd_client from '@inversealtruism/csd-client';
|
|
|
2
2
|
import { CsdClient } from '@inversealtruism/csd-client';
|
|
3
3
|
export { ClientOptions, CsdClient, RpcBlock, RpcHeaderJson, RpcSubmit, RpcTip, RpcTxJson, RpcUtxo, RpcUtxos, rpcHeaderToHeader, rpcTxToTx, verifyInputValues } from '@inversealtruism/csd-client';
|
|
4
4
|
import { BuildResult } from '@inversealtruism/csd-tx';
|
|
5
|
-
export { BuildResult, InputVerifier, Selection, Signed, Utxo, buildAttest, buildPropose, buildSend, buildSendVerified, selectInputs, signTx, txSize, txToNodeJson } from '@inversealtruism/csd-tx';
|
|
5
|
+
export { BuildResult, InputVerifier, Selection, Signed, Utxo, buildAttest, buildAttestVerified, buildPropose, buildProposeVerified, buildSend, buildSendVerified, selectInputs, signTx, txSize, txToNodeJson } from '@inversealtruism/csd-tx';
|
|
6
6
|
export { App, BlockHeader, Tx, TxInput, TxOutput, canonicalJson, deserialize, hb, headerHash, hx, merkleBranch, merkleRoot, payloadHash, serialize, sha256d, sighash, strip0x, strippedTx, txid, verifyContentBytes, verifyMerkleProof } from '@inversealtruism/csd-codec';
|
|
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';
|
|
@@ -17,7 +17,7 @@ interface ChainOptions {
|
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* Convenience handle over a CSD node RPC. `client` is the full typed RPC client;
|
|
20
|
-
* `light()` lazily builds
|
|
20
|
+
* `light()` lazily builds an unseeded light client over the same endpoint.
|
|
21
21
|
*/
|
|
22
22
|
declare class Chain {
|
|
23
23
|
readonly client: CsdClient;
|
|
@@ -36,7 +36,11 @@ declare class Chain {
|
|
|
36
36
|
* chain (`verifyInputValues` recomputes each source tx's txid, so a lying RPC can't under-report) and
|
|
37
37
|
* computes change from the VERIFIED total — closing the UTXO-VALUE-1 implicit-fee-burn. Fail-closed:
|
|
38
38
|
* if any source can't be verified it signs nothing. Prefer this over the pure `buildSend`. Pass
|
|
39
|
-
* `broadcast:false` to get the signed tx back without submitting.
|
|
39
|
+
* `broadcast:false` to get the signed tx back without submitting. A submit request that fails after signing
|
|
40
|
+
* throws a `SubmitInFlightError` carrying the signed `txid` and `nodeJson`: the node may have taken it, so check
|
|
41
|
+
* before resending, and resend that `nodeJson` with `submit` rather than a new send. A 4xx answer (a proxy's
|
|
42
|
+
* body cap or rate limit, or the node refusing the request) broadcast nothing and throws a final `CairnError`
|
|
43
|
+
* with code `SUBMIT_REJECTED`.
|
|
40
44
|
*/
|
|
41
45
|
send(p: {
|
|
42
46
|
priv: string;
|
|
@@ -50,7 +54,8 @@ declare class Chain {
|
|
|
50
54
|
}): Promise<BuildResult & {
|
|
51
55
|
submitResult?: _inversealtruism_csd_client.RpcSubmit;
|
|
52
56
|
}>;
|
|
53
|
-
/** A
|
|
57
|
+
/** A light client over the same RPC (built once, reused), with no checkpoint and no batch header provider: it proves
|
|
58
|
+
* nothing until seeded. The facade's seeded client backs `cairn.index.verifyInclusion` and `verifyOfferForFill`. */
|
|
54
59
|
light(): LightClient;
|
|
55
60
|
}
|
|
56
61
|
|
package/dist/chain.js
CHANGED
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
addrFromPriv,
|
|
7
7
|
addrFromPub,
|
|
8
8
|
buildAttest,
|
|
9
|
+
buildAttestVerified,
|
|
9
10
|
buildPropose,
|
|
11
|
+
buildProposeVerified,
|
|
10
12
|
buildSend,
|
|
11
13
|
buildSendVerified,
|
|
12
14
|
canonicalJson,
|
|
@@ -42,7 +44,8 @@ import {
|
|
|
42
44
|
verifyDigest,
|
|
43
45
|
verifyInputValues,
|
|
44
46
|
verifyMerkleProof
|
|
45
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-IPBMMJ3Z.js";
|
|
48
|
+
import "./chunk-37OHCH7R.js";
|
|
46
49
|
export {
|
|
47
50
|
ADDR_RE,
|
|
48
51
|
Chain,
|
|
@@ -51,7 +54,9 @@ export {
|
|
|
51
54
|
addrFromPriv,
|
|
52
55
|
addrFromPub,
|
|
53
56
|
buildAttest,
|
|
57
|
+
buildAttestVerified,
|
|
54
58
|
buildPropose,
|
|
59
|
+
buildProposeVerified,
|
|
55
60
|
buildSend,
|
|
56
61
|
buildSendVerified,
|
|
57
62
|
canonicalJson,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/errors.ts
|
|
2
|
-
var SDK_VERSION = "0.8.
|
|
2
|
+
var SDK_VERSION = "0.8.2";
|
|
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. */
|
|
@@ -82,17 +82,22 @@ var ContentVerificationError = class extends CairnError {
|
|
|
82
82
|
}
|
|
83
83
|
};
|
|
84
84
|
var SubmitInFlightError = class extends CairnError {
|
|
85
|
-
/** The locally-computed consensus txid to reconcile against the chain (
|
|
85
|
+
/** The locally-computed consensus txid to reconcile against the chain (undefined when it is not known). */
|
|
86
86
|
txid;
|
|
87
|
-
/**
|
|
87
|
+
/** The signed transaction, set only when `Chain.send` threw this (a wallet write keeps it in the wallet).
|
|
88
|
+
* `chain.submit(nodeJson)` resends it unchanged: the same coins under the same txid, so it cannot pay twice,
|
|
89
|
+
* where a new send may select other coins. */
|
|
90
|
+
nodeJson;
|
|
91
|
+
/** Always true: the tx MAY be in the mempool. Reconcile via `txid` or the balance; never blind-retry. */
|
|
88
92
|
maybeSent = true;
|
|
89
|
-
constructor(message, code, txid) {
|
|
90
|
-
super(message, { code, docsPath: "/errors#submit-inflight", retryable: false });
|
|
93
|
+
constructor(message, code, txid, cause, nodeJson) {
|
|
94
|
+
super(message, { code, docsPath: "/errors#submit-inflight", retryable: false, cause });
|
|
91
95
|
this.txid = txid;
|
|
96
|
+
this.nodeJson = nodeJson;
|
|
92
97
|
}
|
|
93
98
|
};
|
|
94
99
|
var NATIVE_CODES = /* @__PURE__ */ new Set(["FIRST_PARTY_ONLY", "RATE_LIMITED", "FORBIDDEN", "INTERNAL"]);
|
|
95
|
-
var NESTED_TRANSIENT = /* @__PURE__ */ new Set(["VERIFY_UNAVAILABLE", "OFFER_UNKNOWN", "WALLET_UNAVAILABLE", "REVIEW_REQUIRED"]);
|
|
100
|
+
var NESTED_TRANSIENT = /* @__PURE__ */ new Set(["VERIFY_UNAVAILABLE", "OFFER_UNKNOWN", "WALLET_UNAVAILABLE", "REVIEW_REQUIRED", "SEAL_NOT_MINED"]);
|
|
96
101
|
var NESTED_TERMINAL = /* @__PURE__ */ new Set([
|
|
97
102
|
"FILL_UNSAFE",
|
|
98
103
|
"FILL_WRONG_TARGET",
|
|
@@ -114,7 +119,9 @@ var NESTED_TERMINAL = /* @__PURE__ */ new Set([
|
|
|
114
119
|
"OFFER_SOURCE_CONFLICT",
|
|
115
120
|
"FILL_LEGACY_SUNSET",
|
|
116
121
|
"VERIFY_HORIZON",
|
|
117
|
-
"VERIFY_UNREPRESENTABLE"
|
|
122
|
+
"VERIFY_UNREPRESENTABLE",
|
|
123
|
+
"RESERVATION_DENIED",
|
|
124
|
+
"RPC_BLOCK_MISMATCH"
|
|
118
125
|
]);
|
|
119
126
|
function mapProviderError(error, code) {
|
|
120
127
|
if (code === "USER_REJECTED" || code === "APPROVAL_CLOSED") return new UserRejectedError(error);
|
|
@@ -132,11 +139,15 @@ function mapProviderError(error, code) {
|
|
|
132
139
|
function mapSubmitResultError(result) {
|
|
133
140
|
const code = result?.code;
|
|
134
141
|
const msg = typeof result?.error === "string" && result.error || code || "The wallet refused the transaction.";
|
|
135
|
-
if (code === "SUBMIT_MAYBE_INFLIGHT" || code === "SUBMIT_DUPLICATE") {
|
|
142
|
+
if (code === "SUBMIT_MAYBE_INFLIGHT" || code === "SUBMIT_DUPLICATE" || code === "WALLET_UNAVAILABLE") {
|
|
136
143
|
return new SubmitInFlightError(msg, code, typeof result?.txid === "string" ? result.txid : void 0);
|
|
137
144
|
}
|
|
138
145
|
return mapProviderError(msg, code);
|
|
139
146
|
}
|
|
147
|
+
function mapWriteProviderError(error, code) {
|
|
148
|
+
if (code === "WALLET_UNAVAILABLE") return new SubmitInFlightError(error || code, code);
|
|
149
|
+
return mapProviderError(error, code);
|
|
150
|
+
}
|
|
140
151
|
function errorCode(err) {
|
|
141
152
|
return err instanceof CairnError ? err.code : "UNKNOWN";
|
|
142
153
|
}
|
|
@@ -153,5 +164,6 @@ export {
|
|
|
153
164
|
SubmitInFlightError,
|
|
154
165
|
mapProviderError,
|
|
155
166
|
mapSubmitResultError,
|
|
167
|
+
mapWriteProviderError,
|
|
156
168
|
errorCode
|
|
157
169
|
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CairnError,
|
|
3
3
|
NotInstalledError,
|
|
4
|
+
SubmitInFlightError,
|
|
4
5
|
UnsupportedMethodError,
|
|
5
6
|
mapProviderError,
|
|
6
|
-
mapSubmitResultError
|
|
7
|
-
|
|
7
|
+
mapSubmitResultError,
|
|
8
|
+
mapWriteProviderError
|
|
9
|
+
} from "./chunk-37OHCH7R.js";
|
|
8
10
|
|
|
9
11
|
// src/connect.ts
|
|
10
12
|
var DEFAULT_PROVIDER_TIMEOUT_MS = 10 * 6e4;
|
|
@@ -78,7 +80,8 @@ function unwrap(reply) {
|
|
|
78
80
|
throw mapProviderError(reply?.error ?? "Unknown wallet error", reply?.code);
|
|
79
81
|
}
|
|
80
82
|
function unwrapWrite(reply) {
|
|
81
|
-
|
|
83
|
+
if (!reply || !reply.ok) throw mapWriteProviderError(reply?.error ?? "Unknown wallet error", reply?.code);
|
|
84
|
+
const result = reply.result;
|
|
82
85
|
if (result && typeof result === "object" && result.ok === false) {
|
|
83
86
|
throw mapSubmitResultError(result);
|
|
84
87
|
}
|
|
@@ -94,10 +97,12 @@ var WalletConnection = class {
|
|
|
94
97
|
this.provider = provider;
|
|
95
98
|
this._timeoutMs = opts.timeoutMs ?? DEFAULT_PROVIDER_TIMEOUT_MS;
|
|
96
99
|
}
|
|
97
|
-
// B7c: race a provider call against the per-call timeout. On timeout, throw a fail-closed
|
|
98
|
-
// (retryable:false
|
|
99
|
-
//
|
|
100
|
-
|
|
100
|
+
// B7c: race a provider call against the per-call timeout. On timeout, throw a fail-closed error
|
|
101
|
+
// (retryable:false). A write's request stays open in the wallet and may still be approved and sent, so its
|
|
102
|
+
// timeout is a maybe-sent SubmitInFlightError (WALLET_TIMEOUT, no txid); while that request stays open, a resend
|
|
103
|
+
// opens a second one, and approving both sends twice. The genuine wallet resolves quickly
|
|
104
|
+
// once the user acts; this only fires on a truly hung/absent response.
|
|
105
|
+
call(op, label, write = false) {
|
|
101
106
|
const ms = this._timeoutMs;
|
|
102
107
|
const invoke = () => new Promise((resolve) => {
|
|
103
108
|
resolve(op());
|
|
@@ -105,7 +110,7 @@ var WalletConnection = class {
|
|
|
105
110
|
if (!Number.isFinite(ms) || ms <= 0) return invoke();
|
|
106
111
|
let timer;
|
|
107
112
|
const timeout = new Promise((_, reject) => {
|
|
108
|
-
timer = setTimeout(() => reject(new CairnError(`the wallet did not respond to ${label} within ${ms}ms`, { retryable: false })), ms);
|
|
113
|
+
timer = setTimeout(() => reject(write ? new SubmitInFlightError(`the wallet did not respond to ${label} within ${ms}ms; the request may still be open in the wallet and be sent if approved, so reject it in the wallet and check the chain before sending it again`, "WALLET_TIMEOUT") : new CairnError(`the wallet did not respond to ${label} within ${ms}ms`, { retryable: false })), ms);
|
|
109
114
|
});
|
|
110
115
|
return Promise.race([invoke(), timeout]).finally(() => clearTimeout(timer));
|
|
111
116
|
}
|
|
@@ -181,7 +186,7 @@ var WalletConnection = class {
|
|
|
181
186
|
return Promise.reject(new UnsupportedMethodError("this wallet predates fillOffer() \u2014 ask the user to update the Cairn Wallet"));
|
|
182
187
|
}
|
|
183
188
|
this.adviseFillSpvCapability();
|
|
184
|
-
return this.call(() => this.provider.fillOffer(params), "fillOffer").then(unwrapWrite);
|
|
189
|
+
return this.call(() => this.provider.fillOffer(params), "fillOffer", true).then(unwrapWrite);
|
|
185
190
|
}
|
|
186
191
|
// P75-5 MF-10: ONE capability advisory per connection. The wallet's on-device fill-SPV (0.2.60+) is the
|
|
187
192
|
// payment-grade fill boundary; when the provider SELF-REPORTS an older version, warn the integrator once
|
|
@@ -229,26 +234,26 @@ var WalletConnection = class {
|
|
|
229
234
|
}
|
|
230
235
|
/** Send CSD (always prompts with clear-signing; wallet picks inputs + returns change to itself). */
|
|
231
236
|
send(params) {
|
|
232
|
-
return this.call(() => this.provider.send(params), "send").then(unwrapWrite);
|
|
237
|
+
return this.call(() => this.provider.send(params), "send", true).then(unwrapWrite);
|
|
233
238
|
}
|
|
234
239
|
/** Low-level Propose (always prompts). The caller computes the payload hash, uri and expiry epoch. */
|
|
235
240
|
propose(params) {
|
|
236
|
-
return this.call(() => this.provider.propose(params), "propose").then(unwrapWrite);
|
|
241
|
+
return this.call(() => this.provider.propose(params), "propose", true).then(unwrapWrite);
|
|
237
242
|
}
|
|
238
243
|
/** Attest/support a proposal (always prompts). */
|
|
239
244
|
attest(params) {
|
|
240
|
-
return this.call(() => this.provider.attest(params), "attest").then(unwrapWrite);
|
|
245
|
+
return this.call(() => this.provider.attest(params), "attest", true).then(unwrapWrite);
|
|
241
246
|
}
|
|
242
247
|
/** Seal a commit-reveal claim (always prompts; salt stays inside the wallet). */
|
|
243
248
|
sealClaim(params) {
|
|
244
|
-
return this.call(() => this.provider.sealClaim(params), "sealClaim").then(unwrapWrite);
|
|
249
|
+
return this.call(() => this.provider.sealClaim(params), "sealClaim", true).then(unwrapWrite);
|
|
245
250
|
}
|
|
246
251
|
/** Reveal a previously sealed claim by its sealing txid (always prompts). */
|
|
247
252
|
revealClaim(txid) {
|
|
248
253
|
if (typeof txid !== "string" || !txid) {
|
|
249
254
|
return Promise.reject(new UnsupportedMethodError("revealClaim requires the sealing txid"));
|
|
250
255
|
}
|
|
251
|
-
return this.call(() => this.provider.revealClaim(txid), "revealClaim").then(unwrapWrite);
|
|
256
|
+
return this.call(() => this.provider.revealClaim(txid), "revealClaim", true).then(unwrapWrite);
|
|
252
257
|
}
|
|
253
258
|
};
|
|
254
259
|
async function getWallet(opts = {}) {
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CairnError,
|
|
3
|
+
SubmitInFlightError
|
|
4
|
+
} from "./chunk-37OHCH7R.js";
|
|
5
|
+
|
|
1
6
|
// src/chain.ts
|
|
2
7
|
import {
|
|
3
8
|
CsdClient,
|
|
@@ -13,7 +18,9 @@ import {
|
|
|
13
18
|
buildSend,
|
|
14
19
|
buildSendVerified,
|
|
15
20
|
buildPropose,
|
|
16
|
-
buildAttest
|
|
21
|
+
buildAttest,
|
|
22
|
+
buildProposeVerified,
|
|
23
|
+
buildAttestVerified
|
|
17
24
|
} from "@inversealtruism/csd-tx";
|
|
18
25
|
import {
|
|
19
26
|
payloadHash,
|
|
@@ -77,7 +84,11 @@ var Chain = class {
|
|
|
77
84
|
* chain (`verifyInputValues` recomputes each source tx's txid, so a lying RPC can't under-report) and
|
|
78
85
|
* computes change from the VERIFIED total — closing the UTXO-VALUE-1 implicit-fee-burn. Fail-closed:
|
|
79
86
|
* if any source can't be verified it signs nothing. Prefer this over the pure `buildSend`. Pass
|
|
80
|
-
* `broadcast:false` to get the signed tx back without submitting.
|
|
87
|
+
* `broadcast:false` to get the signed tx back without submitting. A submit request that fails after signing
|
|
88
|
+
* throws a `SubmitInFlightError` carrying the signed `txid` and `nodeJson`: the node may have taken it, so check
|
|
89
|
+
* before resending, and resend that `nodeJson` with `submit` rather than a new send. A 4xx answer (a proxy's
|
|
90
|
+
* body cap or rate limit, or the node refusing the request) broadcast nothing and throws a final `CairnError`
|
|
91
|
+
* with code `SUBMIT_REJECTED`.
|
|
81
92
|
*/
|
|
82
93
|
async send(p) {
|
|
83
94
|
const from = addrFromPriv2(p.priv);
|
|
@@ -91,10 +102,19 @@ var Chain = class {
|
|
|
91
102
|
verify: (inputs) => verifyInputValues2(this.client, inputs)
|
|
92
103
|
});
|
|
93
104
|
if (!built.ok || p.broadcast === false) return built;
|
|
94
|
-
|
|
105
|
+
let submitResult;
|
|
106
|
+
try {
|
|
107
|
+
submitResult = await this.client.submit(built.nodeJson);
|
|
108
|
+
} catch (e) {
|
|
109
|
+
const what = e?.message ?? String(e);
|
|
110
|
+
if (e?.terminal === true)
|
|
111
|
+
throw new CairnError(`the submit request was refused (${what}), so nothing was broadcast; a 400 or 413 from a proxy is usually its request body cap (a transaction with many inputs), a 429 its rate limit`, { code: "SUBMIT_REJECTED", cause: e });
|
|
112
|
+
throw new SubmitInFlightError(`the submit request failed after signing (${what}); the node may have taken the transaction, so check ${built.txid} before sending again`, "SUBMIT_MAYBE_INFLIGHT", built.txid, e, built.nodeJson);
|
|
113
|
+
}
|
|
95
114
|
return { ...built, ok: built.ok && submitResult.ok !== false, submitResult };
|
|
96
115
|
}
|
|
97
|
-
/** A
|
|
116
|
+
/** A light client over the same RPC (built once, reused), with no checkpoint and no batch header provider: it proves
|
|
117
|
+
* nothing until seeded. The facade's seeded client backs `cairn.index.verifyInclusion` and `verifyOfferForFill`. */
|
|
98
118
|
light() {
|
|
99
119
|
if (!this._light) {
|
|
100
120
|
this._light = new LightClient2({ client: this.client });
|
|
@@ -117,6 +137,8 @@ export {
|
|
|
117
137
|
buildSendVerified,
|
|
118
138
|
buildPropose,
|
|
119
139
|
buildAttest,
|
|
140
|
+
buildProposeVerified,
|
|
141
|
+
buildAttestVerified,
|
|
120
142
|
payloadHash,
|
|
121
143
|
canonicalJson,
|
|
122
144
|
verifyContentBytes,
|
|
@@ -1,7 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HttpError
|
|
3
|
+
} from "./chunk-37OHCH7R.js";
|
|
4
|
+
|
|
1
5
|
// src/indexer.ts
|
|
2
6
|
import { verifyMerkleProof } from "@inversealtruism/csd-codec";
|
|
3
7
|
var HEX64 = /^0x[0-9a-fA-F]{64}$/;
|
|
4
8
|
var MAX_SSE_BUF = 1024 * 1024;
|
|
9
|
+
var UTXO_PAGE = 1e3;
|
|
10
|
+
var UTXO_MAX_PAGES = 100;
|
|
11
|
+
var UTXO_CURSOR = /^\d{1,16}:0x[0-9a-fA-F]{64}:\d{1,10}$/;
|
|
12
|
+
function utxoCursor(row) {
|
|
13
|
+
const r = row;
|
|
14
|
+
const c = `${String(r?.status?.block_height)}:${String(r?.txid)}:${String(r?.vout)}`;
|
|
15
|
+
return UTXO_CURSOR.test(c) ? c : null;
|
|
16
|
+
}
|
|
17
|
+
function cursorAfter(a, b) {
|
|
18
|
+
const [ha, ta, va] = a.split(":"), [hb, tb, vb] = b.split(":");
|
|
19
|
+
if (Number(ha) !== Number(hb)) return Number(ha) > Number(hb);
|
|
20
|
+
if (ta.toLowerCase() !== tb.toLowerCase()) return ta.toLowerCase() > tb.toLowerCase();
|
|
21
|
+
return Number(va) > Number(vb);
|
|
22
|
+
}
|
|
5
23
|
function isValidMerkleProof(p) {
|
|
6
24
|
if (!p || typeof p !== "object") return false;
|
|
7
25
|
const m = p;
|
|
@@ -51,8 +69,29 @@ var IndexerClient = class {
|
|
|
51
69
|
addressTxs(a) {
|
|
52
70
|
return this.http.getJson(`/address/${encodeURIComponent(a)}/txs`);
|
|
53
71
|
}
|
|
54
|
-
|
|
55
|
-
|
|
72
|
+
/** Every unspent output of an address. The indexer answers in pages of at most 1,000 rows; this follows its
|
|
73
|
+
* `?after=` cursor until a short page, reading at most 100 pages: when the 100th is full (100,000 or more
|
|
74
|
+
* outputs) it throws HttpError instead. An indexer that predates paging answers the whole set at once. */
|
|
75
|
+
async addressUtxo(a) {
|
|
76
|
+
const path = `/address/${encodeURIComponent(a)}/utxo`;
|
|
77
|
+
const first = await this.http.getJson(path);
|
|
78
|
+
if (!Array.isArray(first) || first.length !== UTXO_PAGE) return first;
|
|
79
|
+
const out = [...first];
|
|
80
|
+
let page = first;
|
|
81
|
+
let prev = null;
|
|
82
|
+
for (let pages = 1; page.length === UTXO_PAGE; pages++) {
|
|
83
|
+
if (pages === UTXO_MAX_PAGES)
|
|
84
|
+
throw new HttpError(200, this.http.url(path), `too many unspent outputs: addressUtxo reads at most ${UTXO_MAX_PAGES} pages of ${UTXO_PAGE} rows, and page ${UTXO_MAX_PAGES} is full`);
|
|
85
|
+
const after = utxoCursor(page[UTXO_PAGE - 1]);
|
|
86
|
+
if (!after) throw new HttpError(200, this.http.url(path), "malformed response: a utxo row has no block_height, txid and vout to page from");
|
|
87
|
+
if (prev !== null && !cursorAfter(after, prev)) throw new HttpError(200, this.http.url(path, { after }), "malformed response: the utxo pages do not advance");
|
|
88
|
+
page = await this.http.getJson(path, { after });
|
|
89
|
+
if (!Array.isArray(page)) throw new HttpError(200, this.http.url(path, { after }), "malformed response: a utxo page is not an array");
|
|
90
|
+
if (prev === null && page.length > 0 && utxoCursor(page[0]) === utxoCursor(first[0])) return first;
|
|
91
|
+
prev = after;
|
|
92
|
+
out.push(...page);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
56
95
|
}
|
|
57
96
|
reputation(a) {
|
|
58
97
|
return this.http.getJson(`/address/${encodeURIComponent(a)}/reputation`);
|
|
@@ -71,7 +110,8 @@ var IndexerClient = class {
|
|
|
71
110
|
}
|
|
72
111
|
// ---- trust-minimized inclusion ----------------------------------------
|
|
73
112
|
/**
|
|
74
|
-
* Verify
|
|
113
|
+
* Verify that `txid` is a node of a block's merkle tree (an inner node or the root folds too, so pass an id you
|
|
114
|
+
* hashed from a transaction body you hold) by folding its merkle branch to the root
|
|
75
115
|
* with csd-codec.verifyMerkleProof. If `headerMerkleAt` was provided, the
|
|
76
116
|
* proof's root is cross-checked against the header merkle at that height →
|
|
77
117
|
* trustLevel "verified-inclusion" (only as trustworthy as that header source —
|