@leather.io/bitcoin 0.17.0 → 0.18.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.
- package/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +19 -0
- package/dist/index.d.ts +145 -2
- package/dist/index.js +550 -3
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/bitcoin-error.ts +15 -0
- package/src/btc-size-fee-estimator.spec.ts +5 -1
- package/src/coin-selection/calculate-max-spend.ts +3 -2
- package/src/coin-selection/coin-selection.mocks.ts +5 -10
- package/src/coin-selection/coin-selection.spec.ts +10 -10
- package/src/coin-selection/coin-selection.ts +28 -29
- package/src/coin-selection/coin-selection.utils.spec.ts +1 -1
- package/src/coin-selection/coin-selection.utils.ts +13 -16
- package/src/fees/bitcoin-fees.spec.ts +90 -0
- package/src/fees/bitcoin-fees.ts +68 -0
- package/src/index.ts +10 -0
- package/src/transactions/generate-unsigned-transaction.spec.ts +83 -0
- package/src/transactions/generate-unsigned-transaction.ts +71 -0
|
@@ -2,34 +2,33 @@ import BigNumber from 'bignumber.js';
|
|
|
2
2
|
import { validate } from 'bitcoin-address-validation';
|
|
3
3
|
|
|
4
4
|
import { BTC_P2WPKH_DUST_AMOUNT } from '@leather.io/constants';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
filterUneconomicalUtxos,
|
|
11
|
-
getSizeInfo,
|
|
12
|
-
} from './coin-selection.utils';
|
|
13
|
-
|
|
14
|
-
export class InsufficientFundsError extends Error {
|
|
15
|
-
constructor() {
|
|
16
|
-
super('Insufficient funds');
|
|
17
|
-
}
|
|
18
|
-
}
|
|
5
|
+
import { Money } from '@leather.io/models';
|
|
6
|
+
import { createMoney, sumMoney } from '@leather.io/utils';
|
|
7
|
+
|
|
8
|
+
import { BitcoinError, BitcoinErrorMessage } from '../bitcoin-error';
|
|
9
|
+
import { filterUneconomicalUtxos, getSizeInfo, getUtxoTotal } from './coin-selection.utils';
|
|
19
10
|
|
|
20
|
-
interface
|
|
11
|
+
export interface CoinSelectionOutput {
|
|
21
12
|
value: bigint;
|
|
22
13
|
address?: string;
|
|
23
14
|
}
|
|
24
15
|
|
|
25
|
-
export interface
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
export interface CoinSelectionUtxo {
|
|
17
|
+
address: string;
|
|
18
|
+
txid: string;
|
|
19
|
+
value: number;
|
|
20
|
+
vout: number;
|
|
29
21
|
}
|
|
30
22
|
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
export interface CoinSelectionRecipient {
|
|
24
|
+
address: string;
|
|
25
|
+
amount: Money;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DetermineUtxosForSpendArgs {
|
|
29
|
+
feeRate: number;
|
|
30
|
+
recipients: CoinSelectionRecipient[];
|
|
31
|
+
utxos: CoinSelectionUtxo[];
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
export function determineUtxosForSpendAll({
|
|
@@ -61,7 +60,7 @@ export function determineUtxosForSpendAll({
|
|
|
61
60
|
inputs: filteredUtxos,
|
|
62
61
|
outputs,
|
|
63
62
|
size: sizeInfo.txVBytes,
|
|
64
|
-
fee,
|
|
63
|
+
fee: createMoney(new BigNumber(fee), 'BTC'),
|
|
65
64
|
};
|
|
66
65
|
}
|
|
67
66
|
|
|
@@ -75,12 +74,12 @@ export function determineUtxosForSpend({ feeRate, recipients, utxos }: Determine
|
|
|
75
74
|
feeRate,
|
|
76
75
|
recipients,
|
|
77
76
|
});
|
|
78
|
-
if (!filteredUtxos.length) throw new
|
|
77
|
+
if (!filteredUtxos.length) throw new BitcoinError(BitcoinErrorMessage.InsufficientFunds);
|
|
79
78
|
|
|
80
79
|
const amount = sumMoney(recipients.map(recipient => recipient.amount));
|
|
81
80
|
|
|
82
|
-
// Prepopulate with first
|
|
83
|
-
const neededUtxos:
|
|
81
|
+
// Prepopulate with first utxo, at least one is needed
|
|
82
|
+
const neededUtxos: CoinSelectionUtxo[] = [filteredUtxos[0]];
|
|
84
83
|
|
|
85
84
|
function estimateTransactionSize() {
|
|
86
85
|
return getSizeInfo({
|
|
@@ -101,7 +100,7 @@ export function determineUtxosForSpend({ feeRate, recipients, utxos }: Determine
|
|
|
101
100
|
|
|
102
101
|
while (!hasSufficientUtxosForTx()) {
|
|
103
102
|
const [nextUtxo] = getRemainingUnspentUtxos();
|
|
104
|
-
if (!nextUtxo) throw new
|
|
103
|
+
if (!nextUtxo) throw new BitcoinError(BitcoinErrorMessage.InsufficientFunds);
|
|
105
104
|
neededUtxos.push(nextUtxo);
|
|
106
105
|
}
|
|
107
106
|
|
|
@@ -112,7 +111,7 @@ export function determineUtxosForSpend({ feeRate, recipients, utxos }: Determine
|
|
|
112
111
|
const changeAmount =
|
|
113
112
|
BigInt(getUtxoTotal(neededUtxos).toString()) - BigInt(amount.amount.toNumber()) - BigInt(fee);
|
|
114
113
|
|
|
115
|
-
const changeUtxos:
|
|
114
|
+
const changeUtxos: CoinSelectionOutput[] =
|
|
116
115
|
changeAmount > BTC_P2WPKH_DUST_AMOUNT
|
|
117
116
|
? [
|
|
118
117
|
{
|
|
@@ -121,7 +120,7 @@ export function determineUtxosForSpend({ feeRate, recipients, utxos }: Determine
|
|
|
121
120
|
]
|
|
122
121
|
: [];
|
|
123
122
|
|
|
124
|
-
const outputs:
|
|
123
|
+
const outputs: CoinSelectionOutput[] = [
|
|
125
124
|
...recipients.map(({ address, amount }) => ({
|
|
126
125
|
value: BigInt(amount.amount.toNumber()),
|
|
127
126
|
address,
|
|
@@ -134,7 +133,7 @@ export function determineUtxosForSpend({ feeRate, recipients, utxos }: Determine
|
|
|
134
133
|
inputs: neededUtxos,
|
|
135
134
|
outputs,
|
|
136
135
|
size: estimateTransactionSize().txVBytes,
|
|
137
|
-
fee,
|
|
136
|
+
fee: createMoney(new BigNumber(fee), 'BTC'),
|
|
138
137
|
...estimateTransactionSize(),
|
|
139
138
|
};
|
|
140
139
|
}
|
|
@@ -2,23 +2,18 @@ import BigNumber from 'bignumber.js';
|
|
|
2
2
|
import validate, { AddressInfo, AddressType, getAddressInfo } from 'bitcoin-address-validation';
|
|
3
3
|
|
|
4
4
|
import { BTC_P2WPKH_DUST_AMOUNT } from '@leather.io/constants';
|
|
5
|
-
import {
|
|
5
|
+
import { sumNumbers } from '@leather.io/utils';
|
|
6
6
|
|
|
7
7
|
import { BtcSizeFeeEstimator } from '../btc-size-fee-estimator';
|
|
8
|
+
import { CoinSelectionRecipient, CoinSelectionUtxo } from './coin-selection';
|
|
8
9
|
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
amount: Money;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface Utxo extends Record<string, any> {
|
|
15
|
-
txid: string;
|
|
16
|
-
value: number;
|
|
10
|
+
export function getUtxoTotal(utxos: CoinSelectionUtxo[]) {
|
|
11
|
+
return sumNumbers(utxos.map(utxo => utxo.value));
|
|
17
12
|
}
|
|
18
13
|
|
|
19
14
|
export function getSizeInfo(payload: {
|
|
20
15
|
inputLength: number;
|
|
21
|
-
recipients:
|
|
16
|
+
recipients: CoinSelectionRecipient[];
|
|
22
17
|
isSendMax?: boolean;
|
|
23
18
|
}) {
|
|
24
19
|
const { inputLength, recipients, isSendMax } = payload;
|
|
@@ -68,11 +63,13 @@ export function getSpendableAmount({
|
|
|
68
63
|
feeRate,
|
|
69
64
|
recipients,
|
|
70
65
|
}: {
|
|
71
|
-
utxos:
|
|
66
|
+
utxos: CoinSelectionUtxo[];
|
|
72
67
|
feeRate: number;
|
|
73
|
-
recipients:
|
|
68
|
+
recipients: CoinSelectionRecipient[];
|
|
74
69
|
}) {
|
|
75
|
-
const balance = utxos
|
|
70
|
+
const balance = utxos
|
|
71
|
+
.map(utxo => Number(utxo.value))
|
|
72
|
+
.reduce((prevVal, curVal) => prevVal + curVal, 0);
|
|
76
73
|
|
|
77
74
|
const size = getSizeInfo({
|
|
78
75
|
inputLength: utxos.length,
|
|
@@ -92,9 +89,9 @@ export function filterUneconomicalUtxos({
|
|
|
92
89
|
feeRate,
|
|
93
90
|
recipients,
|
|
94
91
|
}: {
|
|
95
|
-
utxos:
|
|
92
|
+
utxos: CoinSelectionUtxo[];
|
|
96
93
|
feeRate: number;
|
|
97
|
-
recipients:
|
|
94
|
+
recipients: CoinSelectionRecipient[];
|
|
98
95
|
}) {
|
|
99
96
|
const { spendableAmount: fullSpendableAmount } = getSpendableAmount({
|
|
100
97
|
utxos,
|
|
@@ -111,7 +108,7 @@ export function filterUneconomicalUtxos({
|
|
|
111
108
|
feeRate,
|
|
112
109
|
recipients,
|
|
113
110
|
});
|
|
114
|
-
// If fullSpendableAmount is greater, do not use
|
|
111
|
+
// If fullSpendableAmount is greater, do not use utxo
|
|
115
112
|
return spendableAmount.toNumber() < fullSpendableAmount.toNumber();
|
|
116
113
|
});
|
|
117
114
|
return filteredUtxos;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import BigNumber from 'bignumber.js';
|
|
2
|
+
|
|
3
|
+
import { AverageBitcoinFeeRates } from '@leather.io/models';
|
|
4
|
+
import { createMoney } from '@leather.io/utils';
|
|
5
|
+
|
|
6
|
+
import { CoinSelectionRecipient, CoinSelectionUtxo } from '../coin-selection/coin-selection';
|
|
7
|
+
import { getBitcoinFees, getBitcoinTransactionFee } from './bitcoin-fees';
|
|
8
|
+
|
|
9
|
+
describe('getBitcoinTransactionFee', () => {
|
|
10
|
+
it('should return the fee for a normal transaction', () => {
|
|
11
|
+
const args = {
|
|
12
|
+
recipients: [
|
|
13
|
+
{ address: 'tb1qsqncyhhqdtfn07t3dhupx7smv5gk83ds6k0gfa', amount: createMoney(1000, 'BTC') },
|
|
14
|
+
],
|
|
15
|
+
utxos: [
|
|
16
|
+
{
|
|
17
|
+
address: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
|
|
18
|
+
txid: '8192e8e20088c5f052fc7351b86b8f60a9454937860b281227e53e19f3e9c3f6',
|
|
19
|
+
vout: 0,
|
|
20
|
+
value: 2000,
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
feeRate: 1,
|
|
24
|
+
};
|
|
25
|
+
const fee = getBitcoinTransactionFee(args);
|
|
26
|
+
const expectedFee = createMoney(141, 'BTC');
|
|
27
|
+
expect(fee).toEqual(expectedFee);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should return the fee for a max send transaction', () => {
|
|
31
|
+
const args = {
|
|
32
|
+
isSendingMax: true,
|
|
33
|
+
recipients: [
|
|
34
|
+
{ address: 'tb1qsqncyhhqdtfn07t3dhupx7smv5gk83ds6k0gfa', amount: createMoney(2000, 'BTC') },
|
|
35
|
+
],
|
|
36
|
+
utxos: [
|
|
37
|
+
{
|
|
38
|
+
address: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
|
|
39
|
+
txid: '8192e8e20088c5f052fc7351b86b8f60a9454937860b281227e53e19f3e9c3f6',
|
|
40
|
+
vout: 0,
|
|
41
|
+
value: 2000,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
feeRate: 2,
|
|
45
|
+
};
|
|
46
|
+
const fee = getBitcoinTransactionFee(args);
|
|
47
|
+
const expectedFee = createMoney(219, 'BTC');
|
|
48
|
+
expect(fee).toEqual(expectedFee);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should return null if an error occurs', () => {
|
|
52
|
+
const args = {
|
|
53
|
+
recipients: [],
|
|
54
|
+
utxos: [],
|
|
55
|
+
feeRate: 1,
|
|
56
|
+
};
|
|
57
|
+
const fee = getBitcoinTransactionFee(args);
|
|
58
|
+
expect(fee).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('getBitcoinFees', () => {
|
|
63
|
+
it('should return the fees for different fee rates', () => {
|
|
64
|
+
const feeRates: AverageBitcoinFeeRates = {
|
|
65
|
+
fastestFee: new BigNumber(3),
|
|
66
|
+
halfHourFee: new BigNumber(2),
|
|
67
|
+
hourFee: new BigNumber(1),
|
|
68
|
+
};
|
|
69
|
+
const recipients: CoinSelectionRecipient[] = [
|
|
70
|
+
{ address: 'tb1qsqncyhhqdtfn07t3dhupx7smv5gk83ds6k0gfa', amount: createMoney(1000, 'BTC') },
|
|
71
|
+
];
|
|
72
|
+
const utxos: CoinSelectionUtxo[] = [
|
|
73
|
+
{
|
|
74
|
+
address: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
|
|
75
|
+
txid: '8192e8e20088c5f052fc7351b86b8f60a9454937860b281227e53e19f3e9c3f6',
|
|
76
|
+
vout: 0,
|
|
77
|
+
value: 2000,
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const fees = getBitcoinFees({ feeRates, recipients, utxos });
|
|
82
|
+
const expectedFees = {
|
|
83
|
+
high: { feeRate: 3, fee: createMoney(422, 'BTC') },
|
|
84
|
+
standard: { feeRate: 2, fee: createMoney(281, 'BTC') },
|
|
85
|
+
low: { feeRate: 1, fee: createMoney(141, 'BTC') },
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
expect(fees).toEqual(expectedFees);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { AverageBitcoinFeeRates, Money } from '@leather.io/models';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CoinSelectionRecipient,
|
|
5
|
+
CoinSelectionUtxo,
|
|
6
|
+
DetermineUtxosForSpendArgs,
|
|
7
|
+
determineUtxosForSpend,
|
|
8
|
+
determineUtxosForSpendAll,
|
|
9
|
+
} from '../coin-selection/coin-selection';
|
|
10
|
+
|
|
11
|
+
type GetBitcoinTransactionFeeArgs = DetermineUtxosForSpendArgs & {
|
|
12
|
+
isSendingMax?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function getBitcoinTransactionFee({ isSendingMax, ...props }: GetBitcoinTransactionFeeArgs) {
|
|
16
|
+
try {
|
|
17
|
+
const { fee } = isSendingMax
|
|
18
|
+
? determineUtxosForSpendAll({ ...props })
|
|
19
|
+
: determineUtxosForSpend({ ...props });
|
|
20
|
+
return fee;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BitcoinFees {
|
|
27
|
+
blockchain: 'bitcoin';
|
|
28
|
+
high: { fee: Money | null; feeRate: number };
|
|
29
|
+
standard: { fee: Money | null; feeRate: number };
|
|
30
|
+
low: { fee: Money | null; feeRate: number };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface GetBitcoinFeesArgs {
|
|
34
|
+
feeRates: AverageBitcoinFeeRates;
|
|
35
|
+
isSendingMax?: boolean;
|
|
36
|
+
recipients: CoinSelectionRecipient[];
|
|
37
|
+
utxos: CoinSelectionUtxo[];
|
|
38
|
+
}
|
|
39
|
+
export function getBitcoinFees({ feeRates, isSendingMax, recipients, utxos }: GetBitcoinFeesArgs) {
|
|
40
|
+
const defaultArgs = {
|
|
41
|
+
isSendingMax,
|
|
42
|
+
recipients,
|
|
43
|
+
utxos,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const highFeeRate = feeRates.fastestFee.toNumber();
|
|
47
|
+
const standardFeeRate = feeRates.halfHourFee.toNumber();
|
|
48
|
+
const lowFeeRate = feeRates.hourFee.toNumber();
|
|
49
|
+
|
|
50
|
+
const highFeeValue = getBitcoinTransactionFee({
|
|
51
|
+
...defaultArgs,
|
|
52
|
+
feeRate: highFeeRate,
|
|
53
|
+
});
|
|
54
|
+
const standardFeeValue = getBitcoinTransactionFee({
|
|
55
|
+
...defaultArgs,
|
|
56
|
+
feeRate: standardFeeRate,
|
|
57
|
+
});
|
|
58
|
+
const lowFeeValue = getBitcoinTransactionFee({
|
|
59
|
+
...defaultArgs,
|
|
60
|
+
feeRate: lowFeeRate,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
high: { feeRate: highFeeRate, fee: highFeeValue },
|
|
65
|
+
standard: { feeRate: standardFeeRate, fee: standardFeeValue },
|
|
66
|
+
low: { feeRate: lowFeeRate, fee: lowFeeValue },
|
|
67
|
+
};
|
|
68
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
export * from './bip322/bip322-utils';
|
|
2
2
|
export * from './bip322/sign-message-bip322-bitcoinjs';
|
|
3
|
+
|
|
4
|
+
export * from './coin-selection/calculate-max-spend';
|
|
5
|
+
export * from './coin-selection/coin-selection';
|
|
6
|
+
export * from './coin-selection/coin-selection.utils';
|
|
7
|
+
|
|
8
|
+
export * from './fees/bitcoin-fees';
|
|
9
|
+
|
|
10
|
+
export * from './transactions/generate-unsigned-transaction';
|
|
11
|
+
|
|
12
|
+
export * from './bitcoin-error';
|
|
3
13
|
export * from './bitcoin-signer';
|
|
4
14
|
export * from './bitcoin.network';
|
|
5
15
|
export * from './bitcoin.utils';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createMoney } from '@leather.io/utils';
|
|
4
|
+
|
|
5
|
+
import { getBtcSignerLibNetworkConfigByMode } from '../bitcoin.network';
|
|
6
|
+
import {
|
|
7
|
+
GenerateBitcoinUnsignedTransactionArgs,
|
|
8
|
+
generateBitcoinUnsignedTransactionNativeSegwit,
|
|
9
|
+
} from './generate-unsigned-transaction';
|
|
10
|
+
|
|
11
|
+
const mockResult = {
|
|
12
|
+
inputs: [
|
|
13
|
+
{
|
|
14
|
+
address: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
|
|
15
|
+
txid: 'c715ea469c8d794f6dd7e0043148631f69d411c428ef0ab2b04e4528ffe8319f',
|
|
16
|
+
vout: 1,
|
|
17
|
+
value: 200000,
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
fee: createMoney(141, 'BTC'),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe('generateBitcoinUnsignedTransactionNativeSegwit', () => {
|
|
24
|
+
const mockArgs: GenerateBitcoinUnsignedTransactionArgs = {
|
|
25
|
+
feeRate: 1,
|
|
26
|
+
isSendingMax: false,
|
|
27
|
+
network: getBtcSignerLibNetworkConfigByMode('testnet'),
|
|
28
|
+
payerAddress: 'tb1qxy5r9rlmpcxgwp92x2594q3gg026y4kdv2rsl8',
|
|
29
|
+
payerPublicKey: '0329b076bc20f7b1592b2a1a5cb91dfefe8c966e50e256458e23dd2c5d63f8f1af',
|
|
30
|
+
recipients: [
|
|
31
|
+
{
|
|
32
|
+
address: 'tb1qsqncyhhqdtfn07t3dhupx7smv5gk83ds6k0gfa',
|
|
33
|
+
amount: createMoney(150000, 'BTC'),
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
utxos: [
|
|
37
|
+
{
|
|
38
|
+
address: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
|
|
39
|
+
txid: '8192e8e20088c5f052fc7351b86b8f60a9454937860b281227e53e19f3e9c3f6',
|
|
40
|
+
vout: 0,
|
|
41
|
+
value: 100000,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
address: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
|
|
45
|
+
txid: 'c715ea469c8d794f6dd7e0043148631f69d411c428ef0ab2b04e4528ffe8319f',
|
|
46
|
+
vout: 1,
|
|
47
|
+
value: 200000,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
it('should generate an unsigned transaction with correct inputs and outputs', async () => {
|
|
53
|
+
const result = await generateBitcoinUnsignedTransactionNativeSegwit(mockArgs);
|
|
54
|
+
if (result) {
|
|
55
|
+
expect(result.inputs).toEqual(mockResult.inputs);
|
|
56
|
+
expect(result.fee).toEqual(mockResult.fee);
|
|
57
|
+
expect(result.hex).toBeDefined();
|
|
58
|
+
expect(result.psbt).toBeDefined();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should add change address to output correctly', async () => {
|
|
63
|
+
const result = await generateBitcoinUnsignedTransactionNativeSegwit(mockArgs);
|
|
64
|
+
|
|
65
|
+
if (result) {
|
|
66
|
+
expect(result.tx.outputsLength).toBe(2);
|
|
67
|
+
expect(result.tx.getOutput(1).script).toBeDefined();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should throw an error if inputs are empty', async () => {
|
|
72
|
+
const argsWithNoInputs: GenerateBitcoinUnsignedTransactionArgs = {
|
|
73
|
+
...mockArgs,
|
|
74
|
+
utxos: [],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const result = await generateBitcoinUnsignedTransactionNativeSegwit(argsWithNoInputs);
|
|
78
|
+
|
|
79
|
+
if (result) {
|
|
80
|
+
expect(result.inputs.length).toBe(0);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { hexToBytes } from '@noble/hashes/utils';
|
|
2
|
+
import * as btc from '@scure/btc-signer';
|
|
3
|
+
|
|
4
|
+
import { BitcoinError, BitcoinErrorMessage } from '../bitcoin-error';
|
|
5
|
+
import { BtcSignerNetwork } from '../bitcoin.network';
|
|
6
|
+
import {
|
|
7
|
+
CoinSelectionRecipient,
|
|
8
|
+
CoinSelectionUtxo,
|
|
9
|
+
determineUtxosForSpend,
|
|
10
|
+
determineUtxosForSpendAll,
|
|
11
|
+
} from '../coin-selection/coin-selection';
|
|
12
|
+
|
|
13
|
+
export interface GenerateBitcoinUnsignedTransactionArgs {
|
|
14
|
+
feeRate: number;
|
|
15
|
+
isSendingMax?: boolean;
|
|
16
|
+
payerAddress: string;
|
|
17
|
+
payerPublicKey: string;
|
|
18
|
+
network: BtcSignerNetwork;
|
|
19
|
+
recipients: CoinSelectionRecipient[];
|
|
20
|
+
utxos: CoinSelectionUtxo[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function generateBitcoinUnsignedTransactionNativeSegwit({
|
|
24
|
+
feeRate,
|
|
25
|
+
isSendingMax,
|
|
26
|
+
payerAddress,
|
|
27
|
+
payerPublicKey,
|
|
28
|
+
network,
|
|
29
|
+
recipients,
|
|
30
|
+
utxos,
|
|
31
|
+
}: GenerateBitcoinUnsignedTransactionArgs) {
|
|
32
|
+
try {
|
|
33
|
+
const determineUtxosArgs = { feeRate, recipients, utxos };
|
|
34
|
+
const { inputs, outputs, fee } = isSendingMax
|
|
35
|
+
? determineUtxosForSpendAll(determineUtxosArgs)
|
|
36
|
+
: determineUtxosForSpend(determineUtxosArgs);
|
|
37
|
+
|
|
38
|
+
if (!inputs.length) throw new BitcoinError(BitcoinErrorMessage.NoInputsToSign);
|
|
39
|
+
if (!outputs.length) throw new BitcoinError(BitcoinErrorMessage.NoOutputsToSign);
|
|
40
|
+
|
|
41
|
+
const tx = new btc.Transaction();
|
|
42
|
+
const p2wpkh = btc.p2wpkh(hexToBytes(payerPublicKey), network);
|
|
43
|
+
|
|
44
|
+
for (const input of inputs) {
|
|
45
|
+
tx.addInput({
|
|
46
|
+
txid: input.txid,
|
|
47
|
+
index: input.vout,
|
|
48
|
+
sequence: 0,
|
|
49
|
+
witnessUtxo: {
|
|
50
|
+
// script = 0014 + pubKeyHash
|
|
51
|
+
script: p2wpkh.script,
|
|
52
|
+
amount: BigInt(input.value),
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
outputs.forEach(output => {
|
|
58
|
+
// When coin selection returns an output with no address,
|
|
59
|
+
// we assume it is a change output
|
|
60
|
+
if (!output.address) {
|
|
61
|
+
tx.addOutputAddress(payerAddress, BigInt(output.value), network);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
tx.addOutputAddress(output.address, BigInt(output.value), network);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return { tx, hex: tx.hex, psbt: tx.toPSBT(), inputs, fee };
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|