@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 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 + a verifying **light client** (re-exports the `csd-*` primitives). |
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()`, `domains()/domainProposals(domain)`, `proposal()/attestations()`, `reputation()`, **`verifyInclusion(txid)`** (trust-minimized), `streamAll()/streamBlocks()/streamDomain()` (SSE), `subscribe()` (WS).
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()` (verifying light client). The package also re-exports the primitives under the `/chain` subpath: `buildSend/buildPropose/buildAttest`, `signTx`, `keygen`, `payloadHash`, `verifyMerkleProof`, `LightClient`, etc.
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
- const submitResult = await this.client.submit(built.nodeJson);
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 verifying light client over the same RPC (built once, reused). */
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 a verifying light client over the same endpoint.
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 verifying light client over the same RPC (built once, reused). */
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 a verifying light client over the same endpoint.
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 verifying light client over the same RPC (built once, reused). */
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-C452UODV.js";
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,7 +1,7 @@
1
1
  import {
2
2
  CairnError,
3
3
  ContentVerificationError
4
- } from "./chunk-UAPWZZQK.js";
4
+ } from "./chunk-37OHCH7R.js";
5
5
 
6
6
  // src/content.ts
7
7
  import { payloadHash, canonicalJson, verifyContentBytes } from "@inversealtruism/csd-codec";
@@ -1,5 +1,5 @@
1
1
  // src/errors.ts
2
- var SDK_VERSION = "0.8.0";
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 (may be undefined if the wallet omitted it). */
85
+ /** The locally-computed consensus txid to reconcile against the chain (undefined when it is not known). */
86
86
  txid;
87
- /** Always true: the tx MAY be in the mempool. Reconcile via `txid`; never blind-retry. */
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,6 +1,6 @@
1
1
  import {
2
2
  getWallet
3
- } from "./chunk-WWO3FXHG.js";
3
+ } from "./chunk-EVEBQSDK.js";
4
4
 
5
5
  // src/controller.ts
6
6
  var DISCONNECTED = Object.freeze({ status: "disconnected", account: null, error: null });
@@ -1,10 +1,12 @@
1
1
  import {
2
2
  CairnError,
3
3
  NotInstalledError,
4
+ SubmitInFlightError,
4
5
  UnsupportedMethodError,
5
6
  mapProviderError,
6
- mapSubmitResultError
7
- } from "./chunk-UAPWZZQK.js";
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
- const result = unwrap(reply);
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 CairnError
98
- // (retryable:false: a write may already have reached the wallet, so never invite a blind auto-retry). The
99
- // genuine wallet resolves quickly once the user acts; this only fires on a truly hung/absent response.
100
- call(op, label) {
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
- const submitResult = await this.client.submit(built.nodeJson);
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 verifying light client over the same RPC (built once, reused). */
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
- addressUtxo(a) {
55
- return this.http.getJson(`/address/${encodeURIComponent(a)}/utxo`);
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 a tx is included in a block by folding its merkle branch to the root
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 —