@chironbuilder/ootle-sdk 0.1.13 → 0.1.14

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
@@ -81,13 +81,57 @@ The claimed funds arrive as a stealth output owned by this account and are recor
81
81
  balance. Validators accept a burn only once its L1 block is well confirmed, so an early claim is
82
82
  rejected and can be retried.
83
83
 
84
+ ### HTLCs and atomic swaps
85
+
86
+ `htlcFund` / `htlcClaim` / `htlcRefund` are built for settlement safety, not just for constructing
87
+ the transactions:
88
+
89
+ ```ts
90
+ // Lock 5,000 units of a stealth-created asset from exact private outputs. The fee stays in its own
91
+ // native-TARI lane (here: private), the asset stays in the main lane, excess comes back as change.
92
+ const fund = await account.htlcFund(asset, 5_000n, claimantAddress, hashLockHex, refundEpoch, 50_000n,
93
+ { kind: "private", feeResourceAddress: TARI }, { source: { kind: "stealth", commitments: [myUtxo] } });
94
+ // fund.conditions → send to the claimant. fund.outputMask is also journaled (fund.journalId).
95
+
96
+ // Claimant: nothing is revealed unless the funded output matches the agreed terms.
97
+ const check = await account.verifyHtlc(asset, fund.ownCommitment, fund.conditions, { amount: 5_000n, minEpochsBeforeRefund: 3n });
98
+ await account.htlcClaim(asset, fund.ownCommitment, fund.conditions, preimageHex, 50_000n, feeType,
99
+ { expected: { amount: 5_000n, refunderPublicKeyHex, minEpochsBeforeRefund: 3n } });
100
+
101
+ // Funder, once currentEpoch >= refundEpoch:
102
+ await account.refundFromJournal(fund.journalId);
103
+ ```
104
+
105
+ - **Two lanes.** The fee (native TARI, public or a private UTXO) never shares a UTXO with the HTLC
106
+ value. `source: { kind: "stealth" }` spends this account's own stealth outputs of the resource,
107
+ so no public balance of it is needed.
108
+ - **Dry run first.** Every fund/claim/refund dry-runs the exact transaction before the real submit
109
+ (`preflight: false` to skip — a claim's dry run sends the preimage to the indexer's dry-run
110
+ endpoint).
111
+ - **Verified before the preimage leaves the device.** `htlcClaim` (and `verifyHtlc`) checks the
112
+ amount, that the claim leaf is your key and the refund leaf the agreed counterparty's, that the
113
+ on-chain condition root equals the tree's root, that the preimage matches the hash lock, and that
114
+ enough claim epochs remain; it throws `HtlcVerificationError` otherwise.
115
+ - **Durable journal.** The sealed transaction, the condition tree and the HTLC output's mask (the
116
+ only thing that makes a refund possible) are persisted *before* submission. An ambiguous outcome
117
+ throws `HtlcUnknownOutcomeError` — "pending", never "failed" — and `reconcileHtlcs()` (call at
118
+ startup) resolves it by resubmitting the identical transaction and applying its bookkeeping.
119
+ `listHtlcs()` shows every operation and its state.
120
+ - **Exact inputs are reserved** for the duration of an operation (and while its outcome is
121
+ unknown), so a concurrent send/unshield/fee selection can't pick them.
122
+ - **Epoch rules match the engine:** the refund leaf is `AfterEpoch` (open when
123
+ `currentEpoch >= refundEpoch`), the claim leaf `BeforeEpoch` — see `isHtlcRefundable` /
124
+ `isHtlcClaimableByEpoch`.
125
+ - Claimed and refunded outputs are recorded in the shielded ledger, so they're spendable at once.
126
+
84
127
  ## What's in here, and what isn't
85
128
 
86
129
  - **Account core** (`wallet.ts`): `OotleAccount`, balance/plan-resolution helpers, the transaction
87
130
  auto-resolve retry loop.
88
131
  - **Crypto/derivation**: `derivation.ts`, `domainHash.ts`, `componentAddress.ts`, `vault.ts`,
89
132
  `ownershipProof.ts`, `confidential.ts`.
90
- - **HTLC**: `htlc.ts`.
133
+ - **HTLC**: `htlc.ts` (condition trees), `htlcSafety.ts` (epoch rules, pre-claim verification,
134
+ per-output masks, outcome classification) + the journaled `OotleAccount` HTLC methods.
91
135
  - **L1 burn claims**: `burnClaim.ts` (proof assembly) + `OotleAccount.claimBurn`.
92
136
  - **Storage abstraction**: `storage.ts` + `adapters.ts`.
93
137
  - **Not included**: UI, an approval/permission model for dApp connections, address books, daemon
@@ -0,0 +1,123 @@
1
+ /** The refund leaf is `AfterEpoch(refundEpoch)`: admissible when `currentEpoch >= refundEpoch`. */
2
+ export declare function isHtlcRefundable(currentEpoch: bigint, refundEpoch: bigint): boolean;
3
+ /** The claim leaf is `BeforeEpoch(refundEpoch)`: admissible only while `currentEpoch < refundEpoch`. */
4
+ export declare function isHtlcClaimableByEpoch(currentEpoch: bigint, refundEpoch: bigint): boolean;
5
+ /** Epochs left in which a claim is still admissible (0 once the claim window has closed). */
6
+ export declare function epochsUntilRefund(currentEpoch: bigint, refundEpoch: bigint): bigint;
7
+ /**
8
+ * The on-chain condition root of a `[claim, refund]` tree — what a `PayTo::Conditions` output
9
+ * commits to (its `auth.Script`). Computed through the refund leaf, which consumes no witness data,
10
+ * so no preimage is needed.
11
+ */
12
+ export declare function conditionRootOf(conditions: object[]): string;
13
+ /**
14
+ * Finds the script-path condition root committed by an on-chain UTXO body — the `Script` variant of
15
+ * its spend authorization. Searched structurally (a `Script` key holding 32 bytes of hex) rather
16
+ * than by a fixed path, so an indexer-side field rename can't turn "is this our HTLC?" into a
17
+ * silent pass: a body with no such field returns `null`, which every caller treats as a mismatch.
18
+ */
19
+ export declare function findScriptRoot(body: unknown, depth?: number): string | null;
20
+ /** What the claimant's side of the swap agreed to — checked against the chain before the preimage
21
+ * is ever revealed. Omit a field to skip that particular check. */
22
+ export interface HtlcExpectations {
23
+ /** Exact amount expected (raw units). */
24
+ amount?: bigint;
25
+ /** Minimum acceptable amount, when an exact figure isn't agreed. */
26
+ minAmount?: bigint;
27
+ hashLockHex?: string;
28
+ refundEpoch?: bigint;
29
+ /** The counterparty (funder) who may refund — checked by rebuilding the tree from expectations. */
30
+ refunderPublicKeyHex?: string;
31
+ /** Refuse to claim with fewer than this many claim-admissible epochs left, so the claim can't be
32
+ * racing the refund window. Default 1 (the claim must land strictly before `refundEpoch`). */
33
+ minEpochsBeforeRefund?: bigint;
34
+ }
35
+ export interface HtlcFacts {
36
+ /** Decrypted value of the funded output (null when it didn't decrypt as ours). */
37
+ value: bigint | null;
38
+ /** Condition root the on-chain output actually commits to (null when absent/unreadable). */
39
+ onChainRoot: string | null;
40
+ /** The condition tree the claimant was handed. */
41
+ conditions: object[];
42
+ /** This account's own owner public key — must be the claim leaf's key. */
43
+ claimantPublicKeyHex: string;
44
+ currentEpoch: bigint;
45
+ /** When a preimage is supplied, SHA-256(preimage) must equal the tree's hash lock. */
46
+ preimageHashHex?: string;
47
+ }
48
+ /** Reads the hash lock and refund epoch back out of a `[claim, refund]` tree built by `htlcConditions`. */
49
+ export declare function describeHtlcConditions(conditions: object[]): {
50
+ hashLockHex: string;
51
+ refundEpoch: bigint;
52
+ };
53
+ /**
54
+ * Every reason the funded HTLC does not match what was agreed — empty means safe to reveal the
55
+ * preimage. Checks, in order: the output decrypts as ours; its amount; the tree's shape (rebuilt
56
+ * from `expected` + this account's key and compared byte-for-byte, which pins the claimant, the
57
+ * refunder, the hash lock and the refund epoch at once); the on-chain root equals the tree's root
58
+ * (so the tree we'd reveal is the one actually committed); the preimage hashes to the lock; and
59
+ * enough claim-admissible epochs remain.
60
+ */
61
+ export declare function verifyHtlcTerms(facts: HtlcFacts, expected?: HtlcExpectations): string[];
62
+ export declare class HtlcVerificationError extends Error {
63
+ readonly problems: string[];
64
+ constructor(problems: string[]);
65
+ }
66
+ /**
67
+ * Whether an error from submitting/polling is a *definitive* rejection (the transaction will never
68
+ * execute), as opposed to an ambiguous failure (timeout, network error, service worker killed)
69
+ * after which the transaction may still land. Only a definitive rejection may be treated as
70
+ * "nothing happened"; everything else must be reconciled, never assumed failed.
71
+ */
72
+ export declare function isDefinitiveRejection(e: unknown): boolean;
73
+ /**
74
+ * Thrown (instead of a plain error) when an HTLC transaction was, or may have been, submitted but
75
+ * its outcome didn't arrive in time. Carries everything the caller needs — including the refund
76
+ * data — and the journal entry that `OotleAccount.reconcileHtlcs()` resolves later. Callers must
77
+ * treat this as "pending", never as "failed".
78
+ */
79
+ export declare class HtlcUnknownOutcomeError extends Error {
80
+ readonly journalId: string;
81
+ readonly details: {
82
+ kind: "fund" | "claim" | "refund";
83
+ transactionId?: string;
84
+ conditions: object[];
85
+ htlcCommitment?: string;
86
+ outputMask?: string;
87
+ };
88
+ constructor(journalId: string, details: {
89
+ kind: "fund" | "claim" | "refund";
90
+ transactionId?: string;
91
+ conditions: object[];
92
+ htlcCommitment?: string;
93
+ outputMask?: string;
94
+ }, cause: unknown);
95
+ }
96
+ /**
97
+ * `(a - b) mod ℓ` for 32-byte little-endian scalars. A transfer's `outputMask` is the *sum* of its
98
+ * outputs' blinding masks (`aggregated_output_mask`), so when a fund creates the HTLC output plus
99
+ * this account's own change, the HTLC output's own mask is `aggregate - changeMask` — the change
100
+ * mask being recoverable because the change is addressed to this account. The result is only ever
101
+ * trusted after `OotleAccount.htlcFund` proves it reproduces the HTLC commitment (see there).
102
+ */
103
+ export declare function subtractScalars(aHex: string, bHex: string): string;
104
+ /**
105
+ * Decrypts one of this account's *own* outputs straight out of a locally built outputs statement
106
+ * (before anything is submitted) and returns its value and blinding mask. `outputJson` is the
107
+ * statement's `outputs[i]` entry; `viewSecret` this account's view secret. Throws if the output
108
+ * isn't addressed to this view key (AEAD or commitment check fails inside `unblindOutput`).
109
+ */
110
+ export declare function decryptOwnStatementOutput(outputJson: unknown, viewSecret: Uint8Array): {
111
+ value: bigint;
112
+ maskHex: string;
113
+ commitment: string;
114
+ };
115
+ /** The Pedersen commitment of input `index` in a full statement JSON (what a spend of it commits to). */
116
+ export declare function statementInputCommitment(statementJson: string, index?: number): string | null;
117
+ /** The commitment of output `index` in a full `StealthTransferStatement` JSON string. Only the
118
+ * commitment strings are read, so parsing precision on large numeric fields doesn't matter here. */
119
+ export declare function statementOutputCommitment(statementJson: string, index?: number): string;
120
+ export declare function sha256Hex(data: Uint8Array): Promise<string>;
121
+ /** A fresh, random operation id (journal ids and reservation holders). */
122
+ export declare function newOperationId(prefix: string): string;
123
+ //# sourceMappingURL=htlcSafety.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"htlcSafety.d.ts","sourceRoot":"","sources":["../src/htlcSafety.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,275 @@
1
+ // Settlement-safety helpers for HTLCs: the epoch rules, condition-root checks, pre-claim term
2
+ // verification and outcome classification that `OotleAccount`'s HTLC methods (and any swap
3
+ // protocol built on them) rely on. Everything here is pure or wasm-only — no provider, no storage —
4
+ // so every rule is unit-testable on its own (htlcSafety.test.ts).
5
+ import { buildScriptPathWitness, encryptedDataDhKdfAead, unblindOutput } from "@tari-project/ootle-wasm";
6
+ import { htlcConditions } from "./htlc.js";
7
+ import { fromHex, toHex } from "./vault.js";
8
+ // ── epoch rules (must match the engine's AfterEpoch / BeforeEpoch atoms exactly) ──────────────
9
+ /** The refund leaf is `AfterEpoch(refundEpoch)`: admissible when `currentEpoch >= refundEpoch`. */
10
+ export function isHtlcRefundable(currentEpoch, refundEpoch) {
11
+ return currentEpoch >= refundEpoch;
12
+ }
13
+ /** The claim leaf is `BeforeEpoch(refundEpoch)`: admissible only while `currentEpoch < refundEpoch`. */
14
+ export function isHtlcClaimableByEpoch(currentEpoch, refundEpoch) {
15
+ return currentEpoch < refundEpoch;
16
+ }
17
+ /** Epochs left in which a claim is still admissible (0 once the claim window has closed). */
18
+ export function epochsUntilRefund(currentEpoch, refundEpoch) {
19
+ return refundEpoch > currentEpoch ? refundEpoch - currentEpoch : 0n;
20
+ }
21
+ // ── condition roots ────────────────────────────────────────────────────────────────────────────
22
+ /**
23
+ * The on-chain condition root of a `[claim, refund]` tree — what a `PayTo::Conditions` output
24
+ * commits to (its `auth.Script`). Computed through the refund leaf, which consumes no witness data,
25
+ * so no preimage is needed.
26
+ */
27
+ export function conditionRootOf(conditions) {
28
+ const refundLeaf = conditions[1];
29
+ if (!refundLeaf)
30
+ throw new Error("conditionRootOf: expected the two-leaf [claim, refund] condition tree");
31
+ const result = JSON.parse(buildScriptPathWitness(JSON.stringify(conditions), JSON.stringify(refundLeaf), new Uint8Array(0)));
32
+ if (typeof result.condition_root !== "string")
33
+ throw new Error("conditionRootOf: wasm returned no condition_root");
34
+ return result.condition_root.toLowerCase();
35
+ }
36
+ /**
37
+ * Finds the script-path condition root committed by an on-chain UTXO body — the `Script` variant of
38
+ * its spend authorization. Searched structurally (a `Script` key holding 32 bytes of hex) rather
39
+ * than by a fixed path, so an indexer-side field rename can't turn "is this our HTLC?" into a
40
+ * silent pass: a body with no such field returns `null`, which every caller treats as a mismatch.
41
+ */
42
+ export function findScriptRoot(body, depth = 0) {
43
+ if (depth > 8 || body === null || typeof body !== "object")
44
+ return null;
45
+ for (const [key, value] of Object.entries(body)) {
46
+ if (key === "Script" && typeof value === "string" && /^[0-9a-f]{64}$/i.test(value))
47
+ return value.toLowerCase();
48
+ const nested = findScriptRoot(value, depth + 1);
49
+ if (nested)
50
+ return nested;
51
+ }
52
+ return null;
53
+ }
54
+ /** Reads the hash lock and refund epoch back out of a `[claim, refund]` tree built by `htlcConditions`. */
55
+ export function describeHtlcConditions(conditions) {
56
+ const claim = conditions[0];
57
+ const hash = claim?.find((a) => a.Builtin?.HashLock)?.Builtin?.HashLock?.hash;
58
+ const before = claim?.find((a) => a.Builtin?.BeforeEpoch !== undefined)?.Builtin?.BeforeEpoch;
59
+ if (typeof hash !== "string" || typeof before !== "number") {
60
+ throw new Error("Not an HTLC condition tree: the claim leaf has no HashLock/BeforeEpoch atoms.");
61
+ }
62
+ return { hashLockHex: hash.toLowerCase(), refundEpoch: BigInt(before) };
63
+ }
64
+ /**
65
+ * Every reason the funded HTLC does not match what was agreed — empty means safe to reveal the
66
+ * preimage. Checks, in order: the output decrypts as ours; its amount; the tree's shape (rebuilt
67
+ * from `expected` + this account's key and compared byte-for-byte, which pins the claimant, the
68
+ * refunder, the hash lock and the refund epoch at once); the on-chain root equals the tree's root
69
+ * (so the tree we'd reveal is the one actually committed); the preimage hashes to the lock; and
70
+ * enough claim-admissible epochs remain.
71
+ */
72
+ export function verifyHtlcTerms(facts, expected = {}) {
73
+ const problems = [];
74
+ if (facts.value === null)
75
+ problems.push("The funded output does not decrypt with this account's view key — it isn't addressed to you.");
76
+ if (facts.value !== null && expected.amount !== undefined && facts.value !== expected.amount) {
77
+ problems.push(`The funded amount is ${facts.value}, not the agreed ${expected.amount}.`);
78
+ }
79
+ if (facts.value !== null && expected.minAmount !== undefined && facts.value < expected.minAmount) {
80
+ problems.push(`The funded amount ${facts.value} is below the agreed minimum ${expected.minAmount}.`);
81
+ }
82
+ let described = null;
83
+ try {
84
+ described = describeHtlcConditions(facts.conditions);
85
+ }
86
+ catch (e) {
87
+ problems.push(e instanceof Error ? e.message : String(e));
88
+ }
89
+ if (described) {
90
+ const hashLockHex = (expected.hashLockHex ?? described.hashLockHex).toLowerCase();
91
+ const refundEpoch = expected.refundEpoch ?? described.refundEpoch;
92
+ if (expected.hashLockHex !== undefined && expected.hashLockHex.toLowerCase() !== described.hashLockHex) {
93
+ problems.push("The condition tree's hash lock is not the agreed one.");
94
+ }
95
+ if (expected.refundEpoch !== undefined && expected.refundEpoch !== described.refundEpoch) {
96
+ problems.push(`The condition tree's refund epoch is ${described.refundEpoch}, not the agreed ${expected.refundEpoch}.`);
97
+ }
98
+ // Rebuild the tree the way a correct funder would have, and demand an exact match: this pins the
99
+ // claim leaf to *our* key (nobody else can take the claim path) and, when known, the refund leaf
100
+ // to the agreed counterparty.
101
+ const refunder = expected.refunderPublicKeyHex ?? refunderKeyOf(facts.conditions);
102
+ if (refunder) {
103
+ try {
104
+ const rebuilt = htlcConditions({ hashLockHex, refundEpoch, claimantPublicKeyHex: facts.claimantPublicKeyHex, refunderPublicKeyHex: refunder });
105
+ if (JSON.stringify(rebuilt) !== JSON.stringify(facts.conditions)) {
106
+ problems.push("The condition tree is not a standard HTLC paying you on the claim path (claimant key, refunder key, hash lock or epoch differ).");
107
+ }
108
+ }
109
+ catch (e) {
110
+ problems.push(e instanceof Error ? e.message : String(e));
111
+ }
112
+ }
113
+ if (facts.preimageHashHex !== undefined && facts.preimageHashHex.toLowerCase() !== described.hashLockHex) {
114
+ problems.push("The preimage does not hash to this HTLC's hash lock.");
115
+ }
116
+ const minLeft = expected.minEpochsBeforeRefund ?? 1n;
117
+ const left = epochsUntilRefund(facts.currentEpoch, described.refundEpoch);
118
+ if (left < minLeft) {
119
+ problems.push(left === 0n
120
+ ? `The claim window has closed (current epoch ${facts.currentEpoch} ≥ refund epoch ${described.refundEpoch}).`
121
+ : `Only ${left} epoch(s) remain before the refund path opens; at least ${minLeft} required.`);
122
+ }
123
+ }
124
+ let treeRoot = null;
125
+ try {
126
+ treeRoot = conditionRootOf(facts.conditions);
127
+ }
128
+ catch (e) {
129
+ problems.push(`The condition tree is malformed: ${e instanceof Error ? e.message : String(e)}`);
130
+ }
131
+ if (treeRoot !== null && facts.onChainRoot !== treeRoot) {
132
+ problems.push(facts.onChainRoot === null
133
+ ? "The on-chain output is not script-locked (no condition root) — it is not this HTLC."
134
+ : "The on-chain output commits to a different condition tree than the one you were given.");
135
+ }
136
+ return problems;
137
+ }
138
+ /** The refund leaf's public key, read back from a tree built by `htlcConditions`. */
139
+ function refunderKeyOf(conditions) {
140
+ const refund = conditions[1];
141
+ const rule = refund?.find((a) => a.AccessRule)?.AccessRule;
142
+ return rule?.Restricted?.Require?.Require?.NonFungibleAddress?.id?.U256 ?? null;
143
+ }
144
+ export class HtlcVerificationError extends Error {
145
+ problems;
146
+ constructor(problems) {
147
+ super(`Refusing to reveal the preimage — the funded HTLC doesn't match the agreed terms:\n- ${problems.join("\n- ")}`);
148
+ this.problems = problems;
149
+ this.name = "HtlcVerificationError";
150
+ }
151
+ }
152
+ // ── outcome classification ────────────────────────────────────────────────────────────────────
153
+ /**
154
+ * Whether an error from submitting/polling is a *definitive* rejection (the transaction will never
155
+ * execute), as opposed to an ambiguous failure (timeout, network error, service worker killed)
156
+ * after which the transaction may still land. Only a definitive rejection may be treated as
157
+ * "nothing happened"; everything else must be reconciled, never assumed failed.
158
+ */
159
+ export function isDefinitiveRejection(e) {
160
+ const msg = e instanceof Error ? e.message : String(e);
161
+ return /was rejected:|accepted the fee but rejected the rest/i.test(msg);
162
+ }
163
+ /**
164
+ * Thrown (instead of a plain error) when an HTLC transaction was, or may have been, submitted but
165
+ * its outcome didn't arrive in time. Carries everything the caller needs — including the refund
166
+ * data — and the journal entry that `OotleAccount.reconcileHtlcs()` resolves later. Callers must
167
+ * treat this as "pending", never as "failed".
168
+ */
169
+ export class HtlcUnknownOutcomeError extends Error {
170
+ journalId;
171
+ details;
172
+ constructor(journalId, details, cause) {
173
+ super(`The HTLC ${details.kind} was submitted but its outcome is not known yet (${cause instanceof Error ? cause.message : String(cause)}). ` +
174
+ `It is recorded as journal entry ${journalId}; call reconcileHtlcs() to resolve it. Do not retry it as a new operation.`, { cause });
175
+ this.journalId = journalId;
176
+ this.details = details;
177
+ this.name = "HtlcUnknownOutcomeError";
178
+ }
179
+ }
180
+ // ── output masks ──────────────────────────────────────────────────────────────────────────────
181
+ /** The Ristretto255 group order ℓ = 2^252 + 27742317777372353535851937790883648493. */
182
+ const RISTRETTO_L = (1n << 252n) + 27742317777372353535851937790883648493n;
183
+ function scalarFromLeHex(hex) {
184
+ if (!/^[0-9a-f]{64}$/i.test(hex))
185
+ throw new Error(`expected a 32-byte scalar in hex, got ${hex.length} chars`);
186
+ let v = 0n;
187
+ for (let i = 62; i >= 0; i -= 2)
188
+ v = (v << 8n) | BigInt(parseInt(hex.slice(i, i + 2), 16));
189
+ return v;
190
+ }
191
+ function scalarToLeHex(v) {
192
+ let out = "";
193
+ for (let i = 0; i < 32; i++) {
194
+ out += Number(v & 0xffn).toString(16).padStart(2, "0");
195
+ v >>= 8n;
196
+ }
197
+ return out;
198
+ }
199
+ /**
200
+ * `(a - b) mod ℓ` for 32-byte little-endian scalars. A transfer's `outputMask` is the *sum* of its
201
+ * outputs' blinding masks (`aggregated_output_mask`), so when a fund creates the HTLC output plus
202
+ * this account's own change, the HTLC output's own mask is `aggregate - changeMask` — the change
203
+ * mask being recoverable because the change is addressed to this account. The result is only ever
204
+ * trusted after `OotleAccount.htlcFund` proves it reproduces the HTLC commitment (see there).
205
+ */
206
+ export function subtractScalars(aHex, bHex) {
207
+ const a = scalarFromLeHex(aHex) % RISTRETTO_L;
208
+ const b = scalarFromLeHex(bHex) % RISTRETTO_L;
209
+ return scalarToLeHex((((a - b) % RISTRETTO_L) + RISTRETTO_L) % RISTRETTO_L);
210
+ }
211
+ /**
212
+ * Decrypts one of this account's *own* outputs straight out of a locally built outputs statement
213
+ * (before anything is submitted) and returns its value and blinding mask. `outputJson` is the
214
+ * statement's `outputs[i]` entry; `viewSecret` this account's view secret. Throws if the output
215
+ * isn't addressed to this view key (AEAD or commitment check fails inside `unblindOutput`).
216
+ */
217
+ export function decryptOwnStatementOutput(outputJson, viewSecret) {
218
+ const commitment = findHexField(outputJson, ["commitment"]);
219
+ const nonce = findHexField(outputJson, ["public_nonce", "sender_public_nonce"]);
220
+ const encrypted = findHexField(outputJson, ["encrypted_data"], true);
221
+ if (!commitment || !nonce || !encrypted)
222
+ throw new Error("decryptOwnStatementOutput: output is missing commitment/public nonce/encrypted data");
223
+ const key = encryptedDataDhKdfAead(viewSecret, fromHex(nonce));
224
+ const result = unblindOutput(fromHex(commitment), fromHex(encrypted), key, true);
225
+ return { value: BigInt(result.value), maskHex: toHex(result.mask), commitment: commitment.toLowerCase() };
226
+ }
227
+ function findHexField(node, keys, anyLength = false, depth = 0) {
228
+ if (depth > 6 || node === null || typeof node !== "object")
229
+ return null;
230
+ for (const [key, value] of Object.entries(node)) {
231
+ if (keys.includes(key) && typeof value === "string" && /^[0-9a-f]+$/i.test(value) && (anyLength || value.length === 64))
232
+ return value;
233
+ const nested = findHexField(value, keys, anyLength, depth + 1);
234
+ if (nested)
235
+ return nested;
236
+ }
237
+ return null;
238
+ }
239
+ /** The Pedersen commitment of input `index` in a full statement JSON (what a spend of it commits to). */
240
+ export function statementInputCommitment(statementJson, index = 0) {
241
+ const parsed = JSON.parse(statementJson);
242
+ const input = parsed.inputs_statement?.inputs?.[index];
243
+ return findCommitment(input);
244
+ }
245
+ function findCommitment(node, depth = 0) {
246
+ if (depth > 6 || node === null || typeof node !== "object")
247
+ return null;
248
+ for (const [key, value] of Object.entries(node)) {
249
+ if (key === "commitment" && typeof value === "string" && /^[0-9a-f]{64}$/i.test(value))
250
+ return value.toLowerCase();
251
+ const nested = findCommitment(value, depth + 1);
252
+ if (nested)
253
+ return nested;
254
+ }
255
+ return null;
256
+ }
257
+ // ── statements / hashing ──────────────────────────────────────────────────────────────────────
258
+ /** The commitment of output `index` in a full `StealthTransferStatement` JSON string. Only the
259
+ * commitment strings are read, so parsing precision on large numeric fields doesn't matter here. */
260
+ export function statementOutputCommitment(statementJson, index = 0) {
261
+ const parsed = JSON.parse(statementJson);
262
+ const commitment = parsed.outputs_statement?.outputs?.[index]?.output?.commitment;
263
+ if (typeof commitment !== "string" || commitment.length === 0) {
264
+ throw new Error(`statementOutputCommitment: no output commitment at index ${index}`);
265
+ }
266
+ return commitment;
267
+ }
268
+ export async function sha256Hex(data) {
269
+ return toHex(new Uint8Array(await crypto.subtle.digest("SHA-256", data)));
270
+ }
271
+ /** A fresh, random operation id (journal ids and reservation holders). */
272
+ export function newOperationId(prefix) {
273
+ const bytes = crypto.getRandomValues(new Uint8Array(12));
274
+ return `${prefix}-${toHex(bytes)}`;
275
+ }
package/dist/index.d.ts CHANGED
@@ -9,15 +9,17 @@ export { deriveAccountKeys } from "./derivation.js";
9
9
  export { DomainSeparatedHasher, KEY_MANAGER_DOMAIN, KEY_MANAGER_DOMAIN_VERSION, keyManagerDomainHasher } from "./domainHash.js";
10
10
  export type { HtlcConditionsParams } from "./htlc.js";
11
11
  export { accessRuleRequiringPublicKey, htlcConditions } from "./htlc.js";
12
+ export type { HtlcExpectations, HtlcFacts } from "./htlcSafety.js";
13
+ export { HtlcUnknownOutcomeError, HtlcVerificationError, conditionRootOf, describeHtlcConditions, epochsUntilRefund, findScriptRoot, isDefinitiveRejection, isHtlcClaimableByEpoch, isHtlcRefundable, verifyHtlcTerms, } from "./htlcSafety.js";
12
14
  export type { NetworkName } from "./ootleNetwork.js";
13
15
  export { toOotleNetwork } from "./ootleNetwork.js";
14
16
  export { OWNERSHIP_PROOF_CHALLENGE_MAX_LENGTH, buildOwnershipProofMessage, buildWalletOwnershipMessage } from "./ownershipProof.js";
15
17
  export type { EncryptedVault } from "./vault.js";
16
18
  export { decryptVault, encryptVault, fromHex, toHex } from "./vault.js";
17
19
  export { withTimeout } from "./timeout.js";
18
- export type { KeyValueStore, PendingShield, ShieldedOutputRecord } from "./storage.js";
19
- export { addPendingShield, addShieldedOutput, configureOotleStorage, getKnownVersions, getPrivatePaymentScanCursor, listPendingShields, listShieldedOutputs, localAccountId, markShieldedOutputSpent, removePendingShield, serialized, setKnownVersions, setPrivatePaymentScanCursor, wipeOotleState, } from "./storage.js";
20
+ export type { CommitmentReservation, HtlcJournalEntry, HtlcJournalStatus, KeyValueStore, PendingShield, ShieldedOutputRecord } from "./storage.js";
21
+ export { addPendingShield, addShieldedOutput, configureOotleStorage, getKnownVersions, getPrivatePaymentScanCursor, listPendingShields, listShieldedOutputs, localAccountId, markShieldedOutputSpent, removePendingShield, serialized, setKnownVersions, setPrivatePaymentScanCursor, wipeOotleState, CommitmentReservedError, RESERVATION_TTL_MS, listHtlcJournal, } from "./storage.js";
20
22
  export { chromeStorageAdapter, inMemoryAdapter, localStorageAdapter } from "./adapters.js";
21
- export type { FeeType, PrivateBalance, TokenBalance } from "./wallet.js";
23
+ export type { FeeType, HtlcClaimOptions, HtlcFundOptions, HtlcFundResult, HtlcSpendResult, PrivateBalance, TokenBalance } from "./wallet.js";
22
24
  export { OotleAccount, assertValidMinimumValuePromise, buildHtlcSpendStatement, extractMissingSubstateAddress, extractStaleLockVersion, forgetKnownVersions, pollTransactionResult, recoverPendingShields, resetKnownVersions, resolveInputsWithRetry, resolveSendPrivatelyPlan, resolveUnshieldPlan, selectPrivateFeeUtxo, selectShieldedUtxosForAmount, selectUnspentShieldedOutputs, substateExists, summarizePrivateHoldings, synthesizeShieldedOnlyBalances, } from "./wallet.js";
23
25
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,YAAY,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEhF,OAAO,EACL,wBAAwB,EACxB,iCAAiC,EACjC,6BAA6B,EAC7B,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClG,OAAO,EAAE,sBAAsB,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAErF,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACrF,OAAO,EAAE,+BAA+B,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AAEhG,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEpD,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,0BAA0B,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEhI,YAAY,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,4BAA4B,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEzE,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,OAAO,EAAE,oCAAoC,EAAE,0BAA0B,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAEpI,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExE,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACvF,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,EAChB,2BAA2B,EAC3B,kBAAkB,EAClB,mBAAmB,EACnB,cAAc,EACd,uBAAuB,EACvB,mBAAmB,EACnB,UAAU,EACV,gBAAgB,EAChB,2BAA2B,EAC3B,cAAc,GACf,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAE3F,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EACL,YAAY,EACZ,8BAA8B,EAC9B,uBAAuB,EACvB,6BAA6B,EAC7B,uBAAuB,EACvB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,EAClB,sBAAsB,EACtB,wBAAwB,EACxB,mBAAmB,EACnB,oBAAoB,EACpB,4BAA4B,EAC5B,4BAA4B,EAC5B,cAAc,EACd,wBAAwB,EACxB,8BAA8B,GAC/B,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,YAAY,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEhF,OAAO,EACL,wBAAwB,EACxB,iCAAiC,EACjC,6BAA6B,EAC7B,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClG,OAAO,EAAE,sBAAsB,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAErF,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACrF,OAAO,EAAE,+BAA+B,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AAEhG,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEpD,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,0BAA0B,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEhI,YAAY,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,4BAA4B,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEzE,YAAY,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EACL,uBAAuB,EACvB,qBAAqB,EACrB,eAAe,EACf,sBAAsB,EACtB,iBAAiB,EACjB,cAAc,EACd,qBAAqB,EACrB,sBAAsB,EACtB,gBAAgB,EAChB,eAAe,GAChB,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,OAAO,EAAE,oCAAoC,EAAE,0BAA0B,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAEpI,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExE,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,YAAY,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACnJ,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,EAChB,2BAA2B,EAC3B,kBAAkB,EAClB,mBAAmB,EACnB,cAAc,EACd,uBAAuB,EACvB,mBAAmB,EACnB,UAAU,EACV,gBAAgB,EAChB,2BAA2B,EAC3B,cAAc,EACd,uBAAuB,EACvB,kBAAkB,EAClB,eAAe,GAChB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAE3F,YAAY,EAAE,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC7I,OAAO,EACL,YAAY,EACZ,8BAA8B,EAC9B,uBAAuB,EACvB,6BAA6B,EAC7B,uBAAuB,EACvB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,EAClB,sBAAsB,EACtB,wBAAwB,EACxB,mBAAmB,EACnB,oBAAoB,EACpB,4BAA4B,EAC5B,4BAA4B,EAC5B,cAAc,EACd,wBAAwB,EACxB,8BAA8B,GAC/B,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -6,10 +6,11 @@ export { scanTransactionsForOwnedOutputs, sumConfidentialCommitments } from "./c
6
6
  export { deriveAccountKeys } from "./derivation.js";
7
7
  export { DomainSeparatedHasher, KEY_MANAGER_DOMAIN, KEY_MANAGER_DOMAIN_VERSION, keyManagerDomainHasher } from "./domainHash.js";
8
8
  export { accessRuleRequiringPublicKey, htlcConditions } from "./htlc.js";
9
+ export { HtlcUnknownOutcomeError, HtlcVerificationError, conditionRootOf, describeHtlcConditions, epochsUntilRefund, findScriptRoot, isDefinitiveRejection, isHtlcClaimableByEpoch, isHtlcRefundable, verifyHtlcTerms, } from "./htlcSafety.js";
9
10
  export { toOotleNetwork } from "./ootleNetwork.js";
10
11
  export { OWNERSHIP_PROOF_CHALLENGE_MAX_LENGTH, buildOwnershipProofMessage, buildWalletOwnershipMessage } from "./ownershipProof.js";
11
12
  export { decryptVault, encryptVault, fromHex, toHex } from "./vault.js";
12
13
  export { withTimeout } from "./timeout.js";
13
- export { addPendingShield, addShieldedOutput, configureOotleStorage, getKnownVersions, getPrivatePaymentScanCursor, listPendingShields, listShieldedOutputs, localAccountId, markShieldedOutputSpent, removePendingShield, serialized, setKnownVersions, setPrivatePaymentScanCursor, wipeOotleState, } from "./storage.js";
14
+ export { addPendingShield, addShieldedOutput, configureOotleStorage, getKnownVersions, getPrivatePaymentScanCursor, listPendingShields, listShieldedOutputs, localAccountId, markShieldedOutputSpent, removePendingShield, serialized, setKnownVersions, setPrivatePaymentScanCursor, wipeOotleState, CommitmentReservedError, RESERVATION_TTL_MS, listHtlcJournal, } from "./storage.js";
14
15
  export { chromeStorageAdapter, inMemoryAdapter, localStorageAdapter } from "./adapters.js";
15
16
  export { OotleAccount, assertValidMinimumValuePromise, buildHtlcSpendStatement, extractMissingSubstateAddress, extractStaleLockVersion, forgetKnownVersions, pollTransactionResult, recoverPendingShields, resetKnownVersions, resolveInputsWithRetry, resolveSendPrivatelyPlan, resolveUnshieldPlan, selectPrivateFeeUtxo, selectShieldedUtxosForAmount, selectUnspentShieldedOutputs, substateExists, summarizePrivateHoldings, synthesizeShieldedOnlyBalances, } from "./wallet.js";
package/dist/storage.d.ts CHANGED
@@ -37,6 +37,72 @@ export interface PendingShield {
37
37
  /** This account's own memo for the operation, if any. */
38
38
  memo?: string;
39
39
  }
40
+ /**
41
+ * Lifecycle of one HTLC operation, persisted *before* anything is submitted so a crash, a killed
42
+ * service worker or a confirmation timeout can never strand the only copy of what's needed to
43
+ * finish it (see `HtlcJournalEntry`):
44
+ *
45
+ * - `prepared` the sealed envelope and every recovery field are stored; not yet submitted
46
+ * - `claim_armed` (claims only) the funded output passed every pre-claim check and a dry run; the
47
+ * next step reveals the preimage on-chain
48
+ * - `submitted` the network accepted the envelope for processing; outcome not yet known
49
+ * - `unknown` submitted (or possibly submitted) but no final result arrived in time — resolved by
50
+ * `OotleAccount.reconcileHtlcs()`, never assumed to have failed
51
+ * - `confirmed` finalized and accepted; local bookkeeping done
52
+ * - `failed` definitively rejected; nothing happened on-chain beyond (possibly) the fee
53
+ */
54
+ export type HtlcJournalStatus = "prepared" | "claim_armed" | "submitted" | "unknown" | "confirmed" | "failed";
55
+ export interface HtlcJournalEntry {
56
+ /** Local id, stable across retries of the same operation. */
57
+ id: string;
58
+ accountId: string;
59
+ kind: "fund" | "claim" | "refund";
60
+ status: HtlcJournalStatus;
61
+ resourceAddress: string;
62
+ /** Raw units. For a fund: the HTLC amount. For a claim/refund: the amount moving to this account. */
63
+ amount: string;
64
+ /** The exact two-leaf `[claim, refund]` condition tree — needed by both sides, never recomputable
65
+ * by the counterparty from on-chain data alone (only its root is committed). */
66
+ conditions: object[];
67
+ hashLockHex: string;
68
+ refundEpoch: string;
69
+ /** The HTLC output's commitment (hex): created by a fund, spent by a claim/refund. */
70
+ htlcCommitment?: string;
71
+ /** Fund only: the HTLC output's blinding mask. The funder can't decrypt an output addressed to the
72
+ * claimant, so without this a refund is impossible — which is why it is written here before submit. */
73
+ outputMask?: string;
74
+ /** This account's own new stealth output from the operation (a fund's change, a claim/refund's
75
+ * received output), recorded in the shielded ledger once the operation confirms. */
76
+ ownCommitment?: string;
77
+ ownAmount?: string;
78
+ /** Stealth inputs this operation spends (fund-from-stealth), marked spent once it confirms. */
79
+ spentCommitments?: string[];
80
+ /** Private-fee bookkeeping, applied once the operation confirms. */
81
+ privateFee?: {
82
+ feeResourceAddress: string;
83
+ spentCommitment: string;
84
+ changeCommitment: string;
85
+ changeAmount: string;
86
+ };
87
+ /** The sealed transaction exactly as submitted — lets reconciliation resubmit the identical
88
+ * transaction (same id, so it can never execute twice) if the first submit never reached the
89
+ * network. For a claim this contains the preimage, so it is only ever resubmitted, never shown. */
90
+ envelope: string;
91
+ transactionId?: string;
92
+ error?: string;
93
+ createdAt: number;
94
+ updatedAt: number;
95
+ }
96
+ /** A stealth commitment held by an in-flight operation, so a concurrent one can't select it too. */
97
+ export interface CommitmentReservation {
98
+ accountId: string;
99
+ commitment: string;
100
+ /** Who holds it — an operation id; the same holder may re-reserve idempotently. */
101
+ holder: string;
102
+ /** Epoch ms after which the reservation lapses on its own (a crashed operation can never lock a
103
+ * UTXO forever). */
104
+ expiresAt: number;
105
+ }
40
106
  export declare function localAccountId(index: number): string;
41
107
  /** Runs a whole read-modify-write cycle to completion before the next one starts, so two
42
108
  * concurrent callers (two tabs, a page request racing a popup request) can't interleave their
@@ -48,6 +114,8 @@ export declare function localAccountId(index: number): string;
48
114
  * provide. */
49
115
  export declare function serialized<T>(fn: () => Promise<T>): Promise<T>;
50
116
  export declare function listShieldedOutputs(accountId: string): Promise<ShieldedOutputRecord[]>;
117
+ /** Idempotent: a commitment already recorded for the same account is left as it is, so a
118
+ * reconciliation that re-applies a finished operation's bookkeeping can never double-count it. */
51
119
  export declare function addShieldedOutput(record: ShieldedOutputRecord): Promise<void>;
52
120
  export declare function markShieldedOutputSpent(accountId: string, commitment: string): Promise<void>;
53
121
  export declare function listPendingShields(): Promise<PendingShield[]>;
@@ -59,6 +127,30 @@ export declare function setPrivatePaymentScanCursor(accountId: string, transacti
59
127
  * this exists at all. */
60
128
  export declare function getKnownVersions(): Promise<Record<string, number>>;
61
129
  export declare function setKnownVersions(known: Record<string, number>): Promise<void>;
130
+ export declare function listHtlcJournal(accountId?: string): Promise<HtlcJournalEntry[]>;
131
+ export declare function getHtlcJournalEntry(id: string): Promise<HtlcJournalEntry | undefined>;
132
+ /** Inserts or replaces (by id) — idempotent, so retrying the same write is always safe. */
133
+ export declare function putHtlcJournalEntry(entry: HtlcJournalEntry): Promise<void>;
134
+ /** Applies `patch` to an existing entry and bumps `updatedAt`. A missing id is a no-op. */
135
+ export declare function updateHtlcJournalEntry(id: string, patch: Partial<HtlcJournalEntry>): Promise<HtlcJournalEntry | undefined>;
136
+ /** Default lifetime of a reservation: long enough for any single operation (prepare + dry run +
137
+ * submit + confirmation), short enough that a crashed one frees its UTXOs within minutes. */
138
+ export declare const RESERVATION_TTL_MS: number;
139
+ /** Commitments currently reserved for `accountId` by anyone other than `exceptHolder`. */
140
+ export declare function listReservedCommitments(accountId: string, exceptHolder?: string, now?: number): Promise<Set<string>>;
141
+ /**
142
+ * Atomically reserves every commitment in `commitments` for `holder`, or none of them: if any is
143
+ * already held (unexpired) by a different holder, throws `CommitmentReservedError` naming them and
144
+ * writes nothing. Re-reserving one's own commitments just extends the lease. Expired leases are
145
+ * swept on every call.
146
+ */
147
+ export declare function reserveCommitments(accountId: string, commitments: string[], holder: string, ttlMs?: number): Promise<void>;
148
+ /** Releases every reservation `holder` holds (all accounts). Safe to call more than once. */
149
+ export declare function releaseReservations(holder: string): Promise<void>;
150
+ export declare class CommitmentReservedError extends Error {
151
+ readonly commitments: string[];
152
+ constructor(commitments: string[]);
153
+ }
62
154
  /** Clears every key this module owns -- call this from a host's own "erase wallet" flow. Does not
63
155
  * touch anything else the host's `KeyValueStore` might hold (address books, settings, ...); this
64
156
  * SDK never wrote those keys, so it isn't this function's place to remove them. */