@leather.io/bitcoin 0.8.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.
@@ -0,0 +1,277 @@
1
+ import { hexToBytes } from '@noble/hashes/utils';
2
+ import { HDKey, Versions } from '@scure/bip32';
3
+ import { mnemonicToSeedSync } from '@scure/bip39';
4
+ import * as btc from '@scure/btc-signer';
5
+ import { TransactionInput, TransactionOutput } from '@scure/btc-signer/psbt';
6
+
7
+ import { DerivationPathDepth, extractAccountIndexFromPath } from '@leather.io/crypto';
8
+ import { BitcoinNetworkModes, NetworkModes } from '@leather.io/models';
9
+ import type { PaymentTypes } from '@leather.io/rpc';
10
+ import { defaultWalletKeyId, isDefined, whenNetwork } from '@leather.io/utils';
11
+
12
+ import { BtcSignerNetwork, getBtcSignerLibNetworkConfigByMode } from './bitcoin.network';
13
+ import { getTaprootPayment } from './p2tr-address-gen';
14
+
15
+ export interface BitcoinAccount {
16
+ type: PaymentTypes;
17
+ derivationPath: string;
18
+ keychain: HDKey;
19
+ accountIndex: number;
20
+ network: BitcoinNetworkModes;
21
+ }
22
+ export function initBitcoinAccount(derivationPath: string, policy: string): BitcoinAccount {
23
+ const xpub = extractExtendedPublicKeyFromPolicy(policy);
24
+ const network = inferNetworkFromPath(derivationPath);
25
+ return {
26
+ keychain: HDKey.fromExtendedKey(xpub, getHdKeyVersionsFromNetwork(network)),
27
+ network,
28
+ derivationPath,
29
+ type: inferPaymentTypeFromPath(derivationPath),
30
+ accountIndex: extractAccountIndexFromPath(derivationPath),
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Represents a map of `BitcoinNetworkModes` to `NetworkModes`. While Bitcoin
36
+ * has a number of networks, its often only necessary to consider the higher
37
+ * level concepts of mainnet and testnet
38
+ */
39
+ export const bitcoinNetworkToCoreNetworkMap: Record<BitcoinNetworkModes, NetworkModes> = {
40
+ mainnet: 'mainnet',
41
+ testnet: 'testnet',
42
+ regtest: 'testnet',
43
+ signet: 'testnet',
44
+ };
45
+ export function bitcoinNetworkModeToCoreNetworkMode(mode: BitcoinNetworkModes) {
46
+ return bitcoinNetworkToCoreNetworkMap[mode];
47
+ }
48
+
49
+ /**
50
+ * Map representing the "Coin Type" section of a derivation path.
51
+ * Consider example below, Coin type is one, thus testnet
52
+ * @example
53
+ * `m/86'/1'/0'/0/0`
54
+ */
55
+ export const coinTypeMap: Record<NetworkModes, 0 | 1> = {
56
+ mainnet: 0,
57
+ testnet: 1,
58
+ };
59
+
60
+ export function getBitcoinCoinTypeIndexByNetwork(network: BitcoinNetworkModes) {
61
+ return coinTypeMap[bitcoinNetworkModeToCoreNetworkMode(network)];
62
+ }
63
+
64
+ export function deriveAddressIndexKeychainFromAccount(keychain: HDKey) {
65
+ if (keychain.depth !== DerivationPathDepth.Account)
66
+ throw new Error('Keychain passed is not an account');
67
+
68
+ return (index: number) => keychain.deriveChild(0).deriveChild(index);
69
+ }
70
+
71
+ export function deriveAddressIndexZeroFromAccount(keychain: HDKey) {
72
+ return deriveAddressIndexKeychainFromAccount(keychain)(0);
73
+ }
74
+
75
+ export const ecdsaPublicKeyLength = 33;
76
+
77
+ export function ecdsaPublicKeyToSchnorr(pubKey: Uint8Array) {
78
+ if (pubKey.byteLength !== ecdsaPublicKeyLength) throw new Error('Invalid public key length');
79
+ return pubKey.slice(1);
80
+ }
81
+
82
+ // Basically same as above, to remove
83
+ export const toXOnly = (pubKey: Buffer) => (pubKey.length === 32 ? pubKey : pubKey.subarray(1, 33));
84
+
85
+ export function decodeBitcoinTx(tx: string): ReturnType<typeof btc.RawTx.decode> {
86
+ return btc.RawTx.decode(hexToBytes(tx));
87
+ }
88
+
89
+ export function getAddressFromOutScript(script: Uint8Array, bitcoinNetwork: BtcSignerNetwork) {
90
+ const outputScript = btc.OutScript.decode(script);
91
+
92
+ if (outputScript.type === 'pk' || outputScript.type === 'tr') {
93
+ return btc.Address(bitcoinNetwork).encode({
94
+ type: outputScript.type,
95
+ pubkey: outputScript.pubkey,
96
+ });
97
+ }
98
+ if (outputScript.type === 'ms' || outputScript.type === 'tr_ms') {
99
+ return btc.Address(bitcoinNetwork).encode({
100
+ type: outputScript.type,
101
+ pubkeys: outputScript.pubkeys,
102
+ m: outputScript.m,
103
+ });
104
+ }
105
+ if (outputScript.type === 'tr_ns') {
106
+ return btc.Address(bitcoinNetwork).encode({
107
+ type: outputScript.type,
108
+ pubkeys: outputScript.pubkeys,
109
+ });
110
+ }
111
+ if (outputScript.type === 'unknown') {
112
+ return 'unknown';
113
+ }
114
+ return btc.Address(bitcoinNetwork).encode({
115
+ type: outputScript.type,
116
+ hash: outputScript.hash,
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Payment type identifiers, as described by `@scure/btc-signer` library
122
+ */
123
+ export type BtcSignerLibPaymentTypeIdentifers = 'wpkh' | 'wsh' | 'tr' | 'pkh' | 'sh';
124
+
125
+ export const paymentTypeMap: Record<BtcSignerLibPaymentTypeIdentifers, PaymentTypes> = {
126
+ wpkh: 'p2wpkh',
127
+ wsh: 'p2wpkh-p2sh',
128
+ tr: 'p2tr',
129
+ pkh: 'p2pkh',
130
+ sh: 'p2sh',
131
+ };
132
+
133
+ export function btcSignerLibPaymentTypeToPaymentTypeMap(
134
+ payment: BtcSignerLibPaymentTypeIdentifers
135
+ ) {
136
+ return paymentTypeMap[payment];
137
+ }
138
+
139
+ export function isBtcSignerLibPaymentType(
140
+ payment: string
141
+ ): payment is BtcSignerLibPaymentTypeIdentifers {
142
+ return payment in paymentTypeMap;
143
+ }
144
+
145
+ export function parseKnownPaymentType(payment: BtcSignerLibPaymentTypeIdentifers | PaymentTypes) {
146
+ return isBtcSignerLibPaymentType(payment)
147
+ ? btcSignerLibPaymentTypeToPaymentTypeMap(payment)
148
+ : payment;
149
+ }
150
+
151
+ export type PaymentTypeMap<T> = Record<PaymentTypes, T>;
152
+ export function whenPaymentType(mode: PaymentTypes | BtcSignerLibPaymentTypeIdentifers) {
153
+ return <T extends unknown>(paymentMap: PaymentTypeMap<T>): T =>
154
+ paymentMap[parseKnownPaymentType(mode)];
155
+ }
156
+
157
+ /**
158
+ * Infers the Bitcoin payment type from the derivation path.
159
+ * Below we see path has 86 in it, per convention, this refers to taproot payments
160
+ * @example
161
+ * `m/86'/1'/0'/0/0`
162
+ */
163
+ export function inferPaymentTypeFromPath(path: string): PaymentTypes {
164
+ if (path.startsWith('m/84')) return 'p2wpkh';
165
+ if (path.startsWith('m/86')) return 'p2tr';
166
+ if (path.startsWith('m/44')) return 'p2pkh';
167
+ throw new Error(`Unable to infer payment type from path=${path}`);
168
+ }
169
+
170
+ export function inferNetworkFromPath(path: string): NetworkModes {
171
+ return path.split('/')[2].startsWith('0') ? 'mainnet' : 'testnet';
172
+ }
173
+
174
+ export function extractExtendedPublicKeyFromPolicy(policy: string) {
175
+ return policy.split(']')[1];
176
+ }
177
+
178
+ export function createWalletIdDecoratedPath(policy: string, walletId: string) {
179
+ return policy.split(']')[0].replace('[', '').replace('m', walletId);
180
+ }
181
+
182
+ // Primarily used to get the correct `Version` when passing Ledger Bitcoin
183
+ // extended public keys to the HDKey constructor
184
+ export function getHdKeyVersionsFromNetwork(network: NetworkModes) {
185
+ return whenNetwork(network)({
186
+ mainnet: undefined,
187
+ testnet: {
188
+ private: 0x00000000,
189
+ public: 0x043587cf,
190
+ } as Versions,
191
+ });
192
+ }
193
+
194
+ export function getBitcoinInputAddress(input: TransactionInput, bitcoinNetwork: BtcSignerNetwork) {
195
+ if (isDefined(input.witnessUtxo))
196
+ return getAddressFromOutScript(input.witnessUtxo.script, bitcoinNetwork);
197
+ if (isDefined(input.nonWitnessUtxo) && isDefined(input.index))
198
+ return getAddressFromOutScript(
199
+ input.nonWitnessUtxo.outputs[input.index]?.script,
200
+ bitcoinNetwork
201
+ );
202
+ return '';
203
+ }
204
+
205
+ export function getInputPaymentType(
206
+ input: TransactionInput,
207
+ network: BitcoinNetworkModes
208
+ ): PaymentTypes {
209
+ const address = getBitcoinInputAddress(input, getBtcSignerLibNetworkConfigByMode(network));
210
+ if (address === '') throw new Error('Input address cannot be empty');
211
+ if (address.startsWith('bc1p') || address.startsWith('tb1p') || address.startsWith('bcrt1p'))
212
+ return 'p2tr';
213
+ if (address.startsWith('bc1q') || address.startsWith('tb1q') || address.startsWith('bcrt1q'))
214
+ return 'p2wpkh';
215
+ throw new Error('Unable to infer payment type from input address');
216
+ }
217
+
218
+ // Ledger wallets are keyed by their derivation path. To reuse the look up logic
219
+ // between payment types, this factory fn accepts a fn that generates the path
220
+ export function lookUpLedgerKeysByPath(
221
+ getDerivationPath: (network: BitcoinNetworkModes, accountIndex: number) => string
222
+ ) {
223
+ return (
224
+ ledgerKeyMap: Record<string, { policy: string } | undefined>,
225
+ network: BitcoinNetworkModes
226
+ ) =>
227
+ (accountIndex: number) => {
228
+ const path = getDerivationPath(network, accountIndex);
229
+ // Single wallet mode, hardcoded default walletId
230
+ const account = ledgerKeyMap[path.replace('m', defaultWalletKeyId)];
231
+ if (!account) return;
232
+ return initBitcoinAccount(path, account.policy);
233
+ };
234
+ }
235
+
236
+ export interface GetTaprootAddressArgs {
237
+ index: number;
238
+ keychain?: HDKey;
239
+ network: BitcoinNetworkModes;
240
+ }
241
+ export function getTaprootAddress({ index, keychain, network }: GetTaprootAddressArgs) {
242
+ if (!keychain) throw new Error('Expected keychain to be provided');
243
+
244
+ if (keychain.depth !== DerivationPathDepth.Account)
245
+ throw new Error('Expects keychain to be on the account index');
246
+
247
+ const addressIndex = deriveAddressIndexKeychainFromAccount(keychain)(index);
248
+
249
+ if (!addressIndex.publicKey) {
250
+ throw new Error('Expected publicKey to be defined');
251
+ }
252
+
253
+ const payment = getTaprootPayment(addressIndex.publicKey, network);
254
+
255
+ if (!payment.address) throw new Error('Expected address to be defined');
256
+
257
+ return payment.address;
258
+ }
259
+
260
+ export function mnemonicToRootNode(secretKey: string) {
261
+ const seed = mnemonicToSeedSync(secretKey);
262
+ return HDKey.fromMasterSeed(seed);
263
+ }
264
+
265
+ export function getPsbtTxInputs(psbtTx: btc.Transaction): TransactionInput[] {
266
+ const inputsLength = psbtTx.inputsLength;
267
+ const inputs: TransactionInput[] = [];
268
+ for (let i = 0; i < inputsLength; i++) inputs.push(psbtTx.getInput(i));
269
+ return inputs;
270
+ }
271
+
272
+ export function getPsbtTxOutputs(psbtTx: btc.Transaction): TransactionOutput[] {
273
+ const outputsLength = psbtTx.outputsLength;
274
+ const outputs: TransactionOutput[] = [];
275
+ for (let i = 0; i < outputsLength; i++) outputs.push(psbtTx.getOutput(i));
276
+ return outputs;
277
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from './bip322/bip322-utils';
2
+ export * from './bip322/sign-message-bip322-bitcoinjs';
3
+ export * from './bitcoin-signer';
4
+ export * from './bitcoin.network';
5
+ export * from './bitcoin.utils';
6
+ export * from './p2tr-address-gen';
7
+ export * from './p2wpkh-address-gen';
8
+ export * from './p2wsh-p2sh-address-gen';
@@ -0,0 +1,37 @@
1
+ import { HDKey } from '@scure/bip32';
2
+ import { mnemonicToSeedSync } from '@scure/bip39';
3
+
4
+ import { deriveAddressIndexKeychainFromAccount } from './bitcoin.utils';
5
+ import { deriveTaprootAccount, getTaprootPaymentFromAddressIndex } from './p2tr-address-gen';
6
+
7
+ // TODO: this is a SECRET_KEY from @tests/mocks folder.
8
+ // Temporary until we move @tests/mocks folder to monorepo.
9
+ export const SECRET_KEY =
10
+ 'invite helmet save lion indicate chuckle world pride afford hard broom draft';
11
+
12
+ // Source:
13
+ // 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
+ ];
22
+
23
+ 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
+ });
37
+ });
@@ -0,0 +1,57 @@
1
+ import { HDKey } from '@scure/bip32';
2
+ import * as btc from '@scure/btc-signer';
3
+
4
+ import { DerivationPathDepth } from '@leather.io/crypto';
5
+ import { BitcoinNetworkModes } from '@leather.io/models';
6
+
7
+ import { getBtcSignerLibNetworkConfigByMode } from './bitcoin.network';
8
+ import {
9
+ BitcoinAccount,
10
+ ecdsaPublicKeyToSchnorr,
11
+ getBitcoinCoinTypeIndexByNetwork,
12
+ } from './bitcoin.utils';
13
+
14
+ export function getTaprootAccountDerivationPath(
15
+ network: BitcoinNetworkModes,
16
+ accountIndex: number
17
+ ) {
18
+ return `m/86'/${getBitcoinCoinTypeIndexByNetwork(network)}'/${accountIndex}'`;
19
+ }
20
+
21
+ export function getTaprootAddressIndexDerivationPath(
22
+ network: BitcoinNetworkModes,
23
+ accountIndex: number,
24
+ addressIndex: number
25
+ ) {
26
+ return getTaprootAccountDerivationPath(network, accountIndex) + `/0/${addressIndex}`;
27
+ }
28
+
29
+ export function deriveTaprootAccount(keychain: HDKey, network: BitcoinNetworkModes) {
30
+ if (keychain.depth !== DerivationPathDepth.Root)
31
+ throw new Error('Keychain passed is not an account');
32
+
33
+ return (accountIndex: number): BitcoinAccount => ({
34
+ type: 'p2tr',
35
+ network,
36
+ accountIndex,
37
+ derivationPath: getTaprootAccountDerivationPath(network, accountIndex),
38
+ keychain: keychain.derive(getTaprootAccountDerivationPath(network, accountIndex)),
39
+ });
40
+ }
41
+
42
+ export function getTaprootPayment(publicKey: Uint8Array, network: BitcoinNetworkModes) {
43
+ return btc.p2tr(
44
+ ecdsaPublicKeyToSchnorr(publicKey),
45
+ undefined,
46
+ getBtcSignerLibNetworkConfigByMode(network)
47
+ );
48
+ }
49
+
50
+ export function getTaprootPaymentFromAddressIndex(keychain: HDKey, network: BitcoinNetworkModes) {
51
+ if (keychain.depth !== DerivationPathDepth.AddressIndex)
52
+ throw new Error('Keychain passed is not an address index');
53
+
54
+ if (!keychain.publicKey) throw new Error('Keychain has no public key');
55
+
56
+ return getTaprootPayment(keychain.publicKey, network);
57
+ }
@@ -0,0 +1,45 @@
1
+ import { deriveNativeSegWitReceiveAddressIndex } from './p2wpkh-address-gen';
2
+
3
+ describe('Bitcoin bech32 (P2WPKH address derivation', () => {
4
+ describe('from extended public key', () => {
5
+ const accounts = [
6
+ {
7
+ path: "m/84'/0'/0'",
8
+ extended_public_key:
9
+ 'xpub6CwY13JDrzeY2oWjP9dbiyLHQh3JVWCvBTCfD7WREBUpBUmtCu4bgxfSGrvaLDbZaMdw2nsPeTFv6AokWkVqh4rbKpsxg7GgEu543Qwvyff',
10
+ private_key: 'L1FA9VHZNkgCBW9fS76zDHcjuK72LE4gGVAMnN67onRRCoDJvZJi',
11
+ public_key: '0211758b68eb9b0e4e9610c49739f2ce039732033ba47e125bbdf64ef6cd586ef3',
12
+ zeroIndexChildAddress: 'bc1qa4ypkks2kfpawyy5mautjfqc6wv703ckm7puux',
13
+ },
14
+ {
15
+ path: "m/84'/0'/1'",
16
+ mnemonic:
17
+ 'token spatial butter drill city debate pipe shoot target pencil tonight gallery dog globe copy hybrid convince spell load maximum impose crazy engage way',
18
+ extended_public_key:
19
+ 'xpub6CwY13JDrzeY55xGbiHxHwZSZpbkmrM7QMag3yVgZi62zaYFsBAUam1kghZZx4hDgDdkDzAMxc8xmpcyGAb1EoXoB7Vn7WTiUEaCEd3CcPq',
20
+ private_key: 'Kyhx4Zz1iYmCGx1gLnPE5ZFphBf16BoRKokU6B8KbxkJ7tM511de',
21
+ public_key: '025f6abba7947109c5e5ba0fed5e7b99b0ce5b06ccbca86539e6eca261c4507559',
22
+ zeroIndexChildAddress: 'bc1q5aptjy5l9q4qcykvccpwlqcvzydg744qkv94d3',
23
+ },
24
+ {
25
+ path: "m/84'/0'/2'",
26
+ mnemonic:
27
+ 'token spatial butter drill city debate pipe shoot target pencil tonight gallery dog globe copy hybrid convince spell load maximum impose crazy engage way',
28
+ extended_public_key:
29
+ 'xpub6CwY13JDrzeY7qyP5MCBqA3hmB9oX8mjpbt6YWPfCRb9fus8Yrt84xxzh1Ci2wyW8intyoxmr3MjCHCtbs458uboWZVV8WFeHZBveJHVG71',
30
+ private_key: 'L1CzwqocLUQgH6GeH6bBKRaRnGLF81249Wbd14uTzLaUGE5qMdD7',
31
+ public_key: '022b804094c9b74a93d51e6bb3b1ae8378027e810058bbcb34ac54f3a307a225d1',
32
+ zeroIndexChildAddress: 'bc1q253fdeyzuwx58xxssd3a2xw2gq7khhpmr6vgnh',
33
+ },
34
+ ];
35
+
36
+ describe.each(accounts)('Account', account => {
37
+ const keychain = deriveNativeSegWitReceiveAddressIndex({
38
+ xpub: account.extended_public_key,
39
+ network: 'mainnet',
40
+ });
41
+ test('bech 32 address', () =>
42
+ expect(keychain?.address).toEqual(account.zeroIndexChildAddress));
43
+ });
44
+ });
45
+ });
@@ -0,0 +1,68 @@
1
+ import { HDKey } from '@scure/bip32';
2
+ import * as btc from '@scure/btc-signer';
3
+
4
+ import { DerivationPathDepth } from '@leather.io/crypto';
5
+ import { BitcoinNetworkModes } from '@leather.io/models';
6
+
7
+ import { getBtcSignerLibNetworkConfigByMode } from './bitcoin.network';
8
+ import {
9
+ BitcoinAccount,
10
+ deriveAddressIndexZeroFromAccount,
11
+ getBitcoinCoinTypeIndexByNetwork,
12
+ } from './bitcoin.utils';
13
+
14
+ export function getNativeSegwitAccountDerivationPath(
15
+ network: BitcoinNetworkModes,
16
+ accountIndex: number
17
+ ) {
18
+ return `m/84'/${getBitcoinCoinTypeIndexByNetwork(network)}'/${accountIndex}'`;
19
+ }
20
+
21
+ export function getNativeSegwitAddressIndexDerivationPath(
22
+ network: BitcoinNetworkModes,
23
+ accountIndex: number,
24
+ addressIndex: number
25
+ ) {
26
+ return getNativeSegwitAccountDerivationPath(network, accountIndex) + `/0/${addressIndex}`;
27
+ }
28
+
29
+ export function deriveNativeSegwitAccountFromRootKeychain(
30
+ keychain: HDKey,
31
+ network: BitcoinNetworkModes
32
+ ) {
33
+ if (keychain.depth !== DerivationPathDepth.Root) throw new Error('Keychain passed is not a root');
34
+ return (accountIndex: number): BitcoinAccount => ({
35
+ type: 'p2wpkh',
36
+ network,
37
+ accountIndex,
38
+ derivationPath: getNativeSegwitAccountDerivationPath(network, accountIndex),
39
+ keychain: keychain.derive(getNativeSegwitAccountDerivationPath(network, accountIndex)),
40
+ });
41
+ }
42
+
43
+ export function getNativeSegWitPaymentFromAddressIndex(
44
+ keychain: HDKey,
45
+ network: BitcoinNetworkModes
46
+ ) {
47
+ if (keychain.depth !== DerivationPathDepth.AddressIndex)
48
+ throw new Error('Keychain passed is not an address index');
49
+
50
+ if (!keychain.publicKey) throw new Error('Keychain does not have a public key');
51
+
52
+ return btc.p2wpkh(keychain.publicKey, getBtcSignerLibNetworkConfigByMode(network));
53
+ }
54
+
55
+ interface DeriveNativeSegWitReceiveAddressIndexArgs {
56
+ xpub: string;
57
+ network: BitcoinNetworkModes;
58
+ }
59
+ export function deriveNativeSegWitReceiveAddressIndex({
60
+ xpub,
61
+ network,
62
+ }: DeriveNativeSegWitReceiveAddressIndexArgs) {
63
+ if (!xpub) return;
64
+ const keychain = HDKey.fromExtendedKey(xpub);
65
+ if (!keychain) return;
66
+ const zeroAddressIndex = deriveAddressIndexZeroFromAccount(keychain);
67
+ return getNativeSegWitPaymentFromAddressIndex(zeroAddressIndex, network);
68
+ }
@@ -0,0 +1,180 @@
1
+ import ecc from '@bitcoinerlab/secp256k1';
2
+ import { sha256 } from '@noble/hashes/sha256';
3
+ import { base58check } from '@scure/base';
4
+ import { HDKey } from '@scure/bip32';
5
+ import { mnemonicToSeedSync } from '@scure/bip39';
6
+ import { hashP2WPKH } from '@stacks/transactions';
7
+ import { BIP32Factory } from 'bip32';
8
+ import * as bitcoin from 'bitcoinjs-lib';
9
+
10
+ import {
11
+ decodeCompressedWifPrivateKey,
12
+ deriveBtcBip49SeedFromMnemonic,
13
+ deriveRootBtcKeychain,
14
+ makePayToScriptHashAddress,
15
+ makePayToScriptHashAddressBytes,
16
+ makePayToScriptHashKeyHash,
17
+ payToScriptHashTestnetPrefix,
18
+ publicKeyToPayToScriptHashAddress,
19
+ } from './p2wsh-p2sh-address-gen';
20
+
21
+ describe('Bitcoin SegWit (P2WPKH-P2SH) address generation', () => {
22
+ const bip32 = BIP32Factory(ecc);
23
+
24
+ const phrase =
25
+ 'above view guide write long gift chimney own guide mirror word ski code monster gauge bracket until stem feed scale smart truth toy limb';
26
+
27
+ describe('Verify against wagyu results', () => {
28
+ // Keys generated with `wagyu`
29
+ // $ wagyu bitcoin import-hd -m "<phrase>" -d "m/49'/0'/0'/0/0" --format segwit --json
30
+ const keys = [
31
+ {
32
+ path: "m/49'/0'/0'/0/0",
33
+ extended_private_key:
34
+ 'xprvA2WTEJy9NLu57C55yCCPvXLzGq6mGjL3oc81T7vMv2WYREFuAJV3HT4pJYF4a3JRCnyU95rgq4eY2X6cCJTJQYEHmHrvyfy5pCnPcqeTikK',
35
+ extended_public_key:
36
+ 'xpub6FVodpW3CiTNKg9Z5DjQHfHiprwFgC3uAq3cFWKyUN3XJ2b3hqoHqFPJ9p9r4QK5f9fs1VztRMrjSy6M6HvVLtpC6KipJ2whmAhk9V3GZZ2',
37
+ private_key: 'L5iYDFDUDSGnjtWUT8gKDvCcsfMna5fAk6pQo5DZandks5r7Av4Q',
38
+ public_key: '03715f44ce96a11743c97e4ef5954e78482107a9658f1c5f33bc9e70dc171e56e5',
39
+ address: '3CTTwjVZ59ykFH2DSQpF3iLWM3fESjFcJ9',
40
+ format: 'p2sh_p2wpkh',
41
+ network: 'mainnet',
42
+ },
43
+ {
44
+ path: "m/49'/0'/0'/0/1",
45
+ extended_private_key:
46
+ 'xprvA2WTEJy9NLu5ANouN242mgeiXNcndxwCRHRj3B3C96zWPj7Cgp22frkXKLGiRK59fg6nkGHHityZkVdjBfp7oLP8gf2jy2iHf21qaTWHQfd',
47
+ extended_public_key:
48
+ 'xpub6FVodpW3CiTNNrtNU3b38pbT5QTH3Rf3nWMKqZSohSXVGXSMEMLHDf51AZpFphHQXCZzAMXGHraNyBmRXHKbgKQETn8mr6oUTAXBYJJBGEy',
49
+ private_key: 'L4Xt5Ricu9HAg3t92uyqNpnFXKXCgt6DuUtVMkaTsqgXs7rnxjSY',
50
+ public_key: '02166ce8acc10a07f877436d673c1876ad2b68d7c78075972d4b2d9f8e1d0d984d',
51
+ address: '36R4QBx4HqRSiRswcFeCe6KUgk2JY9aP87',
52
+ format: 'p2sh_p2wpkh',
53
+ network: 'mainnet',
54
+ },
55
+ {
56
+ path: "m/49'/0'/0'/0/2",
57
+ extended_private_key:
58
+ 'xprvA2WTEJy9NLu5E4vHyjZWFKoTnibqRqNquBzPR7sRoMn44bvpq6ES7cbRmxmxZAtiTDFvRUFWzpsYqbuNF4WapLdJJzrYTDDY6k4QhqHEkXG',
59
+ extended_public_key:
60
+ 'xpub6FVodpW3CiTNSYzm5m6WcTkCLkSKqJ6hGQuzDWH3MhK2wQFyNdYgfQuudCnLj4afakVMnLpHBuAY13aHFh3giri7MRZ8gEddLtr9wdgcvpn',
61
+ private_key: 'L2aBwidPCi2YjxDriNAtxfrMFbS3PsKeUUSnnt8cQQRKvpPciUqo',
62
+ public_key: '0218e2229c75d57f2a0bd6dfdfa50a1a736d19fb40a1f18a675d34960b088df01e',
63
+ address: '3BU1wA95ELhgweMSazGh42CHD5K64XGUop',
64
+ format: 'p2sh_p2wpkh',
65
+ network: 'mainnet',
66
+ },
67
+ {
68
+ path: "m/49'/0'/0'/0/3",
69
+ extended_private_key:
70
+ 'xprvA2WTEJy9NLu5FKNrRe3coYxbKXjjzibJ6uouC9v29s6Ut8KJvmqXWmvzPTb9wPfRjYzvcq91QyV6B7P38XmZpTquTDoVyp4vv5baiyf8EZT',
71
+ extended_public_key:
72
+ 'xpub6FVodpW3CiTNToTKXfadAguKsZaEQBK9U8jVzYKdiCdTkveTUK9n4aFUEiuixKhQeqrrqX9iKTYFmpJXdc2im8y2JzYCuiEZvegLuTAetxJ',
73
+ private_key: 'L2Mx4mkmuQMnRxf1gCYSSEugDj6TeDS45eYjXYdanJ7MEX9Xp8Fe',
74
+ public_key: '02bf94312be9021d61d1ed917c5e8542d215180afe5db35c5574e3382b3b8469f0',
75
+ address: '3MCzNqbNy7k8hnyenwpsdHahY2yBVQJQsz',
76
+ format: 'p2sh_p2wpkh',
77
+ network: 'mainnet',
78
+ },
79
+ ] as const;
80
+
81
+ describe.each(keys)('Core libraries: bip32, bip39, bitcoinjs-lib', key => {
82
+ const seed = mnemonicToSeedSync(phrase);
83
+ const root = bip32.fromSeed(Buffer.from(seed));
84
+ const child = root.derivePath(key.path);
85
+
86
+ describe(key.path, () => {
87
+ test(`public key`, () => expect(child.publicKey.toString('hex')).toEqual(key.public_key));
88
+
89
+ test(`extended public key`, () =>
90
+ expect(child.neutered().toBase58()).toEqual(key.extended_public_key));
91
+
92
+ test(`private key`, () =>
93
+ expect(child.privateKey).toEqual(
94
+ Buffer.from(decodeCompressedWifPrivateKey(key.private_key))
95
+ ));
96
+
97
+ test(`extended private key`, () =>
98
+ expect(child.privateKey).toEqual(bip32.fromBase58(key.extended_private_key).privateKey));
99
+
100
+ test(`segwit address`, () => {
101
+ const bitcoinPayment = bitcoin.payments.p2sh({
102
+ redeem: bitcoin.payments.p2wpkh({ pubkey: child.publicKey }),
103
+ });
104
+ expect(bitcoinPayment.address).toEqual(key.address);
105
+ });
106
+ });
107
+ });
108
+
109
+ describe.each(keys)('@scure/*', key => {
110
+ let seed: Uint8Array;
111
+ let root: HDKey;
112
+ let child: HDKey;
113
+
114
+ beforeAll(async () => {
115
+ seed = await deriveBtcBip49SeedFromMnemonic(phrase);
116
+ root = deriveRootBtcKeychain(seed);
117
+ child = root.derive(key.path);
118
+ });
119
+
120
+ describe(key.path, () => {
121
+ test(`public key`, () =>
122
+ expect(Buffer.from(child.publicKey!).toString('hex')).toEqual(key.public_key));
123
+
124
+ test(`extended public key`, () =>
125
+ expect(child.publicExtendedKey).toEqual(key.extended_public_key));
126
+
127
+ test(`private key`, () =>
128
+ expect(child.privateKey).toEqual(decodeCompressedWifPrivateKey(key.private_key)));
129
+
130
+ test(`extended private key`, () =>
131
+ expect(child.privateKey).toEqual(
132
+ HDKey.fromExtendedKey(key.extended_private_key).privateKey
133
+ ));
134
+
135
+ test(`extended private key`, () =>
136
+ expect(child.privateExtendedKey).toEqual(key.extended_private_key));
137
+
138
+ test(`segwit address`, () => {
139
+ expect(publicKeyToPayToScriptHashAddress(child.publicKey!, key.network)).toEqual(
140
+ key.address
141
+ );
142
+ });
143
+ });
144
+ });
145
+ });
146
+
147
+ // Replicating test vector from BIP
148
+ // https://en.bitcoin.it/wiki/BIP_0049
149
+ test('BIP-0049 test vector', () => {
150
+ const publicKey = Buffer.from(
151
+ '03a1af804ac108a8a51782198c2d034b28bf90c8803f5a53f76276fa69a4eae77f',
152
+ 'hex'
153
+ );
154
+ const hash = makePayToScriptHashKeyHash(publicKey);
155
+
156
+ // stacks.js implementation
157
+ const addressBytesFromStacks = hashP2WPKH(publicKey);
158
+ expect(addressBytesFromStacks).toEqual('336caa13e08b96080a32b5d818d59b4ab3b36742');
159
+
160
+ // wallet implementation
161
+ const addressBytes = makePayToScriptHashAddressBytes(hash);
162
+ const addressBytesHex = Buffer.from(addressBytes).toString('hex');
163
+ expect(addressBytesHex).toEqual('336caa13e08b96080a32b5d818d59b4ab3b36742');
164
+
165
+ // compare lib output
166
+ expect(addressBytesFromStacks).toEqual(addressBytesHex);
167
+
168
+ const address = base58check(sha256).encode(
169
+ Buffer.concat([
170
+ Buffer.of(payToScriptHashTestnetPrefix),
171
+ Buffer.from(addressBytesFromStacks, 'hex'),
172
+ ])
173
+ );
174
+ const addressWithLib = makePayToScriptHashAddress(addressBytes, 'testnet');
175
+
176
+ expect(address).toEqual(addressWithLib);
177
+
178
+ expect(addressWithLib).toEqual('2Mww8dCYPUpKHofjgcXcBCEGmniw9CoaiD2');
179
+ });
180
+ });