@buildonspark/spark-sdk 0.3.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/android/build.gradle +1 -1
  3. package/android/src/main/java/uniffi/uniffi/spark_frost/spark_frost.kt +1361 -1367
  4. package/android/src/main/jniLibs/arm64-v8a/libuniffi_spark_frost.so +0 -0
  5. package/android/src/main/jniLibs/armeabi-v7a/libuniffi_spark_frost.so +0 -0
  6. package/android/src/main/jniLibs/x86/libuniffi_spark_frost.so +0 -0
  7. package/android/src/main/jniLibs/x86_64/libuniffi_spark_frost.so +0 -0
  8. package/dist/bare/index.cjs +322 -142
  9. package/dist/bare/index.d.cts +13 -3
  10. package/dist/bare/index.d.ts +13 -3
  11. package/dist/bare/index.js +321 -142
  12. package/dist/{chunk-S55NZT4P.js → chunk-27ILUWDJ.js} +1 -1
  13. package/dist/{chunk-O4C4HGQL.js → chunk-G3LHXHF3.js} +313 -133
  14. package/dist/{chunk-WRE2T22S.js → chunk-LOXWCMZL.js} +1 -1
  15. package/dist/{chunk-MFCM6GUD.js → chunk-WICAF6BS.js} +1 -1
  16. package/dist/debug.cjs +322 -143
  17. package/dist/debug.d.cts +5 -4
  18. package/dist/debug.d.ts +5 -4
  19. package/dist/debug.js +2 -2
  20. package/dist/index.cjs +323 -143
  21. package/dist/index.d.cts +4 -4
  22. package/dist/index.d.ts +4 -4
  23. package/dist/index.js +5 -3
  24. package/dist/index.node.cjs +323 -143
  25. package/dist/index.node.d.cts +4 -4
  26. package/dist/index.node.d.ts +4 -4
  27. package/dist/index.node.js +4 -2
  28. package/dist/{logging-DDeMLsVN.d.ts → logging-BNGm6dBp.d.ts} +3 -2
  29. package/dist/{logging-CXhvuqJJ.d.cts → logging-D3IfXfHG.d.cts} +3 -2
  30. package/dist/native/index.react-native.cjs +466 -145
  31. package/dist/native/index.react-native.d.cts +21 -3
  32. package/dist/native/index.react-native.d.ts +21 -3
  33. package/dist/native/index.react-native.js +462 -144
  34. package/dist/{spark-wallet.browser-Cz8c4kOW.d.ts → spark-wallet.browser-B2rGwjuM.d.ts} +1 -1
  35. package/dist/{spark-wallet.browser-CbYo8A_U.d.cts → spark-wallet.browser-Ck9No4Ks.d.cts} +1 -1
  36. package/dist/{spark-wallet.node-CmIvxtcC.d.ts → spark-wallet.node-BqmKsGPs.d.ts} +1 -1
  37. package/dist/{spark-wallet.node-4WQgWwB2.d.cts → spark-wallet.node-C2TIkyt4.d.cts} +1 -1
  38. package/dist/tests/test-utils.cjs +321 -143
  39. package/dist/tests/test-utils.d.cts +16 -2
  40. package/dist/tests/test-utils.d.ts +16 -2
  41. package/dist/tests/test-utils.js +4 -4
  42. package/dist/{token-transactions-CV8QD3I7.d.cts → token-transactions-Db8mkjnU.d.cts} +1 -1
  43. package/dist/{token-transactions-Bu023ztN.d.ts → token-transactions-DoMcrxXQ.d.ts} +1 -1
  44. package/dist/{wallet-config-Bmk2eAn8.d.ts → wallet-config-Bg3kWltL.d.ts} +11 -2
  45. package/dist/{wallet-config-DQw5llqA.d.cts → wallet-config-CuZKNo9S.d.cts} +11 -2
  46. package/ios/spark_frostFFI.xcframework/ios-arm64/SparkFrost +0 -0
  47. package/ios/spark_frostFFI.xcframework/ios-arm64/spark_frostFFI.framework/spark_frostFFI +0 -0
  48. package/ios/spark_frostFFI.xcframework/ios-arm64_x86_64-simulator/SparkFrost +0 -0
  49. package/ios/spark_frostFFI.xcframework/ios-arm64_x86_64-simulator/spark_frostFFI.framework/spark_frostFFI +0 -0
  50. package/ios/spark_frostFFI.xcframework/macos-arm64_x86_64/spark_frostFFI.framework/spark_frostFFI +0 -0
  51. package/package.json +1 -1
  52. package/src/index.react-native.ts +8 -2
  53. package/src/services/config.ts +5 -0
  54. package/src/services/wallet-config.ts +10 -0
  55. package/src/services/xhr-transport.ts +13 -3
  56. package/src/signer/signer.react-native.ts +73 -1
  57. package/src/spark-wallet/spark-wallet.ts +98 -76
  58. package/src/tests/integration/lightning.test.ts +0 -28
  59. package/src/tests/integration/static_deposit.test.ts +4 -8
  60. package/src/tests/integration/unilateral-exit.test.ts +117 -0
  61. package/src/tests/optimize.test.ts +31 -1
  62. package/src/tests/utils/signing.ts +33 -0
  63. package/src/tests/utils/test-faucet.ts +61 -0
  64. package/src/utils/optimize.ts +42 -0
  65. package/src/utils/unilateral-exit.ts +1 -40
@@ -443,20 +443,6 @@ describe.each(walletTypes)(
443
443
  for (const leaf of transfer.leaves) {
444
444
  const cpfpRefund = getTxFromRawTxBytes(leaf.intermediateRefundTx);
445
445
  expectedValue += cpfpRefund.getOutput(0)?.amount || 0n;
446
-
447
- if ((leaf.intermediateDirectRefundTx.length || 0) > 0) {
448
- const directRefund = getTxFromRawTxBytes(
449
- leaf.intermediateDirectRefundTx,
450
- );
451
- expectedValue += directRefund.getOutput(0)?.amount || 0n;
452
- }
453
-
454
- if ((leaf.intermediateDirectFromCpfpRefundTx.length || 0) > 0) {
455
- const directFromCpfpRefund = getTxFromRawTxBytes(
456
- leaf.intermediateDirectFromCpfpRefundTx,
457
- );
458
- expectedValue += directFromCpfpRefund.getOutput(0)?.amount || 0n;
459
- }
460
446
  }
461
447
 
462
448
  let totalValue = 0n;
@@ -569,20 +555,6 @@ describe.each(walletTypes)(
569
555
  for (const leaf of transfer!.leaves) {
570
556
  const cpfpRefund = getTxFromRawTxBytes(leaf.intermediateRefundTx);
571
557
  expectedValue += cpfpRefund.getOutput(0)?.amount || 0n;
572
-
573
- if (leaf.intermediateDirectRefundTx.length > 0) {
574
- const directRefund = getTxFromRawTxBytes(
575
- leaf.intermediateDirectRefundTx,
576
- );
577
- expectedValue += directRefund.getOutput(0)?.amount || 0n;
578
- }
579
-
580
- if (leaf.intermediateDirectFromCpfpRefundTx.length > 0) {
581
- const directFromCpfpRefund = getTxFromRawTxBytes(
582
- leaf.intermediateDirectFromCpfpRefundTx,
583
- );
584
- expectedValue += directFromCpfpRefund.getOutput(0)?.amount || 0n;
585
- }
586
558
  }
587
559
 
588
560
  let totalValue = 0n;
@@ -17,7 +17,7 @@ describe("SSP static deposit address integration", () => {
17
17
  } = await initTestingWallet(DEPOSIT_AMOUNT, "LOCAL");
18
18
 
19
19
  // Wait for the transaction to be mined
20
- await faucet.mineBlocks(6);
20
+ await faucet.mineBlocksAndWaitForMiningToComplete(6);
21
21
 
22
22
  const transactionId = signedTx.id;
23
23
 
@@ -30,11 +30,9 @@ describe("SSP static deposit address integration", () => {
30
30
  fee: 301,
31
31
  });
32
32
 
33
- await new Promise((resolve) => setTimeout(resolve, 10000));
34
-
35
33
  await faucet.broadcastTx(txHex);
36
34
 
37
- await faucet.mineBlocks(6);
35
+ await faucet.mineBlocksAndWaitForMiningToComplete(6);
38
36
 
39
37
  // Second refund attempt should fail
40
38
  console.log(
@@ -60,7 +58,7 @@ describe("SSP static deposit address integration", () => {
60
58
  } = await initTestingWallet(DEPOSIT_AMOUNT, "LOCAL");
61
59
 
62
60
  // Wait for the transaction to be mined
63
- await faucet.mineBlocks(6);
61
+ await faucet.mineBlocksAndWaitForMiningToComplete(6);
64
62
 
65
63
  const transactionId = signedTx.id;
66
64
 
@@ -74,8 +72,6 @@ describe("SSP static deposit address integration", () => {
74
72
  }),
75
73
  );
76
74
 
77
- await faucet.mineBlocks(6);
78
-
79
75
  expect(txId).toBeDefined();
80
76
  }, 600000);
81
77
 
@@ -91,7 +87,7 @@ describe("SSP static deposit address integration", () => {
91
87
  } = await initTestingWallet(DEPOSIT_AMOUNT, "LOCAL");
92
88
 
93
89
  // Wait for the transaction to be mined
94
- await faucet.mineBlocks(6);
90
+ await faucet.mineBlocksAndWaitForMiningToComplete(6);
95
91
 
96
92
  const transactionId = signedTx.id;
97
93
 
@@ -0,0 +1,117 @@
1
+ import { describe, expect, it } from "@jest/globals";
2
+ import { bytesToHex } from "@noble/hashes/utils";
3
+
4
+ import { RPCError } from "../../errors/types.js";
5
+ import { Network } from "../../utils/network.js";
6
+ import { SparkWalletTesting } from "../utils/spark-testing-wallet.js";
7
+ import { BitcoinFaucet } from "../utils/test-faucet.js";
8
+ import { waitForClaim } from "../utils/utils.js";
9
+ import {
10
+ constructUnilateralExitFeeBumpPackages,
11
+ hash160,
12
+ } from "../../utils/unilateral-exit.js";
13
+ import { signPsbtWithExternalKey } from "../utils/signing.js";
14
+ import { TreeNode } from "../../proto/spark.js";
15
+ import { WalletConfigService } from "../../services/config.js";
16
+ import { ConnectionManagerNodeJS } from "../../services/connection/connection.node.js";
17
+
18
+ describe("unilateral exit", () => {
19
+ it("should unilateral exit", async () => {
20
+ const faucet = BitcoinFaucet.getInstance();
21
+
22
+ const { wallet: userWallet } = await SparkWalletTesting.initialize({
23
+ options: {
24
+ network: "LOCAL",
25
+ },
26
+ });
27
+
28
+ const depositResp = await userWallet.getSingleUseDepositAddress();
29
+
30
+ if (!depositResp) {
31
+ throw new RPCError("Deposit address not found", {
32
+ method: "getDepositAddress",
33
+ });
34
+ }
35
+
36
+ const signedTx = await faucet.sendToAddress(depositResp, 100_000n);
37
+
38
+ await faucet.mineBlocks(6);
39
+
40
+ await userWallet.claimDeposit(signedTx.id);
41
+
42
+ await waitForClaim({ wallet: userWallet });
43
+
44
+ const leaves = await userWallet.getLeaves();
45
+ expect(leaves.length).toBe(1);
46
+
47
+ const leaf = leaves[0]!;
48
+
49
+ const encodedLeaf = TreeNode.encode(leaf).finish();
50
+ const hexString = bytesToHex(encodedLeaf);
51
+
52
+ const {
53
+ address: fundingWalletAddress,
54
+ key: fundingWalletKey,
55
+ pubKey: fundingWalletPubKey,
56
+ } = await faucet.getNewExternalWallet();
57
+
58
+ const fundingTx = await faucet.sendToAddress(fundingWalletAddress, 50_000n);
59
+
60
+ await faucet.mineBlocks(6);
61
+
62
+ const pubKeyHash = hash160(fundingWalletPubKey);
63
+ const p2wpkhScript = new Uint8Array([0x00, 0x14, ...pubKeyHash]);
64
+
65
+ const utxos = [
66
+ {
67
+ txid: fundingTx.id,
68
+ vout: 0,
69
+ value: 50_000n,
70
+ script: bytesToHex(p2wpkhScript),
71
+ publicKey: bytesToHex(fundingWalletPubKey),
72
+ },
73
+ ];
74
+
75
+ // Create a spark client to be used for signing fee bump transactions.
76
+ const configService = new WalletConfigService(
77
+ { network: "LOCAL" },
78
+ userWallet.getSigner(),
79
+ );
80
+ const connectionManager = new ConnectionManagerNodeJS(configService);
81
+ const sparkClient = await connectionManager.createSparkClient(
82
+ configService.getCoordinatorAddress(),
83
+ );
84
+
85
+ const constructedTx = await constructUnilateralExitFeeBumpPackages(
86
+ [hexString],
87
+ utxos,
88
+ { satPerVbyte: 5 },
89
+ "http://mempool.minikube.local/api",
90
+ sparkClient,
91
+ Network.LOCAL,
92
+ );
93
+
94
+ const txPackages = constructedTx[0]?.txPackages;
95
+
96
+ // Broadcast unilateral exit transactions in order
97
+ txPackages?.forEach(async (txPackage) => {
98
+ const startBlock = await faucet.getBlockCount();
99
+ const feeBumpPsbtSigned = await signPsbtWithExternalKey(
100
+ txPackage.feeBumpPsbt!,
101
+ bytesToHex(fundingWalletKey),
102
+ );
103
+ await faucet.submitPackage([txPackage.tx, feeBumpPsbtSigned]);
104
+
105
+ // Mine 1910 blocks to expire time lock.
106
+ await faucet.mineBlocks(1910);
107
+
108
+ // Since we do not depend on the chain watcher, we just need to wait for the blocks to be mined.
109
+ await faucet.waitForBlocksMined({
110
+ startBlock,
111
+ expectedIncrease: 1910,
112
+ });
113
+ });
114
+
115
+ await connectionManager.closeConnections();
116
+ }, 90000);
117
+ });
@@ -4,10 +4,12 @@ import {
4
4
  swapMinimizingLeaves,
5
5
  maximizeUnilateralExit,
6
6
  minimizeTransferSwap,
7
+ optimize,
7
8
  Swap,
9
+ shouldOptimize,
8
10
  } from "../utils/optimize.js";
9
11
 
10
- describe("keys", () => {
12
+ describe("optimize", () => {
11
13
  it("test greedyLeaves", () => {
12
14
  expect(greedyLeaves(0)).toEqual([]);
13
15
  expect(greedyLeaves(1)).toEqual([1]);
@@ -42,4 +44,32 @@ describe("keys", () => {
42
44
  new Swap([100], [1, 1, 2, 4, 4, 8, 16, 32, 32]),
43
45
  ]);
44
46
  });
47
+
48
+ it("test shouldOptimize for unilateral exit", () => {
49
+ expect(shouldOptimize([16], 0)).toEqual(false);
50
+ expect(shouldOptimize([16, 16], 0)).toEqual(false);
51
+ expect(shouldOptimize([16, 16, 16, 16, 16, 16, 16, 16], 0)).toEqual(true);
52
+ });
53
+
54
+ it("test shouldOptimize for swap minimization", () => {
55
+ expect(shouldOptimize([2], 1)).toEqual(false);
56
+ expect(shouldOptimize([64], 1)).toEqual(true);
57
+ });
58
+
59
+ it("test optimize for unilateral exit", () => {
60
+ expect(optimize([8], 0)).toEqual([]);
61
+ expect(optimize([16], 0)).toEqual([]);
62
+ expect(optimize([16, 16, 16, 16, 16, 16, 16, 16], 0)).toEqual([
63
+ new Swap([16, 16, 16, 16, 16, 16, 16, 16], [128]),
64
+ ]);
65
+ expect(optimize([100000], 0)).toEqual([
66
+ new Swap([100000], [32, 128, 512, 1024, 32768, 65536]),
67
+ ]);
68
+ });
69
+
70
+ it("test optimize for swap minimization", () => {
71
+ expect(optimize([8], 1)).toEqual([new Swap([8], [1, 1, 2, 4])]);
72
+ expect(optimize([1, 4], 1)).toEqual([new Swap([4], [2, 2])]);
73
+ expect(optimize([1, 16], 1)).toEqual([new Swap([16], [2, 2, 4, 8])]);
74
+ });
45
75
  });
@@ -0,0 +1,33 @@
1
+ import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
2
+ import * as btc from "@scure/btc-signer";
3
+
4
+ import { isEphemeralAnchorOutput } from "../../utils/unilateral-exit.js";
5
+
6
+ export async function signPsbtWithExternalKey(
7
+ psbtHex: string,
8
+ privateKeyInput: string,
9
+ ): Promise<string> {
10
+ const tx = btc.Transaction.fromPSBT(hexToBytes(psbtHex), {
11
+ allowUnknown: true,
12
+ allowLegacyWitnessUtxo: true,
13
+ version: 3,
14
+ });
15
+ const privateKey = hexToBytes(privateKeyInput);
16
+ for (let i = 0; i < tx.inputsLength; i++) {
17
+ const input = tx.getInput(i);
18
+ if (
19
+ isEphemeralAnchorOutput(
20
+ input?.witnessUtxo?.script,
21
+ input?.witnessUtxo?.amount,
22
+ )
23
+ ) {
24
+ continue;
25
+ }
26
+ tx.updateInput(i, {
27
+ witnessScript: input?.witnessUtxo?.script,
28
+ });
29
+ tx.signIdx(privateKey, i);
30
+ tx.finalizeIdx(i);
31
+ }
32
+ return bytesToHex(tx.toBytes(true, true));
33
+ }
@@ -311,6 +311,17 @@ export class BitcoinFaucet {
311
311
  return await this.generateToAddress(numBlocks, this.miningAddress);
312
312
  }
313
313
 
314
+ async mineBlocksAndWaitForMiningToComplete(numBlocks: number) {
315
+ const startBlock = await this.getBlockCount();
316
+
317
+ await this.mineBlocks(numBlocks);
318
+
319
+ await this.waitForBlocksMined({
320
+ startBlock,
321
+ expectedIncrease: numBlocks,
322
+ });
323
+ }
324
+
314
325
  private async call(method: string, params: any[]) {
315
326
  try {
316
327
  const { fetch, Headers } = getFetch();
@@ -369,17 +380,67 @@ export class BitcoinFaucet {
369
380
  return await this.call("getblock", [blockHash, 2]);
370
381
  }
371
382
 
383
+ async getBlockCount(): Promise<number> {
384
+ return await this.call("getblockcount", []);
385
+ }
386
+
387
+ async waitForBlocksMined({
388
+ startBlock,
389
+ expectedIncrease,
390
+ timeoutMs = 30000,
391
+ intervalMs = 5000,
392
+ }: {
393
+ startBlock: number;
394
+ expectedIncrease: number;
395
+ timeoutMs?: number;
396
+ intervalMs?: number;
397
+ }) {
398
+ const deadline = Date.now() + timeoutMs;
399
+ // Give some time for the blocks to be mined and the chain watcher to catch up.
400
+ await new Promise((r) => setTimeout(r, intervalMs));
401
+
402
+ const start = startBlock;
403
+ const target = start + expectedIncrease;
404
+ while (Date.now() < deadline) {
405
+ const currentBlock = await this.getBlockCount();
406
+ if (currentBlock >= target) return currentBlock;
407
+ await new Promise((r) => setTimeout(r, intervalMs));
408
+ }
409
+ throw new Error(
410
+ `Timed out waiting for ${expectedIncrease} blocks (target height ${target})`,
411
+ );
412
+ }
413
+
372
414
  async broadcastTx(txHex: string) {
373
415
  let response = await this.call("sendrawtransaction", [txHex, 0]);
374
416
  return response;
375
417
  }
376
418
 
419
+ async submitPackage(txHexs: string[]) {
420
+ let response = await this.call("submitpackage", [txHexs]);
421
+ return response;
422
+ }
423
+
377
424
  async getNewAddress(): Promise<string> {
378
425
  const key = secp256k1.utils.randomPrivateKey();
379
426
  const pubKey = secp256k1.getPublicKey(key);
380
427
  return getP2TRAddressFromPublicKey(pubKey, Network.LOCAL);
381
428
  }
382
429
 
430
+ async getNewExternalWallet(): Promise<{
431
+ address: string;
432
+ key: Uint8Array;
433
+ pubKey: Uint8Array;
434
+ }> {
435
+ const key = secp256k1.utils.randomPrivateKey();
436
+ const pubKey = secp256k1.getPublicKey(key);
437
+ return {
438
+ address: getP2TRAddressFromPublicKey(pubKey, Network.LOCAL),
439
+ key,
440
+ pubKey,
441
+ };
442
+ }
443
+
383
444
  async sendToAddress(
384
445
  address: string,
385
446
  amount: bigint,
@@ -224,3 +224,45 @@ export function minimizeTransferSwap(
224
224
 
225
225
  return swaps;
226
226
  }
227
+
228
+ export function shouldOptimize(
229
+ inputLeaves: number[],
230
+ multiplicity: number = 1,
231
+ maxLeavesPerSwap: number = 64,
232
+ ): boolean {
233
+ if (multiplicity == 0) {
234
+ // When optimizing for unilateral exits, we should only optimize if it reduces the
235
+ // number of leaves by more than 5x.
236
+ const swaps = maximizeUnilateralExit(inputLeaves, maxLeavesPerSwap);
237
+ const numInputs = sum(swaps.map((swap) => swap.inLeaves.length));
238
+ const numOutputs = sum(swaps.map((swap) => swap.outLeaves.length));
239
+ return numOutputs * 5 < numInputs;
240
+ } else {
241
+ // When optimizing for swap-minimization, we should only optimize if it changes the
242
+ // number of active denominations by more than 1.
243
+ const swaps = minimizeTransferSwap(
244
+ inputLeaves,
245
+ multiplicity,
246
+ maxLeavesPerSwap,
247
+ );
248
+ const inputCounter = countOccurrences(
249
+ swaps.flatMap((swap) => swap.inLeaves),
250
+ );
251
+ const outputCounter = countOccurrences(
252
+ swaps.flatMap((swap) => swap.outLeaves),
253
+ );
254
+ return Math.abs(inputCounter.size - outputCounter.size) > 1;
255
+ }
256
+ }
257
+
258
+ export function optimize(
259
+ inputLeaves: number[],
260
+ multiplicity: number = 1,
261
+ maxLeavesPerSwap: number = 64,
262
+ ): Swap[] {
263
+ if (multiplicity == 0) {
264
+ return maximizeUnilateralExit(inputLeaves, maxLeavesPerSwap);
265
+ } else {
266
+ return minimizeTransferSwap(inputLeaves, multiplicity, maxLeavesPerSwap);
267
+ }
268
+ }
@@ -60,45 +60,6 @@ export interface BroadcastResult {
60
60
  broadcastedPackages?: number;
61
61
  }
62
62
 
63
- // Helper function to convert WIF private key to hex
64
- function wifToHex(wif: string): string {
65
- try {
66
- // WIF decoding using base58 (simplified version)
67
- const base58Alphabet =
68
- "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
69
-
70
- // Decode base58
71
- let decoded = BigInt(0);
72
- for (let i = 0; i < wif.length; i++) {
73
- const char = wif[i];
74
- if (!char) {
75
- throw new Error("Invalid character in WIF at position " + i);
76
- }
77
- const index = base58Alphabet.indexOf(char);
78
- if (index === -1) {
79
- throw new Error("Invalid character in WIF");
80
- }
81
- decoded = decoded * BigInt(58) + BigInt(index);
82
- }
83
-
84
- // Convert to hex and pad to ensure proper length
85
- let hex = decoded.toString(16);
86
-
87
- // WIF format: [version][32-byte private key][compression flag][4-byte checksum]
88
- // We want the 32-byte private key part (skip version byte, take 32 bytes)
89
- if (hex.length >= 74) {
90
- // 1 + 32 + 1 + 4 = 38 bytes = 76 hex chars minimum
91
- // Skip version byte (2 hex chars) and take 32 bytes (64 hex chars)
92
- const privateKeyHex = hex.substring(2, 66);
93
- return privateKeyHex;
94
- }
95
-
96
- throw new Error("Invalid WIF length");
97
- } catch (error) {
98
- throw new Error(`Failed to convert WIF to hex: ${error}`);
99
- }
100
- }
101
-
102
63
  export function isEphemeralAnchorOutput(
103
64
  script?: Uint8Array,
104
65
  amount?: bigint,
@@ -542,7 +503,7 @@ export async function constructUnilateralExitFeeBumpPackages(
542
503
  }
543
504
 
544
505
  // Helper function to create RIPEMD160(SHA256(data)) hash
545
- function hash160(data: Uint8Array): Uint8Array {
506
+ export function hash160(data: Uint8Array): Uint8Array {
546
507
  // Proper implementation using RIPEMD160(SHA256(data))
547
508
  const sha256Hash = sha256(data);
548
509
  return ripemd160(sha256Hash);