@chironbuilder/ootle-sdk 0.1.13 → 0.1.15

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.
@@ -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
@@ -1,7 +1,7 @@
1
1
  export type { TransactionExecuteOpts, WalletAccountApi } from "./accountApi.js";
2
2
  export { ACCOUNT_TEMPLATE_ADDRESS, componentAddressFromWalletAddress, deriveAccountComponentAddress, deriveComponentAddress, } from "./componentAddress.js";
3
- export type { BurnClaimProofContents, KernelMerkleProof, L1BurnProofParts } from "./burnClaim.js";
4
- export { assembleBurnClaimProof, parseConsoleWalletBurnProof } from "./burnClaim.js";
3
+ export type { BurnClaimProofContents, BurnOutputProof, L1BurnProofParts, L1MmrInclusionProof } from "./burnClaim.js";
4
+ export { assembleBurnClaimProof, claimProofFromL1, decodeBurnOutputFeatures, parseConsoleWalletBurnProof } from "./burnClaim.js";
5
5
  export type { ConfidentialSumResult, ScannedStealthOutput } from "./confidential.js";
6
6
  export { scanTransactionsForOwnedOutputs, sumConfidentialCommitments } from "./confidential.js";
7
7
  export type { DerivedAccountKeys } from "./derivation.js";
@@ -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,eAAe,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACrH,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAEjI,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
@@ -1,15 +1,16 @@
1
1
  // Public surface. Import from the package root ("@chironbuilder/ootle-sdk") rather than reaching
2
2
  // into individual files -- the module layout underneath is free to change; this file is not.
3
3
  export { ACCOUNT_TEMPLATE_ADDRESS, componentAddressFromWalletAddress, deriveAccountComponentAddress, deriveComponentAddress, } from "./componentAddress.js";
4
- export { assembleBurnClaimProof, parseConsoleWalletBurnProof } from "./burnClaim.js";
4
+ export { assembleBurnClaimProof, claimProofFromL1, decodeBurnOutputFeatures, parseConsoleWalletBurnProof } from "./burnClaim.js";
5
5
  export { scanTransactionsForOwnedOutputs, sumConfidentialCommitments } from "./confidential.js";
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. */
@@ -1 +1 @@
1
- {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IAC5C,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAID;0EAC0E;AAC1E,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAEhE;AAcD,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,KAAK,EAAE,OAAO,CAAC;IACf,2FAA2F;IAC3F,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,sFAAsF;IACtF,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,yEAAyE;IACzE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAiBD,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AA0BD;;;;;;;cAOc;AACd,wBAAgB,UAAU,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAgB9D;AAED,wBAAsB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAE5F;AAED,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAInF;AAED,wBAAsB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASlG;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAEnE;AAED,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAM5E;AAED,wBAAsB,mBAAmB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK9E;AAED,wBAAsB,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAE3F;AAED,wBAAsB,2BAA2B,CAAC,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKzG;AAED;yBACyB;AACzB,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAExE;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAInF;AAED;;mFAEmF;AACnF,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAIpD"}
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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"}
package/dist/storage.js CHANGED
@@ -28,6 +28,8 @@ const DEFAULTS = {
28
28
  pendingShields: [],
29
29
  privatePaymentScanCursors: {},
30
30
  knownVersions: {},
31
+ htlcJournal: [],
32
+ reservations: [],
31
33
  };
32
34
  export function localAccountId(index) {
33
35
  return `local:${index}`;
@@ -79,9 +81,14 @@ export function serialized(fn) {
79
81
  export async function listShieldedOutputs(accountId) {
80
82
  return (await read()).shieldedOutputs.filter((r) => r.accountId === accountId);
81
83
  }
84
+ /** Idempotent: a commitment already recorded for the same account is left as it is, so a
85
+ * reconciliation that re-applies a finished operation's bookkeeping can never double-count it. */
82
86
  export async function addShieldedOutput(record) {
83
87
  await serialized(async () => {
84
- write({ shieldedOutputs: [...(await read()).shieldedOutputs, record] });
88
+ const existing = (await read()).shieldedOutputs;
89
+ if (existing.some((r) => r.accountId === record.accountId && r.commitment === record.commitment))
90
+ return;
91
+ await write({ shieldedOutputs: [...existing, record] });
85
92
  });
86
93
  }
87
94
  export async function markShieldedOutputSpent(accountId, commitment) {
@@ -128,6 +135,83 @@ export async function setKnownVersions(known) {
128
135
  await write({ knownVersions: known });
129
136
  });
130
137
  }
138
+ // ── HTLC journal ─────────────────────────────────────────────────────────────────────────────
139
+ export async function listHtlcJournal(accountId) {
140
+ const all = (await read()).htlcJournal;
141
+ return accountId ? all.filter((e) => e.accountId === accountId) : all;
142
+ }
143
+ export async function getHtlcJournalEntry(id) {
144
+ return (await read()).htlcJournal.find((e) => e.id === id);
145
+ }
146
+ /** Inserts or replaces (by id) — idempotent, so retrying the same write is always safe. */
147
+ export async function putHtlcJournalEntry(entry) {
148
+ await serialized(async () => {
149
+ const state = await read();
150
+ await write({ htlcJournal: [...state.htlcJournal.filter((e) => e.id !== entry.id), entry] });
151
+ });
152
+ }
153
+ /** Applies `patch` to an existing entry and bumps `updatedAt`. A missing id is a no-op. */
154
+ export async function updateHtlcJournalEntry(id, patch) {
155
+ return serialized(async () => {
156
+ const state = await read();
157
+ const current = state.htlcJournal.find((e) => e.id === id);
158
+ if (!current)
159
+ return undefined;
160
+ const next = { ...current, ...patch, id, updatedAt: Date.now() };
161
+ await write({ htlcJournal: state.htlcJournal.map((e) => (e.id === id ? next : e)) });
162
+ return next;
163
+ });
164
+ }
165
+ // ── commitment reservations ──────────────────────────────────────────────────────────────────
166
+ /** Default lifetime of a reservation: long enough for any single operation (prepare + dry run +
167
+ * submit + confirmation), short enough that a crashed one frees its UTXOs within minutes. */
168
+ export const RESERVATION_TTL_MS = 10 * 60_000;
169
+ /** Commitments currently reserved for `accountId` by anyone other than `exceptHolder`. */
170
+ export async function listReservedCommitments(accountId, exceptHolder, now = Date.now()) {
171
+ return new Set((await read()).reservations
172
+ .filter((r) => r.accountId === accountId && r.expiresAt > now && r.holder !== exceptHolder)
173
+ .map((r) => r.commitment));
174
+ }
175
+ /**
176
+ * Atomically reserves every commitment in `commitments` for `holder`, or none of them: if any is
177
+ * already held (unexpired) by a different holder, throws `CommitmentReservedError` naming them and
178
+ * writes nothing. Re-reserving one's own commitments just extends the lease. Expired leases are
179
+ * swept on every call.
180
+ */
181
+ export async function reserveCommitments(accountId, commitments, holder, ttlMs = RESERVATION_TTL_MS) {
182
+ if (commitments.length === 0)
183
+ return;
184
+ await serialized(async () => {
185
+ const now = Date.now();
186
+ const state = await read();
187
+ const live = state.reservations.filter((r) => r.expiresAt > now);
188
+ const taken = commitments.filter((c) => live.some((r) => r.accountId === accountId && r.commitment === c && r.holder !== holder));
189
+ if (taken.length > 0)
190
+ throw new CommitmentReservedError(taken);
191
+ const wanted = new Set(commitments);
192
+ const kept = live.filter((r) => !(r.accountId === accountId && wanted.has(r.commitment)));
193
+ const expiresAt = now + ttlMs;
194
+ await write({ reservations: [...kept, ...commitments.map((commitment) => ({ accountId, commitment, holder, expiresAt }))] });
195
+ });
196
+ }
197
+ /** Releases every reservation `holder` holds (all accounts). Safe to call more than once. */
198
+ export async function releaseReservations(holder) {
199
+ await serialized(async () => {
200
+ const now = Date.now();
201
+ const state = await read();
202
+ await write({ reservations: state.reservations.filter((r) => r.holder !== holder && r.expiresAt > now) });
203
+ });
204
+ }
205
+ export class CommitmentReservedError extends Error {
206
+ commitments;
207
+ constructor(commitments) {
208
+ super(`${commitments.length === 1 ? "A private output this operation needs is" : "Private outputs this operation needs are"} ` +
209
+ `already in use by another operation in progress (${commitments.map((c) => c.slice(0, 12)).join(", ")}). ` +
210
+ "Wait for it to finish, then try again.");
211
+ this.commitments = commitments;
212
+ this.name = "CommitmentReservedError";
213
+ }
214
+ }
131
215
  /** Clears every key this module owns -- call this from a host's own "erase wallet" flow. Does not
132
216
  * touch anything else the host's `KeyValueStore` might hold (address books, settings, ...); this
133
217
  * SDK never wrote those keys, so it isn't this function's place to remove them. */