@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.
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
@@ -7,8 +7,8 @@ export interface BurnClaimProofContents {
7
7
  }
8
8
  /**
9
9
  * The claim material an L1 wallet has as soon as it builds a burn (see tari_l1_wasm's
10
- * `WasmSignedBurn`). Everything except the kernel merkle proof, which only exists once the burn
11
- * is mined.
10
+ * `WasmSignedBurn`). Everything except the output's inclusion proof, which only exists once the
11
+ * burn is mined.
12
12
  */
13
13
  export interface L1BurnProofParts {
14
14
  /** The Ootle account public key `P` the burn was addressed to. */
@@ -19,29 +19,71 @@ export interface L1BurnProofParts {
19
19
  senderOffsetPublicKeyHex: string;
20
20
  encryptedDataHex: string;
21
21
  amount: bigint;
22
- kernel: {
23
- version: number;
24
- fee: bigint;
25
- lockHeight: bigint;
26
- excessHex: string;
27
- nonceHex: string;
28
- signatureHex: string;
29
- };
22
+ /** No longer part of a claim (tari-ootle#2709); kept so records written before it still load. */
23
+ kernel?: unknown;
30
24
  }
31
- /** A base node's `/generate_kernel_merkle_proof` response, passed through unchanged. */
32
- export interface KernelMerkleProof {
33
- /** base64 */
34
- block_hash: string;
35
- /** base64 */
36
- encoded_merkle_proof: string;
25
+ /** An L1 MMR inclusion proof, as a base node serialises it (hashes are hex). */
26
+ export interface L1MmrInclusionProof {
37
27
  leaf_index: number | string;
38
- block_height?: number | null;
28
+ mmr_size: number | string;
29
+ path: string[];
30
+ peaks: string[];
39
31
  }
40
- /** Joins an L1 burn's claim material with its kernel merkle proof into a claimable proof. */
41
- export declare function assembleBurnClaimProof(parts: L1BurnProofParts, merkle: KernelMerkleProof): BurnClaimProofContents;
32
+ /**
33
+ * A base node's `BurnOutputProof` (the `proof` of a `/generate_burn_output_proof` response, or
34
+ * `claim_proof.output_proof` of a `minotari_console_wallet` proof file). Byte fields are hex.
35
+ */
36
+ export interface BurnOutputProof {
37
+ block_hash: string;
38
+ block_height?: number | string;
39
+ output: {
40
+ version: number;
41
+ /** Consensus (borsh) encoded `OutputFeatures` */
42
+ features: string;
43
+ commitment: string;
44
+ rangeproof_hash: string;
45
+ script: string;
46
+ sender_offset_public_key: string;
47
+ metadata_signature: {
48
+ ephemeral_commitment: string;
49
+ ephemeral_pubkey: string;
50
+ u_a: string;
51
+ u_x: string;
52
+ u_y: string;
53
+ };
54
+ covenant: string;
55
+ encrypted_data: string;
56
+ minimum_value_promise: number | string;
57
+ };
58
+ normal_output_proof: L1MmrInclusionProof;
59
+ normal_output_mr: string;
60
+ block_output_proof: L1MmrInclusionProof;
61
+ }
62
+ type BurnOutputFeatures = MinotariBurnClaimProof["output"]["features"];
63
+ /**
64
+ * Decodes a burn output's consensus-encoded `OutputFeatures` into the fields a claim carries,
65
+ * refusing anything Ootle can't mint against (not a burn, or no confidential-output sidechain
66
+ * feature naming a claimant) -- the same checks as `claim_proof_from_l1`.
67
+ */
68
+ export declare function decodeBurnOutputFeatures(featuresHex: string): BurnOutputFeatures;
69
+ /**
70
+ * Converts a base node's `BurnOutputProof` plus the burn's ownership proof and value into the
71
+ * claim Ootle verifies (`claim_proof_from_l1`).
72
+ */
73
+ export declare function claimProofFromL1(outputProof: BurnOutputProof, ownershipProof: {
74
+ public_nonce: string;
75
+ signature: string;
76
+ }, value: bigint | string | number): MinotariBurnClaimProof;
77
+ /**
78
+ * Joins an L1 burn's claim material with its mined output's inclusion proof (a base node's
79
+ * `/generate_burn_output_proof` `proof`) into a claimable proof.
80
+ */
81
+ export declare function assembleBurnClaimProof(parts: L1BurnProofParts, outputProof: BurnOutputProof): BurnClaimProofContents;
42
82
  /**
43
83
  * Reads a proof file written by `minotari_console_wallet` into its `burn_proofs` directory
44
- * (`CompleteClaimBurnProof`). Its layout matches Ootle's except that `encrypted_data` is base64.
84
+ * (`CompleteClaimBurnProof`: `claim_proof` is a Minotari `BurnClaimProof` carrying the
85
+ * `output_proof`), converting it to Ootle's claim. A file already in Ootle's shape passes through.
45
86
  */
46
87
  export declare function parseConsoleWalletBurnProof(json: string | Record<string, unknown>): BurnClaimProofContents;
88
+ export {};
47
89
  //# sourceMappingURL=burnClaim.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"burnClaim.d.ts","sourceRoot":"","sources":["../src/burnClaim.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AAE9E,gGAAgG;AAChG,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,sBAAsB,CAAC;IACpC,mDAAmD;IACnD,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,wBAAwB,EAAE,MAAM,CAAC;IACjC,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE;QACN,OAAO,EAAE,MAAM,CAAC;QAChB,GAAG,EAAE,MAAM,CAAC;QACZ,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;CACH;AAED,wFAAwF;AACxF,MAAM,WAAW,iBAAiB;IAChC,aAAa;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa;IACb,oBAAoB,EAAE,MAAM,CAAC;IAC7B,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAUD,6FAA6F;AAC7F,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,gBAAgB,EAAE,MAAM,EAAE,iBAAiB,GAAG,sBAAsB,CAgCjH;AASD;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,sBAAsB,CAc1G"}
1
+ {"version":3,"file":"burnClaim.d.ts","sourceRoot":"","sources":["../src/burnClaim.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AAE9E,gGAAgG;AAChG,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,sBAAsB,CAAC;IACpC,mDAAmD;IACnD,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,wBAAwB,EAAE,MAAM,CAAC;IACjC,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,iGAAiG;IACjG,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,MAAM,EAAE;QACN,OAAO,EAAE,MAAM,CAAC;QAChB,iDAAiD;QACjD,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,eAAe,EAAE,MAAM,CAAC;QACxB,MAAM,EAAE,MAAM,CAAC;QACf,wBAAwB,EAAE,MAAM,CAAC;QACjC,kBAAkB,EAAE;YAClB,oBAAoB,EAAE,MAAM,CAAC;YAC7B,gBAAgB,EAAE,MAAM,CAAC;YACzB,GAAG,EAAE,MAAM,CAAC;YACZ,GAAG,EAAE,MAAM,CAAC;YACZ,GAAG,EAAE,MAAM,CAAC;SACb,CAAC;QACF,QAAQ,EAAE,MAAM,CAAC;QACjB,cAAc,EAAE,MAAM,CAAC;QACvB,qBAAqB,EAAE,MAAM,GAAG,MAAM,CAAC;KACxC,CAAC;IACF,mBAAmB,EAAE,mBAAmB,CAAC;IACzC,gBAAgB,EAAE,MAAM,CAAC;IACzB,kBAAkB,EAAE,mBAAmB,CAAC;CACzC;AAgFD,KAAK,kBAAkB,GAAG,sBAAsB,CAAC,QAAQ,CAAC,CAAC,UAAU,CAAC,CAAC;AAEvE;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,kBAAkB,CA6BhF;AA6BD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,eAAe,EAC5B,cAAc,EAAE;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,EAC3D,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAC9B,sBAAsB,CA2BxB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,eAAe,GAAG,sBAAsB,CAWpH;AASD;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,sBAAsB,CA2B1G"}
package/dist/burnClaim.js CHANGED
@@ -1,7 +1,12 @@
1
1
  // Claim proofs for Layer 1 (minotari) burns, in the exact serde shape Ootle's `ClaimBurn`
2
- // instruction deserialises (`tari_engine_types::confidential::MinotariBurnClaimProof`): byte fields
3
- // are hex, the merkle proof's two byte fields are base64, and every u64 is carried as a decimal
4
- // string -- a burn value can exceed 2^53, and the engine accepts strings for all of them.
2
+ // instruction deserialises (`tari_engine_types::confidential::MinotariBurnClaimProof`).
3
+ //
4
+ // Since tari-ootle#2709 (Ootle 0.42, Minotari 6.0.1-pre.1) a claim proves the L1 burn *output*
5
+ // rather than its kernel: the output's hashed fields plus a two-level MMR inclusion proof (output
6
+ // hash -> normal output MMR root -> last leaf of the block's `block_output_mr`). A base node serves
7
+ // that proof as a Minotari `BurnOutputProof` (`/generate_burn_output_proof?commitment=<hex>`); this
8
+ // module converts it the way `claim_proof_from_l1` (tari_ootle_app_utilities) does, and
9
+ // `burnClaim.test.ts` checks the conversion against vectors produced by that Rust function.
5
10
  const HEX_32 = /^[0-9a-f]{64}$/;
6
11
  function hex32(value, field) {
7
12
  const v = value.toLowerCase();
@@ -9,40 +14,192 @@ function hex32(value, field) {
9
14
  throw new Error(`${field} must be 32 bytes of hex`);
10
15
  return v;
11
16
  }
12
- /** Joins an L1 burn's claim material with its kernel merkle proof into a claimable proof. */
13
- export function assembleBurnClaimProof(parts, merkle) {
14
- if (!/^[0-9a-f]+$/i.test(parts.encryptedDataHex) || parts.encryptedDataHex.length % 2 !== 0) {
15
- throw new Error("encryptedDataHex must be hex");
17
+ function hexToBytes(hex, field) {
18
+ if (!/^[0-9a-f]*$/i.test(hex) || hex.length % 2 !== 0)
19
+ throw new Error(`${field} must be hex`);
20
+ const out = new Uint8Array(hex.length / 2);
21
+ for (let i = 0; i < out.length; i++)
22
+ out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
23
+ return out;
24
+ }
25
+ function bytesToHex(bytes) {
26
+ let out = "";
27
+ for (const b of bytes)
28
+ out += b.toString(16).padStart(2, "0");
29
+ return out;
30
+ }
31
+ function bytesToBase64(bytes) {
32
+ let bin = "";
33
+ for (const b of bytes)
34
+ bin += String.fromCharCode(b);
35
+ return btoa(bin);
36
+ }
37
+ /** Reads the L1 consensus (borsh) encoding. Every read is bounds-checked. */
38
+ class BorshReader {
39
+ buf;
40
+ what;
41
+ pos = 0;
42
+ constructor(buf, what) {
43
+ this.buf = buf;
44
+ this.what = what;
45
+ }
46
+ take(n) {
47
+ if (this.pos + n > this.buf.length)
48
+ throw new Error(`${this.what}: truncated`);
49
+ const out = this.buf.subarray(this.pos, this.pos + n);
50
+ this.pos += n;
51
+ return out;
52
+ }
53
+ u8() {
54
+ return this.take(1)[0];
55
+ }
56
+ u32() {
57
+ const b = this.take(4);
58
+ return (b[0] | (b[1] << 8) | (b[2] << 16)) + b[3] * 0x1000000;
59
+ }
60
+ u64() {
61
+ const b = this.take(8);
62
+ let v = 0n;
63
+ for (let i = 7; i >= 0; i--)
64
+ v = (v << 8n) | BigInt(b[i]);
65
+ return v;
66
+ }
67
+ /** A `Vec<u8>` / `&[u8]`: u32 length, then the bytes. */
68
+ bytes(max) {
69
+ const n = this.u32();
70
+ if (n > max)
71
+ throw new Error(`${this.what}: byte string of ${n} exceeds ${max}`);
72
+ return this.take(n);
73
+ }
74
+ /** A compressed key or scalar, which L1 encodes as a 32-byte `&[u8]`. */
75
+ key(field) {
76
+ const b = this.bytes(32);
77
+ if (b.length !== 32)
78
+ throw new Error(`${this.what}: ${field} is ${b.length} bytes, expected 32`);
79
+ return bytesToHex(b);
80
+ }
81
+ option() {
82
+ const tag = this.u8();
83
+ if (tag > 1)
84
+ throw new Error(`${this.what}: invalid Option tag ${tag}`);
85
+ return tag === 1;
86
+ }
87
+ end() {
88
+ if (this.pos !== this.buf.length)
89
+ throw new Error(`${this.what}: ${this.buf.length - this.pos} trailing bytes`);
90
+ }
91
+ }
92
+ const OUTPUT_TYPE_BURN = 2;
93
+ /** `SideChainFeatureData::ConfidentialOutput` */
94
+ const SIDECHAIN_CONFIDENTIAL_OUTPUT = 2;
95
+ /** `CoinBaseExtra = MaxSizeBytes<258>` */
96
+ const COINBASE_EXTRA_MAX = 258;
97
+ /**
98
+ * Decodes a burn output's consensus-encoded `OutputFeatures` into the fields a claim carries,
99
+ * refusing anything Ootle can't mint against (not a burn, or no confidential-output sidechain
100
+ * feature naming a claimant) -- the same checks as `claim_proof_from_l1`.
101
+ */
102
+ export function decodeBurnOutputFeatures(featuresHex) {
103
+ const r = new BorshReader(hexToBytes(featuresHex, "burn output features"), "burn output features");
104
+ const version = r.u8();
105
+ const outputType = r.u8();
106
+ const maturity = r.u64();
107
+ r.bytes(COINBASE_EXTRA_MAX);
108
+ if (outputType !== OUTPUT_TYPE_BURN)
109
+ throw new Error(`the output is not a burn (output type ${outputType})`);
110
+ if (!r.option())
111
+ throw new Error("the burn output has no sidechain feature, so it names no claimant");
112
+ const variant = r.u8();
113
+ if (variant !== SIDECHAIN_CONFIDENTIAL_OUTPUT) {
114
+ throw new Error(`the burn output's sidechain feature is not a confidential output (variant ${variant})`);
115
+ }
116
+ const claimPublicKey = r.key("claim_public_key");
117
+ let sidechainId = null;
118
+ if (r.option()) {
119
+ const publicKey = r.key("sidechain public_key");
120
+ const nonce = r.key("sidechain knowledge_proof.public_nonce");
121
+ const signature = r.key("sidechain knowledge_proof.signature");
122
+ sidechainId = { public_key: publicKey, knowledge_proof: { public_nonce: nonce, signature } };
16
123
  }
124
+ const rangeProofType = r.u8();
125
+ r.end();
17
126
  return {
18
- claim_proof: {
19
- burn_public_key: hex32(parts.claimPublicKeyHex, "claimPublicKeyHex"),
20
- commitment: hex32(parts.commitmentHex, "commitmentHex"),
21
- ownership_proof: {
22
- public_nonce: hex32(parts.ownershipNonceHex, "ownershipNonceHex"),
23
- signature: hex32(parts.ownershipSignatureHex, "ownershipSignatureHex"),
24
- },
25
- encoded_merkle_proof: {
26
- block_hash: merkle.block_hash,
27
- encoded_merkle_proof: merkle.encoded_merkle_proof,
28
- leaf_index: String(merkle.leaf_index),
29
- },
30
- kernel: {
31
- version: parts.kernel.version,
32
- fee: parts.kernel.fee.toString(),
33
- lock_height: parts.kernel.lockHeight.toString(),
34
- excess: hex32(parts.kernel.excessHex, "kernel.excessHex"),
35
- excess_sig: {
36
- public_nonce: hex32(parts.kernel.nonceHex, "kernel.nonceHex"),
37
- signature: hex32(parts.kernel.signatureHex, "kernel.signatureHex"),
38
- },
39
- },
40
- value: parts.amount.toString(),
41
- sender_offset_public_key: hex32(parts.senderOffsetPublicKeyHex, "senderOffsetPublicKeyHex"),
127
+ version,
128
+ maturity: maturity.toString(),
129
+ claim_public_key: claimPublicKey,
130
+ sidechain_id: sidechainId,
131
+ range_proof_type: rangeProofType,
132
+ };
133
+ }
134
+ /** The L1 consensus (borsh) encoding of a `ComAndPubSignature`: five 32-byte fields, each length-prefixed. */
135
+ function encodeMetadataSignature(sig) {
136
+ const fields = [sig.ephemeral_commitment, sig.ephemeral_pubkey, sig.u_a, sig.u_x, sig.u_y];
137
+ const out = new Uint8Array(fields.length * 36);
138
+ fields.forEach((f, i) => {
139
+ out.set([32, 0, 0, 0], i * 36);
140
+ out.set(hexToBytes(hex32(f, "metadata_signature field"), "metadata_signature field"), i * 36 + 4);
141
+ });
142
+ return out;
143
+ }
144
+ function mmrProof(p) {
145
+ return {
146
+ leaf_index: String(p.leaf_index),
147
+ mmr_size: String(p.mmr_size),
148
+ path: p.path.map((h) => hex32(h, "MMR path hash")),
149
+ peaks: p.peaks.map((h) => hex32(h, "MMR peak hash")),
150
+ };
151
+ }
152
+ /** A `BoundedVec<u8, 1, N>` field: non-empty, at most `max` bytes, serialised as base64. */
153
+ function opaque(hex, field, max) {
154
+ const b = hexToBytes(hex, field);
155
+ if (b.length < 1 || b.length > max)
156
+ throw new Error(`burn output ${field} length ${b.length} is out of range`);
157
+ return bytesToBase64(b);
158
+ }
159
+ /**
160
+ * Converts a base node's `BurnOutputProof` plus the burn's ownership proof and value into the
161
+ * claim Ootle verifies (`claim_proof_from_l1`).
162
+ */
163
+ export function claimProofFromL1(outputProof, ownershipProof, value) {
164
+ const o = outputProof.output;
165
+ return {
166
+ commitment: hex32(o.commitment, "commitment"),
167
+ ownership_proof: {
168
+ public_nonce: hex32(ownershipProof.public_nonce, "ownership_proof.public_nonce"),
169
+ signature: hex32(ownershipProof.signature, "ownership_proof.signature"),
170
+ },
171
+ value: BigInt(value).toString(),
172
+ output: {
173
+ version: o.version,
174
+ features: decodeBurnOutputFeatures(o.features),
175
+ rangeproof_hash: hex32(o.rangeproof_hash, "rangeproof_hash"),
176
+ script: opaque(o.script, "script", 4096),
177
+ sender_offset_public_key: hex32(o.sender_offset_public_key, "sender_offset_public_key"),
178
+ metadata_signature: bytesToBase64(encodeMetadataSignature(o.metadata_signature)),
179
+ covenant: opaque(o.covenant, "covenant", 4096),
180
+ encrypted_data: opaque(o.encrypted_data, "encrypted data", 4096),
181
+ minimum_value_promise: BigInt(o.minimum_value_promise).toString(),
182
+ },
183
+ inclusion_proof: {
184
+ block_hash: hex32(outputProof.block_hash, "block_hash"),
185
+ normal_output_proof: mmrProof(outputProof.normal_output_proof),
186
+ normal_output_mr: hex32(outputProof.normal_output_mr, "normal_output_mr"),
187
+ block_output_proof: mmrProof(outputProof.block_output_proof),
42
188
  },
43
- encrypted_data: parts.encryptedDataHex.toLowerCase(),
44
189
  };
45
190
  }
191
+ /**
192
+ * Joins an L1 burn's claim material with its mined output's inclusion proof (a base node's
193
+ * `/generate_burn_output_proof` `proof`) into a claimable proof.
194
+ */
195
+ export function assembleBurnClaimProof(parts, outputProof) {
196
+ const commitment = hex32(parts.commitmentHex, "commitmentHex");
197
+ if (outputProof.output.commitment.toLowerCase() !== commitment) {
198
+ throw new Error("The base node's burn output proof is for a different commitment than this burn");
199
+ }
200
+ const claim = claimProofFromL1(outputProof, { public_nonce: parts.ownershipNonceHex, signature: parts.ownershipSignatureHex }, parts.amount);
201
+ return { claim_proof: claim, encrypted_data: bytesToHex(hexToBytes(parts.encryptedDataHex, "encryptedDataHex")) };
202
+ }
46
203
  function base64ToHex(b64) {
47
204
  const bin = atob(b64);
48
205
  let out = "";
@@ -52,7 +209,8 @@ function base64ToHex(b64) {
52
209
  }
53
210
  /**
54
211
  * Reads a proof file written by `minotari_console_wallet` into its `burn_proofs` directory
55
- * (`CompleteClaimBurnProof`). Its layout matches Ootle's except that `encrypted_data` is base64.
212
+ * (`CompleteClaimBurnProof`: `claim_proof` is a Minotari `BurnClaimProof` carrying the
213
+ * `output_proof`), converting it to Ootle's claim. A file already in Ootle's shape passes through.
56
214
  */
57
215
  export function parseConsoleWalletBurnProof(json) {
58
216
  const file = (typeof json === "string" ? JSON.parse(json) : json);
@@ -64,5 +222,14 @@ export function parseConsoleWalletBurnProof(json) {
64
222
  : /^[0-9a-f]+$/i.test(file.encrypted_data) && file.encrypted_data.length % 2 === 0
65
223
  ? file.encrypted_data.toLowerCase()
66
224
  : base64ToHex(file.encrypted_data);
67
- return { claim_proof: file.claim_proof, encrypted_data: encrypted };
225
+ const cp = file.claim_proof;
226
+ if (cp.output_proof) {
227
+ if (!cp.ownership_proof || cp.value === undefined)
228
+ throw new Error("Burn proof is missing its ownership proof or value");
229
+ return { claim_proof: claimProofFromL1(cp.output_proof, cp.ownership_proof, cp.value), encrypted_data: encrypted };
230
+ }
231
+ if (cp.inclusion_proof && cp.output)
232
+ return { claim_proof: cp, encrypted_data: encrypted };
233
+ throw new Error("This burn proof predates Ootle 0.42 (it proves the burn kernel, not the burn output). Regenerate it with a " +
234
+ "Minotari 6.0.1+ wallet, or claim the burn from its commitment.");
68
235
  }
@@ -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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"}