@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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +52 -0
- package/dist/index.d.ts +50 -33
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +81 -46
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/coin-selection/calculate-max-spend.spec.ts +6 -11
- package/src/coin-selection/calculate-max-spend.ts +7 -13
- package/src/index.ts +1 -1
- package/src/payments/p2tr-address-gen.spec.ts +73 -21
- package/src/payments/p2tr-address-gen.ts +14 -6
- package/src/payments/p2wpkh-address-gen.spec.ts +37 -1
- package/src/payments/p2wpkh-address-gen.ts +14 -6
- package/src/utils/bitcoin.descriptors.ts +21 -9
- package/src/utils/bitcoin.utils.spec.ts +110 -1
- package/src/utils/bitcoin.utils.ts +40 -11
- package/src/utils/deconstruct-btc-address.ts +32 -0
- package/src/utils/lookup-derivation-by-address.ts +16 -5
- package/src/validation/transaction-validation.spec.ts +0 -97
- package/src/validation/transaction-validation.ts +0 -44
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.
|
|
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.
|
|
36
|
-
"@leather.io/crypto": "1.12.
|
|
37
|
-
"@leather.io/
|
|
38
|
-
"@leather.io/
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
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)(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
26
|
-
accountIndex
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 {
|
|
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
|
|
26
|
-
accountIndex
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 {
|
|
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 (
|
|
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)(
|
|
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
|
-
|
|
275
|
+
changeIndex: number;
|
|
276
|
+
addressIndex: number;
|
|
272
277
|
keychain?: HDKey;
|
|
273
278
|
network: BitcoinNetworkModes;
|
|
274
279
|
}
|
|
275
280
|
|
|
276
|
-
export function getTaprootAddress({
|
|
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
|
|
292
|
+
const addresskeychain = deriveAddressIndexKeychainFromAccount(keychain)({
|
|
293
|
+
changeIndex,
|
|
294
|
+
addressIndex,
|
|
295
|
+
});
|
|
283
296
|
|
|
284
|
-
if (!
|
|
297
|
+
if (!addresskeychain.publicKey) throw new Error('Expected publicKey to be defined');
|
|
285
298
|
|
|
286
|
-
const payment = getTaprootPayment(
|
|
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({
|
|
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
|
|
316
|
+
const addressKeychain = deriveAddressIndexKeychainFromAccount(keychain)({
|
|
317
|
+
changeIndex,
|
|
318
|
+
addressIndex,
|
|
319
|
+
});
|
|
299
320
|
|
|
300
|
-
if (!
|
|
321
|
+
if (!addressKeychain.publicKey) throw new Error('Expected publicKey to be defined');
|
|
301
322
|
|
|
302
|
-
const payment = getNativeSegwitPaymentFromAddressIndex(
|
|
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
|
+
}
|