Skip to content

Commit 26ef673

Browse files
committed
refactor: bitcoin methods
1 parent 974aa25 commit 26ef673

File tree

8 files changed

+128
-63
lines changed

8 files changed

+128
-63
lines changed

packages/bitcoin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"@stacks/common": "6.13.0",
4242
"@stacks/transactions": "6.15.0",
4343
"bip32": "4.0.0",
44-
"bitcoinjs-lib": "6.1.3",
44+
"bitcoinjs-lib": "6.1.5",
4545
"ecpair": "2.1.0",
4646
"varuint-bitcoin": "1.1.2"
4747
},

packages/bitcoin/src/bip322/bip322-utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import ecc from '@bitcoinerlab/secp256k1';
22
import { sha256 } from '@noble/hashes/sha256';
33
import { hexToBytes, utf8ToBytes } from '@stacks/common';
44
import * as bitcoin from 'bitcoinjs-lib';
5-
import { ECPairFactory } from 'ecpair';
5+
import ECPairFactory from 'ecpair';
66
import { encode } from 'varuint-bitcoin';
77

8-
import type { PaymentTypes } from '@leather-wallet/rpc';
8+
import { PaymentTypes } from '@leather-wallet/rpc';
99
import { isString } from '@leather-wallet/utils';
1010

1111
import { toXOnly } from '../bitcoin.utils';

packages/bitcoin/src/bip322/sign-message-bip322-bitcoinjs.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { base64 } from '@scure/base';
2+
import * as btc from '@scure/btc-signer';
23
import * as bitcoin from 'bitcoinjs-lib';
34

45
import { BitcoinNetworkModes } from '@leather-wallet/models';
@@ -44,7 +45,7 @@ function createToSignTx(toSpendTxHex: Buffer, script: Buffer, network: BitcoinNe
4445
virtualToSign.setVersion(0);
4546
const prevTxHash = toSpendTxHex;
4647
const prevOutIndex = 0;
47-
const toSignScriptSig = bitcoin.script.compile([106]);
48+
const toSignScriptSig = bitcoin.script.compile([bitcoin.script.OPS.OP_RETURN]);
4849

4950
virtualToSign.addInput({
5051
hash: prevTxHash,
@@ -61,23 +62,25 @@ interface SignBip322MessageSimple {
6162
address: string;
6263
message: string;
6364
network: BitcoinNetworkModes;
64-
signPsbt(psbt: bitcoin.Psbt): void;
65+
signPsbt(psbt: bitcoin.Psbt): Promise<btc.Transaction>;
6566
}
66-
export function signBip322MessageSimple(args: SignBip322MessageSimple) {
67+
export async function signBip322MessageSimple(args: SignBip322MessageSimple) {
6768
const { address, message, network, signPsbt } = args;
6869

6970
const { virtualToSpend, script } = createToSpendTx(address, message, network);
7071

7172
const virtualToSign = createToSignTx(virtualToSpend.getHash(), script, network);
7273

73-
signPsbt(virtualToSign);
74+
const signedTx = await signPsbt(virtualToSign);
7475

75-
virtualToSign.finalizeInput(0);
76+
const asBitcoinJsTransaction = bitcoin.Psbt.fromBuffer(Buffer.from(signedTx.toPSBT()));
77+
78+
asBitcoinJsTransaction.finalizeInput(0);
7679

7780
// sign the tx
7881
// section 5.1
7982
// github.com/LegReq/bip0322-signatures/blob/master/BIP0322_signing.ipynb
80-
const toSignTx = virtualToSign.extractTransaction();
83+
const toSignTx = asBitcoinJsTransaction.extractTransaction();
8184

8285
const result = encodeMessageWitnessData(toSignTx.ins[0].witness);
8386

packages/bitcoin/src/bip322/sign-message-bip322.spec.ts

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ describe(signBip322MessageSimple.name, () => {
3636
const nativeSegwitAddress = btc.getAddress('wpkh', testVectorKey);
3737
const payment = btc.p2wpkh(secp.getPublicKey(testVectorKey, true));
3838

39-
function signPsbt(psbt: bitcoin.Psbt) {
39+
async function signPsbt(psbt: bitcoin.Psbt) {
4040
psbt.signAllInputs(createNativeSegwitBitcoinJsSigner(Buffer.from(testVectorKey)));
41+
return btc.Transaction.fromPSBT(psbt.toBuffer());
4142
}
4243

4344
if (!nativeSegwitAddress) throw new Error('nativeSegwitAddress is undefined');
@@ -47,16 +48,16 @@ describe(signBip322MessageSimple.name, () => {
4748
expect(payment.address).toEqual('bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l');
4849
});
4950

50-
test('Signature: "" (empty string)', () => {
51+
test('Signature: "" (empty string)', async () => {
5152
const {
5253
virtualToSpend: emptyStringToSpend,
5354
virtualToSign: emptyStringToSign,
5455
signature: emptyStringSig,
55-
} = signBip322MessageSimple({
56+
} = await signBip322MessageSimple({
5657
address: nativeSegwitAddress,
5758
message: '',
58-
signPsbt,
5959
network: 'mainnet',
60+
signPsbt,
6061
});
6162
expect(emptyStringToSpend.getId()).toEqual(
6263
'c5680aa69bb8d860bf82d4e9cd3504b55dde018de765a91bb566283c545a99a7'
@@ -74,13 +75,14 @@ describe(signBip322MessageSimple.name, () => {
7475
});
7576

7677
const helloWorld = 'Hello World';
77-
test(`Signature: "${helloWorld}"`, () => {
78-
const { virtualToSpend, virtualToSign, unencodedSig, signature } = signBip322MessageSimple({
79-
address: nativeSegwitAddress,
80-
message: helloWorld,
81-
signPsbt,
82-
network: 'mainnet',
83-
});
78+
test(`Signature: "${helloWorld}"`, async () => {
79+
const { virtualToSpend, virtualToSign, unencodedSig, signature } =
80+
await signBip322MessageSimple({
81+
address: nativeSegwitAddress,
82+
message: helloWorld,
83+
network: 'mainnet',
84+
signPsbt,
85+
});
8486

8587
// section 3
8688
expect(virtualToSpend.getId()).toEqual(
@@ -119,11 +121,12 @@ describe(signBip322MessageSimple.name, () => {
119121
ecdsaPublicKeyToSchnorr(secp.getPublicKey(Buffer.from(testVectorKey), true))
120122
);
121123

122-
function signPsbt(psbt: bitcoin.Psbt) {
124+
async function signPsbt(psbt: bitcoin.Psbt) {
123125
psbt.data.inputs.forEach(
124126
input => (input.tapInternalKey = Buffer.from(payment.tapInternalKey))
125127
);
126128
psbt.signAllInputs(createTaprootBitcoinJsSigner(Buffer.from(testVectorKey)));
129+
return btc.Transaction.fromPSBT(psbt.toBuffer());
127130
}
128131

129132
test('Addresses against taproot test vectors', () => {
@@ -136,8 +139,8 @@ describe(signBip322MessageSimple.name, () => {
136139
});
137140

138141
// Taproot signatures verified with verifymessage request to node
139-
test('Signature: "" (empty string)', () => {
140-
const { signature } = signBip322MessageSimple({
142+
test('Signature: "" (empty string)', async () => {
143+
const { signature } = await signBip322MessageSimple({
141144
address: taprootAddress,
142145
message: '',
143146
network: 'mainnet',
@@ -149,16 +152,16 @@ describe(signBip322MessageSimple.name, () => {
149152
);
150153
});
151154

152-
test('Signature: "HiroWalletIsTheBest"', () => {
153-
const { signature } = signBip322MessageSimple({
155+
test('Signature: "WearLeather"', async () => {
156+
const { signature } = await signBip322MessageSimple({
154157
address: taprootAddress,
155-
message: 'HiroWalletIsTheBest',
158+
message: 'WearLeather',
156159
network: 'mainnet',
157160
signPsbt,
158161
});
159162

160163
expect(signature).toEqual(
161-
'AUAJNdp5SEAFCDYrIR8TQRkgbNo6P+dUeL97eadGjPWC8iPrhngZSBZvcImVk/HEb3zq3xuyGqyP0dqR7CH2HCa7'
164+
'AUDjK8SJX34boek3m3EKXI94AMBZynJUmdqgO7i4z6JKG6gkUgp+brkWl0ylzWb+8enM4s4B4TWel0iCmcQrKNWS'
162165
);
163166
});
164167
});

packages/bitcoin/src/bip322/sign-message-bip322.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@ import { hashBip322Message } from './bip322-utils';
77
// Ran into difficiulties with btc-signer vs bitcoinjs-lib
88
// Using that library to unblock for now, but we should go
99
// back and replace it when possible.
10-
// Ref PR: https://github.com/leather-wallet/extension/pull/3679
11-
// Prefixing it with underscore for now until we find a use for this function.
1210
// ts-unused-exports:disable-next-line
13-
export function _signBip322MessageSimple(script: Uint8Array, message: string) {
11+
export function _UNSAFE_signBip322MessageSimple(script: Uint8Array, message: string) {
1412
// nVersion = 0
1513
// nLockTime = 0
1614
// vin[0].prevout.hash = 0000...000

packages/bitcoin/src/bitcoin.utils.ts

Lines changed: 89 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { hexToBytes } from '@noble/hashes/utils';
22
import { HDKey, Versions } from '@scure/bip32';
33
import { mnemonicToSeedSync } from '@scure/bip39';
44
import * as btc from '@scure/btc-signer';
5+
import { TransactionInput, TransactionOutput } from '@scure/btc-signer/psbt';
56

67
import { DerivationPathDepth, extractAccountIndexFromPath } from '@leather-wallet/crypto';
78
import { BitcoinNetworkModes, NetworkModes } from '@leather-wallet/models';
89
import type { PaymentTypes } from '@leather-wallet/rpc';
9-
import { defaultWalletKeyId, whenNetwork } from '@leather-wallet/utils';
10+
import { defaultWalletKeyId, isDefined, whenNetwork } from '@leather-wallet/utils';
1011

11-
import { BtcSignerNetwork } from './bitcoin.network';
12+
import { BtcSignerNetwork, getBtcSignerLibNetworkConfigByMode } from './bitcoin.network';
1213
import { getTaprootPayment } from './p2tr-address-gen';
1314

1415
export interface BitcoinAccount {
@@ -18,8 +19,24 @@ export interface BitcoinAccount {
1819
accountIndex: number;
1920
network: BitcoinNetworkModes;
2021
}
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+
}
2133

22-
const bitcoinNetworkToCoreNetworkMap: Record<BitcoinNetworkModes, NetworkModes> = {
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> = {
2340
mainnet: 'mainnet',
2441
testnet: 'testnet',
2542
regtest: 'testnet',
@@ -29,7 +46,13 @@ export function bitcoinNetworkModeToCoreNetworkMode(mode: BitcoinNetworkModes) {
2946
return bitcoinNetworkToCoreNetworkMap[mode];
3047
}
3148

32-
const coinTypeMap: Record<NetworkModes, 0 | 1> = {
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> = {
3356
mainnet: 0,
3457
testnet: 1,
3558
};
@@ -49,7 +72,7 @@ export function deriveAddressIndexZeroFromAccount(keychain: HDKey) {
4972
return deriveAddressIndexKeychainFromAccount(keychain)(0);
5073
}
5174

52-
const ecdsaPublicKeyLength = 33;
75+
export const ecdsaPublicKeyLength = 33;
5376

5477
export function ecdsaPublicKeyToSchnorr(pubKey: Uint8Array) {
5578
if (pubKey.byteLength !== ecdsaPublicKeyLength) throw new Error('Invalid public key length');
@@ -94,48 +117,61 @@ export function getAddressFromOutScript(script: Uint8Array, bitcoinNetwork: BtcS
94117
});
95118
}
96119

97-
type BtcSignerLibPaymentTypeIdentifers = 'wpkh' | 'wsh' | 'tr' | 'pkh' | 'sh';
120+
/**
121+
* Payment type identifiers, as described by `@scure/btc-signer` library
122+
*/
123+
export type BtcSignerLibPaymentTypeIdentifers = 'wpkh' | 'wsh' | 'tr' | 'pkh' | 'sh';
98124

99-
const paymentTypeMap: Record<BtcSignerLibPaymentTypeIdentifers, PaymentTypes> = {
125+
export const paymentTypeMap: Record<BtcSignerLibPaymentTypeIdentifers, PaymentTypes> = {
100126
wpkh: 'p2wpkh',
101127
wsh: 'p2wpkh-p2sh',
102128
tr: 'p2tr',
103129
pkh: 'p2pkh',
104130
sh: 'p2sh',
105131
};
106132

107-
function btcSignerLibPaymentTypeToPaymentTypeMap(payment: BtcSignerLibPaymentTypeIdentifers) {
133+
export function btcSignerLibPaymentTypeToPaymentTypeMap(
134+
payment: BtcSignerLibPaymentTypeIdentifers
135+
) {
108136
return paymentTypeMap[payment];
109137
}
110138

111-
function isBtcSignerLibPaymentType(payment: string): payment is BtcSignerLibPaymentTypeIdentifers {
139+
export function isBtcSignerLibPaymentType(
140+
payment: string
141+
): payment is BtcSignerLibPaymentTypeIdentifers {
112142
return payment in paymentTypeMap;
113143
}
114144

115-
function parseKnownPaymentType(payment: BtcSignerLibPaymentTypeIdentifers | PaymentTypes) {
145+
export function parseKnownPaymentType(payment: BtcSignerLibPaymentTypeIdentifers | PaymentTypes) {
116146
return isBtcSignerLibPaymentType(payment)
117147
? btcSignerLibPaymentTypeToPaymentTypeMap(payment)
118148
: payment;
119149
}
120150

121-
type PaymentTypeMap<T> = Record<PaymentTypes, T>;
151+
export type PaymentTypeMap<T> = Record<PaymentTypes, T>;
122152
export function whenPaymentType(mode: PaymentTypes | BtcSignerLibPaymentTypeIdentifers) {
123153
return <T extends unknown>(paymentMap: PaymentTypeMap<T>): T =>
124154
paymentMap[parseKnownPaymentType(mode)];
125155
}
126156

127-
function inferPaymentTypeFromPath(path: string): PaymentTypes {
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 {
128164
if (path.startsWith('m/84')) return 'p2wpkh';
129165
if (path.startsWith('m/86')) return 'p2tr';
130166
if (path.startsWith('m/44')) return 'p2pkh';
131167
throw new Error(`Unable to infer payment type from path=${path}`);
132168
}
133169

134-
function inferNetworkFromPath(path: string): NetworkModes {
170+
export function inferNetworkFromPath(path: string): NetworkModes {
135171
return path.split('/')[2].startsWith('0') ? 'mainnet' : 'testnet';
136172
}
137173

138-
function extractExtendedPublicKeyFromPolicy(policy: string) {
174+
export function extractExtendedPublicKeyFromPolicy(policy: string) {
139175
return policy.split(']')[1];
140176
}
141177

@@ -155,6 +191,30 @@ export function getHdKeyVersionsFromNetwork(network: NetworkModes) {
155191
});
156192
}
157193

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+
158218
// Ledger wallets are keyed by their derivation path. To reuse the look up logic
159219
// between payment types, this factory fn accepts a fn that generates the path
160220
export function lookUpLedgerKeysByPath(
@@ -173,19 +233,7 @@ export function lookUpLedgerKeysByPath(
173233
};
174234
}
175235

176-
function initBitcoinAccount(derivationPath: string, policy: string): BitcoinAccount {
177-
const xpub = extractExtendedPublicKeyFromPolicy(policy);
178-
const network = inferNetworkFromPath(derivationPath);
179-
return {
180-
keychain: HDKey.fromExtendedKey(xpub, getHdKeyVersionsFromNetwork(network)),
181-
network,
182-
derivationPath,
183-
type: inferPaymentTypeFromPath(derivationPath),
184-
accountIndex: extractAccountIndexFromPath(derivationPath),
185-
};
186-
}
187-
188-
interface GetTaprootAddressArgs {
236+
export interface GetTaprootAddressArgs {
189237
index: number;
190238
keychain?: HDKey;
191239
network: BitcoinNetworkModes;
@@ -213,3 +261,17 @@ export function mnemonicToRootNode(secretKey: string) {
213261
const seed = mnemonicToSeedSync(secretKey);
214262
return HDKey.fromMasterSeed(seed);
215263
}
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+
}

packages/bitcoin/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
export * from './bip322/bip322-utils';
22
export * from './bip322/sign-message-bip322-bitcoinjs';
3-
export * from './bip322/sign-message-bip322';
43
export * from './bitcoin-signer';
54
export * from './bitcoin.network';
65
export * from './bitcoin.utils';

0 commit comments

Comments
 (0)