@leather.io/bitcoin 0.26.8 → 0.26.10
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 +6 -6
- package/CHANGELOG.md +25 -0
- package/dist/index.d.ts +69 -45
- package/dist/index.js +41 -21
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/coin-selection/calculate-max-spend.ts +1 -2
- package/src/coin-selection/coin-selection.mocks.ts +4 -2
- package/src/coin-selection/coin-selection.ts +10 -14
- package/src/coin-selection/coin-selection.utils.ts +13 -11
- package/src/fees/bitcoin-fees.spec.ts +3 -3
- package/src/fees/bitcoin-fees.ts +2 -3
- package/src/signer/bitcoin-signer.ts +13 -4
- package/src/transactions/generate-unsigned-transaction.spec.ts +38 -19
- package/src/transactions/generate-unsigned-transaction.ts +36 -20
|
@@ -3,14 +3,13 @@ import BigNumber from 'bignumber.js';
|
|
|
3
3
|
import type { AverageBitcoinFeeRates, Money } from '@leather.io/models';
|
|
4
4
|
import { createMoney, satToBtc } from '@leather.io/utils';
|
|
5
5
|
|
|
6
|
-
import { CoinSelectionUtxo } from './coin-selection';
|
|
7
6
|
import { filterUneconomicalUtxos, getSpendableAmount } from './coin-selection.utils';
|
|
8
7
|
|
|
9
8
|
interface CalculateMaxSpendArgs {
|
|
10
9
|
// recipient is intentionally string instead of BitcoinAddress, as it's being validated
|
|
11
10
|
// with a fallback in a subroutine.
|
|
12
11
|
recipient: string;
|
|
13
|
-
utxos:
|
|
12
|
+
utxos: { value: number; txid: string }[];
|
|
14
13
|
feeRates?: AverageBitcoinFeeRates;
|
|
15
14
|
feeRate?: number;
|
|
16
15
|
}
|
|
@@ -2,7 +2,7 @@ import { sha256 } from '@noble/hashes/sha256';
|
|
|
2
2
|
import { hexToBytes } from '@noble/hashes/utils';
|
|
3
3
|
import BigNumber from 'bignumber.js';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { OwnedUtxo } from '@leather.io/models';
|
|
6
6
|
|
|
7
7
|
function generateMockHex() {
|
|
8
8
|
return Math.floor(Math.random() * 0xffffff)
|
|
@@ -10,9 +10,11 @@ function generateMockHex() {
|
|
|
10
10
|
.padEnd(6, '0');
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
function generateMockUtxo(value: number):
|
|
13
|
+
function generateMockUtxo(value: number): OwnedUtxo {
|
|
14
14
|
return {
|
|
15
15
|
address: 'tb1qxy5r9rlmpcxgwp92x2594q3gg026y4kdv2rsl8',
|
|
16
|
+
path: `m/84'/1'/0'/0/0`,
|
|
17
|
+
keyOrigin: `deadbeef/84'/1'/0'/0/0`,
|
|
16
18
|
txid: sha256(sha256(hexToBytes(generateMockHex()))).toString(),
|
|
17
19
|
value,
|
|
18
20
|
vout: 0,
|
|
@@ -13,29 +13,21 @@ export interface CoinSelectionOutput {
|
|
|
13
13
|
address?: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export interface CoinSelectionUtxo {
|
|
17
|
-
address: string;
|
|
18
|
-
txid: string;
|
|
19
|
-
value: number;
|
|
20
|
-
vout: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
16
|
export interface CoinSelectionRecipient {
|
|
24
17
|
address: string;
|
|
25
18
|
amount: Money;
|
|
26
19
|
}
|
|
27
20
|
|
|
28
|
-
export interface DetermineUtxosForSpendArgs {
|
|
21
|
+
export interface DetermineUtxosForSpendArgs<T> {
|
|
29
22
|
feeRate: number;
|
|
30
23
|
recipients: CoinSelectionRecipient[];
|
|
31
|
-
utxos:
|
|
24
|
+
utxos: T[];
|
|
32
25
|
}
|
|
33
|
-
|
|
34
|
-
export function determineUtxosForSpendAll({
|
|
26
|
+
export function determineUtxosForSpendAll<T extends { value: number; txid: string }>({
|
|
35
27
|
feeRate,
|
|
36
28
|
recipients,
|
|
37
29
|
utxos,
|
|
38
|
-
}: DetermineUtxosForSpendArgs) {
|
|
30
|
+
}: DetermineUtxosForSpendArgs<T>) {
|
|
39
31
|
recipients.forEach(recipient => {
|
|
40
32
|
if (!validate(recipient.address)) throw new BitcoinError('InvalidAddress');
|
|
41
33
|
});
|
|
@@ -63,7 +55,11 @@ export function determineUtxosForSpendAll({
|
|
|
63
55
|
};
|
|
64
56
|
}
|
|
65
57
|
|
|
66
|
-
export function determineUtxosForSpend
|
|
58
|
+
export function determineUtxosForSpend<T extends { value: number; txid: string }>({
|
|
59
|
+
feeRate,
|
|
60
|
+
recipients,
|
|
61
|
+
utxos,
|
|
62
|
+
}: DetermineUtxosForSpendArgs<T>) {
|
|
67
63
|
recipients.forEach(recipient => {
|
|
68
64
|
if (!validate(recipient.address)) throw new BitcoinError('InvalidAddress');
|
|
69
65
|
});
|
|
@@ -77,7 +73,7 @@ export function determineUtxosForSpend({ feeRate, recipients, utxos }: Determine
|
|
|
77
73
|
const amount = sumMoney(recipients.map(recipient => recipient.amount));
|
|
78
74
|
|
|
79
75
|
// Prepopulate with first utxo, at least one is needed
|
|
80
|
-
const neededUtxos:
|
|
76
|
+
const neededUtxos: T[] = [filteredUtxos[0]];
|
|
81
77
|
|
|
82
78
|
function estimateTransactionSize() {
|
|
83
79
|
return getSizeInfo({
|
|
@@ -5,9 +5,9 @@ import { BTC_P2WPKH_DUST_AMOUNT } from '@leather.io/constants';
|
|
|
5
5
|
import { sumNumbers } from '@leather.io/utils';
|
|
6
6
|
|
|
7
7
|
import { BtcSizeFeeEstimator } from '../fees/btc-size-fee-estimator';
|
|
8
|
-
import { CoinSelectionRecipient
|
|
8
|
+
import { CoinSelectionRecipient } from './coin-selection';
|
|
9
9
|
|
|
10
|
-
export function getUtxoTotal(utxos:
|
|
10
|
+
export function getUtxoTotal<T extends { value: number }>(utxos: T[]) {
|
|
11
11
|
return sumNumbers(utxos.map(utxo => utxo.value));
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -57,16 +57,18 @@ export function getSizeInfo(payload: {
|
|
|
57
57
|
|
|
58
58
|
return sizeInfo;
|
|
59
59
|
}
|
|
60
|
-
interface GetSpendableAmountArgs {
|
|
61
|
-
utxos:
|
|
60
|
+
interface GetSpendableAmountArgs<T> {
|
|
61
|
+
utxos: T[];
|
|
62
62
|
feeRate: number;
|
|
63
63
|
recipients: CoinSelectionRecipient[];
|
|
64
64
|
isSendMax?: boolean;
|
|
65
65
|
}
|
|
66
|
-
export function getSpendableAmount
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
export function getSpendableAmount<T extends { value: number }>({
|
|
67
|
+
utxos,
|
|
68
|
+
feeRate,
|
|
69
|
+
recipients,
|
|
70
|
+
}: GetSpendableAmountArgs<T>) {
|
|
71
|
+
const balance = utxos.map(utxo => utxo.value).reduce((prevVal, curVal) => prevVal + curVal, 0);
|
|
70
72
|
|
|
71
73
|
const size = getSizeInfo({
|
|
72
74
|
inputLength: utxos.length,
|
|
@@ -81,12 +83,12 @@ export function getSpendableAmount({ utxos, feeRate, recipients }: GetSpendableA
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
// Check if the spendable amount drops when adding a utxo
|
|
84
|
-
export function filterUneconomicalUtxos({
|
|
86
|
+
export function filterUneconomicalUtxos<T extends { value: number; txid: string }>({
|
|
85
87
|
utxos,
|
|
86
88
|
feeRate,
|
|
87
89
|
recipients,
|
|
88
90
|
}: {
|
|
89
|
-
utxos:
|
|
91
|
+
utxos: T[];
|
|
90
92
|
feeRate: number;
|
|
91
93
|
recipients: CoinSelectionRecipient[];
|
|
92
94
|
}) {
|
|
@@ -97,7 +99,7 @@ export function filterUneconomicalUtxos({
|
|
|
97
99
|
});
|
|
98
100
|
|
|
99
101
|
const filteredUtxos = utxos
|
|
100
|
-
.filter(utxo => utxo.value >= BTC_P2WPKH_DUST_AMOUNT)
|
|
102
|
+
.filter(utxo => Number(utxo.value) >= BTC_P2WPKH_DUST_AMOUNT)
|
|
101
103
|
.filter(utxo => {
|
|
102
104
|
// Calculate spendableAmount without that utxo
|
|
103
105
|
const { spendableAmount } = getSpendableAmount({
|
|
@@ -3,7 +3,7 @@ import BigNumber from 'bignumber.js';
|
|
|
3
3
|
import { AverageBitcoinFeeRates } from '@leather.io/models';
|
|
4
4
|
import { createMoney } from '@leather.io/utils';
|
|
5
5
|
|
|
6
|
-
import { CoinSelectionRecipient
|
|
6
|
+
import { CoinSelectionRecipient } from '../coin-selection/coin-selection';
|
|
7
7
|
import { recipientAddress, taprootAddress } from '../mocks/mocks';
|
|
8
8
|
import { getBitcoinFees, getBitcoinTransactionFee } from './bitcoin-fees';
|
|
9
9
|
|
|
@@ -66,13 +66,13 @@ describe('getBitcoinFees', () => {
|
|
|
66
66
|
const recipients: CoinSelectionRecipient[] = [
|
|
67
67
|
{ address: recipientAddress, amount: createMoney(1000, 'BTC') },
|
|
68
68
|
];
|
|
69
|
-
const utxos
|
|
69
|
+
const utxos = [
|
|
70
70
|
{
|
|
71
71
|
address: taprootAddress,
|
|
72
72
|
txid: '8192e8e20088c5f052fc7351b86b8f60a9454937860b281227e53e19f3e9c3f6',
|
|
73
73
|
vout: 0,
|
|
74
74
|
value: 2000,
|
|
75
|
-
},
|
|
75
|
+
} as const,
|
|
76
76
|
];
|
|
77
77
|
|
|
78
78
|
const fees = getBitcoinFees({ feeRates, recipients, utxos });
|
package/src/fees/bitcoin-fees.ts
CHANGED
|
@@ -2,13 +2,12 @@ import { AverageBitcoinFeeRates, Money } from '@leather.io/models';
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
CoinSelectionRecipient,
|
|
5
|
-
CoinSelectionUtxo,
|
|
6
5
|
DetermineUtxosForSpendArgs,
|
|
7
6
|
determineUtxosForSpend,
|
|
8
7
|
determineUtxosForSpendAll,
|
|
9
8
|
} from '../coin-selection/coin-selection';
|
|
10
9
|
|
|
11
|
-
type GetBitcoinTransactionFeeArgs = DetermineUtxosForSpendArgs & {
|
|
10
|
+
type GetBitcoinTransactionFeeArgs = DetermineUtxosForSpendArgs<{ value: number; txid: string }> & {
|
|
12
11
|
isSendingMax?: boolean;
|
|
13
12
|
};
|
|
14
13
|
|
|
@@ -34,7 +33,7 @@ export interface GetBitcoinFeesArgs {
|
|
|
34
33
|
feeRates: AverageBitcoinFeeRates;
|
|
35
34
|
isSendingMax?: boolean;
|
|
36
35
|
recipients: CoinSelectionRecipient[];
|
|
37
|
-
utxos:
|
|
36
|
+
utxos: { value: number; txid: string }[];
|
|
38
37
|
}
|
|
39
38
|
export function getBitcoinFees({ feeRates, isSendingMax, recipients, utxos }: GetBitcoinFeesArgs) {
|
|
40
39
|
const defaultArgs = {
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
appendAddressIndexToPath,
|
|
10
10
|
decomposeDescriptor,
|
|
11
11
|
deriveKeychainFromXpub,
|
|
12
|
+
extractAddressIndexFromPath,
|
|
13
|
+
extractChangeIndexFromPath,
|
|
12
14
|
keyOriginToDerivationPath,
|
|
13
15
|
} from '@leather.io/crypto';
|
|
14
16
|
import type { BitcoinAddress, BitcoinNetworkModes, ValueOf } from '@leather.io/models';
|
|
@@ -83,7 +85,7 @@ export function initializeBitcoinAccountKeychainFromDescriptor(
|
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
export interface BitcoinPayerInfo {
|
|
86
|
-
|
|
88
|
+
change: number;
|
|
87
89
|
addressIndex: number;
|
|
88
90
|
}
|
|
89
91
|
export function deriveBitcoinPayerFromAccount(descriptor: string, network: BitcoinNetworkModes) {
|
|
@@ -94,8 +96,8 @@ export function deriveBitcoinPayerFromAccount(descriptor: string, network: Bitco
|
|
|
94
96
|
if (accountKeychain.depth !== DerivationPathDepth.Account)
|
|
95
97
|
throw new Error('Keychain passed is not an account');
|
|
96
98
|
|
|
97
|
-
return ({
|
|
98
|
-
const childKeychain = accountKeychain.deriveChild(
|
|
99
|
+
return ({ change, addressIndex }: BitcoinPayerInfo) => {
|
|
100
|
+
const childKeychain = accountKeychain.deriveChild(change).deriveChild(addressIndex);
|
|
99
101
|
|
|
100
102
|
const derivePayerFromAccount = whenSupportedPaymentType(paymentType)({
|
|
101
103
|
p2tr: getTaprootPaymentFromAddressIndex,
|
|
@@ -105,7 +107,7 @@ export function deriveBitcoinPayerFromAccount(descriptor: string, network: Bitco
|
|
|
105
107
|
const payment = derivePayerFromAccount(childKeychain, network);
|
|
106
108
|
|
|
107
109
|
return {
|
|
108
|
-
keyOrigin: appendAddressIndexToPath(keyOrigin,
|
|
110
|
+
keyOrigin: appendAddressIndexToPath(keyOrigin, change, addressIndex),
|
|
109
111
|
masterKeyFingerprint: fingerprint,
|
|
110
112
|
paymentType,
|
|
111
113
|
network,
|
|
@@ -213,6 +215,13 @@ export function payerToBip32DerivationBitcoinJsLib(
|
|
|
213
215
|
};
|
|
214
216
|
}
|
|
215
217
|
|
|
218
|
+
export function extractPayerInfoFromDerivationPath(path: string) {
|
|
219
|
+
return {
|
|
220
|
+
change: extractChangeIndexFromPath(path),
|
|
221
|
+
addressIndex: extractAddressIndexFromPath(path),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
216
225
|
/**
|
|
217
226
|
* @description
|
|
218
227
|
* Turns key format from @scure/btc-signer lib back into key origin string
|
|
@@ -1,18 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { hex } from '@scure/base';
|
|
2
|
+
import * as btc from '@scure/btc-signer';
|
|
2
3
|
import { describe, expect, it } from 'vitest';
|
|
3
4
|
|
|
5
|
+
import { OwnedUtxo } from '@leather.io/models';
|
|
4
6
|
import { createMoney } from '@leather.io/utils';
|
|
5
7
|
|
|
6
8
|
import { getBtcSignerLibNetworkConfigByMode } from '../utils/bitcoin.network';
|
|
9
|
+
import { createBitcoinAddress } from '../validation/bitcoin-address';
|
|
7
10
|
import {
|
|
8
11
|
GenerateBitcoinUnsignedTransactionArgs,
|
|
9
|
-
|
|
12
|
+
generateBitcoinUnsignedTransaction,
|
|
10
13
|
} from './generate-unsigned-transaction';
|
|
11
14
|
|
|
15
|
+
const publicKey = hex.decode('030000000000000000000000000000000000000000000000000000000000000001');
|
|
16
|
+
const payment = btc.p2wpkh(publicKey, btc.TEST_NETWORK);
|
|
17
|
+
|
|
12
18
|
const mockResult = {
|
|
13
19
|
inputs: [
|
|
14
20
|
{
|
|
15
|
-
|
|
21
|
+
path: "m/84'/1'/0'/0/1",
|
|
22
|
+
keyOrigin: "deadbeef/84'/1'/0'/0/1",
|
|
23
|
+
address: payment.address!,
|
|
16
24
|
txid: 'c715ea469c8d794f6dd7e0043148631f69d411c428ef0ab2b04e4528ffe8319f',
|
|
17
25
|
vout: 1,
|
|
18
26
|
value: 200000,
|
|
@@ -21,19 +29,12 @@ const mockResult = {
|
|
|
21
29
|
fee: createMoney(141, 'BTC'),
|
|
22
30
|
};
|
|
23
31
|
|
|
24
|
-
describe(
|
|
25
|
-
const mockArgs: GenerateBitcoinUnsignedTransactionArgs = {
|
|
32
|
+
describe(generateBitcoinUnsignedTransaction.name, () => {
|
|
33
|
+
const mockArgs: GenerateBitcoinUnsignedTransactionArgs<OwnedUtxo> = {
|
|
26
34
|
feeRate: 1,
|
|
27
35
|
isSendingMax: false,
|
|
28
36
|
network: getBtcSignerLibNetworkConfigByMode('testnet'),
|
|
29
|
-
|
|
30
|
-
payerPublicKey: '0329b076bc20f7b1592b2a1a5cb91dfefe8c966e50e256458e23dd2c5d63f8f1af',
|
|
31
|
-
bip32Derivation: [
|
|
32
|
-
[
|
|
33
|
-
hexToBytes('0329b076bc20f7b1592b2a1a5cb91dfefe8c966e50e256458e23dd2c5d63f8f1af'),
|
|
34
|
-
{ fingerprint: 400738063, path: [2147483732, 2147483649, 2147483648, 0, 0] },
|
|
35
|
-
],
|
|
36
|
-
],
|
|
37
|
+
changeAddress: createBitcoinAddress(payment.address!),
|
|
37
38
|
recipients: [
|
|
38
39
|
{
|
|
39
40
|
address: 'tb1qsqncyhhqdtfn07t3dhupx7smv5gk83ds6k0gfa',
|
|
@@ -42,22 +43,40 @@ describe('generateBitcoinUnsignedTransactionNativeSegwit', () => {
|
|
|
42
43
|
],
|
|
43
44
|
utxos: [
|
|
44
45
|
{
|
|
45
|
-
address:
|
|
46
|
+
address: payment.address!,
|
|
47
|
+
path: "m/84'/1'/0'/0/0",
|
|
48
|
+
keyOrigin: "deadbeef/84'/1'/0'/0/0",
|
|
46
49
|
txid: '8192e8e20088c5f052fc7351b86b8f60a9454937860b281227e53e19f3e9c3f6',
|
|
47
50
|
vout: 0,
|
|
48
51
|
value: 100000,
|
|
49
52
|
},
|
|
50
53
|
{
|
|
51
|
-
address:
|
|
54
|
+
address: payment.address!,
|
|
55
|
+
path: "m/84'/1'/0'/0/1",
|
|
56
|
+
keyOrigin: "deadbeef/84'/1'/0'/0/1",
|
|
52
57
|
txid: 'c715ea469c8d794f6dd7e0043148631f69d411c428ef0ab2b04e4528ffe8319f',
|
|
53
58
|
vout: 1,
|
|
54
59
|
value: 200000,
|
|
55
60
|
},
|
|
56
61
|
],
|
|
62
|
+
payerLookup() {
|
|
63
|
+
return {
|
|
64
|
+
paymentType: 'p2wpkh',
|
|
65
|
+
address: createBitcoinAddress(payment.address!),
|
|
66
|
+
keyOrigin: "deadbeef/84'/1'/0'/0/0",
|
|
67
|
+
masterKeyFingerprint: 'deadbeef',
|
|
68
|
+
network: 'testnet',
|
|
69
|
+
payment: {
|
|
70
|
+
script: payment.script,
|
|
71
|
+
type: 'p2wpkh',
|
|
72
|
+
},
|
|
73
|
+
publicKey,
|
|
74
|
+
};
|
|
75
|
+
},
|
|
57
76
|
};
|
|
58
77
|
|
|
59
78
|
it('should generate an unsigned transaction with correct inputs and outputs', () => {
|
|
60
|
-
const result =
|
|
79
|
+
const result = generateBitcoinUnsignedTransaction(mockArgs);
|
|
61
80
|
if (result) {
|
|
62
81
|
expect(result.inputs).toEqual(mockResult.inputs);
|
|
63
82
|
expect(result.fee).toEqual(mockResult.fee);
|
|
@@ -67,7 +86,7 @@ describe('generateBitcoinUnsignedTransactionNativeSegwit', () => {
|
|
|
67
86
|
});
|
|
68
87
|
|
|
69
88
|
it('should add change address to output correctly', () => {
|
|
70
|
-
const result =
|
|
89
|
+
const result = generateBitcoinUnsignedTransaction(mockArgs);
|
|
71
90
|
|
|
72
91
|
if (result) {
|
|
73
92
|
expect(result.tx.outputsLength).toBe(2);
|
|
@@ -76,12 +95,12 @@ describe('generateBitcoinUnsignedTransactionNativeSegwit', () => {
|
|
|
76
95
|
});
|
|
77
96
|
|
|
78
97
|
it('should throw an error if inputs are empty', () => {
|
|
79
|
-
const argsWithNoInputs: GenerateBitcoinUnsignedTransactionArgs = {
|
|
98
|
+
const argsWithNoInputs: GenerateBitcoinUnsignedTransactionArgs<any> = {
|
|
80
99
|
...mockArgs,
|
|
81
100
|
utxos: [],
|
|
82
101
|
};
|
|
83
102
|
|
|
84
|
-
expect(() =>
|
|
103
|
+
expect(() => generateBitcoinUnsignedTransaction(argsWithNoInputs)).toThrowError(
|
|
85
104
|
'InsufficientFunds'
|
|
86
105
|
);
|
|
87
106
|
});
|
|
@@ -1,37 +1,39 @@
|
|
|
1
|
-
import { hexToBytes } from '@noble/hashes/utils';
|
|
2
1
|
import * as btc from '@scure/btc-signer';
|
|
3
2
|
|
|
4
3
|
import {
|
|
5
4
|
CoinSelectionRecipient,
|
|
6
|
-
CoinSelectionUtxo,
|
|
7
5
|
determineUtxosForSpend,
|
|
8
6
|
determineUtxosForSpendAll,
|
|
9
7
|
} from '../coin-selection/coin-selection';
|
|
10
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
BitcoinNativeSegwitPayer,
|
|
10
|
+
BitcoinTaprootPayer,
|
|
11
|
+
payerToBip32Derivation,
|
|
12
|
+
payerToTapBip32Derivation,
|
|
13
|
+
} from '../signer/bitcoin-signer';
|
|
11
14
|
import { BtcSignerNetwork } from '../utils/bitcoin.network';
|
|
12
15
|
import { BitcoinError } from '../validation/bitcoin-error';
|
|
13
16
|
|
|
14
|
-
export interface GenerateBitcoinUnsignedTransactionArgs {
|
|
17
|
+
export interface GenerateBitcoinUnsignedTransactionArgs<T> {
|
|
15
18
|
feeRate: number;
|
|
16
19
|
isSendingMax?: boolean;
|
|
17
|
-
payerAddress: string;
|
|
18
|
-
payerPublicKey: string;
|
|
19
|
-
bip32Derivation: BtcSignerDefaultBip32Derivation[];
|
|
20
20
|
network: BtcSignerNetwork;
|
|
21
21
|
recipients: CoinSelectionRecipient[];
|
|
22
|
-
utxos:
|
|
22
|
+
utxos: T[];
|
|
23
|
+
changeAddress: string;
|
|
24
|
+
payerLookup(keyOrigin: string): BitcoinNativeSegwitPayer | BitcoinTaprootPayer | undefined;
|
|
23
25
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
export function generateBitcoinUnsignedTransaction<
|
|
27
|
+
T extends { txid: string; vout: number; value: number; keyOrigin: string },
|
|
28
|
+
>({
|
|
26
29
|
feeRate,
|
|
27
30
|
isSendingMax,
|
|
28
|
-
payerAddress,
|
|
29
|
-
payerPublicKey,
|
|
30
|
-
bip32Derivation,
|
|
31
31
|
network,
|
|
32
32
|
recipients,
|
|
33
|
+
changeAddress,
|
|
33
34
|
utxos,
|
|
34
|
-
|
|
35
|
+
payerLookup,
|
|
36
|
+
}: GenerateBitcoinUnsignedTransactionArgs<T>) {
|
|
35
37
|
const determineUtxosArgs = { feeRate, recipients, utxos };
|
|
36
38
|
const { inputs, outputs, fee } = isSendingMax
|
|
37
39
|
? determineUtxosForSpendAll(determineUtxosArgs)
|
|
@@ -41,19 +43,33 @@ export function generateBitcoinUnsignedTransactionNativeSegwit({
|
|
|
41
43
|
if (!outputs.length) throw new BitcoinError('NoOutputsToSign');
|
|
42
44
|
|
|
43
45
|
const tx = new btc.Transaction();
|
|
44
|
-
const p2wpkh = btc.p2wpkh(hexToBytes(payerPublicKey), network);
|
|
45
46
|
|
|
46
47
|
for (const input of inputs) {
|
|
48
|
+
const payer = payerLookup(input.keyOrigin);
|
|
49
|
+
|
|
50
|
+
if (!payer) {
|
|
51
|
+
// eslint-disable-next-line no-console
|
|
52
|
+
console.log(`No payer found for input with keyOrigin ${input.keyOrigin}`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const bip32Derivation =
|
|
57
|
+
payer.paymentType === 'p2tr'
|
|
58
|
+
? { tapBip32Derivation: [payerToTapBip32Derivation(payer)] }
|
|
59
|
+
: { bip32Derivation: [payerToBip32Derivation(payer)] };
|
|
60
|
+
|
|
61
|
+
const tapInternalKey =
|
|
62
|
+
payer.paymentType === 'p2tr' ? { tapInternalKey: payer.payment.tapInternalKey } : {};
|
|
63
|
+
|
|
47
64
|
tx.addInput({
|
|
48
65
|
txid: input.txid,
|
|
49
66
|
index: input.vout,
|
|
50
|
-
sequence: 0,
|
|
51
|
-
bip32Derivation,
|
|
52
67
|
witnessUtxo: {
|
|
53
|
-
|
|
54
|
-
script: p2wpkh.script,
|
|
68
|
+
script: payer.payment.script,
|
|
55
69
|
amount: BigInt(input.value),
|
|
56
70
|
},
|
|
71
|
+
...bip32Derivation,
|
|
72
|
+
...tapInternalKey,
|
|
57
73
|
});
|
|
58
74
|
}
|
|
59
75
|
|
|
@@ -61,7 +77,7 @@ export function generateBitcoinUnsignedTransactionNativeSegwit({
|
|
|
61
77
|
// When coin selection returns an output with no address,
|
|
62
78
|
// we assume it is a change output
|
|
63
79
|
if (!output.address) {
|
|
64
|
-
tx.addOutputAddress(
|
|
80
|
+
tx.addOutputAddress(changeAddress, BigInt(output.value), network);
|
|
65
81
|
return;
|
|
66
82
|
}
|
|
67
83
|
tx.addOutputAddress(output.address, BigInt(output.value), network);
|