@inversealtruism/cairn-sdk 0.2.3 → 0.3.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/dist/index.d.cts CHANGED
@@ -10,12 +10,13 @@ export { ContentClientOptions, PreparedContent } from './content.cjs';
10
10
  import { RegistryClient } from './registry.cjs';
11
11
  import { WalletConnection, DetectOptions } from './connect.cjs';
12
12
  export { AttestParams, CairnProvider, ConnectResult, FillParams, ProposeParams, ProviderDetail, ProviderInfo, SealClaimParams, SendParams, SignInResult, SiwcParams, SiwcResult, TxResult, WalletCapabilities, WalletPermission, connect, detectProvider, discoverProviders, getWallet, isInstalled } from './connect.cjs';
13
+ import { InclusionResult } from '@inversealtruism/csd-light';
13
14
  export { CairnController, CairnControllerOptions, CairnState } from './controller.cjs';
15
+ export { bindOfferTerms, feeBpsAt } from '@inversealtruism/cairnx-core';
14
16
  import '@inversealtruism/csd-client';
15
17
  import '@inversealtruism/csd-tx';
16
18
  import '@inversealtruism/csd-codec';
17
19
  import '@inversealtruism/csd-crypto';
18
- import '@inversealtruism/csd-light';
19
20
  import '@inversealtruism/csd-registry';
20
21
 
21
22
  /** `/cairnx/resolve/:name` — the display-path name→address resolution. */
@@ -104,13 +105,60 @@ declare class NamesClient {
104
105
  state(): Promise<unknown>;
105
106
  }
106
107
 
108
+ /** The fee/rebate-relevant fields of an offer, derived from the MERKLE-PROVEN offer (never a served object). */
109
+ interface ProvenOfferTerms {
110
+ height: number;
111
+ feeBps: number;
112
+ value?: string;
113
+ taker?: string;
114
+ bid?: string;
115
+ min?: string;
116
+ }
117
+ /** The trust-labeled verdict. `ok:false` + `transient:true` = a retryable chain-catching-up soft-fail (NOT a
118
+ * proven mismatch). `payto`/`seller`/`terms` are populated whenever they could be merkle-derived. */
119
+ interface OfferFillCheck {
120
+ ok: boolean;
121
+ /** "verified" = merkle-proven inclusion + on-chain author; "proof-consistent" = a proof that folds but the
122
+ * header could not be PoW-verified (indexer-trusted); "unverified" = could not prove. */
123
+ trust: "verified" | "proof-consistent" | "unverified";
124
+ reason?: string;
125
+ transient?: boolean;
126
+ payto?: string;
127
+ seller?: string;
128
+ terms?: ProvenOfferTerms;
129
+ blockHeight?: number;
130
+ }
131
+
132
+ /**
133
+ * Pre-verify an offer on-chain before building a `fillOffer` payment. `light` is a PoW-verifying light client
134
+ * (its `verifyTxInclusion` surfaces the merkle-proven tx + committed appPayloadHash); `client` reads source txs
135
+ * for the prevout-owner author bind. Pass `servedOffer` (the resolver's offer object) to also bind its
136
+ * payto/seller/terms to the proven values. See the module header for the trust posture.
137
+ */
138
+ declare function preverifyOffer(opts: {
139
+ light: {
140
+ verifyTxInclusion(txidHex: string): Promise<InclusionResult>;
141
+ };
142
+ client: {
143
+ tx(id: string): Promise<unknown>;
144
+ };
145
+ offerId: string;
146
+ servedOffer?: unknown;
147
+ }): Promise<OfferFillCheck>;
148
+
107
149
  /** Stable machine-readable error codes (branch on these, not the message text). */
108
- type CairnErrorCode = "NOT_INSTALLED" | "USER_REJECTED" | "WALLET_LOCKED" | "UNSUPPORTED_METHOD" | "HTTP_ERROR" | "CONTENT_VERIFICATION" | "ACCOUNT_CHANGED" | "FIRST_PARTY_ONLY" | "RATE_LIMITED" | "APPROVAL_CLOSED" | "FORBIDDEN" | "INTERNAL" | "UNKNOWN";
109
- declare const SDK_VERSION = "0.2.3";
150
+ type CairnErrorCode = "NOT_INSTALLED" | "USER_REJECTED" | "WALLET_LOCKED" | "UNSUPPORTED_METHOD" | "HTTP_ERROR" | "CONTENT_VERIFICATION" | "ACCOUNT_CHANGED" | "FIRST_PARTY_ONLY" | "RATE_LIMITED" | "APPROVAL_CLOSED" | "FORBIDDEN" | "INTERNAL" | "GHOST_INPUTS_SKIPPED" | "VERIFY_UNAVAILABLE" | "VERIFY_TAMPER" | "INSUFFICIENT" | "TOO_MANY_INPUTS" | "FEE_TOO_LOW" | "FEE_CAP" | "BAD_FEE" | "ZERO_ADDR_REFUSED" | "BAD_OUTPUTS" | "NO_OUTPUTS" | "BAD_REQUEST" | "OFFER_UNKNOWN" | "FILL_UNSAFE" | "FILL_WRONG_TARGET" | "SOURCE_DIVERGENCE" | "SUBMIT_REJECTED" | "SUBMIT_MAYBE_INFLIGHT" | "SUBMIT_DUPLICATE" | "NOTHING_TO_CONSOLIDATE" | "UNKNOWN";
151
+ declare const SDK_VERSION = "0.3.2";
110
152
  interface CairnErrorOptions {
111
153
  code?: CairnErrorCode;
112
154
  docsPath?: string;
113
155
  cause?: unknown;
156
+ /**
157
+ * Whether a dApp may safely AUTO-retry this failure without fresh user intent. Transient infra faults
158
+ * (e.g. a node that was briefly unreachable, nothing signed) are `true`; fund-safety refusals are `false`.
159
+ * Defaults to `false` (fail-closed: never invite a retry unless the error is provably transient).
160
+ */
161
+ retryable?: boolean;
114
162
  }
115
163
  declare class CairnError extends Error {
116
164
  /** Stable machine code for programmatic branching. */
@@ -120,7 +168,13 @@ declare class CairnError extends Error {
120
168
  /** Relative docs path (under the SDK docs base), if any. */
121
169
  readonly docsPath?: string;
122
170
  /** The cairn-sdk version that produced this error. */
123
- readonly version = "0.2.3";
171
+ readonly version = "0.3.2";
172
+ /**
173
+ * Whether a dApp may safely AUTO-retry this failure. `false` for fund-safety refusals and user-intent
174
+ * declines (the default); `true` only for provably transient infra faults where nothing was signed.
175
+ * A `SubmitInFlightError` is `false` too: reconcile its `txid`, never blind-retry.
176
+ */
177
+ readonly retryable: boolean;
124
178
  constructor(shortMessage: string, opts?: CairnErrorOptions);
125
179
  /**
126
180
  * Walk the `cause` chain. With a predicate, returns the FIRST matching error (or null). Without one,
@@ -156,7 +210,40 @@ declare class ContentVerificationError extends CairnError {
156
210
  expected: string;
157
211
  constructor(expected: string);
158
212
  }
213
+ /**
214
+ * The wallet SUBMITTED but the outcome is AMBIGUOUS: the tx MAY already be in the mempool. Thrown by the
215
+ * write wrappers for the nested `SUBMIT_MAYBE_INFLIGHT` (request threw / timed out / 5xx gateway / unreadable
216
+ * body) and `SUBMIT_DUPLICATE` (node answered "already present or mempool conflict") codes. It CARRIES the
217
+ * locally-computed consensus `txid` so a dApp can RECONCILE (check the explorer / balance) instead of blindly
218
+ * re-broadcasting, which would re-invite the double-broadcast this contract exists to prevent (F14). It is NOT
219
+ * retryable (`retryable === false`); `maybeSent === true` distinguishes it from a definitely-not-sent terminal
220
+ * refusal. See WALLET-ERROR-CODES.md in the cairn-wallet repo.
221
+ */
222
+ declare class SubmitInFlightError extends CairnError {
223
+ /** The locally-computed consensus txid to reconcile against the chain (may be undefined if the wallet omitted it). */
224
+ readonly txid?: string;
225
+ /** Always true: the tx MAY be in the mempool. Reconcile via `txid`; never blind-retry. */
226
+ readonly maybeSent = true;
227
+ constructor(message: string, code: "SUBMIT_MAYBE_INFLIGHT" | "SUBMIT_DUPLICATE", txid?: string);
228
+ }
159
229
  declare function mapProviderError(error: string, code?: string): CairnError;
230
+ /**
231
+ * F14: map a NESTED SubmitResult refusal (`{ ok:false, code, error, txid }`, ridden inside an outer-ok reply)
232
+ * onto a typed error the write wrappers THROW. Without this a fund-safety refusal would RESOLVE with a phantom
233
+ * `txid:undefined`, so a dApp doing `const { txid } = await fillOffer(...); markPaid(txid)` marks a REFUSED
234
+ * fill as paid. Three buckets:
235
+ * - AMBIGUOUS-INFLIGHT (`SUBMIT_MAYBE_INFLIGHT` / `SUBMIT_DUPLICATE`): a `SubmitInFlightError` that CARRIES
236
+ * the locally-computed `txid` to reconcile (never blind-retry).
237
+ * - TRANSIENT (`VERIFY_UNAVAILABLE` / `OFFER_UNKNOWN`): `retryable === true` (nothing signed; retry shortly).
238
+ * - DEFINITIVE fund-safety refusal (everything else, incl. an unknown / absent code): `retryable === false`.
239
+ * Fail-closed: an explicit `ok:false` with no recognized code still throws a non-retryable refusal.
240
+ */
241
+ declare function mapSubmitResultError(result: {
242
+ ok?: boolean;
243
+ code?: string;
244
+ error?: string;
245
+ txid?: string;
246
+ }): CairnError;
160
247
  /** Convenience: the stable code of any error (or "UNKNOWN" for a non-CairnError). */
161
248
  declare function errorCode(err: unknown): CairnErrorCode;
162
249
 
@@ -232,6 +319,22 @@ declare class Cairn {
232
319
  * header chain is cached across calls; the first call near tip syncs the post-checkpoint span.
233
320
  */
234
321
  private verifiedHeaderMerkleAt;
322
+ /** The PoW-verifying SPV light client, seeded ONCE from the pinned checkpoint (shared by the M3 header-merkle
323
+ * bind and the F13 offer pre-verify). Reused across calls; the first call near tip syncs the post-checkpoint span. */
324
+ private seededSpvLight;
325
+ /**
326
+ * F13 — corroborate an offer ON-CHAIN before building a `fillOffer` payment's `outputs`. Merkle-proves the
327
+ * offer into the PoW-verified header chain (from the pinned SPV checkpoint), binds the record to its on-chain
328
+ * commitment, derives the payment recipient + seller from the offer's on-chain AUTHOR (the funding input's
329
+ * prevout owner, txid-committed, NOT the malleable scriptSig), and, if you pass the resolver-SERVED `offer`
330
+ * object, binds its payto/seller + fee/rebate/partial terms to the proven ones. Returns a TRUST-LABELED result
331
+ * (fail-closed on a positive mismatch, fail-soft `transient` on a lagging / below-checkpoint chain view).
332
+ *
333
+ * This is best-effort corroboration, NOT the payment-grade boundary: the Cairn Wallet's OWN on-device
334
+ * fill-SPV (0.2.60+, fails-closed before signing) is that. A dApp settling real value should still clear this
335
+ * before building `outputs`, so it never hands the wallet a payment a lying resolver would redirect.
336
+ */
337
+ verifyOfferForFill(offerId: string, servedOffer?: unknown): Promise<OfferFillCheck>;
235
338
  /**
236
339
  * Detect + connect the Cairn Wallet (browser only). Wires the wallet into
237
340
  * `cairn.board` for signed writes. Resolves to the connected address.
@@ -243,4 +346,4 @@ declare class Cairn {
243
346
  get connected(): boolean;
244
347
  }
245
348
 
246
- export { BoardClient, Cairn, type CairnBaseUrls, type CairnConfig, CairnError, type CairnErrorCode, type CairnErrorOptions, Chain, ContentClient, ContentVerificationError, DEFAULT_SPV_CHECKPOINT, DetectOptions, FetchLike, Http, HttpError, IndexerClient, type NameDetail, type NameResolution, NamesClient, NotInstalledError, type PrimaryName, RegistryClient, SDK_VERSION, type TokenInfo, UnsupportedMethodError, UserRejectedError, WalletConnection, WalletLockedError, errorCode, mapProviderError };
349
+ export { BoardClient, Cairn, type CairnBaseUrls, type CairnConfig, CairnError, type CairnErrorCode, type CairnErrorOptions, Chain, ContentClient, ContentVerificationError, DEFAULT_SPV_CHECKPOINT, DetectOptions, FetchLike, Http, HttpError, IndexerClient, type NameDetail, type NameResolution, NamesClient, NotInstalledError, type OfferFillCheck, type PrimaryName, type ProvenOfferTerms, RegistryClient, SDK_VERSION, SubmitInFlightError, type TokenInfo, UnsupportedMethodError, UserRejectedError, WalletConnection, WalletLockedError, errorCode, mapProviderError, mapSubmitResultError, preverifyOffer };
package/dist/index.d.ts CHANGED
@@ -10,12 +10,13 @@ export { ContentClientOptions, PreparedContent } from './content.js';
10
10
  import { RegistryClient } from './registry.js';
11
11
  import { WalletConnection, DetectOptions } from './connect.js';
12
12
  export { AttestParams, CairnProvider, ConnectResult, FillParams, ProposeParams, ProviderDetail, ProviderInfo, SealClaimParams, SendParams, SignInResult, SiwcParams, SiwcResult, TxResult, WalletCapabilities, WalletPermission, connect, detectProvider, discoverProviders, getWallet, isInstalled } from './connect.js';
13
+ import { InclusionResult } from '@inversealtruism/csd-light';
13
14
  export { CairnController, CairnControllerOptions, CairnState } from './controller.js';
15
+ export { bindOfferTerms, feeBpsAt } from '@inversealtruism/cairnx-core';
14
16
  import '@inversealtruism/csd-client';
15
17
  import '@inversealtruism/csd-tx';
16
18
  import '@inversealtruism/csd-codec';
17
19
  import '@inversealtruism/csd-crypto';
18
- import '@inversealtruism/csd-light';
19
20
  import '@inversealtruism/csd-registry';
20
21
 
21
22
  /** `/cairnx/resolve/:name` — the display-path name→address resolution. */
@@ -104,13 +105,60 @@ declare class NamesClient {
104
105
  state(): Promise<unknown>;
105
106
  }
106
107
 
108
+ /** The fee/rebate-relevant fields of an offer, derived from the MERKLE-PROVEN offer (never a served object). */
109
+ interface ProvenOfferTerms {
110
+ height: number;
111
+ feeBps: number;
112
+ value?: string;
113
+ taker?: string;
114
+ bid?: string;
115
+ min?: string;
116
+ }
117
+ /** The trust-labeled verdict. `ok:false` + `transient:true` = a retryable chain-catching-up soft-fail (NOT a
118
+ * proven mismatch). `payto`/`seller`/`terms` are populated whenever they could be merkle-derived. */
119
+ interface OfferFillCheck {
120
+ ok: boolean;
121
+ /** "verified" = merkle-proven inclusion + on-chain author; "proof-consistent" = a proof that folds but the
122
+ * header could not be PoW-verified (indexer-trusted); "unverified" = could not prove. */
123
+ trust: "verified" | "proof-consistent" | "unverified";
124
+ reason?: string;
125
+ transient?: boolean;
126
+ payto?: string;
127
+ seller?: string;
128
+ terms?: ProvenOfferTerms;
129
+ blockHeight?: number;
130
+ }
131
+
132
+ /**
133
+ * Pre-verify an offer on-chain before building a `fillOffer` payment. `light` is a PoW-verifying light client
134
+ * (its `verifyTxInclusion` surfaces the merkle-proven tx + committed appPayloadHash); `client` reads source txs
135
+ * for the prevout-owner author bind. Pass `servedOffer` (the resolver's offer object) to also bind its
136
+ * payto/seller/terms to the proven values. See the module header for the trust posture.
137
+ */
138
+ declare function preverifyOffer(opts: {
139
+ light: {
140
+ verifyTxInclusion(txidHex: string): Promise<InclusionResult>;
141
+ };
142
+ client: {
143
+ tx(id: string): Promise<unknown>;
144
+ };
145
+ offerId: string;
146
+ servedOffer?: unknown;
147
+ }): Promise<OfferFillCheck>;
148
+
107
149
  /** Stable machine-readable error codes (branch on these, not the message text). */
108
- type CairnErrorCode = "NOT_INSTALLED" | "USER_REJECTED" | "WALLET_LOCKED" | "UNSUPPORTED_METHOD" | "HTTP_ERROR" | "CONTENT_VERIFICATION" | "ACCOUNT_CHANGED" | "FIRST_PARTY_ONLY" | "RATE_LIMITED" | "APPROVAL_CLOSED" | "FORBIDDEN" | "INTERNAL" | "UNKNOWN";
109
- declare const SDK_VERSION = "0.2.3";
150
+ type CairnErrorCode = "NOT_INSTALLED" | "USER_REJECTED" | "WALLET_LOCKED" | "UNSUPPORTED_METHOD" | "HTTP_ERROR" | "CONTENT_VERIFICATION" | "ACCOUNT_CHANGED" | "FIRST_PARTY_ONLY" | "RATE_LIMITED" | "APPROVAL_CLOSED" | "FORBIDDEN" | "INTERNAL" | "GHOST_INPUTS_SKIPPED" | "VERIFY_UNAVAILABLE" | "VERIFY_TAMPER" | "INSUFFICIENT" | "TOO_MANY_INPUTS" | "FEE_TOO_LOW" | "FEE_CAP" | "BAD_FEE" | "ZERO_ADDR_REFUSED" | "BAD_OUTPUTS" | "NO_OUTPUTS" | "BAD_REQUEST" | "OFFER_UNKNOWN" | "FILL_UNSAFE" | "FILL_WRONG_TARGET" | "SOURCE_DIVERGENCE" | "SUBMIT_REJECTED" | "SUBMIT_MAYBE_INFLIGHT" | "SUBMIT_DUPLICATE" | "NOTHING_TO_CONSOLIDATE" | "UNKNOWN";
151
+ declare const SDK_VERSION = "0.3.2";
110
152
  interface CairnErrorOptions {
111
153
  code?: CairnErrorCode;
112
154
  docsPath?: string;
113
155
  cause?: unknown;
156
+ /**
157
+ * Whether a dApp may safely AUTO-retry this failure without fresh user intent. Transient infra faults
158
+ * (e.g. a node that was briefly unreachable, nothing signed) are `true`; fund-safety refusals are `false`.
159
+ * Defaults to `false` (fail-closed: never invite a retry unless the error is provably transient).
160
+ */
161
+ retryable?: boolean;
114
162
  }
115
163
  declare class CairnError extends Error {
116
164
  /** Stable machine code for programmatic branching. */
@@ -120,7 +168,13 @@ declare class CairnError extends Error {
120
168
  /** Relative docs path (under the SDK docs base), if any. */
121
169
  readonly docsPath?: string;
122
170
  /** The cairn-sdk version that produced this error. */
123
- readonly version = "0.2.3";
171
+ readonly version = "0.3.2";
172
+ /**
173
+ * Whether a dApp may safely AUTO-retry this failure. `false` for fund-safety refusals and user-intent
174
+ * declines (the default); `true` only for provably transient infra faults where nothing was signed.
175
+ * A `SubmitInFlightError` is `false` too: reconcile its `txid`, never blind-retry.
176
+ */
177
+ readonly retryable: boolean;
124
178
  constructor(shortMessage: string, opts?: CairnErrorOptions);
125
179
  /**
126
180
  * Walk the `cause` chain. With a predicate, returns the FIRST matching error (or null). Without one,
@@ -156,7 +210,40 @@ declare class ContentVerificationError extends CairnError {
156
210
  expected: string;
157
211
  constructor(expected: string);
158
212
  }
213
+ /**
214
+ * The wallet SUBMITTED but the outcome is AMBIGUOUS: the tx MAY already be in the mempool. Thrown by the
215
+ * write wrappers for the nested `SUBMIT_MAYBE_INFLIGHT` (request threw / timed out / 5xx gateway / unreadable
216
+ * body) and `SUBMIT_DUPLICATE` (node answered "already present or mempool conflict") codes. It CARRIES the
217
+ * locally-computed consensus `txid` so a dApp can RECONCILE (check the explorer / balance) instead of blindly
218
+ * re-broadcasting, which would re-invite the double-broadcast this contract exists to prevent (F14). It is NOT
219
+ * retryable (`retryable === false`); `maybeSent === true` distinguishes it from a definitely-not-sent terminal
220
+ * refusal. See WALLET-ERROR-CODES.md in the cairn-wallet repo.
221
+ */
222
+ declare class SubmitInFlightError extends CairnError {
223
+ /** The locally-computed consensus txid to reconcile against the chain (may be undefined if the wallet omitted it). */
224
+ readonly txid?: string;
225
+ /** Always true: the tx MAY be in the mempool. Reconcile via `txid`; never blind-retry. */
226
+ readonly maybeSent = true;
227
+ constructor(message: string, code: "SUBMIT_MAYBE_INFLIGHT" | "SUBMIT_DUPLICATE", txid?: string);
228
+ }
159
229
  declare function mapProviderError(error: string, code?: string): CairnError;
230
+ /**
231
+ * F14: map a NESTED SubmitResult refusal (`{ ok:false, code, error, txid }`, ridden inside an outer-ok reply)
232
+ * onto a typed error the write wrappers THROW. Without this a fund-safety refusal would RESOLVE with a phantom
233
+ * `txid:undefined`, so a dApp doing `const { txid } = await fillOffer(...); markPaid(txid)` marks a REFUSED
234
+ * fill as paid. Three buckets:
235
+ * - AMBIGUOUS-INFLIGHT (`SUBMIT_MAYBE_INFLIGHT` / `SUBMIT_DUPLICATE`): a `SubmitInFlightError` that CARRIES
236
+ * the locally-computed `txid` to reconcile (never blind-retry).
237
+ * - TRANSIENT (`VERIFY_UNAVAILABLE` / `OFFER_UNKNOWN`): `retryable === true` (nothing signed; retry shortly).
238
+ * - DEFINITIVE fund-safety refusal (everything else, incl. an unknown / absent code): `retryable === false`.
239
+ * Fail-closed: an explicit `ok:false` with no recognized code still throws a non-retryable refusal.
240
+ */
241
+ declare function mapSubmitResultError(result: {
242
+ ok?: boolean;
243
+ code?: string;
244
+ error?: string;
245
+ txid?: string;
246
+ }): CairnError;
160
247
  /** Convenience: the stable code of any error (or "UNKNOWN" for a non-CairnError). */
161
248
  declare function errorCode(err: unknown): CairnErrorCode;
162
249
 
@@ -232,6 +319,22 @@ declare class Cairn {
232
319
  * header chain is cached across calls; the first call near tip syncs the post-checkpoint span.
233
320
  */
234
321
  private verifiedHeaderMerkleAt;
322
+ /** The PoW-verifying SPV light client, seeded ONCE from the pinned checkpoint (shared by the M3 header-merkle
323
+ * bind and the F13 offer pre-verify). Reused across calls; the first call near tip syncs the post-checkpoint span. */
324
+ private seededSpvLight;
325
+ /**
326
+ * F13 — corroborate an offer ON-CHAIN before building a `fillOffer` payment's `outputs`. Merkle-proves the
327
+ * offer into the PoW-verified header chain (from the pinned SPV checkpoint), binds the record to its on-chain
328
+ * commitment, derives the payment recipient + seller from the offer's on-chain AUTHOR (the funding input's
329
+ * prevout owner, txid-committed, NOT the malleable scriptSig), and, if you pass the resolver-SERVED `offer`
330
+ * object, binds its payto/seller + fee/rebate/partial terms to the proven ones. Returns a TRUST-LABELED result
331
+ * (fail-closed on a positive mismatch, fail-soft `transient` on a lagging / below-checkpoint chain view).
332
+ *
333
+ * This is best-effort corroboration, NOT the payment-grade boundary: the Cairn Wallet's OWN on-device
334
+ * fill-SPV (0.2.60+, fails-closed before signing) is that. A dApp settling real value should still clear this
335
+ * before building `outputs`, so it never hands the wallet a payment a lying resolver would redirect.
336
+ */
337
+ verifyOfferForFill(offerId: string, servedOffer?: unknown): Promise<OfferFillCheck>;
235
338
  /**
236
339
  * Detect + connect the Cairn Wallet (browser only). Wires the wallet into
237
340
  * `cairn.board` for signed writes. Resolves to the connected address.
@@ -243,4 +346,4 @@ declare class Cairn {
243
346
  get connected(): boolean;
244
347
  }
245
348
 
246
- export { BoardClient, Cairn, type CairnBaseUrls, type CairnConfig, CairnError, type CairnErrorCode, type CairnErrorOptions, Chain, ContentClient, ContentVerificationError, DEFAULT_SPV_CHECKPOINT, DetectOptions, FetchLike, Http, HttpError, IndexerClient, type NameDetail, type NameResolution, NamesClient, NotInstalledError, type PrimaryName, RegistryClient, SDK_VERSION, type TokenInfo, UnsupportedMethodError, UserRejectedError, WalletConnection, WalletLockedError, errorCode, mapProviderError };
349
+ export { BoardClient, Cairn, type CairnBaseUrls, type CairnConfig, CairnError, type CairnErrorCode, type CairnErrorOptions, Chain, ContentClient, ContentVerificationError, DEFAULT_SPV_CHECKPOINT, DetectOptions, FetchLike, Http, HttpError, IndexerClient, type NameDetail, type NameResolution, NamesClient, NotInstalledError, type OfferFillCheck, type PrimaryName, type ProvenOfferTerms, RegistryClient, SDK_VERSION, SubmitInFlightError, type TokenInfo, UnsupportedMethodError, UserRejectedError, WalletConnection, WalletLockedError, errorCode, mapProviderError, mapSubmitResultError, preverifyOffer };
package/dist/index.js CHANGED
@@ -3,20 +3,20 @@ import {
3
3
  } from "./chunk-BGJZF7S2.js";
4
4
  import {
5
5
  BoardClient
6
- } from "./chunk-SVW7FUBJ.js";
6
+ } from "./chunk-CZWDA3A5.js";
7
7
  import {
8
8
  Chain,
9
9
  LightClient
10
10
  } from "./chunk-C452UODV.js";
11
11
  import {
12
12
  ContentClient
13
- } from "./chunk-AAOETAUE.js";
13
+ } from "./chunk-JKC5K253.js";
14
14
  import {
15
15
  IndexerClient
16
16
  } from "./chunk-J5J4JEJ6.js";
17
17
  import {
18
18
  CairnController
19
- } from "./chunk-72YSLTLO.js";
19
+ } from "./chunk-AGU6K5EZ.js";
20
20
  import {
21
21
  WalletConnection,
22
22
  connect,
@@ -24,19 +24,21 @@ import {
24
24
  discoverProviders,
25
25
  getWallet,
26
26
  isInstalled
27
- } from "./chunk-TKZXUBDX.js";
27
+ } from "./chunk-BN3PFJDY.js";
28
28
  import {
29
29
  CairnError,
30
30
  ContentVerificationError,
31
31
  HttpError,
32
32
  NotInstalledError,
33
33
  SDK_VERSION,
34
+ SubmitInFlightError,
34
35
  UnsupportedMethodError,
35
36
  UserRejectedError,
36
37
  WalletLockedError,
37
38
  errorCode,
38
- mapProviderError
39
- } from "./chunk-M6OSWJVC.js";
39
+ mapProviderError,
40
+ mapSubmitResultError
41
+ } from "./chunk-FMXNJQWL.js";
40
42
 
41
43
  // src/http.ts
42
44
  var DEFAULT_MAX_BYTES = 16 * 1024 * 1024;
@@ -143,8 +145,9 @@ var Http = class {
143
145
  try {
144
146
  const res = await this.raw("GET", path, { query });
145
147
  if (!res.ok) {
146
- const err = new HttpError(res.status, url, await safeText(res));
147
- if (!(res.status >= 500 && attempt < this.retries)) throw err;
148
+ if (!(res.status >= 500 && attempt < this.retries)) throw new HttpError(res.status, url, await safeText(res));
149
+ res.body?.cancel().catch(() => {
150
+ });
148
151
  } else {
149
152
  return this.parseJson(res, new TextDecoder().decode(await readCapped(res, this.maxBytes)), url);
150
153
  }
@@ -189,7 +192,7 @@ var NamesClient = class {
189
192
  async resolve(name) {
190
193
  if (!nameOk(name)) return null;
191
194
  try {
192
- return await this.http.getJson(`/trade/api/cairnx/resolve/${name}`);
195
+ return await this.http.getJson(`/trade/api/cairnx/resolve/${encodeURIComponent(name)}`);
193
196
  } catch (e) {
194
197
  if (e?.status === 404) return null;
195
198
  throw e;
@@ -199,7 +202,7 @@ var NamesClient = class {
199
202
  async name(name) {
200
203
  if (!nameOk(name)) return null;
201
204
  try {
202
- return await this.http.getJson(`/trade/api/cairnx/name/${name}`);
205
+ return await this.http.getJson(`/trade/api/cairnx/name/${encodeURIComponent(name)}`);
203
206
  } catch (e) {
204
207
  if (e?.status === 404) return null;
205
208
  throw e;
@@ -209,7 +212,7 @@ var NamesClient = class {
209
212
  async nameHistory(name) {
210
213
  if (!nameOk(name)) return null;
211
214
  try {
212
- return await this.http.getJson(`/trade/api/cairnx/name-history/${name}`);
215
+ return await this.http.getJson(`/trade/api/cairnx/name-history/${encodeURIComponent(name)}`);
213
216
  } catch (e) {
214
217
  if (e?.status === 404) return null;
215
218
  throw e;
@@ -267,6 +270,92 @@ var NamesClient = class {
267
270
  }
268
271
  };
269
272
 
273
+ // src/fillverify.ts
274
+ import { rpcTxToTx } from "@inversealtruism/csd-client";
275
+ import { txid, payloadHash } from "@inversealtruism/csd-codec";
276
+ import { parseRecord, feeBpsAt, bindOfferTerms } from "@inversealtruism/cairnx-core";
277
+ var ADDR = /^0x[0-9a-f]{40}$/;
278
+ var HASH = /^0x[0-9a-f]{64}$/;
279
+ var COINBASE = "0x" + "00".repeat(32);
280
+ async function provenAuthor(client, provenTx) {
281
+ const in0 = provenTx.inputs?.[0];
282
+ if (!in0 || String(in0.prevTxid).toLowerCase() === COINBASE && Number(in0.vout) === 4294967295) return null;
283
+ let info;
284
+ try {
285
+ info = await client.tx(String(in0.prevTxid));
286
+ } catch {
287
+ return null;
288
+ }
289
+ const body = info?.tx ?? info;
290
+ if (!body || !Array.isArray(body.outputs) || !Array.isArray(body.inputs)) return null;
291
+ let src;
292
+ try {
293
+ src = rpcTxToTx(body);
294
+ } catch {
295
+ return null;
296
+ }
297
+ if (String(txid(src)).toLowerCase() !== String(in0.prevTxid).toLowerCase()) return null;
298
+ const out = src.outputs?.[Number(in0.vout)];
299
+ const spk = out?.scriptPubkey ? String(out.scriptPubkey).toLowerCase() : null;
300
+ return spk && ADDR.test(spk) ? spk : null;
301
+ }
302
+ async function preverifyOffer(opts) {
303
+ const id = String(opts.offerId).toLowerCase();
304
+ if (!HASH.test(id)) return { ok: false, trust: "unverified", reason: "invalid offer id" };
305
+ let incl;
306
+ try {
307
+ incl = await opts.light.verifyTxInclusion(id);
308
+ } catch (e) {
309
+ return { ok: false, trust: "unverified", transient: true, reason: `couldn't reach the chain to verify the offer (${e?.message ?? e})` };
310
+ }
311
+ if (!incl.included || incl.trustLevel !== "verified-inclusion" || !incl.tx || !incl.appPayloadHash)
312
+ return { ok: false, trust: "unverified", transient: true, blockHeight: incl.blockHeight, reason: incl.reason ?? `offer not merkle-proven on-chain (${incl.trustLevel})` };
313
+ const blockHeight = Number(incl.blockHeight);
314
+ let provenTx, provenId;
315
+ try {
316
+ provenTx = rpcTxToTx(incl.tx);
317
+ provenId = txid(provenTx);
318
+ } catch {
319
+ return { ok: false, trust: "unverified", reason: "the on-chain offer tx couldn't be re-derived" };
320
+ }
321
+ if (String(provenId).toLowerCase() !== id) return { ok: false, trust: "unverified", reason: "the merkle-proven tx doesn't match the offer id" };
322
+ const app = incl.tx.app;
323
+ if (!app || app.type !== "Propose") return { ok: false, trust: "unverified", reason: "the offer tx is not a Propose" };
324
+ let rec;
325
+ try {
326
+ rec = parseRecord(app.uri, incl.appPayloadHash);
327
+ } catch {
328
+ rec = null;
329
+ }
330
+ if (!rec || rec.t !== "offer") return { ok: false, trust: "unverified", reason: "the offer record doesn't bind to its on-chain commitment" };
331
+ if (String(payloadHash(rec)).toLowerCase() !== String(incl.appPayloadHash).toLowerCase())
332
+ return { ok: false, trust: "unverified", reason: "the offer record doesn't match the on-chain (merkle-proven) commitment" };
333
+ const seller = await provenAuthor(opts.client, provenTx);
334
+ if (!seller) return { ok: false, trust: "verified", transient: true, blockHeight, reason: "couldn't prove the offer's on-chain author yet; the chain view may be catching up, try again" };
335
+ const w = rec.want ?? {};
336
+ const payto = w.payto && ADDR.test(String(w.payto).toLowerCase()) ? String(w.payto).toLowerCase() : seller;
337
+ const terms = {
338
+ height: blockHeight,
339
+ feeBps: feeBpsAt(blockHeight),
340
+ value: w.value !== void 0 ? String(w.value) : void 0,
341
+ taker: rec.taker !== void 0 ? String(rec.taker).toLowerCase() : void 0,
342
+ bid: rec.bid !== void 0 ? String(rec.bid).toLowerCase() : void 0,
343
+ min: rec.min !== void 0 ? String(rec.min) : void 0
344
+ };
345
+ const base = { payto, seller, terms, blockHeight };
346
+ if (opts.servedOffer !== void 0 && opts.servedOffer !== null) {
347
+ const so = opts.servedOffer;
348
+ const servedPayto = so?.want?.payto !== void 0 && so?.want?.payto !== null ? String(so.want.payto).toLowerCase() : "";
349
+ if (servedPayto && servedPayto !== payto)
350
+ return { ok: false, trust: "verified", reason: "the served payment recipient doesn't match the offer's on-chain author/record", ...base };
351
+ if (so?.seller !== void 0 && so?.seller !== null && String(so.seller).toLowerCase() !== seller)
352
+ return { ok: false, trust: "verified", reason: "the served seller doesn't match the offer's on-chain author (a lying resolver may be redirecting your payment)", ...base };
353
+ if (bindOfferTerms(opts.servedOffer, terms))
354
+ return { ok: false, trust: "verified", reason: "the served fee/rebate/partial terms don't match the offer's on-chain record (a lying resolver could mis-size the fill, causing a rejected fill after your payment)", ...base };
355
+ }
356
+ return { ok: true, trust: "verified", ...base };
357
+ }
358
+
270
359
  // src/index.ts
271
360
  var DEFAULT_SPV_CHECKPOINT = { height: 38142, hash: "0x00000000000140f023cc0ee1457a40833f2fcb4de44291b9d373e50f17b97232" };
272
361
  var DEFAULT_CAIRN = "https://cairn-substrate.com";
@@ -319,18 +408,39 @@ var Cairn = class {
319
408
  async verifiedHeaderMerkleAt(height) {
320
409
  if (!Number.isInteger(height) || height < this._spvCp.height)
321
410
  throw new Error(`height ${height} is below the SPV checkpoint ${this._spvCp.height} (cannot PoW-verify backward)`);
322
- if (!this._spvLight) this._spvLight = new LightClient({ client: this.chain.client, checkpoints: { [this._spvCp.height]: this._spvCp.hash } });
323
- const lc = this._spvLight;
324
- if (!this._spvSeeded) {
325
- await lc.syncFromCheckpoint(this._spvCp.height, this._spvCp.hash);
326
- this._spvSeeded = true;
327
- }
411
+ const lc = await this.seededSpvLight();
328
412
  const haveTo = lc.baseHeight + lc.chain.length - 1;
329
413
  if (haveTo < height) await lc.sync(height);
330
414
  const vh = lc.chain[height - lc.baseHeight];
331
415
  if (!vh) throw new Error(`light client has no verified header at ${height}`);
332
416
  return String(vh.header.merkle);
333
417
  }
418
+ /** The PoW-verifying SPV light client, seeded ONCE from the pinned checkpoint (shared by the M3 header-merkle
419
+ * bind and the F13 offer pre-verify). Reused across calls; the first call near tip syncs the post-checkpoint span. */
420
+ async seededSpvLight() {
421
+ if (!this._spvLight) this._spvLight = new LightClient({ client: this.chain.client, checkpoints: { [this._spvCp.height]: this._spvCp.hash } });
422
+ if (!this._spvSeeded) {
423
+ await this._spvLight.syncFromCheckpoint(this._spvCp.height, this._spvCp.hash);
424
+ this._spvSeeded = true;
425
+ }
426
+ return this._spvLight;
427
+ }
428
+ /**
429
+ * F13 — corroborate an offer ON-CHAIN before building a `fillOffer` payment's `outputs`. Merkle-proves the
430
+ * offer into the PoW-verified header chain (from the pinned SPV checkpoint), binds the record to its on-chain
431
+ * commitment, derives the payment recipient + seller from the offer's on-chain AUTHOR (the funding input's
432
+ * prevout owner, txid-committed, NOT the malleable scriptSig), and, if you pass the resolver-SERVED `offer`
433
+ * object, binds its payto/seller + fee/rebate/partial terms to the proven ones. Returns a TRUST-LABELED result
434
+ * (fail-closed on a positive mismatch, fail-soft `transient` on a lagging / below-checkpoint chain view).
435
+ *
436
+ * This is best-effort corroboration, NOT the payment-grade boundary: the Cairn Wallet's OWN on-device
437
+ * fill-SPV (0.2.60+, fails-closed before signing) is that. A dApp settling real value should still clear this
438
+ * before building `outputs`, so it never hands the wallet a payment a lying resolver would redirect.
439
+ */
440
+ async verifyOfferForFill(offerId, servedOffer) {
441
+ const light = await this.seededSpvLight();
442
+ return preverifyOffer({ light, client: this.chain.client, offerId, servedOffer });
443
+ }
334
444
  /**
335
445
  * Detect + connect the Cairn Wallet (browser only). Wires the wallet into
336
446
  * `cairn.board` for signed writes. Resolves to the connected address.
@@ -366,15 +476,20 @@ export {
366
476
  NotInstalledError,
367
477
  RegistryClient,
368
478
  SDK_VERSION,
479
+ SubmitInFlightError,
369
480
  UnsupportedMethodError,
370
481
  UserRejectedError,
371
482
  WalletConnection,
372
483
  WalletLockedError,
484
+ bindOfferTerms,
373
485
  connect,
374
486
  detectProvider,
375
487
  discoverProviders,
376
488
  errorCode,
489
+ feeBpsAt,
377
490
  getWallet,
378
491
  isInstalled,
379
- mapProviderError
492
+ mapProviderError,
493
+ mapSubmitResultError,
494
+ preverifyOffer
380
495
  };