@leather.io/bitcoin 0.35.8 → 0.36.3

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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@leather.io/bitcoin",
3
3
  "author": "Leather.io contact@leather.io",
4
4
  "description": "Shared bitcoin utilities",
5
- "version": "0.35.8",
5
+ "version": "0.36.3",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/leather.io/mono/tree/dev/packages/bitcoin",
8
8
  "repository": {
@@ -32,10 +32,10 @@
32
32
  "just-memoize": "2.2.0",
33
33
  "varuint-bitcoin": "1.1.2",
34
34
  "zod": "4.0.17",
35
- "@leather.io/constants": "0.28.4",
36
- "@leather.io/crypto": "1.12.10",
37
- "@leather.io/models": "0.51.0",
38
- "@leather.io/utils": "0.49.4"
35
+ "@leather.io/constants": "0.30.0",
36
+ "@leather.io/crypto": "1.12.13",
37
+ "@leather.io/utils": "0.49.7",
38
+ "@leather.io/models": "0.52.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "prettier": "3.5.1",
@@ -44,9 +44,9 @@
44
44
  "typescript": "5.9.3",
45
45
  "vitest": "2.1.9",
46
46
  "@leather.io/prettier-config": "0.9.0",
47
+ "@leather.io/rpc": "2.21.7",
47
48
  "@leather.io/test-config": "0.1.2",
48
- "@leather.io/tsconfig-config": "0.11.1",
49
- "@leather.io/rpc": "2.21.4"
49
+ "@leather.io/tsconfig-config": "0.11.1"
50
50
  },
51
51
  "keywords": [
52
52
  "bitcoin",
@@ -60,7 +60,7 @@
60
60
  "types": "./dist/index.d.ts",
61
61
  "scripts": {
62
62
  "build": "tsdown",
63
- "build:watch": "tsdown --watch",
63
+ "build:watch": "tsdown --no-clean --watch",
64
64
  "format": "prettier . --write --ignore-path ../../.prettierignore",
65
65
  "format:check": "prettier . --check --ignore-path ../../.prettierignore",
66
66
  "lint": "eslint --cache --max-warnings 0",
@@ -1,55 +1,50 @@
1
1
  import { createBitcoinAddress } from '../validation/bitcoin-address';
2
2
  import { calculateMaxSpend } from './calculate-max-spend';
3
- import { generateMockAverageFee, mockUtxos } from './coin-selection.mocks';
3
+ import { mockUtxos } from './coin-selection.mocks';
4
4
 
5
5
  const recipient = createBitcoinAddress('');
6
6
  describe(calculateMaxSpend.name, () => {
7
7
  test('with 1 sat/vb fee', () => {
8
- const fee = 1;
9
8
  const maxBitcoinSpend = calculateMaxSpend({
10
9
  recipient,
11
10
  utxos: mockUtxos,
12
- feeRates: generateMockAverageFee(fee),
11
+ feeRate: 1,
13
12
  });
14
13
  expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(50087948);
15
14
  });
16
15
 
17
16
  test('with 5 sat/vb fee', () => {
18
- const fee = 5;
19
17
  const maxBitcoinSpend = calculateMaxSpend({
20
18
  recipient,
21
19
  utxos: mockUtxos,
22
- feeRates: generateMockAverageFee(fee),
20
+ feeRate: 5,
23
21
  });
24
22
  expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(50085342);
25
23
  });
26
24
 
27
25
  test('with 30 sat/vb fee', () => {
28
- const fee = 30;
29
26
  const maxBitcoinSpend = calculateMaxSpend({
30
27
  recipient,
31
28
  utxos: mockUtxos,
32
- feeRates: generateMockAverageFee(fee),
29
+ feeRate: 30,
33
30
  });
34
31
  expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(50073585);
35
32
  });
36
33
 
37
34
  test('with 100 sat/vb fee', () => {
38
- const fee = 100;
39
35
  const maxBitcoinSpend = calculateMaxSpend({
40
36
  recipient,
41
37
  utxos: mockUtxos,
42
- feeRates: generateMockAverageFee(fee),
38
+ feeRate: 100,
43
39
  });
44
40
  expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(50046950);
45
41
  });
46
42
 
47
43
  test('with 400 sat/vb fee', () => {
48
- const fee = 400;
49
44
  const maxBitcoinSpend = calculateMaxSpend({
50
45
  recipient,
51
46
  utxos: mockUtxos,
52
- feeRates: generateMockAverageFee(fee),
47
+ feeRate: 400,
53
48
  });
54
49
  expect(maxBitcoinSpend.amount.amount.toNumber()).toEqual(49969100);
55
50
  });
@@ -1,17 +1,14 @@
1
1
  import BigNumber from 'bignumber.js';
2
2
 
3
- import type { AverageBitcoinFeeRates, Money } from '@leather.io/models';
3
+ import type { Money } from '@leather.io/models';
4
4
  import { createMoney, satToBtc } from '@leather.io/utils';
5
5
 
6
6
  import { filterUneconomicalUtxos, getSpendableAmount } from './coin-selection.utils';
7
7
 
8
- interface CalculateMaxSpendArgs {
9
- // recipient is intentionally string instead of BitcoinAddress, as it's being validated
10
- // with a fallback in a subroutine.
8
+ interface CalculateMaxSpendParams {
11
9
  recipient: string;
12
10
  utxos: { value: number; txid: string }[];
13
- feeRates?: AverageBitcoinFeeRates;
14
- feeRate?: number;
11
+ feeRate: number;
15
12
  }
16
13
 
17
14
  export interface CalculateMaxSpendResponse {
@@ -23,26 +20,23 @@ export function calculateMaxSpend({
23
20
  recipient,
24
21
  utxos,
25
22
  feeRate,
26
- feeRates,
27
- }: CalculateMaxSpendArgs): CalculateMaxSpendResponse {
28
- if (!utxos.length || !feeRates)
23
+ }: CalculateMaxSpendParams): CalculateMaxSpendResponse {
24
+ if (!utxos.length)
29
25
  return {
30
26
  spendAllFee: 0,
31
27
  amount: createMoney(0, 'BTC'),
32
28
  spendableBitcoin: new BigNumber(0),
33
29
  };
34
30
 
35
- const currentFeeRate = feeRate ?? feeRates.halfHourFee.toNumber();
36
-
37
31
  const filteredUtxos = filterUneconomicalUtxos({
38
32
  utxos,
39
- feeRate: currentFeeRate,
33
+ feeRate,
40
34
  recipients: [{ address: recipient, amount: createMoney(0, 'BTC') }],
41
35
  });
42
36
 
43
37
  const { spendableAmount, fee } = getSpendableAmount({
44
38
  utxos: filteredUtxos,
45
- feeRate: currentFeeRate,
39
+ feeRate,
46
40
  recipients: [{ address: recipient, amount: createMoney(0, 'BTC') }],
47
41
  isSendMax: true,
48
42
  });
package/src/index.ts CHANGED
@@ -30,9 +30,9 @@ export * from './validation/address-validation';
30
30
  export * from './validation/amount-validation';
31
31
  export * from './validation/bitcoin-address';
32
32
  export * from './validation/bitcoin-error';
33
- export * from './validation/transaction-validation';
34
33
 
35
34
  export * from './utils/bitcoin.descriptors';
36
35
  export * from './utils/bitcoin.network';
37
36
  export * from './utils/bitcoin.utils';
38
37
  export * from './utils/lookup-derivation-by-address';
38
+ export * from './utils/deconstruct-btc-address';
@@ -1,8 +1,14 @@
1
1
  import { HDKey } from '@scure/bip32';
2
2
  import { mnemonicToSeedSync } from '@scure/bip39';
3
3
 
4
+ import { extractAddressIndexFromPath, extractChangeIndexFromPath } from '@leather.io/crypto';
5
+
4
6
  import { deriveAddressIndexKeychainFromAccount } from '../utils/bitcoin.utils';
5
- import { deriveTaprootAccount, getTaprootPaymentFromAddressIndex } from './p2tr-address-gen';
7
+ import {
8
+ deriveTaprootAccount,
9
+ getTaprootPaymentFromAddressIndex,
10
+ makeTaprootAddressIndexDerivationPath,
11
+ } from './p2tr-address-gen';
6
12
 
7
13
  // TODO: this is a SECRET_KEY from @tests/mocks folder.
8
14
  // Temporary until we move @tests/mocks folder to monorepo.
@@ -11,27 +17,73 @@ export const SECRET_KEY =
11
17
 
12
18
  // Source:
13
19
  // generated in Sparrow with same secret key used in tests
14
- const addresses = [
15
- 'tb1p05uectcay8ptepqneycknxf0ewvdejcl0zdqex98ux87w7tzqjfsd7yxyl',
16
- 'tb1papsqvj9s2yn9mavhtuk9jyw4arlwcxey33n49g02rpjcajx88qrszpytxl',
17
- 'tb1pfnegsp8x0gnjrgzu0p5xrltrms50prpl8c5a3rwfcrp9p9vumnfsv7zn84',
18
- 'tb1pzqp06cvvcmftc4g69kuqt5z59k3uyuuwzsg796c00scav0vxjevs3gsvpr',
19
- 'tb1p2acyvr7wzvdr2m9fprg2e48k03rjvvq8au680jtrxqrz5m9m5kdsurrp2z',
20
- 'tb1p3kautzlyralsnxf2fv7rudlgyhu6u0lcvzdnlhaywl4h8l7yk0ds59lvfg',
21
- ];
20
+ const addresses = {
21
+ "m/86'/0'/0'/0/0": 'tb1p05uectcay8ptepqneycknxf0ewvdejcl0zdqex98ux87w7tzqjfsd7yxyl',
22
+ "m/86'/0'/0'/1/0": 'tb1p8vf4ljcj43f0dqd8xprsf2w8g8wcqktsmly2rd92sh235nnjlg6qf2g4c6',
23
+ "m/86'/0'/0'/0/1": 'tb1papsqvj9s2yn9mavhtuk9jyw4arlwcxey33n49g02rpjcajx88qrszpytxl',
24
+ "m/86'/0'/0'/1/1": 'tb1p40xhx5m44fznk6vzxmyn4grlgmzunwrx3slqwzslg8rag9r236vsqmnkf3',
25
+ "m/86'/0'/0'/0/2": 'tb1pfnegsp8x0gnjrgzu0p5xrltrms50prpl8c5a3rwfcrp9p9vumnfsv7zn84',
26
+ "m/86'/0'/0'/1/2": 'tb1pn98uf3ln2lxn6n99rkk7nxx5u54g9rrlcfe2ere0w7m3hww93vuqrwmwwy',
27
+ "m/86'/0'/0'/0/3": 'tb1pzqp06cvvcmftc4g69kuqt5z59k3uyuuwzsg796c00scav0vxjevs3gsvpr',
28
+ "m/86'/0'/0'/1/3": 'tb1pnap8rexj2gqcjtq3vncav29duu4ungcys0kgcste5sdy04mg98usfcp90t',
29
+ "m/86'/0'/0'/0/4": 'tb1p2acyvr7wzvdr2m9fprg2e48k03rjvvq8au680jtrxqrz5m9m5kdsurrp2z',
30
+ "m/86'/0'/0'/1/4": 'tb1p4ssvcjmhuzlvwp48828zswvktjw8g6s9lwrcq5ecc5yjqwv5pp9s9624v6',
31
+ "m/86'/0'/0'/0/5": 'tb1p3kautzlyralsnxf2fv7rudlgyhu6u0lcvzdnlhaywl4h8l7yk0ds59lvfg',
32
+ "m/86'/0'/0'/1/5": 'tb1p3m3yxhjtnq86r9s62h6t9cnweu5se07c0srdureyly3z9kny68pqc5cnnn',
33
+ };
22
34
 
23
35
  describe('taproot address gen', () => {
24
- test.each(addresses)('should generate taproot addresses', address => {
25
- const keychain = HDKey.fromMasterSeed(mnemonicToSeedSync(SECRET_KEY));
26
- const index = addresses.indexOf(address);
27
- const accountZero = deriveTaprootAccount(keychain, 'testnet')(0);
28
-
29
- const addressIndexDetails = getTaprootPaymentFromAddressIndex(
30
- deriveAddressIndexKeychainFromAccount(accountZero.keychain)(index),
31
- 'testnet'
32
- );
33
- if (!accountZero.keychain.privateKey) throw new Error('No private key found');
34
-
35
- expect(addressIndexDetails.address).toEqual(address);
36
+ test.each(Object.entries(addresses))(
37
+ 'should generate taproot addresses',
38
+ (derivationPath, address) => {
39
+ const keychain = HDKey.fromMasterSeed(mnemonicToSeedSync(SECRET_KEY));
40
+ const addressIndex = extractAddressIndexFromPath(derivationPath);
41
+ const changeIndex = extractChangeIndexFromPath(derivationPath);
42
+ const accountZero = deriveTaprootAccount(keychain, 'testnet')(0);
43
+
44
+ const addressIndexDetails = getTaprootPaymentFromAddressIndex(
45
+ deriveAddressIndexKeychainFromAccount(accountZero.keychain)({
46
+ addressIndex,
47
+ changeIndex,
48
+ }),
49
+ 'testnet'
50
+ );
51
+ if (!accountZero.keychain.privateKey) throw new Error('No private key found');
52
+
53
+ expect(addressIndexDetails.address).toEqual(address);
54
+ }
55
+ );
56
+ });
57
+
58
+ describe(makeTaprootAddressIndexDerivationPath.name, () => {
59
+ it('creates mainnet receive path', () => {
60
+ expect(
61
+ makeTaprootAddressIndexDerivationPath({
62
+ network: 'mainnet',
63
+ accountIndex: 5,
64
+ changeIndex: 0,
65
+ addressIndex: 0,
66
+ })
67
+ ).toEqual("m/86'/0'/5'/0/0");
68
+ });
69
+ it('creates mainnet change path', () => {
70
+ expect(
71
+ makeTaprootAddressIndexDerivationPath({
72
+ network: 'mainnet',
73
+ accountIndex: 42,
74
+ changeIndex: 1,
75
+ addressIndex: 5,
76
+ })
77
+ ).toEqual("m/86'/0'/42'/1/5");
78
+ });
79
+ it('creates testnet change path', () => {
80
+ expect(
81
+ makeTaprootAddressIndexDerivationPath({
82
+ network: 'testnet',
83
+ accountIndex: 0,
84
+ changeIndex: 1,
85
+ addressIndex: 1,
86
+ })
87
+ ).toEqual("m/86'/1'/0'/1/1");
36
88
  });
37
89
  });
@@ -21,12 +21,20 @@ export function makeTaprootAccountDerivationPath(
21
21
  /** @deprecated Use makeTaprootAccountDerivationPath */
22
22
  export const getTaprootAccountDerivationPath = makeTaprootAccountDerivationPath;
23
23
 
24
- export function makeTaprootAddressIndexDerivationPath(
25
- network: BitcoinNetworkModes,
26
- accountIndex: number,
27
- addressIndex: number
28
- ) {
29
- return makeTaprootAccountDerivationPath(network, accountIndex) + `/0/${addressIndex}`;
24
+ export function makeTaprootAddressIndexDerivationPath({
25
+ network,
26
+ accountIndex,
27
+ changeIndex,
28
+ addressIndex,
29
+ }: {
30
+ network: BitcoinNetworkModes;
31
+ accountIndex: number;
32
+ changeIndex: number;
33
+ addressIndex: number;
34
+ }) {
35
+ return (
36
+ makeTaprootAccountDerivationPath(network, accountIndex) + `/${changeIndex}/${addressIndex}`
37
+ );
30
38
  }
31
39
  /** @deprecated Use makeTaprootAddressIndexDerivationPath */
32
40
  export const getTaprootAddressIndexDerivationPath = makeTaprootAddressIndexDerivationPath;
@@ -1,6 +1,9 @@
1
1
  import { deriveKeychainFromXpub } from '@leather.io/crypto';
2
2
 
3
- import { deriveNativeSegwitReceiveAddressIndexZero } from './p2wpkh-address-gen';
3
+ import {
4
+ deriveNativeSegwitReceiveAddressIndexZero,
5
+ makeNativeSegwitAddressIndexDerivationPath,
6
+ } from './p2wpkh-address-gen';
4
7
 
5
8
  describe('Bitcoin bech32 (P2WPKH address derivation', () => {
6
9
  describe('from extended public key', () => {
@@ -46,3 +49,36 @@ describe('Bitcoin bech32 (P2WPKH address derivation', () => {
46
49
  });
47
50
  });
48
51
  });
52
+
53
+ describe(makeNativeSegwitAddressIndexDerivationPath.name, () => {
54
+ it('creates mainnet receive path', () => {
55
+ expect(
56
+ makeNativeSegwitAddressIndexDerivationPath({
57
+ network: 'mainnet',
58
+ accountIndex: 5,
59
+ changeIndex: 0,
60
+ addressIndex: 0,
61
+ })
62
+ ).toEqual("m/84'/0'/5'/0/0");
63
+ });
64
+ it('creates mainnet change path', () => {
65
+ expect(
66
+ makeNativeSegwitAddressIndexDerivationPath({
67
+ network: 'mainnet',
68
+ accountIndex: 42,
69
+ changeIndex: 1,
70
+ addressIndex: 5,
71
+ })
72
+ ).toEqual("m/84'/0'/42'/1/5");
73
+ });
74
+ it('creates testnet change path', () => {
75
+ expect(
76
+ makeNativeSegwitAddressIndexDerivationPath({
77
+ network: 'testnet',
78
+ accountIndex: 0,
79
+ changeIndex: 1,
80
+ addressIndex: 1,
81
+ })
82
+ ).toEqual("m/84'/1'/0'/1/1");
83
+ });
84
+ });
@@ -21,12 +21,20 @@ export function makeNativeSegwitAccountDerivationPath(
21
21
  /** @deprecated Use makeNativeSegwitAccountDerivationPath */
22
22
  export const getNativeSegwitAccountDerivationPath = makeNativeSegwitAccountDerivationPath;
23
23
 
24
- export function makeNativeSegwitAddressIndexDerivationPath(
25
- network: BitcoinNetworkModes,
26
- accountIndex: number,
27
- addressIndex: number
28
- ) {
29
- return makeNativeSegwitAccountDerivationPath(network, accountIndex) + `/0/${addressIndex}`;
24
+ export function makeNativeSegwitAddressIndexDerivationPath({
25
+ network,
26
+ accountIndex,
27
+ changeIndex,
28
+ addressIndex,
29
+ }: {
30
+ network: BitcoinNetworkModes;
31
+ accountIndex: number;
32
+ changeIndex: number;
33
+ addressIndex: number;
34
+ }) {
35
+ return (
36
+ makeNativeSegwitAccountDerivationPath(network, accountIndex) + `/${changeIndex}/${addressIndex}`
37
+ );
30
38
  }
31
39
 
32
40
  /** @deprecated Use makeNativeSegwitAddressIndexDerivationPath */
@@ -71,15 +71,27 @@ export function deriveAddressesFromDescriptor({
71
71
 
72
72
  const results: DeriveAddressesFromDescriptorResult[] = [];
73
73
 
74
- for (let i = 0; i < limit; i++) {
75
- const address = whenSupportedPaymentType(paymentType)({
76
- p2tr: getTaprootAddress({ index: i, keychain: accountKeychain, network }),
77
- p2wpkh: getNativeSegwitAddress({ index: i, keychain: accountKeychain, network }),
78
- });
79
- results.push({
80
- address,
81
- path: derivationPathFn(network, accountKeychain.index - HARDENED_OFFSET, i),
82
- });
74
+ for (let addressIndex = 0; addressIndex < limit; ++addressIndex) {
75
+ for (let changeIndex = 0; changeIndex < 2; ++changeIndex) {
76
+ const address = whenSupportedPaymentType(paymentType)({
77
+ p2tr: getTaprootAddress({ addressIndex, changeIndex, keychain: accountKeychain, network }),
78
+ p2wpkh: getNativeSegwitAddress({
79
+ addressIndex,
80
+ changeIndex,
81
+ keychain: accountKeychain,
82
+ network,
83
+ }),
84
+ });
85
+ results.push({
86
+ address,
87
+ path: derivationPathFn({
88
+ network,
89
+ accountIndex: accountKeychain.index - HARDENED_OFFSET,
90
+ addressIndex,
91
+ changeIndex,
92
+ }),
93
+ });
94
+ }
83
95
  }
84
96
  return results;
85
97
  }
@@ -1,7 +1,20 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
+ import { deriveRootKeychainFromMnemonic } from '@leather.io/crypto';
4
+ import { testMnemonic } from '@leather.io/test-config';
5
+
6
+ import { deriveTaprootAccount } from '../payments/p2tr-address-gen';
7
+ import { deriveNativeSegwitAccountFromRootKeychain } from '../payments/p2wpkh-address-gen';
3
8
  import { createBitcoinAddress } from '../validation/bitcoin-address';
4
- import { inferNetworkFromAddress, inferPaymentTypeFromAddress } from './bitcoin.utils';
9
+ import {
10
+ deriveAddressIndexZeroFromAccount,
11
+ getNativeSegwitAddress,
12
+ getTaprootAddress,
13
+ inferNetworkFromAddress,
14
+ inferPaymentTypeFromAddress,
15
+ isNativeSegwitDerivationPath,
16
+ isTaprootDerivationPath,
17
+ } from './bitcoin.utils';
5
18
 
6
19
  describe(inferNetworkFromAddress.name, () => {
7
20
  it('should return "mainnet" for P2PKH mainnet addresses', () => {
@@ -83,3 +96,99 @@ describe(inferPaymentTypeFromAddress.name, () => {
83
96
  expect(inferPaymentTypeFromAddress(address)).toBe('p2tr');
84
97
  });
85
98
  });
99
+
100
+ describe(getNativeSegwitAddress.name, async () => {
101
+ const rootKeychain = await deriveRootKeychainFromMnemonic(testMnemonic);
102
+
103
+ const nativeSegwitKeychain = deriveNativeSegwitAccountFromRootKeychain(rootKeychain, 'mainnet');
104
+ it('creates mainnet receive address', () => {
105
+ expect(
106
+ getNativeSegwitAddress({
107
+ network: 'mainnet',
108
+ keychain: nativeSegwitKeychain(5).keychain,
109
+ changeIndex: 0,
110
+ addressIndex: 0,
111
+ })
112
+ ).toEqual('bc1q32mr73d2hynzw8wwdg58ek62fll0hsl8n9d2cn');
113
+ });
114
+ it('creates mainnet change address', () => {
115
+ expect(
116
+ getNativeSegwitAddress({
117
+ network: 'mainnet',
118
+ keychain: nativeSegwitKeychain(42).keychain,
119
+ changeIndex: 1,
120
+ addressIndex: 5,
121
+ })
122
+ ).toEqual('bc1qph55pqy085m9ktqz5w7c70dznkkjhyw2wpxh9t');
123
+ });
124
+ it('creates testnet change address', () => {
125
+ expect(
126
+ getNativeSegwitAddress({
127
+ network: 'testnet',
128
+ keychain: nativeSegwitKeychain(0).keychain,
129
+ changeIndex: 1,
130
+ addressIndex: 1,
131
+ })
132
+ ).toEqual('tb1qy4qarz6r4n283hez44sxrtanz6adkg04r86nlt');
133
+ });
134
+ });
135
+
136
+ describe(getTaprootAddress.name, async () => {
137
+ const rootKeychain = await deriveRootKeychainFromMnemonic(testMnemonic);
138
+ const taprootKeychain = deriveTaprootAccount(rootKeychain, 'mainnet');
139
+ it('creates mainnet receive address', () => {
140
+ expect(
141
+ getTaprootAddress({
142
+ network: 'mainnet',
143
+ keychain: taprootKeychain(5).keychain,
144
+ changeIndex: 0,
145
+ addressIndex: 0,
146
+ })
147
+ ).toEqual('bc1p3afedj4370xaqhj8yj5nv2pksh8xh2xzz5xdk5pg42twxh0xyx6ql4kwwk');
148
+ });
149
+ it('creates mainnet change address', () => {
150
+ expect(
151
+ getTaprootAddress({
152
+ network: 'mainnet',
153
+ keychain: taprootKeychain(42).keychain,
154
+ changeIndex: 1,
155
+ addressIndex: 5,
156
+ })
157
+ ).toEqual('bc1phj8a25u5wdvcc936uv3usu36l680jt2g3ddgc7thy6hu934e9ltqx7fmnk');
158
+ });
159
+ it('creates testnet change address', () => {
160
+ expect(
161
+ getTaprootAddress({
162
+ network: 'testnet',
163
+ keychain: taprootKeychain(0).keychain,
164
+ changeIndex: 1,
165
+ addressIndex: 1,
166
+ })
167
+ ).toEqual('tb1p7heq86wmpyj26fjt97yhuq2f5z6xk5nhhkc4xx64lu77ufsdhl0s73c6u5');
168
+ });
169
+ });
170
+
171
+ describe(deriveAddressIndexZeroFromAccount.name, async () => {
172
+ const rootKeychain = await deriveRootKeychainFromMnemonic(testMnemonic);
173
+ const taprootKeychain = deriveTaprootAccount(rootKeychain, 'mainnet');
174
+ const accountKeychain = taprootKeychain(0).keychain;
175
+ const changeKeychain = accountKeychain.deriveChild(0);
176
+ expect(accountKeychain.fingerprint).toBe(changeKeychain.parentFingerprint);
177
+
178
+ const addressKeychain = deriveAddressIndexZeroFromAccount(accountKeychain);
179
+ expect(changeKeychain.fingerprint).toBe(addressKeychain.parentFingerprint);
180
+ });
181
+
182
+ describe(isTaprootDerivationPath.name, () => {
183
+ test('should correctly check taproot derivation path', () => {
184
+ expect(isTaprootDerivationPath("m/86'/1'/3'/0/5")).toBe(true);
185
+ expect(isTaprootDerivationPath("m/84'/0'/5'/1/7")).toBe(false);
186
+ });
187
+ });
188
+
189
+ describe(isNativeSegwitDerivationPath.name, () => {
190
+ test('should correctly check native segwit derivation path', () => {
191
+ expect(isNativeSegwitDerivationPath("m/86'/1'/3'/0/5")).toBe(false);
192
+ expect(isNativeSegwitDerivationPath("m/84'/0'/5'/1/7")).toBe(true);
193
+ });
194
+ });
@@ -78,11 +78,15 @@ export function deriveAddressIndexKeychainFromAccount(keychain: HDKey) {
78
78
  if (keychain.depth !== DerivationPathDepth.Account)
79
79
  throw new Error('Keychain passed is not an account');
80
80
 
81
- return (index: number) => keychain.deriveChild(0).deriveChild(index);
81
+ return ({ changeIndex, addressIndex }: { changeIndex: number; addressIndex: number }) =>
82
+ keychain.deriveChild(changeIndex).deriveChild(addressIndex);
82
83
  }
83
84
 
84
85
  export function deriveAddressIndexZeroFromAccount(keychain: HDKey) {
85
- return deriveAddressIndexKeychainFromAccount(keychain)(0);
86
+ return deriveAddressIndexKeychainFromAccount(keychain)({
87
+ changeIndex: 0,
88
+ addressIndex: 0,
89
+ });
86
90
  }
87
91
 
88
92
  export const ecdsaPublicKeyLength = 33;
@@ -268,38 +272,55 @@ export function lookUpLedgerKeysByPath(
268
272
  }
269
273
 
270
274
  interface GetAddressArgs {
271
- index: number;
275
+ changeIndex: number;
276
+ addressIndex: number;
272
277
  keychain?: HDKey;
273
278
  network: BitcoinNetworkModes;
274
279
  }
275
280
 
276
- export function getTaprootAddress({ index, keychain, network }: GetAddressArgs) {
281
+ export function getTaprootAddress({
282
+ changeIndex,
283
+ addressIndex,
284
+ keychain,
285
+ network,
286
+ }: GetAddressArgs) {
277
287
  if (!keychain) throw new Error('Expected keychain to be provided');
278
288
 
279
289
  if (keychain.depth !== DerivationPathDepth.Account)
280
290
  throw new Error('Expects keychain to be on the account index');
281
291
 
282
- const addressIndex = deriveAddressIndexKeychainFromAccount(keychain)(index);
292
+ const addresskeychain = deriveAddressIndexKeychainFromAccount(keychain)({
293
+ changeIndex,
294
+ addressIndex,
295
+ });
283
296
 
284
- if (!addressIndex.publicKey) throw new Error('Expected publicKey to be defined');
297
+ if (!addresskeychain.publicKey) throw new Error('Expected publicKey to be defined');
285
298
 
286
- const payment = getTaprootPayment(addressIndex.publicKey, network);
299
+ const payment = getTaprootPayment(addresskeychain.publicKey, network);
287
300
 
288
301
  if (!payment.address) throw new Error('Expected address to be defined');
289
302
  return payment.address;
290
303
  }
291
304
 
292
- export function getNativeSegwitAddress({ index, keychain, network }: GetAddressArgs) {
305
+ export function getNativeSegwitAddress({
306
+ changeIndex,
307
+ addressIndex,
308
+ keychain,
309
+ network,
310
+ }: GetAddressArgs) {
293
311
  if (!keychain) throw new Error('Expected keychain to be provided');
294
312
 
295
313
  if (keychain.depth !== DerivationPathDepth.Account)
296
314
  throw new Error('Expects keychain to be on the account index');
297
315
 
298
- const addressIndex = deriveAddressIndexKeychainFromAccount(keychain)(index);
316
+ const addressKeychain = deriveAddressIndexKeychainFromAccount(keychain)({
317
+ changeIndex,
318
+ addressIndex,
319
+ });
299
320
 
300
- if (!addressIndex.publicKey) throw new Error('Expected publicKey to be defined');
321
+ if (!addressKeychain.publicKey) throw new Error('Expected publicKey to be defined');
301
322
 
302
- const payment = getNativeSegwitPaymentFromAddressIndex(addressIndex, network);
323
+ const payment = getNativeSegwitPaymentFromAddressIndex(addressKeychain, network);
303
324
 
304
325
  if (!payment.address) throw new Error('Expected address to be defined');
305
326
  return payment.address;
@@ -359,3 +380,11 @@ export function getBitcoinInputValue(input: TransactionInput) {
359
380
  // logger.warn('Unable to find either `witnessUtxo` or `nonWitnessUtxo` in input. Defaulting to 0');
360
381
  return 0;
361
382
  }
383
+
384
+ export function isTaprootDerivationPath(path: string) {
385
+ return extractPurposeFromPath(path) === 86;
386
+ }
387
+
388
+ export function isNativeSegwitDerivationPath(path: string) {
389
+ return extractPurposeFromPath(path) === 84;
390
+ }
@@ -0,0 +1,32 @@
1
+ import { AddressType, getAddressInfo } from 'bitcoin-address-validation';
2
+ import * as bitcoin from 'bitcoinjs-lib';
3
+
4
+ export function deconstructBtcAddress(address: string) {
5
+ const typeMapping = {
6
+ [AddressType.p2pkh]: '0x00',
7
+ [AddressType.p2sh]: '0x01',
8
+ [AddressType.p2wpkh]: '0x04',
9
+ [AddressType.p2wsh]: '0x05',
10
+ [AddressType.p2tr]: '0x06',
11
+ };
12
+
13
+ const addressInfo = getAddressInfo(address);
14
+
15
+ const { bech32 } = addressInfo;
16
+ let hashbytes: Uint8Array;
17
+ if (bech32) {
18
+ hashbytes = bitcoin.address.fromBech32(address).data;
19
+ } else {
20
+ hashbytes = bitcoin.address.fromBase58Check(address).hash;
21
+ }
22
+
23
+ const type = typeMapping[addressInfo.type];
24
+ if (!type) {
25
+ throw new Error(`Unsupported address type: ${addressInfo.type}`);
26
+ }
27
+
28
+ return {
29
+ type,
30
+ hashbytes,
31
+ };
32
+ }