@utxopia/sdk 0.1.0-alpha.3 → 0.1.0-alpha.5

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/src/taproot.ts CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { sha256 } from "@noble/hashes/sha2.js";
10
10
  import { taggedHash, hexToBytes, bytesToHex } from "./crypto";
11
- import * as bech32 from "bech32";
11
+ import { bech32, bech32m } from "@scure/base";
12
12
  import { secp256k1 } from "@noble/curves/secp256k1.js";
13
13
 
14
14
  // Never use the secp256k1 generator as a custody key. Its discrete log is
@@ -31,7 +31,7 @@ const UNSAFE_GENERATOR_INTERNAL_KEY_HEX =
31
31
  */
32
32
  export function deriveTaprootAddress(
33
33
  commitment: Uint8Array,
34
- network: "mainnet" | "testnet" | "regtest" = "testnet",
34
+ network: BitcoinNetwork = "testnet",
35
35
  internalKey?: Uint8Array
36
36
  ): {
37
37
  address: string;
@@ -77,11 +77,7 @@ export function deriveTaprootAddress(
77
77
  const outputKeyHex = outputPoint.toHex(true); // 33-byte compressed hex
78
78
  const outputKey = hexToBytes(outputKeyHex.slice(2)); // drop "02"/"03" prefix
79
79
 
80
- // Encode as bech32m address
81
- const hrp = network === "mainnet" ? "bc" : network === "regtest" ? "bcrt" : "tb";
82
- const words = bech32.bech32m.toWords(outputKey);
83
- // Witness version 1 for taproot
84
- const address = bech32.bech32m.encode(hrp, [1, ...words]);
80
+ const address = p2trAddress(outputKey, network);
85
81
 
86
82
  return {
87
83
  address,
@@ -164,7 +160,7 @@ export function depositLeafScript(
164
160
  export function deriveDepositAddress(
165
161
  commitment: Uint8Array,
166
162
  ikaXOnlyPubkey: Uint8Array,
167
- network: "mainnet" | "testnet" | "regtest" = "testnet"
163
+ network: BitcoinNetwork = "testnet"
168
164
  ): {
169
165
  address: string;
170
166
  outputKey: Uint8Array;
@@ -203,25 +199,73 @@ export function verifyTaprootAddress(
203
199
  internalKey?: Uint8Array
204
200
  ): boolean {
205
201
  try {
206
- const decoded = bech32.bech32m.decode(address);
202
+ const decoded = bech32m.decode(address as `${string}1${string}`);
207
203
  const witnessVersion = decoded.words[0];
208
204
  if (witnessVersion !== 1) {
209
205
  return false;
210
206
  }
211
207
 
212
- const actualOutputKey = new Uint8Array(
213
- bech32.bech32m.fromWords(decoded.words.slice(1))
214
- );
208
+ const network = networkForHrp(decoded.prefix);
209
+ if (!network) return false;
215
210
 
216
- const network = decoded.prefix === "bc" ? "mainnet" : "testnet";
217
211
  const expected = deriveTaprootAddress(commitment, network, internalKey);
218
212
 
219
- return arraysEqual(actualOutputKey, expected.outputKey);
213
+ // Compare the whole address, not just the output key: the key is identical
214
+ // on every network, so an output-key match said nothing about the encoding.
215
+ // bech32 is case-insensitive, hence the fold.
216
+ return address.toLowerCase() === expected.address;
220
217
  } catch {
221
218
  return false;
222
219
  }
223
220
  }
224
221
 
222
+ /**
223
+ * The networks whose addresses this SDK derives.
224
+ *
225
+ * Kept as one alias because the members drifted when it was spelled out at each
226
+ * call site: psbt.ts folded regtest into testnet while taproot.ts did not, and
227
+ * the two disagreed about what a bcrt1 address was.
228
+ */
229
+ export type BitcoinNetwork = "mainnet" | "testnet" | "signet" | "regtest";
230
+
231
+ /**
232
+ * bech32 human-readable prefix for a Bitcoin network.
233
+ *
234
+ * Regtest is the reason this exists: it shares testnet's version bytes but not
235
+ * its prefix, and every place that folded the two together produced addresses
236
+ * no regtest node would accept.
237
+ */
238
+ export function bech32Hrp(network: BitcoinNetwork): string {
239
+ return network === "mainnet" ? "bc" : network === "regtest" ? "bcrt" : "tb";
240
+ }
241
+
242
+ /**
243
+ * The network an address's bech32 prefix names, or null if it names none.
244
+ *
245
+ * Null rather than a testnet default: an unrecognised prefix is an address this
246
+ * SDK cannot place, and quietly calling it testnet is how bcrt1 addresses ended
247
+ * up classified as testnet everywhere.
248
+ */
249
+ export function networkForHrp(prefix: string): BitcoinNetwork | null {
250
+ return prefix === "bc"
251
+ ? "mainnet"
252
+ : prefix === "tb"
253
+ ? "testnet"
254
+ : prefix === "bcrt"
255
+ ? "regtest"
256
+ : null;
257
+ }
258
+
259
+ /**
260
+ * Encode an x-only output key as a witness-v1 (P2TR) address.
261
+ */
262
+ export function p2trAddress(
263
+ outputKey: Uint8Array,
264
+ network: BitcoinNetwork,
265
+ ): string {
266
+ return bech32m.encode(bech32Hrp(network), [1, ...bech32m.toWords(outputKey)]);
267
+ }
268
+
225
269
  /**
226
270
  * Generate a P2TR (Pay-to-Taproot) script pubkey
227
271
  *
@@ -264,18 +308,19 @@ export function parseP2TRScriptPubkey(
264
308
  export function isValidBitcoinAddress(address: string): {
265
309
  valid: boolean;
266
310
  type: "p2pkh" | "p2sh" | "p2wpkh" | "p2wsh" | "p2tr" | "unknown";
267
- network: "mainnet" | "testnet" | "unknown";
311
+ network: BitcoinNetwork | "unknown";
268
312
  } {
269
313
  try {
270
314
  // Bech32m (Taproot)
271
- if (address.startsWith("bc1p") || address.startsWith("tb1p")) {
272
- const decoded = bech32.bech32m.decode(address);
273
- if (decoded.words[0] === 1 && decoded.words.length === 53) {
274
- return {
275
- valid: true,
276
- type: "p2tr",
277
- network: decoded.prefix === "bc" ? "mainnet" : "testnet",
278
- };
315
+ if (
316
+ address.startsWith("bc1p") ||
317
+ address.startsWith("tb1p") ||
318
+ address.startsWith("bcrt1p")
319
+ ) {
320
+ const decoded = bech32m.decode(address as `${string}1${string}`);
321
+ const network = networkForHrp(decoded.prefix);
322
+ if (network && decoded.words[0] === 1 && decoded.words.length === 53) {
323
+ return { valid: true, type: "p2tr", network };
279
324
  }
280
325
  }
281
326
 
@@ -285,19 +330,11 @@ export function isValidBitcoinAddress(address: string): {
285
330
  address.startsWith("tb1q") ||
286
331
  address.startsWith("bcrt1q")
287
332
  ) {
288
- const decoded = bech32.bech32.decode(address);
289
- if (decoded.words[0] === 0) {
333
+ const decoded = bech32.decode(address as `${string}1${string}`);
334
+ const network = networkForHrp(decoded.prefix);
335
+ if (network && decoded.words[0] === 0) {
290
336
  const type = decoded.words.length === 33 ? "p2wpkh" : "p2wsh";
291
- return {
292
- valid: true,
293
- type,
294
- network:
295
- decoded.prefix === "bc"
296
- ? "mainnet"
297
- : decoded.prefix === "bcrt"
298
- ? "testnet"
299
- : "testnet",
300
- };
337
+ return { valid: true, type, network };
301
338
  }
302
339
  }
303
340
 
@@ -623,7 +660,7 @@ export function deriveTaprootAddressWithRefund(
623
660
  npk: Uint8Array,
624
661
  userRefundPubkey: Uint8Array,
625
662
  internalKey: Uint8Array,
626
- network: "mainnet" | "testnet" | "regtest" = "testnet"
663
+ network: BitcoinNetwork = "testnet"
627
664
  ): {
628
665
  address: string;
629
666
  outputKey: Uint8Array;
@@ -672,9 +709,7 @@ export function deriveTaprootAddressWithRefund(
672
709
  controlBlock.set(internalKey, 1);
673
710
 
674
711
  // 7. Encode as bech32m address
675
- const hrp = network === "mainnet" ? "bc" : network === "regtest" ? "bcrt" : "tb";
676
- const words = bech32.bech32m.toWords(outputKey);
677
- const address = bech32.bech32m.encode(hrp, [1, ...words]);
712
+ const address = p2trAddress(outputKey, network);
678
713
 
679
714
  return {
680
715
  address,