@inversealtruism/cairn-sdk 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -10,12 +10,12 @@ 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';
14
15
  import '@inversealtruism/csd-client';
15
16
  import '@inversealtruism/csd-tx';
16
17
  import '@inversealtruism/csd-codec';
17
18
  import '@inversealtruism/csd-crypto';
18
- import '@inversealtruism/csd-light';
19
19
  import '@inversealtruism/csd-registry';
20
20
 
21
21
  /** `/cairnx/resolve/:name` — the display-path name→address resolution. */
@@ -104,13 +104,64 @@ declare class NamesClient {
104
104
  state(): Promise<unknown>;
105
105
  }
106
106
 
107
+ /** The fee/rebate-relevant fields of an offer, derived from the MERKLE-PROVEN offer (never a served object). */
108
+ interface ProvenOfferTerms {
109
+ height: number;
110
+ feeBps: number;
111
+ value?: string;
112
+ taker?: string;
113
+ bid?: string;
114
+ min?: string;
115
+ }
116
+ /** The trust-labeled verdict. `ok:false` + `transient:true` = a retryable chain-catching-up soft-fail (NOT a
117
+ * proven mismatch). `payto`/`seller`/`terms` are populated whenever they could be merkle-derived. */
118
+ interface OfferFillCheck {
119
+ ok: boolean;
120
+ /** "verified" = merkle-proven inclusion + on-chain author; "proof-consistent" = a proof that folds but the
121
+ * header could not be PoW-verified (indexer-trusted); "unverified" = could not prove. */
122
+ trust: "verified" | "proof-consistent" | "unverified";
123
+ reason?: string;
124
+ transient?: boolean;
125
+ payto?: string;
126
+ seller?: string;
127
+ terms?: ProvenOfferTerms;
128
+ blockHeight?: number;
129
+ }
130
+ /** The treasury fee rate stamped on an offer at its creation height (resolve.ts: v11 ? (v16 ? 150 : 100) : 0). */
131
+ declare const feeBpsAt: (height: number) => number;
132
+ /** true iff any fee/rebate/partial-sizing field of the SERVED offer diverges from the merkle-proven terms `t`
133
+ * (fail-closed: any divergence => the caller refuses). Byte-identical to cairnx-core `bindOfferTerms`. */
134
+ declare function bindOfferTerms(servedOffer: unknown, t: ProvenOfferTerms): boolean;
135
+ /**
136
+ * Pre-verify an offer on-chain before building a `fillOffer` payment. `light` is a PoW-verifying light client
137
+ * (its `verifyTxInclusion` surfaces the merkle-proven tx + committed appPayloadHash); `client` reads source txs
138
+ * for the prevout-owner author bind. Pass `servedOffer` (the resolver's offer object) to also bind its
139
+ * payto/seller/terms to the proven values. See the module header for the trust posture.
140
+ */
141
+ declare function preverifyOffer(opts: {
142
+ light: {
143
+ verifyTxInclusion(txidHex: string): Promise<InclusionResult>;
144
+ };
145
+ client: {
146
+ tx(id: string): Promise<unknown>;
147
+ };
148
+ offerId: string;
149
+ servedOffer?: unknown;
150
+ }): Promise<OfferFillCheck>;
151
+
107
152
  /** 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.2";
153
+ 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";
154
+ declare const SDK_VERSION = "0.3.0";
110
155
  interface CairnErrorOptions {
111
156
  code?: CairnErrorCode;
112
157
  docsPath?: string;
113
158
  cause?: unknown;
159
+ /**
160
+ * Whether a dApp may safely AUTO-retry this failure without fresh user intent. Transient infra faults
161
+ * (e.g. a node that was briefly unreachable, nothing signed) are `true`; fund-safety refusals are `false`.
162
+ * Defaults to `false` (fail-closed: never invite a retry unless the error is provably transient).
163
+ */
164
+ retryable?: boolean;
114
165
  }
115
166
  declare class CairnError extends Error {
116
167
  /** Stable machine code for programmatic branching. */
@@ -120,7 +171,13 @@ declare class CairnError extends Error {
120
171
  /** Relative docs path (under the SDK docs base), if any. */
121
172
  readonly docsPath?: string;
122
173
  /** The cairn-sdk version that produced this error. */
123
- readonly version = "0.2.2";
174
+ readonly version = "0.3.0";
175
+ /**
176
+ * Whether a dApp may safely AUTO-retry this failure. `false` for fund-safety refusals and user-intent
177
+ * declines (the default); `true` only for provably transient infra faults where nothing was signed.
178
+ * A `SubmitInFlightError` is `false` too: reconcile its `txid`, never blind-retry.
179
+ */
180
+ readonly retryable: boolean;
124
181
  constructor(shortMessage: string, opts?: CairnErrorOptions);
125
182
  /**
126
183
  * Walk the `cause` chain. With a predicate, returns the FIRST matching error (or null). Without one,
@@ -156,7 +213,40 @@ declare class ContentVerificationError extends CairnError {
156
213
  expected: string;
157
214
  constructor(expected: string);
158
215
  }
216
+ /**
217
+ * The wallet SUBMITTED but the outcome is AMBIGUOUS: the tx MAY already be in the mempool. Thrown by the
218
+ * write wrappers for the nested `SUBMIT_MAYBE_INFLIGHT` (request threw / timed out / 5xx gateway / unreadable
219
+ * body) and `SUBMIT_DUPLICATE` (node answered "already present or mempool conflict") codes. It CARRIES the
220
+ * locally-computed consensus `txid` so a dApp can RECONCILE (check the explorer / balance) instead of blindly
221
+ * re-broadcasting, which would re-invite the double-broadcast this contract exists to prevent (F14). It is NOT
222
+ * retryable (`retryable === false`); `maybeSent === true` distinguishes it from a definitely-not-sent terminal
223
+ * refusal. See WALLET-ERROR-CODES.md in the cairn-wallet repo.
224
+ */
225
+ declare class SubmitInFlightError extends CairnError {
226
+ /** The locally-computed consensus txid to reconcile against the chain (may be undefined if the wallet omitted it). */
227
+ readonly txid?: string;
228
+ /** Always true: the tx MAY be in the mempool. Reconcile via `txid`; never blind-retry. */
229
+ readonly maybeSent = true;
230
+ constructor(message: string, code: "SUBMIT_MAYBE_INFLIGHT" | "SUBMIT_DUPLICATE", txid?: string);
231
+ }
159
232
  declare function mapProviderError(error: string, code?: string): CairnError;
233
+ /**
234
+ * F14: map a NESTED SubmitResult refusal (`{ ok:false, code, error, txid }`, ridden inside an outer-ok reply)
235
+ * onto a typed error the write wrappers THROW. Without this a fund-safety refusal would RESOLVE with a phantom
236
+ * `txid:undefined`, so a dApp doing `const { txid } = await fillOffer(...); markPaid(txid)` marks a REFUSED
237
+ * fill as paid. Three buckets:
238
+ * - AMBIGUOUS-INFLIGHT (`SUBMIT_MAYBE_INFLIGHT` / `SUBMIT_DUPLICATE`): a `SubmitInFlightError` that CARRIES
239
+ * the locally-computed `txid` to reconcile (never blind-retry).
240
+ * - TRANSIENT (`VERIFY_UNAVAILABLE` / `OFFER_UNKNOWN`): `retryable === true` (nothing signed; retry shortly).
241
+ * - DEFINITIVE fund-safety refusal (everything else, incl. an unknown / absent code): `retryable === false`.
242
+ * Fail-closed: an explicit `ok:false` with no recognized code still throws a non-retryable refusal.
243
+ */
244
+ declare function mapSubmitResultError(result: {
245
+ ok?: boolean;
246
+ code?: string;
247
+ error?: string;
248
+ txid?: string;
249
+ }): CairnError;
160
250
  /** Convenience: the stable code of any error (or "UNKNOWN" for a non-CairnError). */
161
251
  declare function errorCode(err: unknown): CairnErrorCode;
162
252
 
@@ -232,6 +322,22 @@ declare class Cairn {
232
322
  * header chain is cached across calls; the first call near tip syncs the post-checkpoint span.
233
323
  */
234
324
  private verifiedHeaderMerkleAt;
325
+ /** The PoW-verifying SPV light client, seeded ONCE from the pinned checkpoint (shared by the M3 header-merkle
326
+ * bind and the F13 offer pre-verify). Reused across calls; the first call near tip syncs the post-checkpoint span. */
327
+ private seededSpvLight;
328
+ /**
329
+ * F13 — corroborate an offer ON-CHAIN before building a `fillOffer` payment's `outputs`. Merkle-proves the
330
+ * offer into the PoW-verified header chain (from the pinned SPV checkpoint), binds the record to its on-chain
331
+ * commitment, derives the payment recipient + seller from the offer's on-chain AUTHOR (the funding input's
332
+ * prevout owner, txid-committed, NOT the malleable scriptSig), and, if you pass the resolver-SERVED `offer`
333
+ * object, binds its payto/seller + fee/rebate/partial terms to the proven ones. Returns a TRUST-LABELED result
334
+ * (fail-closed on a positive mismatch, fail-soft `transient` on a lagging / below-checkpoint chain view).
335
+ *
336
+ * This is best-effort corroboration, NOT the payment-grade boundary: the Cairn Wallet's OWN on-device
337
+ * fill-SPV (0.2.60+, fails-closed before signing) is that. A dApp settling real value should still clear this
338
+ * before building `outputs`, so it never hands the wallet a payment a lying resolver would redirect.
339
+ */
340
+ verifyOfferForFill(offerId: string, servedOffer?: unknown): Promise<OfferFillCheck>;
235
341
  /**
236
342
  * Detect + connect the Cairn Wallet (browser only). Wires the wallet into
237
343
  * `cairn.board` for signed writes. Resolves to the connected address.
@@ -243,4 +349,4 @@ declare class Cairn {
243
349
  get connected(): boolean;
244
350
  }
245
351
 
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 };
352
+ 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, bindOfferTerms, errorCode, feeBpsAt, mapProviderError, mapSubmitResultError, preverifyOffer };
package/dist/index.d.ts CHANGED
@@ -10,12 +10,12 @@ 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';
14
15
  import '@inversealtruism/csd-client';
15
16
  import '@inversealtruism/csd-tx';
16
17
  import '@inversealtruism/csd-codec';
17
18
  import '@inversealtruism/csd-crypto';
18
- import '@inversealtruism/csd-light';
19
19
  import '@inversealtruism/csd-registry';
20
20
 
21
21
  /** `/cairnx/resolve/:name` — the display-path name→address resolution. */
@@ -104,13 +104,64 @@ declare class NamesClient {
104
104
  state(): Promise<unknown>;
105
105
  }
106
106
 
107
+ /** The fee/rebate-relevant fields of an offer, derived from the MERKLE-PROVEN offer (never a served object). */
108
+ interface ProvenOfferTerms {
109
+ height: number;
110
+ feeBps: number;
111
+ value?: string;
112
+ taker?: string;
113
+ bid?: string;
114
+ min?: string;
115
+ }
116
+ /** The trust-labeled verdict. `ok:false` + `transient:true` = a retryable chain-catching-up soft-fail (NOT a
117
+ * proven mismatch). `payto`/`seller`/`terms` are populated whenever they could be merkle-derived. */
118
+ interface OfferFillCheck {
119
+ ok: boolean;
120
+ /** "verified" = merkle-proven inclusion + on-chain author; "proof-consistent" = a proof that folds but the
121
+ * header could not be PoW-verified (indexer-trusted); "unverified" = could not prove. */
122
+ trust: "verified" | "proof-consistent" | "unverified";
123
+ reason?: string;
124
+ transient?: boolean;
125
+ payto?: string;
126
+ seller?: string;
127
+ terms?: ProvenOfferTerms;
128
+ blockHeight?: number;
129
+ }
130
+ /** The treasury fee rate stamped on an offer at its creation height (resolve.ts: v11 ? (v16 ? 150 : 100) : 0). */
131
+ declare const feeBpsAt: (height: number) => number;
132
+ /** true iff any fee/rebate/partial-sizing field of the SERVED offer diverges from the merkle-proven terms `t`
133
+ * (fail-closed: any divergence => the caller refuses). Byte-identical to cairnx-core `bindOfferTerms`. */
134
+ declare function bindOfferTerms(servedOffer: unknown, t: ProvenOfferTerms): boolean;
135
+ /**
136
+ * Pre-verify an offer on-chain before building a `fillOffer` payment. `light` is a PoW-verifying light client
137
+ * (its `verifyTxInclusion` surfaces the merkle-proven tx + committed appPayloadHash); `client` reads source txs
138
+ * for the prevout-owner author bind. Pass `servedOffer` (the resolver's offer object) to also bind its
139
+ * payto/seller/terms to the proven values. See the module header for the trust posture.
140
+ */
141
+ declare function preverifyOffer(opts: {
142
+ light: {
143
+ verifyTxInclusion(txidHex: string): Promise<InclusionResult>;
144
+ };
145
+ client: {
146
+ tx(id: string): Promise<unknown>;
147
+ };
148
+ offerId: string;
149
+ servedOffer?: unknown;
150
+ }): Promise<OfferFillCheck>;
151
+
107
152
  /** 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.2";
153
+ 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";
154
+ declare const SDK_VERSION = "0.3.0";
110
155
  interface CairnErrorOptions {
111
156
  code?: CairnErrorCode;
112
157
  docsPath?: string;
113
158
  cause?: unknown;
159
+ /**
160
+ * Whether a dApp may safely AUTO-retry this failure without fresh user intent. Transient infra faults
161
+ * (e.g. a node that was briefly unreachable, nothing signed) are `true`; fund-safety refusals are `false`.
162
+ * Defaults to `false` (fail-closed: never invite a retry unless the error is provably transient).
163
+ */
164
+ retryable?: boolean;
114
165
  }
115
166
  declare class CairnError extends Error {
116
167
  /** Stable machine code for programmatic branching. */
@@ -120,7 +171,13 @@ declare class CairnError extends Error {
120
171
  /** Relative docs path (under the SDK docs base), if any. */
121
172
  readonly docsPath?: string;
122
173
  /** The cairn-sdk version that produced this error. */
123
- readonly version = "0.2.2";
174
+ readonly version = "0.3.0";
175
+ /**
176
+ * Whether a dApp may safely AUTO-retry this failure. `false` for fund-safety refusals and user-intent
177
+ * declines (the default); `true` only for provably transient infra faults where nothing was signed.
178
+ * A `SubmitInFlightError` is `false` too: reconcile its `txid`, never blind-retry.
179
+ */
180
+ readonly retryable: boolean;
124
181
  constructor(shortMessage: string, opts?: CairnErrorOptions);
125
182
  /**
126
183
  * Walk the `cause` chain. With a predicate, returns the FIRST matching error (or null). Without one,
@@ -156,7 +213,40 @@ declare class ContentVerificationError extends CairnError {
156
213
  expected: string;
157
214
  constructor(expected: string);
158
215
  }
216
+ /**
217
+ * The wallet SUBMITTED but the outcome is AMBIGUOUS: the tx MAY already be in the mempool. Thrown by the
218
+ * write wrappers for the nested `SUBMIT_MAYBE_INFLIGHT` (request threw / timed out / 5xx gateway / unreadable
219
+ * body) and `SUBMIT_DUPLICATE` (node answered "already present or mempool conflict") codes. It CARRIES the
220
+ * locally-computed consensus `txid` so a dApp can RECONCILE (check the explorer / balance) instead of blindly
221
+ * re-broadcasting, which would re-invite the double-broadcast this contract exists to prevent (F14). It is NOT
222
+ * retryable (`retryable === false`); `maybeSent === true` distinguishes it from a definitely-not-sent terminal
223
+ * refusal. See WALLET-ERROR-CODES.md in the cairn-wallet repo.
224
+ */
225
+ declare class SubmitInFlightError extends CairnError {
226
+ /** The locally-computed consensus txid to reconcile against the chain (may be undefined if the wallet omitted it). */
227
+ readonly txid?: string;
228
+ /** Always true: the tx MAY be in the mempool. Reconcile via `txid`; never blind-retry. */
229
+ readonly maybeSent = true;
230
+ constructor(message: string, code: "SUBMIT_MAYBE_INFLIGHT" | "SUBMIT_DUPLICATE", txid?: string);
231
+ }
159
232
  declare function mapProviderError(error: string, code?: string): CairnError;
233
+ /**
234
+ * F14: map a NESTED SubmitResult refusal (`{ ok:false, code, error, txid }`, ridden inside an outer-ok reply)
235
+ * onto a typed error the write wrappers THROW. Without this a fund-safety refusal would RESOLVE with a phantom
236
+ * `txid:undefined`, so a dApp doing `const { txid } = await fillOffer(...); markPaid(txid)` marks a REFUSED
237
+ * fill as paid. Three buckets:
238
+ * - AMBIGUOUS-INFLIGHT (`SUBMIT_MAYBE_INFLIGHT` / `SUBMIT_DUPLICATE`): a `SubmitInFlightError` that CARRIES
239
+ * the locally-computed `txid` to reconcile (never blind-retry).
240
+ * - TRANSIENT (`VERIFY_UNAVAILABLE` / `OFFER_UNKNOWN`): `retryable === true` (nothing signed; retry shortly).
241
+ * - DEFINITIVE fund-safety refusal (everything else, incl. an unknown / absent code): `retryable === false`.
242
+ * Fail-closed: an explicit `ok:false` with no recognized code still throws a non-retryable refusal.
243
+ */
244
+ declare function mapSubmitResultError(result: {
245
+ ok?: boolean;
246
+ code?: string;
247
+ error?: string;
248
+ txid?: string;
249
+ }): CairnError;
160
250
  /** Convenience: the stable code of any error (or "UNKNOWN" for a non-CairnError). */
161
251
  declare function errorCode(err: unknown): CairnErrorCode;
162
252
 
@@ -232,6 +322,22 @@ declare class Cairn {
232
322
  * header chain is cached across calls; the first call near tip syncs the post-checkpoint span.
233
323
  */
234
324
  private verifiedHeaderMerkleAt;
325
+ /** The PoW-verifying SPV light client, seeded ONCE from the pinned checkpoint (shared by the M3 header-merkle
326
+ * bind and the F13 offer pre-verify). Reused across calls; the first call near tip syncs the post-checkpoint span. */
327
+ private seededSpvLight;
328
+ /**
329
+ * F13 — corroborate an offer ON-CHAIN before building a `fillOffer` payment's `outputs`. Merkle-proves the
330
+ * offer into the PoW-verified header chain (from the pinned SPV checkpoint), binds the record to its on-chain
331
+ * commitment, derives the payment recipient + seller from the offer's on-chain AUTHOR (the funding input's
332
+ * prevout owner, txid-committed, NOT the malleable scriptSig), and, if you pass the resolver-SERVED `offer`
333
+ * object, binds its payto/seller + fee/rebate/partial terms to the proven ones. Returns a TRUST-LABELED result
334
+ * (fail-closed on a positive mismatch, fail-soft `transient` on a lagging / below-checkpoint chain view).
335
+ *
336
+ * This is best-effort corroboration, NOT the payment-grade boundary: the Cairn Wallet's OWN on-device
337
+ * fill-SPV (0.2.60+, fails-closed before signing) is that. A dApp settling real value should still clear this
338
+ * before building `outputs`, so it never hands the wallet a payment a lying resolver would redirect.
339
+ */
340
+ verifyOfferForFill(offerId: string, servedOffer?: unknown): Promise<OfferFillCheck>;
235
341
  /**
236
342
  * Detect + connect the Cairn Wallet (browser only). Wires the wallet into
237
343
  * `cairn.board` for signed writes. Resolves to the connected address.
@@ -243,4 +349,4 @@ declare class Cairn {
243
349
  get connected(): boolean;
244
350
  }
245
351
 
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 };
352
+ 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, bindOfferTerms, errorCode, feeBpsAt, 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-HI5STMH3.js";
6
+ } from "./chunk-NLCF5T2F.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-3EXZQPPT.js";
13
+ } from "./chunk-4RDXEW35.js";
14
14
  import {
15
15
  IndexerClient
16
16
  } from "./chunk-J5J4JEJ6.js";
17
17
  import {
18
18
  CairnController
19
- } from "./chunk-LCE4IWMW.js";
19
+ } from "./chunk-UMXLWUEF.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-QRVMYGSL.js";
27
+ } from "./chunk-QV2GIXO7.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-ZJURNM5P.js";
39
+ mapProviderError,
40
+ mapSubmitResultError
41
+ } from "./chunk-E2Z3XSCW.js";
40
42
 
41
43
  // src/http.ts
42
44
  var DEFAULT_MAX_BYTES = 16 * 1024 * 1024;
@@ -267,6 +269,106 @@ var NamesClient = class {
267
269
  }
268
270
  };
269
271
 
272
+ // src/fillverify.ts
273
+ import { rpcTxToTx } from "@inversealtruism/csd-client";
274
+ import { txid, payloadHash } from "@inversealtruism/csd-codec";
275
+ import { parseRecord, V11_HEIGHT, V16_HEIGHT, FEE_BPS, FEE_BPS_V16 } from "@inversealtruism/cairnx-core";
276
+ var ADDR = /^0x[0-9a-f]{40}$/;
277
+ var HASH = /^0x[0-9a-f]{64}$/;
278
+ var COINBASE = "0x" + "00".repeat(32);
279
+ var feeBpsAt = (height) => height >= V11_HEIGHT ? height >= V16_HEIGHT ? FEE_BPS_V16 : FEE_BPS : 0;
280
+ function bindOfferTerms(servedOffer, t) {
281
+ const o = servedOffer;
282
+ const s = (v) => v === void 0 || v === null ? "" : String(v).toLowerCase();
283
+ if (Number(o?.height) !== t.height) return true;
284
+ if (Number(o?.feeBps) !== t.feeBps) return true;
285
+ if (t.value !== void 0 && String(o?.want?.value) !== t.value) return true;
286
+ if (s(o?.taker) !== s(t.taker)) return true;
287
+ if (s(o?.bid) !== s(t.bid)) return true;
288
+ const om = o?.min;
289
+ if ((om !== void 0 && om !== null) !== (t.min !== void 0)) return true;
290
+ if (t.min !== void 0 && String(om) !== t.min) return true;
291
+ return false;
292
+ }
293
+ async function provenAuthor(client, provenTx) {
294
+ const in0 = provenTx.inputs?.[0];
295
+ if (!in0 || String(in0.prevTxid).toLowerCase() === COINBASE && Number(in0.vout) === 4294967295) return null;
296
+ let info;
297
+ try {
298
+ info = await client.tx(String(in0.prevTxid));
299
+ } catch {
300
+ return null;
301
+ }
302
+ const body = info?.tx ?? info;
303
+ if (!body || !Array.isArray(body.outputs) || !Array.isArray(body.inputs)) return null;
304
+ let src;
305
+ try {
306
+ src = rpcTxToTx(body);
307
+ } catch {
308
+ return null;
309
+ }
310
+ if (String(txid(src)).toLowerCase() !== String(in0.prevTxid).toLowerCase()) return null;
311
+ const out = src.outputs?.[Number(in0.vout)];
312
+ const spk = out?.scriptPubkey ? String(out.scriptPubkey).toLowerCase() : null;
313
+ return spk && ADDR.test(spk) ? spk : null;
314
+ }
315
+ async function preverifyOffer(opts) {
316
+ const id = String(opts.offerId).toLowerCase();
317
+ if (!HASH.test(id)) return { ok: false, trust: "unverified", reason: "invalid offer id" };
318
+ let incl;
319
+ try {
320
+ incl = await opts.light.verifyTxInclusion(id);
321
+ } catch (e) {
322
+ return { ok: false, trust: "unverified", transient: true, reason: `couldn't reach the chain to verify the offer (${e?.message ?? e})` };
323
+ }
324
+ if (!incl.included || incl.trustLevel !== "verified-inclusion" || !incl.tx || !incl.appPayloadHash)
325
+ return { ok: false, trust: "unverified", transient: true, blockHeight: incl.blockHeight, reason: incl.reason ?? `offer not merkle-proven on-chain (${incl.trustLevel})` };
326
+ const blockHeight = Number(incl.blockHeight);
327
+ let provenTx, provenId;
328
+ try {
329
+ provenTx = rpcTxToTx(incl.tx);
330
+ provenId = txid(provenTx);
331
+ } catch {
332
+ return { ok: false, trust: "unverified", reason: "the on-chain offer tx couldn't be re-derived" };
333
+ }
334
+ if (String(provenId).toLowerCase() !== id) return { ok: false, trust: "unverified", reason: "the merkle-proven tx doesn't match the offer id" };
335
+ const app = incl.tx.app;
336
+ if (!app || app.type !== "Propose") return { ok: false, trust: "unverified", reason: "the offer tx is not a Propose" };
337
+ let rec;
338
+ try {
339
+ rec = parseRecord(app.uri, incl.appPayloadHash);
340
+ } catch {
341
+ rec = null;
342
+ }
343
+ if (!rec || rec.t !== "offer") return { ok: false, trust: "unverified", reason: "the offer record doesn't bind to its on-chain commitment" };
344
+ if (String(payloadHash(rec)).toLowerCase() !== String(incl.appPayloadHash).toLowerCase())
345
+ return { ok: false, trust: "unverified", reason: "the offer record doesn't match the on-chain (merkle-proven) commitment" };
346
+ const seller = await provenAuthor(opts.client, provenTx);
347
+ 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" };
348
+ const w = rec.want ?? {};
349
+ const payto = w.payto && ADDR.test(String(w.payto).toLowerCase()) ? String(w.payto).toLowerCase() : seller;
350
+ const terms = {
351
+ height: blockHeight,
352
+ feeBps: feeBpsAt(blockHeight),
353
+ value: w.value !== void 0 ? String(w.value) : void 0,
354
+ taker: rec.taker !== void 0 ? String(rec.taker).toLowerCase() : void 0,
355
+ bid: rec.bid !== void 0 ? String(rec.bid).toLowerCase() : void 0,
356
+ min: rec.min !== void 0 ? String(rec.min) : void 0
357
+ };
358
+ const base = { payto, seller, terms, blockHeight };
359
+ if (opts.servedOffer !== void 0 && opts.servedOffer !== null) {
360
+ const so = opts.servedOffer;
361
+ const servedPayto = so?.want?.payto !== void 0 && so?.want?.payto !== null ? String(so.want.payto).toLowerCase() : "";
362
+ if (servedPayto && servedPayto !== payto)
363
+ return { ok: false, trust: "verified", reason: "the served payment recipient doesn't match the offer's on-chain author/record", ...base };
364
+ if (so?.seller !== void 0 && so?.seller !== null && String(so.seller).toLowerCase() !== seller)
365
+ 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 };
366
+ if (bindOfferTerms(opts.servedOffer, terms))
367
+ 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 };
368
+ }
369
+ return { ok: true, trust: "verified", ...base };
370
+ }
371
+
270
372
  // src/index.ts
271
373
  var DEFAULT_SPV_CHECKPOINT = { height: 38142, hash: "0x00000000000140f023cc0ee1457a40833f2fcb4de44291b9d373e50f17b97232" };
272
374
  var DEFAULT_CAIRN = "https://cairn-substrate.com";
@@ -319,18 +421,39 @@ var Cairn = class {
319
421
  async verifiedHeaderMerkleAt(height) {
320
422
  if (!Number.isInteger(height) || height < this._spvCp.height)
321
423
  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
- }
424
+ const lc = await this.seededSpvLight();
328
425
  const haveTo = lc.baseHeight + lc.chain.length - 1;
329
426
  if (haveTo < height) await lc.sync(height);
330
427
  const vh = lc.chain[height - lc.baseHeight];
331
428
  if (!vh) throw new Error(`light client has no verified header at ${height}`);
332
429
  return String(vh.header.merkle);
333
430
  }
431
+ /** The PoW-verifying SPV light client, seeded ONCE from the pinned checkpoint (shared by the M3 header-merkle
432
+ * bind and the F13 offer pre-verify). Reused across calls; the first call near tip syncs the post-checkpoint span. */
433
+ async seededSpvLight() {
434
+ if (!this._spvLight) this._spvLight = new LightClient({ client: this.chain.client, checkpoints: { [this._spvCp.height]: this._spvCp.hash } });
435
+ if (!this._spvSeeded) {
436
+ await this._spvLight.syncFromCheckpoint(this._spvCp.height, this._spvCp.hash);
437
+ this._spvSeeded = true;
438
+ }
439
+ return this._spvLight;
440
+ }
441
+ /**
442
+ * F13 — corroborate an offer ON-CHAIN before building a `fillOffer` payment's `outputs`. Merkle-proves the
443
+ * offer into the PoW-verified header chain (from the pinned SPV checkpoint), binds the record to its on-chain
444
+ * commitment, derives the payment recipient + seller from the offer's on-chain AUTHOR (the funding input's
445
+ * prevout owner, txid-committed, NOT the malleable scriptSig), and, if you pass the resolver-SERVED `offer`
446
+ * object, binds its payto/seller + fee/rebate/partial terms to the proven ones. Returns a TRUST-LABELED result
447
+ * (fail-closed on a positive mismatch, fail-soft `transient` on a lagging / below-checkpoint chain view).
448
+ *
449
+ * This is best-effort corroboration, NOT the payment-grade boundary: the Cairn Wallet's OWN on-device
450
+ * fill-SPV (0.2.60+, fails-closed before signing) is that. A dApp settling real value should still clear this
451
+ * before building `outputs`, so it never hands the wallet a payment a lying resolver would redirect.
452
+ */
453
+ async verifyOfferForFill(offerId, servedOffer) {
454
+ const light = await this.seededSpvLight();
455
+ return preverifyOffer({ light, client: this.chain.client, offerId, servedOffer });
456
+ }
334
457
  /**
335
458
  * Detect + connect the Cairn Wallet (browser only). Wires the wallet into
336
459
  * `cairn.board` for signed writes. Resolves to the connected address.
@@ -366,15 +489,20 @@ export {
366
489
  NotInstalledError,
367
490
  RegistryClient,
368
491
  SDK_VERSION,
492
+ SubmitInFlightError,
369
493
  UnsupportedMethodError,
370
494
  UserRejectedError,
371
495
  WalletConnection,
372
496
  WalletLockedError,
497
+ bindOfferTerms,
373
498
  connect,
374
499
  detectProvider,
375
500
  discoverProviders,
376
501
  errorCode,
502
+ feeBpsAt,
377
503
  getWallet,
378
504
  isInstalled,
379
- mapProviderError
505
+ mapProviderError,
506
+ mapSubmitResultError,
507
+ preverifyOffer
380
508
  };