@teleportdao/bitcoin 3.0.3 → 4.0.0-alpha.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/dist/bitcoin-interface-ordinal.d.ts +24 -24
- package/dist/bitcoin-interface-ordinal.d.ts.map +1 -1
- package/dist/bitcoin-interface-ordinal.js +89 -107
- package/dist/bitcoin-interface-ordinal.js.map +1 -1
- package/dist/bitcoin-interface-teleswap.d.ts +2 -2
- package/dist/bitcoin-interface-teleswap.d.ts.map +1 -1
- package/dist/bitcoin-interface-teleswap.js +77 -88
- package/dist/bitcoin-interface-teleswap.js.map +1 -1
- package/dist/bitcoin-interface-utils.d.ts +7 -4
- package/dist/bitcoin-interface-utils.d.ts.map +1 -1
- package/dist/bitcoin-interface-utils.js +11 -5
- package/dist/bitcoin-interface-utils.js.map +1 -1
- package/dist/bitcoin-interface-wallet.d.ts +1 -0
- package/dist/bitcoin-interface-wallet.d.ts.map +1 -1
- package/dist/bitcoin-interface-wallet.js +87 -95
- package/dist/bitcoin-interface-wallet.js.map +1 -1
- package/dist/bitcoin-interface.d.ts.map +1 -1
- package/dist/bitcoin-interface.js +58 -87
- package/dist/bitcoin-interface.js.map +1 -1
- package/dist/bitcoin-utils.d.ts +7 -4
- package/dist/bitcoin-utils.d.ts.map +1 -1
- package/dist/bitcoin-utils.js +93 -54
- package/dist/bitcoin-utils.js.map +1 -1
- package/dist/bitcoin-wallet-base.d.ts +23 -11
- package/dist/bitcoin-wallet-base.d.ts.map +1 -1
- package/dist/bitcoin-wallet-base.js +148 -138
- package/dist/bitcoin-wallet-base.js.map +1 -1
- package/dist/helper/brc20-helper.d.ts.map +1 -1
- package/dist/helper/brc20-helper.js +4 -6
- package/dist/helper/brc20-helper.js.map +1 -1
- package/dist/helper/index.js +17 -7
- package/dist/helper/index.js.map +1 -1
- package/dist/helper/ordinal-helper.d.ts +3 -9
- package/dist/helper/ordinal-helper.d.ts.map +1 -1
- package/dist/helper/ordinal-helper.js +20 -85
- package/dist/helper/ordinal-helper.js.map +1 -1
- package/dist/helper/teleswap-helper.d.ts.map +1 -1
- package/dist/helper/teleswap-helper.js +13 -12
- package/dist/helper/teleswap-helper.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -7
- package/dist/index.js.map +1 -1
- package/dist/multisig-wallet-helper.d.ts +14 -1
- package/dist/multisig-wallet-helper.d.ts.map +1 -1
- package/dist/multisig-wallet-helper.js +135 -149
- package/dist/multisig-wallet-helper.js.map +1 -1
- package/dist/ordinal-wallet.d.ts +27 -19
- package/dist/ordinal-wallet.d.ts.map +1 -1
- package/dist/ordinal-wallet.js +263 -293
- package/dist/ordinal-wallet.js.map +1 -1
- package/dist/sign/sign-transaction.d.ts +5 -2
- package/dist/sign/sign-transaction.d.ts.map +1 -1
- package/dist/sign/sign-transaction.js +32 -39
- package/dist/sign/sign-transaction.js.map +1 -1
- package/dist/teleswap-wallet.d.ts +1 -0
- package/dist/teleswap-wallet.d.ts.map +1 -1
- package/dist/teleswap-wallet.js +40 -53
- package/dist/teleswap-wallet.js.map +1 -1
- package/dist/transaction-builder/bitcoin-transaction-builder.js +21 -21
- package/dist/transaction-builder/bitcoin-transaction-builder.js.map +1 -1
- package/dist/transaction-builder/coin-select.d.ts +19 -3
- package/dist/transaction-builder/coin-select.d.ts.map +1 -1
- package/dist/transaction-builder/coin-select.js +193 -181
- package/dist/transaction-builder/coin-select.js.map +1 -1
- package/dist/transaction-builder/index.d.ts +1 -0
- package/dist/transaction-builder/index.d.ts.map +1 -1
- package/dist/transaction-builder/index.js +1 -0
- package/dist/transaction-builder/index.js.map +1 -1
- package/dist/transaction-builder/ordinal-transaction-builder.d.ts +4 -4
- package/dist/transaction-builder/ordinal-transaction-builder.d.ts.map +1 -1
- package/dist/transaction-builder/ordinal-transaction-builder.js +51 -48
- package/dist/transaction-builder/ordinal-transaction-builder.js.map +1 -1
- package/dist/transaction-builder/transaction-builder.d.ts +57 -54
- package/dist/transaction-builder/transaction-builder.d.ts.map +1 -1
- package/dist/transaction-builder/transaction-builder.js +394 -308
- package/dist/transaction-builder/transaction-builder.js.map +1 -1
- package/dist/utils/networks.js +17 -7
- package/dist/utils/networks.js.map +1 -1
- package/dist/utils/tools.js +24 -35
- package/dist/utils/tools.js.map +1 -1
- package/package.json +4 -4
- package/src/bitcoin-interface-utils.ts +17 -8
- package/src/bitcoin-interface-wallet.ts +15 -1
- package/src/bitcoin-utils.ts +52 -3
- package/src/bitcoin-wallet-base.ts +67 -23
- package/src/helper/ordinal-helper.ts +0 -83
- package/src/index.ts +2 -0
- package/src/multisig-wallet-helper.ts +296 -0
- package/src/sign/sign-transaction.ts +5 -7
- package/src/transaction-builder/coin-select.ts +633 -0
- package/src/transaction-builder/index.ts +1 -0
- package/src/transaction-builder/transaction-builder.ts +424 -279
- package/dist/bitcoin-wallet-base copy.d.ts +0 -122
- package/dist/bitcoin-wallet-base copy.d.ts.map +0 -1
- package/dist/bitcoin-wallet-base copy.js +0 -279
- package/dist/bitcoin-wallet-base copy.js.map +0 -1
- package/dist/multisig-coordinator-wallet.d.ts +0 -2
- package/dist/multisig-coordinator-wallet.d.ts.map +0 -1
- package/dist/multisig-coordinator-wallet.js +0 -6
- package/dist/multisig-coordinator-wallet.js.map +0 -1
- package/dist/multisig-wallet.d.ts +0 -20
- package/dist/multisig-wallet.d.ts.map +0 -1
- package/dist/multisig-wallet.js +0 -119
- package/dist/multisig-wallet.js.map +0 -1
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
import * as bitcoin from "bitcoinjs-lib"
|
|
2
|
+
import { ExtendedUtxo, Target } from "./transaction-builder"
|
|
3
|
+
import { getAddressType } from "../bitcoin-utils"
|
|
4
|
+
|
|
5
|
+
export const componentBytes = {
|
|
6
|
+
bytePerInput: {
|
|
7
|
+
p2pkh: 152, // 149
|
|
8
|
+
p2wpkh: 72, // 68
|
|
9
|
+
"p2sh-p2wpkh": 92, // 91
|
|
10
|
+
p2tr: 62, // 58
|
|
11
|
+
default: 92,
|
|
12
|
+
},
|
|
13
|
+
baseTxBytes: 10 + 5, // +5 extra bytes to be sure
|
|
14
|
+
bytePerOutput: {
|
|
15
|
+
p2pkh: 34, // 33
|
|
16
|
+
p2wpkh: 31, // 30
|
|
17
|
+
p2sh: 32, // 31
|
|
18
|
+
p2tr: 43, // 42
|
|
19
|
+
p2wsh: 44, // 43
|
|
20
|
+
default: 35,
|
|
21
|
+
max: 45,
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
scriptExtraBytes: {
|
|
25
|
+
lessThan255: 12,
|
|
26
|
+
moreThan255: 15,
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// https://bitcoin.stackexchange.com/questions/84004/how-do-virtual-size-stripped-size-and-raw-size-compare-between-legacy-address-f
|
|
31
|
+
// export const componentBytes = {
|
|
32
|
+
// bytePerInput: {
|
|
33
|
+
// p2pkh: 148,
|
|
34
|
+
// p2wpkh: 70, // 68
|
|
35
|
+
// "p2sh-p2wpkh": 91,
|
|
36
|
+
// p2tr: 60, // actual 58
|
|
37
|
+
// },
|
|
38
|
+
// baseTxBytes: 10 + 5, // +5 extra bytes to be sure
|
|
39
|
+
// bytePerOutput: {
|
|
40
|
+
// p2pkh: 35, // 34
|
|
41
|
+
// p2wpkh: 35, // 31
|
|
42
|
+
// p2sh: 35, // 32
|
|
43
|
+
// p2tr: 45, // 43
|
|
44
|
+
// p2tr: 45, // 43
|
|
45
|
+
// default: 35,
|
|
46
|
+
// },
|
|
47
|
+
//
|
|
48
|
+
// }
|
|
49
|
+
|
|
50
|
+
// P2SH | 46 + 74n + 34m
|
|
51
|
+
// P2SH-P2WSH | (306 + 76n + 34m) / 4
|
|
52
|
+
// P2WSH | (166 + 76n + 34m) / 4
|
|
53
|
+
|
|
54
|
+
export const DUST = 600
|
|
55
|
+
|
|
56
|
+
export function getInputSize(
|
|
57
|
+
addressType: string,
|
|
58
|
+
details?: {
|
|
59
|
+
// op return or other script
|
|
60
|
+
script?: string
|
|
61
|
+
// n of m multisig
|
|
62
|
+
n?: number
|
|
63
|
+
m?: number
|
|
64
|
+
},
|
|
65
|
+
) {
|
|
66
|
+
let { m = 3 } = details || {}
|
|
67
|
+
const n = details?.n || m || 2
|
|
68
|
+
if (addressType === "p2sh") {
|
|
69
|
+
return 46 + 74 * n + 34 * m
|
|
70
|
+
}
|
|
71
|
+
if (addressType === "p2sh-p2wsh") {
|
|
72
|
+
return +((306 + 76 * n + 34 * m) / 4).toFixed()
|
|
73
|
+
}
|
|
74
|
+
if (addressType === "p2wsh") {
|
|
75
|
+
return +((166 + 76 * n + 34 * m) / 4).toFixed()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
componentBytes.bytePerInput[addressType as keyof typeof componentBytes.bytePerInput] ||
|
|
80
|
+
componentBytes.bytePerInput.default
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getOutputSize(
|
|
85
|
+
output: {
|
|
86
|
+
addressType?: string
|
|
87
|
+
address?: string
|
|
88
|
+
script?: Buffer
|
|
89
|
+
},
|
|
90
|
+
// use network to decode address
|
|
91
|
+
network: bitcoin.Network = bitcoin.networks.bitcoin,
|
|
92
|
+
) {
|
|
93
|
+
if (output.addressType) {
|
|
94
|
+
return (
|
|
95
|
+
componentBytes.bytePerOutput[
|
|
96
|
+
output.addressType as keyof typeof componentBytes.bytePerOutput
|
|
97
|
+
] || componentBytes.bytePerOutput.default
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
if (output.address) {
|
|
101
|
+
let addressType = "max"
|
|
102
|
+
try {
|
|
103
|
+
addressType = getAddressType(output.address, network)
|
|
104
|
+
} catch {
|
|
105
|
+
addressType = "max"
|
|
106
|
+
}
|
|
107
|
+
return (
|
|
108
|
+
componentBytes.bytePerOutput[addressType as keyof typeof componentBytes.bytePerOutput] ||
|
|
109
|
+
componentBytes.bytePerOutput.max
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (output.script) {
|
|
114
|
+
if (output.script.byteLength < 255) {
|
|
115
|
+
return output.script.byteLength + componentBytes.scriptExtraBytes.lessThan255
|
|
116
|
+
}
|
|
117
|
+
return output.script.byteLength + componentBytes.scriptExtraBytes.moreThan255
|
|
118
|
+
}
|
|
119
|
+
throw new Error("invalid output")
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function calculateTxSize(
|
|
123
|
+
inputTypes: {
|
|
124
|
+
addressType: string
|
|
125
|
+
n?: number
|
|
126
|
+
m?: number
|
|
127
|
+
script?: string
|
|
128
|
+
}[],
|
|
129
|
+
outputs: {
|
|
130
|
+
script?: Buffer
|
|
131
|
+
address?: string
|
|
132
|
+
value: number
|
|
133
|
+
}[],
|
|
134
|
+
changeAddressType = "default",
|
|
135
|
+
network: bitcoin.Network = bitcoin.networks.bitcoin,
|
|
136
|
+
) {
|
|
137
|
+
const inputsSizes = inputTypes.map(({ addressType, n, m }) => getInputSize(addressType, { n, m }))
|
|
138
|
+
const outputSizes = outputs.map((outP: any) => {
|
|
139
|
+
if (!outP.address && !outP.script) {
|
|
140
|
+
return getOutputSize(
|
|
141
|
+
{
|
|
142
|
+
addressType: changeAddressType,
|
|
143
|
+
},
|
|
144
|
+
network,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
return getOutputSize(outP, network)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const txSize: number =
|
|
151
|
+
componentBytes.baseTxBytes +
|
|
152
|
+
inputsSizes.reduce((a, c) => a + c, 0) +
|
|
153
|
+
outputSizes.reduce((a, c) => a + c, 0)
|
|
154
|
+
|
|
155
|
+
return txSize
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function sumValues(outputs: { value: number }[]): number {
|
|
159
|
+
return outputs.reduce((sum: number, o) => sum + o.value, 0)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function fixNumberToInteger(value: number): number {
|
|
163
|
+
return Math.ceil(value)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function finalCheckAndAddChangeAndExtraInput(
|
|
167
|
+
inputs: ExtendedUtxo[],
|
|
168
|
+
outputs: Target[],
|
|
169
|
+
outputValue: number,
|
|
170
|
+
outputSize: number,
|
|
171
|
+
feeRate: number,
|
|
172
|
+
changeSize: number,
|
|
173
|
+
highFeeThd: number,
|
|
174
|
+
// for optimization
|
|
175
|
+
extraInput?: ExtendedUtxo,
|
|
176
|
+
smallUtxoAggregate?: ExtendedUtxo[],
|
|
177
|
+
) {
|
|
178
|
+
const selectedInputs = inputs.concat(smallUtxoAggregate || [])
|
|
179
|
+
let selectedInputsValue = sumValues(selectedInputs)
|
|
180
|
+
const selectedInputsSize = selectedInputs.reduce(
|
|
181
|
+
(sum, i) =>
|
|
182
|
+
sum +
|
|
183
|
+
getInputSize(i.signerInfo.addressType, {
|
|
184
|
+
n: i.signerInfo.numberOfSigners,
|
|
185
|
+
m: i.signerInfo.publicKeys?.length,
|
|
186
|
+
}),
|
|
187
|
+
0,
|
|
188
|
+
)
|
|
189
|
+
let txSize = selectedInputsSize + outputSize + componentBytes.baseTxBytes
|
|
190
|
+
let txFee = fixNumberToInteger(txSize * feeRate)
|
|
191
|
+
|
|
192
|
+
let totalNeeded = txFee + outputValue
|
|
193
|
+
let changeValue = 0
|
|
194
|
+
const changeFee = fixNumberToInteger(changeSize * feeRate)
|
|
195
|
+
|
|
196
|
+
if (extraInput && (!smallUtxoAggregate || smallUtxoAggregate.length === 0)) {
|
|
197
|
+
const extraInputSize = fixNumberToInteger(
|
|
198
|
+
getInputSize(extraInput.signerInfo.addressType, {
|
|
199
|
+
n: extraInput.signerInfo.numberOfSigners,
|
|
200
|
+
m: extraInput.signerInfo.publicKeys?.length,
|
|
201
|
+
}),
|
|
202
|
+
)
|
|
203
|
+
const extraInputFee = fixNumberToInteger(extraInputSize * feeRate)
|
|
204
|
+
if (
|
|
205
|
+
selectedInputsValue - totalNeeded > extraInputFee + changeFee &&
|
|
206
|
+
selectedInputsValue - totalNeeded < (feeRate > highFeeThd ? DUST : 2 * DUST)
|
|
207
|
+
) {
|
|
208
|
+
selectedInputs.push(extraInput)
|
|
209
|
+
selectedInputsValue += extraInput.value
|
|
210
|
+
txSize = txSize + extraInputSize
|
|
211
|
+
txFee = fixNumberToInteger(txSize * feeRate)
|
|
212
|
+
totalNeeded = txFee + outputValue
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (selectedInputsValue - totalNeeded > DUST) {
|
|
217
|
+
txSize = txSize + changeSize
|
|
218
|
+
txFee = fixNumberToInteger(txSize * feeRate)
|
|
219
|
+
totalNeeded = txFee + outputValue
|
|
220
|
+
changeValue = selectedInputsValue - totalNeeded
|
|
221
|
+
}
|
|
222
|
+
let needed = totalNeeded - selectedInputsValue
|
|
223
|
+
|
|
224
|
+
// fee absorption for feeRate > 2
|
|
225
|
+
// for feeRate < 2, it could cause the tx to be rejected by the mempool (due to small tx fee per byte)
|
|
226
|
+
if (feeRate >= 2 && needed > 0) {
|
|
227
|
+
// If needed amount is < 10% of fee, absorb it into fee instead of failing
|
|
228
|
+
if (needed < txFee * 0.1) {
|
|
229
|
+
txFee -= needed
|
|
230
|
+
needed = 0
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
success: needed <= 0,
|
|
235
|
+
inputs: selectedInputs,
|
|
236
|
+
outputs,
|
|
237
|
+
change: changeValue > 0 ? { value: changeValue } : undefined,
|
|
238
|
+
fee: txFee,
|
|
239
|
+
bytes: txSize,
|
|
240
|
+
totalInputValue: selectedInputsValue,
|
|
241
|
+
totalOutputValue: outputValue + changeValue,
|
|
242
|
+
needed: needed > 0 ? needed : 0,
|
|
243
|
+
feeRate,
|
|
244
|
+
effectiveFeeRate: +(txFee / txSize).toFixed(2),
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function coinSelectNormal(
|
|
249
|
+
inputs: ExtendedUtxo[],
|
|
250
|
+
outputs: Target[],
|
|
251
|
+
feeRate: number,
|
|
252
|
+
changeAddressType = "max",
|
|
253
|
+
network: bitcoin.Network = bitcoin.networks.bitcoin,
|
|
254
|
+
highFeeThd = 5,
|
|
255
|
+
requiredUtxo?: ExtendedUtxo[],
|
|
256
|
+
smallUtxoAggregate?: {
|
|
257
|
+
minInputCount: number
|
|
258
|
+
maxInputValue: number
|
|
259
|
+
},
|
|
260
|
+
): {
|
|
261
|
+
success: boolean
|
|
262
|
+
inputs: ExtendedUtxo[]
|
|
263
|
+
outputs: Target[]
|
|
264
|
+
fee: number
|
|
265
|
+
bytes: number
|
|
266
|
+
totalInputValue: number
|
|
267
|
+
totalOutputValue: number
|
|
268
|
+
needed: number
|
|
269
|
+
change?: { value: number }
|
|
270
|
+
} {
|
|
271
|
+
// Sort inputs descending
|
|
272
|
+
const totalOutputValue = sumValues(outputs)
|
|
273
|
+
const sortedInputs = [...inputs].sort((a, b) => b.value - a.value)
|
|
274
|
+
const numberOfInputsPerOutputs = feeRate > highFeeThd ? 1 : 2
|
|
275
|
+
const minRequiredInputCount = requiredUtxo ? requiredUtxo.length : 1
|
|
276
|
+
|
|
277
|
+
// randomize priority inputs to another array
|
|
278
|
+
let priorityInputs = sortedInputs.filter(
|
|
279
|
+
(inp) => inp.value >= totalOutputValue / outputs.length / numberOfInputsPerOutputs,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
priorityInputs = priorityInputs.sort(() => Math.random() - 0.5)
|
|
283
|
+
|
|
284
|
+
// select rest of the inputs from sorted inputs
|
|
285
|
+
const otherInputs = sortedInputs.slice(priorityInputs.length)
|
|
286
|
+
let allInputs = priorityInputs.concat(otherInputs)
|
|
287
|
+
|
|
288
|
+
if (requiredUtxo) {
|
|
289
|
+
// remove required utxo from all inputs
|
|
290
|
+
allInputs = allInputs.filter(
|
|
291
|
+
(inp) => !requiredUtxo.some((r) => r.hash === inp.hash && r.index === inp.index),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
// check if its better to put require utxo in utxo list or not
|
|
295
|
+
// if (sortedInputs.length - allInputs.length !== requiredUtxo.length) {
|
|
296
|
+
// throw new Error("required utxo is not exist in utxo list")
|
|
297
|
+
// }
|
|
298
|
+
//
|
|
299
|
+
allInputs = requiredUtxo.concat(allInputs)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const outputSize = outputs.reduce((sum, o) => sum + getOutputSize(o, network), 0)
|
|
303
|
+
const changeSize = getOutputSize({ addressType: changeAddressType }, network)
|
|
304
|
+
const baseTxSize = componentBytes.baseTxBytes
|
|
305
|
+
|
|
306
|
+
let txSize = outputSize + baseTxSize
|
|
307
|
+
let txFee = fixNumberToInteger(txSize * feeRate)
|
|
308
|
+
const selected: ExtendedUtxo[] = []
|
|
309
|
+
|
|
310
|
+
for (let index = 0; index < allInputs.length; index += 1) {
|
|
311
|
+
const input = allInputs[index]
|
|
312
|
+
selected.push(input)
|
|
313
|
+
const inputSize = getInputSize(input.signerInfo.addressType, {
|
|
314
|
+
n: input.signerInfo.numberOfSigners,
|
|
315
|
+
m: input.signerInfo.publicKeys?.length,
|
|
316
|
+
})
|
|
317
|
+
const selectedValue = sumValues(selected)
|
|
318
|
+
|
|
319
|
+
txSize += inputSize
|
|
320
|
+
txFee = txSize * feeRate
|
|
321
|
+
let totalNeeded = txFee + totalOutputValue
|
|
322
|
+
|
|
323
|
+
if (selectedValue >= totalNeeded && selected.length >= minRequiredInputCount) {
|
|
324
|
+
let smallUtxo: ExtendedUtxo[] | undefined
|
|
325
|
+
|
|
326
|
+
if (
|
|
327
|
+
feeRate < 1.3 &&
|
|
328
|
+
smallUtxoAggregate &&
|
|
329
|
+
selected.length < smallUtxoAggregate.minInputCount
|
|
330
|
+
) {
|
|
331
|
+
const selectedInputIds = new Set(selected.map((inp) => `${inp.hash}:${inp.index}`))
|
|
332
|
+
smallUtxo = allInputs
|
|
333
|
+
.filter((_, inpIndex) => inpIndex > index)
|
|
334
|
+
.filter((inp) => inp.value < smallUtxoAggregate.maxInputValue)
|
|
335
|
+
// todo: check if we need check if the input is already selected because we already filter it using index
|
|
336
|
+
.filter((inp) => !selectedInputIds.has(`${inp.hash}:${inp.index}`))
|
|
337
|
+
|
|
338
|
+
smallUtxo = smallUtxo.sort((a, b) => a.value - b.value)
|
|
339
|
+
smallUtxo = smallUtxo.slice(0, smallUtxoAggregate.minInputCount - selected.length)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return finalCheckAndAddChangeAndExtraInput(
|
|
343
|
+
selected,
|
|
344
|
+
outputs,
|
|
345
|
+
totalOutputValue,
|
|
346
|
+
outputSize,
|
|
347
|
+
feeRate,
|
|
348
|
+
changeSize,
|
|
349
|
+
highFeeThd,
|
|
350
|
+
smallUtxo && smallUtxo.length > 0 ? undefined : allInputs[index + 1],
|
|
351
|
+
smallUtxo, // if there is small utxo, extraInput will be ignored so its ok if extraInput exists in small utxo
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return finalCheckAndAddChangeAndExtraInput(
|
|
356
|
+
allInputs,
|
|
357
|
+
outputs,
|
|
358
|
+
totalOutputValue,
|
|
359
|
+
outputSize,
|
|
360
|
+
feeRate,
|
|
361
|
+
changeSize,
|
|
362
|
+
highFeeThd,
|
|
363
|
+
undefined,
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Branch & Bound coin selection
|
|
368
|
+
export function coinSelectBnB(
|
|
369
|
+
inputs: ExtendedUtxo[],
|
|
370
|
+
outputs: Target[],
|
|
371
|
+
feeRate: number,
|
|
372
|
+
changeAddressType = "max",
|
|
373
|
+
network: bitcoin.Network = bitcoin.networks.bitcoin,
|
|
374
|
+
highFeeThd = 5,
|
|
375
|
+
smallUtxoAggregate?: {
|
|
376
|
+
minInputCount: number
|
|
377
|
+
maxInputValue: number
|
|
378
|
+
},
|
|
379
|
+
): {
|
|
380
|
+
success: boolean
|
|
381
|
+
inputs: ExtendedUtxo[]
|
|
382
|
+
outputs: Target[]
|
|
383
|
+
fee: number
|
|
384
|
+
bytes: number
|
|
385
|
+
totalInputValue: number
|
|
386
|
+
totalOutputValue: number
|
|
387
|
+
needed: number
|
|
388
|
+
change?: { value: number }
|
|
389
|
+
} {
|
|
390
|
+
if (inputs.length > 20) {
|
|
391
|
+
return coinSelectNormal(
|
|
392
|
+
inputs,
|
|
393
|
+
outputs,
|
|
394
|
+
feeRate,
|
|
395
|
+
changeAddressType,
|
|
396
|
+
network,
|
|
397
|
+
highFeeThd,
|
|
398
|
+
undefined,
|
|
399
|
+
smallUtxoAggregate,
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Sort inputs descending
|
|
404
|
+
const totalOutputValue = sumValues(outputs)
|
|
405
|
+
|
|
406
|
+
const outputSize = outputs.reduce((sum, o) => sum + getOutputSize(o, network), 0)
|
|
407
|
+
const changeSize = getOutputSize({ addressType: changeAddressType }, network)
|
|
408
|
+
const baseTxSize = componentBytes.baseTxBytes
|
|
409
|
+
|
|
410
|
+
const sortedInputs = [...inputs].sort((a, b) => b.value - a.value)
|
|
411
|
+
const numberOfInputsPerOutputs = feeRate > highFeeThd ? 1 : 2
|
|
412
|
+
|
|
413
|
+
let lastInput: ExtendedUtxo | undefined = sortedInputs[sortedInputs.length - 1]
|
|
414
|
+
let bnbInputs = sortedInputs
|
|
415
|
+
.slice(0, -1)
|
|
416
|
+
.filter((inp) => inp.value > totalOutputValue / outputs.length / numberOfInputsPerOutputs)
|
|
417
|
+
|
|
418
|
+
let bestWaste = Infinity
|
|
419
|
+
let bestSelection: ExtendedUtxo[] | null = null
|
|
420
|
+
function bnb(idx: number, selected: ExtendedUtxo[]) {
|
|
421
|
+
const selectedValue = sumValues(selected)
|
|
422
|
+
const inputSize = selected.reduce(
|
|
423
|
+
(sum, i) =>
|
|
424
|
+
sum +
|
|
425
|
+
getInputSize(i.signerInfo.addressType, {
|
|
426
|
+
n: i.signerInfo.numberOfSigners,
|
|
427
|
+
m: i.signerInfo.publicKeys?.length,
|
|
428
|
+
}),
|
|
429
|
+
0,
|
|
430
|
+
)
|
|
431
|
+
const txSize = outputSize + baseTxSize + inputSize
|
|
432
|
+
const txFee = fixNumberToInteger(txSize * feeRate)
|
|
433
|
+
let totalNeeded = txFee + totalOutputValue
|
|
434
|
+
|
|
435
|
+
if (selectedValue - totalNeeded > DUST) {
|
|
436
|
+
totalNeeded = totalNeeded + fixNumberToInteger(changeSize * feeRate)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Return if over target+fee+bestWaste
|
|
440
|
+
if (selectedValue > totalNeeded + bestWaste) return
|
|
441
|
+
|
|
442
|
+
// If enough, check if best
|
|
443
|
+
if (selectedValue >= totalNeeded) {
|
|
444
|
+
const waste = selectedValue - totalNeeded
|
|
445
|
+
if (waste < bestWaste) {
|
|
446
|
+
bestWaste = waste
|
|
447
|
+
bestSelection = selected.slice()
|
|
448
|
+
}
|
|
449
|
+
return
|
|
450
|
+
}
|
|
451
|
+
// If out of inputs, stop
|
|
452
|
+
if (idx >= bnbInputs.length) return
|
|
453
|
+
// Branch: include current input
|
|
454
|
+
bnb(idx + 1, [...selected, bnbInputs[idx]])
|
|
455
|
+
// Branch: exclude current input
|
|
456
|
+
bnb(idx + 1, selected)
|
|
457
|
+
}
|
|
458
|
+
bnb(0, [])
|
|
459
|
+
|
|
460
|
+
if (!bestSelection) {
|
|
461
|
+
bnbInputs = sortedInputs
|
|
462
|
+
bestWaste = Infinity
|
|
463
|
+
bestSelection = null
|
|
464
|
+
bnb(0, [])
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// aggregate small utxo
|
|
468
|
+
const selectedInputs = bestSelection || inputs
|
|
469
|
+
let smallUtxo: ExtendedUtxo[] | undefined
|
|
470
|
+
if (
|
|
471
|
+
feeRate < 1.3 &&
|
|
472
|
+
smallUtxoAggregate &&
|
|
473
|
+
selectedInputs.length < smallUtxoAggregate.minInputCount
|
|
474
|
+
) {
|
|
475
|
+
const selectedInputIds = new Set(selectedInputs.map((input) => `${input.hash}:${input.index}`))
|
|
476
|
+
smallUtxo = inputs
|
|
477
|
+
.filter((input) => !selectedInputIds.has(`${input.hash}:${input.index}`))
|
|
478
|
+
.filter((input) => input.value < smallUtxoAggregate.maxInputValue)
|
|
479
|
+
smallUtxo = smallUtxo.sort((a, b) => a.value - b.value)
|
|
480
|
+
smallUtxo = smallUtxo.slice(0, smallUtxoAggregate.minInputCount - selectedInputs.length)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return finalCheckAndAddChangeAndExtraInput(
|
|
484
|
+
selectedInputs,
|
|
485
|
+
outputs,
|
|
486
|
+
totalOutputValue,
|
|
487
|
+
outputSize,
|
|
488
|
+
feeRate,
|
|
489
|
+
changeSize,
|
|
490
|
+
highFeeThd,
|
|
491
|
+
lastInput,
|
|
492
|
+
smallUtxo,
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export function coinSelectAccumulative(
|
|
497
|
+
inputs: ExtendedUtxo[],
|
|
498
|
+
outputs: Target[],
|
|
499
|
+
feeRate: number,
|
|
500
|
+
changeAddressType = "max",
|
|
501
|
+
network: bitcoin.Network = bitcoin.networks.bitcoin,
|
|
502
|
+
highFeeThd = 5,
|
|
503
|
+
): {
|
|
504
|
+
success: boolean
|
|
505
|
+
inputs: ExtendedUtxo[]
|
|
506
|
+
outputs: Target[]
|
|
507
|
+
fee: number
|
|
508
|
+
totalInputValue: number
|
|
509
|
+
totalOutputValue: number
|
|
510
|
+
needed: number
|
|
511
|
+
change?: { value: number }
|
|
512
|
+
} {
|
|
513
|
+
const totalOutputValue = sumValues(outputs)
|
|
514
|
+
|
|
515
|
+
const outputSize = outputs.reduce((sum, o) => sum + getOutputSize(o, network), 0)
|
|
516
|
+
const changeSize = getOutputSize({ addressType: changeAddressType }, network)
|
|
517
|
+
const baseTxSize = componentBytes.baseTxBytes
|
|
518
|
+
|
|
519
|
+
const selected: ExtendedUtxo[] = []
|
|
520
|
+
|
|
521
|
+
let txSize = outputSize + baseTxSize
|
|
522
|
+
let txFee = fixNumberToInteger(txSize * feeRate)
|
|
523
|
+
for (let index = 0; index < inputs.length; index += 1) {
|
|
524
|
+
const input = inputs[index]
|
|
525
|
+
selected.push(input)
|
|
526
|
+
const inputSize = getInputSize(input.signerInfo.addressType, {
|
|
527
|
+
n: input.signerInfo.numberOfSigners,
|
|
528
|
+
m: input.signerInfo.publicKeys?.length,
|
|
529
|
+
})
|
|
530
|
+
const selectedValue = sumValues(selected)
|
|
531
|
+
|
|
532
|
+
txSize += inputSize
|
|
533
|
+
txFee = txSize * feeRate
|
|
534
|
+
|
|
535
|
+
let totalNeeded = txFee + totalOutputValue
|
|
536
|
+
|
|
537
|
+
if (selectedValue >= totalNeeded) {
|
|
538
|
+
return finalCheckAndAddChangeAndExtraInput(
|
|
539
|
+
selected,
|
|
540
|
+
outputs,
|
|
541
|
+
totalOutputValue,
|
|
542
|
+
outputSize,
|
|
543
|
+
feeRate,
|
|
544
|
+
changeSize,
|
|
545
|
+
highFeeThd,
|
|
546
|
+
inputs[index + 1] || undefined,
|
|
547
|
+
)
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return finalCheckAndAddChangeAndExtraInput(
|
|
551
|
+
selected,
|
|
552
|
+
outputs,
|
|
553
|
+
totalOutputValue,
|
|
554
|
+
outputSize,
|
|
555
|
+
feeRate,
|
|
556
|
+
changeSize,
|
|
557
|
+
highFeeThd,
|
|
558
|
+
undefined,
|
|
559
|
+
)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export function coinSelectAll(
|
|
563
|
+
inputs: ExtendedUtxo[],
|
|
564
|
+
targets: Target[],
|
|
565
|
+
feeRate: number,
|
|
566
|
+
changeAddressType = "max",
|
|
567
|
+
network: bitcoin.Network = bitcoin.networks.bitcoin,
|
|
568
|
+
): {
|
|
569
|
+
success: boolean
|
|
570
|
+
inputs: ExtendedUtxo[]
|
|
571
|
+
outputs: Target[]
|
|
572
|
+
fee: number
|
|
573
|
+
totalInputValue: number
|
|
574
|
+
totalOutputValue: number
|
|
575
|
+
needed: number
|
|
576
|
+
change?: { value: number }
|
|
577
|
+
} {
|
|
578
|
+
if (!targets[0].address) {
|
|
579
|
+
throw new Error("target address is required")
|
|
580
|
+
}
|
|
581
|
+
const changeSize = getOutputSize({ addressType: changeAddressType }, network)
|
|
582
|
+
return finalCheckAndAddChangeAndExtraInput(inputs, [], 0, 0, feeRate, changeSize, 0)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// todo : remove --- Sample usage/demo ---
|
|
586
|
+
if (require.main === module) {
|
|
587
|
+
let utxos: ExtendedUtxo[] = [
|
|
588
|
+
{
|
|
589
|
+
signerInfo: { address: "A", publicKey: "", addressType: "p2pkh" },
|
|
590
|
+
hash: "h1",
|
|
591
|
+
value: 50000,
|
|
592
|
+
index: 0,
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
signerInfo: { address: "B", publicKey: "", addressType: "p2pkh" },
|
|
596
|
+
hash: "h2",
|
|
597
|
+
value: 30000,
|
|
598
|
+
index: 1,
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
signerInfo: { address: "C", publicKey: "", addressType: "p2pkh" },
|
|
602
|
+
hash: "h3",
|
|
603
|
+
value: 4000,
|
|
604
|
+
index: 2,
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
signerInfo: { address: "D", publicKey: "", addressType: "p2pkh" },
|
|
608
|
+
hash: "h4",
|
|
609
|
+
value: 10000,
|
|
610
|
+
index: 3,
|
|
611
|
+
},
|
|
612
|
+
]
|
|
613
|
+
// Mock outputs
|
|
614
|
+
|
|
615
|
+
const outputs: Target[] = [
|
|
616
|
+
{ address: "X", value: 6640 },
|
|
617
|
+
// { address: "Y", value: 15000 },
|
|
618
|
+
// { address: "Y", value: 15000 },
|
|
619
|
+
]
|
|
620
|
+
const outputs2: Target[] = [
|
|
621
|
+
{ address: "X", value: 40150 },
|
|
622
|
+
{ address: "Y", value: 2484 },
|
|
623
|
+
{ address: "Z", value: 50000 },
|
|
624
|
+
]
|
|
625
|
+
const feeRate = 2 // sats/byte
|
|
626
|
+
|
|
627
|
+
console.log("Coin selection result:", coinSelectBnB(utxos, outputs, feeRate))
|
|
628
|
+
console.log("Coin selection result:", coinSelectAccumulative(utxos, outputs, feeRate))
|
|
629
|
+
console.log("Coin selection result:", coinSelectAll(utxos, outputs, feeRate))
|
|
630
|
+
|
|
631
|
+
console.log("Selection failed:", coinSelectBnB(utxos, outputs2, feeRate))
|
|
632
|
+
console.log("Selection failed:", coinSelectAccumulative(utxos, outputs2, feeRate))
|
|
633
|
+
}
|