@teleportdao/bitcoin 1.7.21 → 1.8.4
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/.tmp/ordinal-helper.ts +133 -0
- package/.tmp/ordinal.ts +25 -0
- package/.tmp/rbf.ts +27 -24
- package/dist/bitcoin-base.d.ts +93 -0
- package/dist/bitcoin-base.d.ts.map +1 -0
- package/dist/bitcoin-base.js +236 -0
- package/dist/bitcoin-base.js.map +1 -0
- package/dist/helper/brc20-helper.d.ts +1 -1
- package/dist/helper/brc20-helper.d.ts.map +1 -1
- package/dist/helper/brc20-helper.js +2 -3
- package/dist/helper/brc20-helper.js.map +1 -1
- package/dist/helper/burn-request-helper.d.ts +7 -0
- package/dist/helper/burn-request-helper.d.ts.map +1 -0
- package/dist/helper/burn-request-helper.js +26 -0
- package/dist/helper/burn-request-helper.js.map +1 -0
- package/dist/helper/teleport-request-helper.d.ts +47 -0
- package/dist/helper/teleport-request-helper.d.ts.map +1 -0
- package/dist/helper/teleport-request-helper.js +146 -0
- package/dist/helper/teleport-request-helper.js.map +1 -0
- package/dist/teleport-dao-payments.d.ts +76 -0
- package/dist/teleport-dao-payments.d.ts.map +1 -0
- package/dist/teleport-dao-payments.js +217 -0
- package/dist/teleport-dao-payments.js.map +1 -0
- package/package.json +4 -4
- package/src/bitcoin-interface-ordinal.ts +181 -181
- package/src/bitcoin-interface-teleswap.ts +252 -252
- package/src/bitcoin-interface-utils.ts +60 -60
- package/src/bitcoin-interface.ts +241 -241
- package/src/bitcoin-utils.ts +591 -591
- package/src/bitcoin-wallet-base.ts +310 -310
- package/src/helper/brc20-helper.ts +179 -181
- package/src/helper/ordinal-helper.ts +118 -118
- package/src/index.ts +15 -15
- package/src/ordinal-wallet.ts +738 -738
- package/src/sign/index.ts +1 -1
- package/src/sign/sign-transaction.ts +108 -108
- package/src/teleswap-wallet.ts +155 -155
- package/src/transaction-builder/bitcoin-transaction-builder.ts +44 -44
- package/src/transaction-builder/index.ts +3 -3
- package/src/transaction-builder/ordinal-transaction-builder.ts +147 -147
- package/src/transaction-builder/transaction-builder.ts +706 -706
- package/src/type.ts +48 -48
- package/src/utils/networks.ts +33 -33
- package/src/utils/tools.ts +90 -90
- package/tsconfig.json +9 -9
- package/webpack.config.js +16 -16
|
@@ -1,706 +1,706 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
2
|
-
/* eslint-disable no-underscore-dangle */
|
|
3
|
-
import * as bitcoin from "bitcoinjs-lib"
|
|
4
|
-
|
|
5
|
-
import { createAddressObjectByPublicKey, getAddressType } from "../bitcoin-utils"
|
|
6
|
-
|
|
7
|
-
const coinselect = require("coinselect")
|
|
8
|
-
const coinselectSplit = require("coinselect/split")
|
|
9
|
-
const coinselectAccumulative = require("coinselect/accumulative")
|
|
10
|
-
|
|
11
|
-
// https://bitcoin.stackexchange.com/questions/84004/how-do-virtual-size-stripped-size-and-raw-size-compare-between-legacy-address-f
|
|
12
|
-
// export const componentBytes = {
|
|
13
|
-
// bytePerInput: {
|
|
14
|
-
// p2pkh: 148,
|
|
15
|
-
// p2wpkh: 70, // 68
|
|
16
|
-
// "p2sh-p2wpkh": 91,
|
|
17
|
-
// p2tr: 60, // actual 58
|
|
18
|
-
// },
|
|
19
|
-
// baseTxBytes: 10 + 5, // +5 extra bytes to be sure
|
|
20
|
-
// bytePerOutput: {
|
|
21
|
-
// p2pkh: 35, // 34
|
|
22
|
-
// p2wpkh: 35, // 31
|
|
23
|
-
// p2sh: 35, // 32
|
|
24
|
-
// p2tr: 45, // 43
|
|
25
|
-
// default: 35,
|
|
26
|
-
// },
|
|
27
|
-
//
|
|
28
|
-
// }
|
|
29
|
-
export const componentBytes = {
|
|
30
|
-
bytePerInput: {
|
|
31
|
-
p2pkh: 148,
|
|
32
|
-
p2wpkh: 68, // 68
|
|
33
|
-
"p2sh-p2wpkh": 91,
|
|
34
|
-
p2tr: 58, // actual 58
|
|
35
|
-
default: 100,
|
|
36
|
-
},
|
|
37
|
-
baseTxBytes: 10 + 5, // +5 extra bytes to be sure
|
|
38
|
-
bytePerOutput: {
|
|
39
|
-
p2pkh: 34, // 34
|
|
40
|
-
p2wpkh: 31, // 31
|
|
41
|
-
p2sh: 32, // 32
|
|
42
|
-
p2tr: 43, // 43
|
|
43
|
-
default: 35,
|
|
44
|
-
max: 45,
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
scriptExtraBytes: {
|
|
48
|
-
lessThan255: 12,
|
|
49
|
-
moreThan255: 15,
|
|
50
|
-
},
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export const DUST = 1000
|
|
54
|
-
|
|
55
|
-
export type Utxo = {
|
|
56
|
-
hash: string
|
|
57
|
-
value: number
|
|
58
|
-
index: number
|
|
59
|
-
}
|
|
60
|
-
export type SignerInfo = {
|
|
61
|
-
address: string
|
|
62
|
-
publicKey: string
|
|
63
|
-
addressType: string
|
|
64
|
-
derivationPath?: string
|
|
65
|
-
masterFingerprint?: string
|
|
66
|
-
includeHex?: boolean
|
|
67
|
-
}
|
|
68
|
-
export type ExtendedUtxo = {
|
|
69
|
-
signerInfo: SignerInfo
|
|
70
|
-
hash: string
|
|
71
|
-
value: number
|
|
72
|
-
index: number
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export type TargetAddress = {
|
|
76
|
-
address: string
|
|
77
|
-
value: number
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export type TargetScript = {
|
|
81
|
-
script: Buffer
|
|
82
|
-
value: number
|
|
83
|
-
}
|
|
84
|
-
export type Target = TargetAddress | TargetScript
|
|
85
|
-
export type ChangeTarget = TargetAddress & {
|
|
86
|
-
bip32Derivation?: {
|
|
87
|
-
path: string
|
|
88
|
-
pubkey: Buffer
|
|
89
|
-
masterFingerprint: Buffer
|
|
90
|
-
}[]
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export type BitcoinJSInputInfo = ExtendedUtxo & {
|
|
94
|
-
bip32Derivation?: {
|
|
95
|
-
path: string
|
|
96
|
-
pubkey: Buffer
|
|
97
|
-
masterFingerprint: Buffer
|
|
98
|
-
}[]
|
|
99
|
-
nonWitnessUtxo?: Buffer
|
|
100
|
-
witnessUtxo?: {
|
|
101
|
-
script: Buffer
|
|
102
|
-
value: number
|
|
103
|
-
}
|
|
104
|
-
redeemScript?: Buffer
|
|
105
|
-
tapInternalKey?: Buffer
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export type ExtendedUnsignedTransaction = {
|
|
109
|
-
unsignedTransaction: string
|
|
110
|
-
outputs: Target[]
|
|
111
|
-
inputs: {
|
|
112
|
-
hash: string
|
|
113
|
-
value: number
|
|
114
|
-
index: number
|
|
115
|
-
signerInfo: SignerInfo
|
|
116
|
-
}[]
|
|
117
|
-
fee: number
|
|
118
|
-
change: TargetAddress | undefined
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function coinSelectInOrder(
|
|
122
|
-
utxos: { hash: string; index: number; value: number }[],
|
|
123
|
-
outputs: {
|
|
124
|
-
address?: string
|
|
125
|
-
script?: Buffer
|
|
126
|
-
value: number
|
|
127
|
-
}[],
|
|
128
|
-
feeRate: number,
|
|
129
|
-
) {
|
|
130
|
-
let response = coinselectAccumulative(utxos, outputs, 1)
|
|
131
|
-
return {
|
|
132
|
-
...response,
|
|
133
|
-
fee: +(+response.fee * feeRate).toFixed(),
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export class BaseTransactionBuilder {
|
|
138
|
-
testnet: boolean
|
|
139
|
-
network: bitcoin.Network
|
|
140
|
-
maximumNumberOfOutputsInTransaction: number
|
|
141
|
-
feeMin: number
|
|
142
|
-
dustLimit: number
|
|
143
|
-
// abstract
|
|
144
|
-
constructor({
|
|
145
|
-
network,
|
|
146
|
-
testnet,
|
|
147
|
-
feeMin = 0,
|
|
148
|
-
dustLimit,
|
|
149
|
-
maximumNumberOfOutputsInTransaction = 50,
|
|
150
|
-
}: {
|
|
151
|
-
network: bitcoin.Network
|
|
152
|
-
testnet: boolean
|
|
153
|
-
feeMin?: number
|
|
154
|
-
dustLimit?: number
|
|
155
|
-
maximumNumberOfOutputsInTransaction?: number
|
|
156
|
-
}) {
|
|
157
|
-
this.testnet = testnet
|
|
158
|
-
this.network = network
|
|
159
|
-
this.maximumNumberOfOutputsInTransaction = maximumNumberOfOutputsInTransaction
|
|
160
|
-
this.feeMin = feeMin
|
|
161
|
-
this.dustLimit = dustLimit || 1 * 2 * componentBytes.bytePerInput.p2pkh
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// eslint-disable-next-line no-unused-vars, class-methods-use-this
|
|
165
|
-
async _getUtxo(userAddress: string): Promise<Utxo[]> {
|
|
166
|
-
// The child has implemented this method.
|
|
167
|
-
throw new Error("Do not call abstract method directly")
|
|
168
|
-
// return utxo
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// eslint-disable-next-line no-unused-vars, class-methods-use-this
|
|
172
|
-
async _getTransactionHex(transactionId: string): Promise<string> {
|
|
173
|
-
// The child has implemented this method.
|
|
174
|
-
throw new Error("Do not call abstract method directly")
|
|
175
|
-
// return utxo
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// eslint-disable-next-line no-unused-vars, class-methods-use-this
|
|
179
|
-
createAddressObject(input: { addressType: string; publicKey: Buffer }) {
|
|
180
|
-
return createAddressObjectByPublicKey(input, this.network)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// methods
|
|
184
|
-
validateAddress(address: string) {
|
|
185
|
-
try {
|
|
186
|
-
getAddressType(address, this.network)
|
|
187
|
-
return true
|
|
188
|
-
} catch (error) {
|
|
189
|
-
return false
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
getOpReturnTarget(dataHex: string) {
|
|
194
|
-
if (!(dataHex.length > 0)) throw new Error("invalid data in hex")
|
|
195
|
-
const embed = bitcoin.payments.embed({
|
|
196
|
-
data: [Buffer.from(dataHex, "hex")],
|
|
197
|
-
network: this.network,
|
|
198
|
-
})
|
|
199
|
-
return {
|
|
200
|
-
script: embed.output!,
|
|
201
|
-
value: 0,
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
async getExtendedUtxo(signerInfo: SignerInfo) {
|
|
206
|
-
let utxo = await this._getUtxo(signerInfo.address)
|
|
207
|
-
const extendedUtxo = utxo.map((input) => ({
|
|
208
|
-
...input,
|
|
209
|
-
signerInfo,
|
|
210
|
-
}))
|
|
211
|
-
if (!extendedUtxo || extendedUtxo.length === 0) {
|
|
212
|
-
return []
|
|
213
|
-
}
|
|
214
|
-
return extendedUtxo
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
calculateTxSize(
|
|
218
|
-
inputTypes: string[],
|
|
219
|
-
outputs: {
|
|
220
|
-
script?: Buffer
|
|
221
|
-
address?: string
|
|
222
|
-
value: number
|
|
223
|
-
}[],
|
|
224
|
-
changeAddressType = "default",
|
|
225
|
-
) {
|
|
226
|
-
const inputsSizes = inputTypes.map(
|
|
227
|
-
(addressType) =>
|
|
228
|
-
componentBytes.bytePerInput[addressType as keyof typeof componentBytes.bytePerInput],
|
|
229
|
-
)
|
|
230
|
-
const outputSizes = outputs.map((outP: any) => {
|
|
231
|
-
if (outP.address) {
|
|
232
|
-
let addressType = "default"
|
|
233
|
-
try {
|
|
234
|
-
addressType = getAddressType(outP.address, this.network)
|
|
235
|
-
} catch {
|
|
236
|
-
addressType = "default"
|
|
237
|
-
}
|
|
238
|
-
return componentBytes.bytePerOutput[
|
|
239
|
-
addressType as keyof typeof componentBytes.bytePerOutput
|
|
240
|
-
]
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (outP.script) {
|
|
244
|
-
if (outP.script.byteLength < 255) {
|
|
245
|
-
return outP.script.byteLength + componentBytes.scriptExtraBytes.lessThan255
|
|
246
|
-
}
|
|
247
|
-
return outP.script.byteLength + componentBytes.scriptExtraBytes.moreThan255
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return componentBytes.bytePerOutput[
|
|
251
|
-
changeAddressType as keyof typeof componentBytes.bytePerOutput
|
|
252
|
-
]
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
const txSize: number =
|
|
256
|
-
componentBytes.baseTxBytes +
|
|
257
|
-
inputsSizes.reduce((a, c) => a + c, 0) +
|
|
258
|
-
outputSizes.reduce((a, c) => a + c, 0)
|
|
259
|
-
|
|
260
|
-
return txSize
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
helperHandleInputsAndOutputs({
|
|
264
|
-
targets,
|
|
265
|
-
extendedUtxo,
|
|
266
|
-
feeRate,
|
|
267
|
-
changeObject,
|
|
268
|
-
selectType = "normal", // "accumulative" | "normal" | "full"
|
|
269
|
-
}: {
|
|
270
|
-
extendedUtxo: ExtendedUtxo[]
|
|
271
|
-
targets: Target[]
|
|
272
|
-
feeRate: number
|
|
273
|
-
changeObject?: {
|
|
274
|
-
address: string
|
|
275
|
-
publicKey?: string
|
|
276
|
-
addressType?: string
|
|
277
|
-
derivationPath?: string
|
|
278
|
-
masterFingerprint?: string
|
|
279
|
-
}
|
|
280
|
-
selectType?: "normal" | "accumulative" | "full" | "inOrder"
|
|
281
|
-
}) {
|
|
282
|
-
const filteredUtxo = extendedUtxo.filter(
|
|
283
|
-
(u) =>
|
|
284
|
-
u.value >
|
|
285
|
-
+feeRate *
|
|
286
|
-
componentBytes.bytePerInput[
|
|
287
|
-
u.signerInfo.addressType as keyof typeof componentBytes.bytePerInput
|
|
288
|
-
],
|
|
289
|
-
)
|
|
290
|
-
let selectResponse
|
|
291
|
-
switch (selectType) {
|
|
292
|
-
case "normal":
|
|
293
|
-
selectResponse = coinselect(filteredUtxo, targets, Math.round(feeRate))
|
|
294
|
-
break
|
|
295
|
-
case "accumulative":
|
|
296
|
-
selectResponse = coinselectAccumulative(filteredUtxo, targets, Math.round(feeRate))
|
|
297
|
-
break
|
|
298
|
-
case "inOrder":
|
|
299
|
-
selectResponse = coinSelectInOrder(filteredUtxo, targets, Math.round(feeRate))
|
|
300
|
-
break
|
|
301
|
-
case "full":
|
|
302
|
-
if (!(targets[0] as TargetAddress).address) {
|
|
303
|
-
throw new Error()
|
|
304
|
-
}
|
|
305
|
-
selectResponse = coinselectSplit(
|
|
306
|
-
filteredUtxo,
|
|
307
|
-
[{ address: (targets[0] as TargetAddress).address }],
|
|
308
|
-
Math.round(feeRate),
|
|
309
|
-
)
|
|
310
|
-
break
|
|
311
|
-
default:
|
|
312
|
-
break
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
let {
|
|
316
|
-
inputs,
|
|
317
|
-
outputs,
|
|
318
|
-
fee,
|
|
319
|
-
}: {
|
|
320
|
-
inputs?: ExtendedUtxo[]
|
|
321
|
-
outputs?: {
|
|
322
|
-
script?: Buffer
|
|
323
|
-
address?: string
|
|
324
|
-
value: number
|
|
325
|
-
}[]
|
|
326
|
-
fee: number
|
|
327
|
-
} = selectResponse
|
|
328
|
-
|
|
329
|
-
if (!inputs || !outputs) {
|
|
330
|
-
inputs = filteredUtxo
|
|
331
|
-
outputs = targets
|
|
332
|
-
fee = inputs.reduce((a, b) => a + b.value, 0) - outputs.reduce((a, b) => a + b.value, 0)
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
let changeAddressType = "default"
|
|
336
|
-
try {
|
|
337
|
-
changeAddressType = getAddressType(changeObject?.address || "", this.network)
|
|
338
|
-
} catch {
|
|
339
|
-
changeAddressType = "default"
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const txSize =
|
|
343
|
-
this.calculateTxSize(
|
|
344
|
-
inputs.map((i) => i.signerInfo.addressType),
|
|
345
|
-
outputs,
|
|
346
|
-
changeAddressType,
|
|
347
|
-
) + componentBytes.bytePerOutput.default
|
|
348
|
-
|
|
349
|
-
let txFee = Math.round(txSize * feeRate)
|
|
350
|
-
if (Math.round(feeRate) === 1) {
|
|
351
|
-
txFee = Math.round(txFee + txFee * 0.1)
|
|
352
|
-
}
|
|
353
|
-
if (
|
|
354
|
-
inputs.reduce((a, b) => a + b.value, 0) -
|
|
355
|
-
outputs.filter((o) => o.address || o.script).reduce((a, b) => a + b.value, 0) -
|
|
356
|
-
txFee <
|
|
357
|
-
0
|
|
358
|
-
) {
|
|
359
|
-
let spendableBalance = inputs.reduce((a, b) => a + b.value, 0)
|
|
360
|
-
let totalOutputAmount = outputs
|
|
361
|
-
.filter((o) => o.address || o.script)
|
|
362
|
-
.reduce((a, b) => a + b.value, 0)
|
|
363
|
-
let need = spendableBalance - totalOutputAmount - txFee
|
|
364
|
-
throw new Error(
|
|
365
|
-
`not enough balance. details: ${JSON.stringify(
|
|
366
|
-
{ spendableBalance, totalOutputAmount, txFee, need },
|
|
367
|
-
null,
|
|
368
|
-
2,
|
|
369
|
-
)}`,
|
|
370
|
-
)
|
|
371
|
-
}
|
|
372
|
-
let diff = fee - txFee
|
|
373
|
-
let changeIndex = outputs.findIndex((x) => !x?.address && !x.script && (x.value || 0) > 0)
|
|
374
|
-
let change: ChangeTarget | undefined
|
|
375
|
-
if (changeIndex >= 0 || diff > DUST) {
|
|
376
|
-
if (changeIndex >= 0) {
|
|
377
|
-
diff = diff + componentBytes.bytePerOutput.default * Math.round(feeRate)
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (diff < 0) {
|
|
381
|
-
diff = 0
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
if (selectType === "full") {
|
|
385
|
-
outputs[0].value = outputs[0].value + diff
|
|
386
|
-
fee = fee - diff
|
|
387
|
-
} else {
|
|
388
|
-
if (!changeObject) throw new Error("change not exist")
|
|
389
|
-
change = {
|
|
390
|
-
address: changeObject.address,
|
|
391
|
-
value: changeIndex >= 0 ? outputs[changeIndex].value + diff : diff,
|
|
392
|
-
}
|
|
393
|
-
fee = fee - diff
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (changeIndex >= 0) {
|
|
397
|
-
outputs.splice(changeIndex, 1)
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return {
|
|
402
|
-
inputs,
|
|
403
|
-
fee,
|
|
404
|
-
outputs: outputs as Target[],
|
|
405
|
-
change,
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
async filterAndConvertTxDataToStandardFormat({
|
|
410
|
-
extendedUtxo,
|
|
411
|
-
targets,
|
|
412
|
-
changeObject,
|
|
413
|
-
feeRate,
|
|
414
|
-
selectType,
|
|
415
|
-
}: {
|
|
416
|
-
extendedUtxo: ExtendedUtxo[]
|
|
417
|
-
targets: Target[]
|
|
418
|
-
feeRate: number
|
|
419
|
-
changeObject?: {
|
|
420
|
-
address: string
|
|
421
|
-
publicKey?: string
|
|
422
|
-
addressType?: string
|
|
423
|
-
derivationPath?: string
|
|
424
|
-
masterFingerprint?: string
|
|
425
|
-
}
|
|
426
|
-
selectType?: "normal" | "accumulative" | "full" | "inOrder"
|
|
427
|
-
}) {
|
|
428
|
-
let {
|
|
429
|
-
inputs: filteredInputs,
|
|
430
|
-
outputs,
|
|
431
|
-
change,
|
|
432
|
-
fee,
|
|
433
|
-
} = this.helperHandleInputsAndOutputs({
|
|
434
|
-
targets,
|
|
435
|
-
extendedUtxo,
|
|
436
|
-
feeRate,
|
|
437
|
-
changeObject,
|
|
438
|
-
selectType,
|
|
439
|
-
})
|
|
440
|
-
|
|
441
|
-
let inputs = await this.convertExtendedUtxoToInputs(filteredInputs)
|
|
442
|
-
|
|
443
|
-
return {
|
|
444
|
-
inputs,
|
|
445
|
-
outputs,
|
|
446
|
-
change,
|
|
447
|
-
fee,
|
|
448
|
-
feeRate,
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// ?note : we can extend this class and change this method for network other than bitcoin
|
|
453
|
-
async convertExtendedUtxoToInputs(baseInputs: ExtendedUtxo[] = []) {
|
|
454
|
-
let inputs: (BitcoinJSInputInfo & {
|
|
455
|
-
signerInfo: SignerInfo
|
|
456
|
-
})[] = baseInputs
|
|
457
|
-
let transactionId: string | null = null
|
|
458
|
-
let transactionHex: string | null = null
|
|
459
|
-
for (let i in inputs) {
|
|
460
|
-
let { address, publicKey, derivationPath, masterFingerprint, addressType } =
|
|
461
|
-
inputs[i].signerInfo
|
|
462
|
-
// todo : support without publicKey
|
|
463
|
-
let addressObject = this.createAddressObject({
|
|
464
|
-
publicKey: Buffer.from(publicKey, "hex"),
|
|
465
|
-
addressType,
|
|
466
|
-
})
|
|
467
|
-
if (derivationPath && masterFingerprint && addressObject.pubkey) {
|
|
468
|
-
inputs[i].bip32Derivation = [
|
|
469
|
-
{
|
|
470
|
-
path: derivationPath,
|
|
471
|
-
pubkey: addressObject.pubkey,
|
|
472
|
-
masterFingerprint: Buffer.from(masterFingerprint, "hex"),
|
|
473
|
-
},
|
|
474
|
-
]
|
|
475
|
-
}
|
|
476
|
-
if (addressType === "p2pkh") {
|
|
477
|
-
// add p2pkh data
|
|
478
|
-
if (transactionHex && transactionId === inputs[i].hash) {
|
|
479
|
-
inputs[i].nonWitnessUtxo = Buffer.from(transactionHex, "hex")
|
|
480
|
-
} else {
|
|
481
|
-
transactionHex = await this._getTransactionHex(inputs[i].hash)
|
|
482
|
-
transactionId = inputs[i].hash
|
|
483
|
-
inputs[i].nonWitnessUtxo = Buffer.from(transactionHex, "hex")
|
|
484
|
-
}
|
|
485
|
-
} else if (addressType === "p2wpkh") {
|
|
486
|
-
// add p2wpkh data
|
|
487
|
-
if (!addressObject.output) throw new Error("invalid signer info")
|
|
488
|
-
inputs[i].witnessUtxo = {
|
|
489
|
-
script: addressObject.output,
|
|
490
|
-
value: inputs[i].value,
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
if (inputs[i].signerInfo.includeHex) {
|
|
494
|
-
if (transactionHex && transactionId === inputs[i].hash) {
|
|
495
|
-
inputs[i].nonWitnessUtxo = Buffer.from(transactionHex, "hex")
|
|
496
|
-
} else {
|
|
497
|
-
transactionHex = await this._getTransactionHex(inputs[i].hash)
|
|
498
|
-
transactionId = inputs[i].hash
|
|
499
|
-
inputs[i].nonWitnessUtxo = Buffer.from(transactionHex, "hex")
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
} else if (addressType === "p2sh-p2wpkh") {
|
|
503
|
-
// add p2sh-p2wpkh data
|
|
504
|
-
if (!addressObject.output) throw new Error("invalid signer info")
|
|
505
|
-
inputs[i].witnessUtxo = {
|
|
506
|
-
script: addressObject.output,
|
|
507
|
-
value: inputs[i].value,
|
|
508
|
-
}
|
|
509
|
-
if (!addressObject?.redeem?.output) throw new Error("invalid signer info for p2sh address")
|
|
510
|
-
inputs[i].redeemScript = addressObject.redeem.output
|
|
511
|
-
|
|
512
|
-
if (inputs[i].signerInfo.includeHex) {
|
|
513
|
-
if (transactionHex && transactionId === inputs[i].hash) {
|
|
514
|
-
inputs[i].nonWitnessUtxo = Buffer.from(transactionHex, "hex")
|
|
515
|
-
} else {
|
|
516
|
-
transactionHex = await this._getTransactionHex(inputs[i].hash)
|
|
517
|
-
transactionId = inputs[i].hash
|
|
518
|
-
inputs[i].nonWitnessUtxo = Buffer.from(transactionHex, "hex")
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
} else if (addressType === "p2tr") {
|
|
522
|
-
if (!addressObject.output) throw new Error("invalid signer info")
|
|
523
|
-
inputs[i].witnessUtxo = {
|
|
524
|
-
script: addressObject.output,
|
|
525
|
-
value: inputs[i].value,
|
|
526
|
-
}
|
|
527
|
-
if (!addressObject.pubkey) throw new Error("invalid signer info for p2tr address (pubkey)")
|
|
528
|
-
inputs[i].tapInternalKey = addressObject.internalPubkey
|
|
529
|
-
|
|
530
|
-
if (inputs[i].signerInfo.includeHex) {
|
|
531
|
-
if (transactionHex && transactionId === inputs[i].hash) {
|
|
532
|
-
inputs[i].nonWitnessUtxo = Buffer.from(transactionHex, "hex")
|
|
533
|
-
} else {
|
|
534
|
-
transactionHex = await this._getTransactionHex(inputs[i].hash)
|
|
535
|
-
transactionId = inputs[i].hash
|
|
536
|
-
inputs[i].nonWitnessUtxo = Buffer.from(transactionHex, "hex")
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
return inputs
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// ?note : we can extend this class and change this method for network other than bitcoin
|
|
546
|
-
createUnsignedTransaction({
|
|
547
|
-
inputs,
|
|
548
|
-
outputs,
|
|
549
|
-
change,
|
|
550
|
-
fee, // not used in this section - just returned
|
|
551
|
-
feeRate,
|
|
552
|
-
}: {
|
|
553
|
-
inputs: BitcoinJSInputInfo[]
|
|
554
|
-
outputs: Target[]
|
|
555
|
-
change?: ChangeTarget
|
|
556
|
-
fee: number
|
|
557
|
-
feeRate: number
|
|
558
|
-
}) {
|
|
559
|
-
const sequence = 0xffffffff - 2
|
|
560
|
-
const { network } = this
|
|
561
|
-
const newPsbt = new bitcoin.Psbt({ network })
|
|
562
|
-
newPsbt.setMaximumFeeRate(+(feeRate + feeRate / 100).toFixed())
|
|
563
|
-
// add input
|
|
564
|
-
for (const input of inputs) {
|
|
565
|
-
let { addressType } = input.signerInfo
|
|
566
|
-
switch (addressType) {
|
|
567
|
-
case "p2pkh": {
|
|
568
|
-
let i = {
|
|
569
|
-
hash: input.hash,
|
|
570
|
-
index: Number(input.index),
|
|
571
|
-
nonWitnessUtxo: input.nonWitnessUtxo,
|
|
572
|
-
sequence,
|
|
573
|
-
bip32Derivation: input.bip32Derivation,
|
|
574
|
-
}
|
|
575
|
-
if (!i.bip32Derivation) delete i.bip32Derivation
|
|
576
|
-
newPsbt.addInput(i)
|
|
577
|
-
break
|
|
578
|
-
}
|
|
579
|
-
case "p2wpkh": {
|
|
580
|
-
let i = {
|
|
581
|
-
hash: input.hash,
|
|
582
|
-
index: Number(input.index),
|
|
583
|
-
witnessUtxo: input.witnessUtxo,
|
|
584
|
-
// we dont need nonWitnessUtxo. bud some application force nonWitnessUtxo
|
|
585
|
-
nonWitnessUtxo: input.nonWitnessUtxo,
|
|
586
|
-
sequence,
|
|
587
|
-
bip32Derivation: input.bip32Derivation,
|
|
588
|
-
}
|
|
589
|
-
if (!i.bip32Derivation) delete i.bip32Derivation
|
|
590
|
-
if (!i.nonWitnessUtxo) delete i.nonWitnessUtxo
|
|
591
|
-
newPsbt.addInput(i)
|
|
592
|
-
break
|
|
593
|
-
}
|
|
594
|
-
case "p2sh-p2wpkh": {
|
|
595
|
-
let i = {
|
|
596
|
-
hash: input.hash,
|
|
597
|
-
index: Number(input.index),
|
|
598
|
-
witnessUtxo: input.witnessUtxo,
|
|
599
|
-
// we dont need nonWitnessUtxo. bud some application force nonWitnessUtxo
|
|
600
|
-
nonWitnessUtxo: input.nonWitnessUtxo,
|
|
601
|
-
redeemScript: input.redeemScript,
|
|
602
|
-
sequence,
|
|
603
|
-
bip32Derivation: input.bip32Derivation,
|
|
604
|
-
}
|
|
605
|
-
if (!i.bip32Derivation) delete i.bip32Derivation
|
|
606
|
-
if (!i.nonWitnessUtxo) delete i.nonWitnessUtxo
|
|
607
|
-
newPsbt.addInput(i)
|
|
608
|
-
break
|
|
609
|
-
}
|
|
610
|
-
case "p2tr": {
|
|
611
|
-
let i = {
|
|
612
|
-
hash: input.hash,
|
|
613
|
-
index: Number(input.index),
|
|
614
|
-
witnessUtxo: input.witnessUtxo,
|
|
615
|
-
// we dont need nonWitnessUtxo. bud some application force nonWitnessUtxo
|
|
616
|
-
nonWitnessUtxo: input.nonWitnessUtxo,
|
|
617
|
-
tapInternalKey: input.tapInternalKey,
|
|
618
|
-
sequence,
|
|
619
|
-
bip32Derivation: input.bip32Derivation,
|
|
620
|
-
}
|
|
621
|
-
if (!i.bip32Derivation) delete i.bip32Derivation
|
|
622
|
-
if (!i.nonWitnessUtxo) delete i.nonWitnessUtxo
|
|
623
|
-
newPsbt.addInput(i)
|
|
624
|
-
break
|
|
625
|
-
}
|
|
626
|
-
default:
|
|
627
|
-
throw new Error("address type is incorrect")
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// add outputs
|
|
632
|
-
for (const target of outputs) {
|
|
633
|
-
newPsbt.addOutput(target)
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
// add changeAddress
|
|
637
|
-
if (change && Object.keys(change).length !== 0) {
|
|
638
|
-
newPsbt.addOutput(change)
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// check created outputs with targets
|
|
642
|
-
if (change && Object.keys(change).length !== 0) {
|
|
643
|
-
if (newPsbt.txOutputs[outputs.length].address !== change.address) {
|
|
644
|
-
throw new Error("error change address")
|
|
645
|
-
}
|
|
646
|
-
// if (newPsbt.txOutputs[outputs.length].value !== change.value) {
|
|
647
|
-
// throw new Error("error change value")
|
|
648
|
-
// }
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const unsignedPsbtBaseText = newPsbt.toBase64()
|
|
652
|
-
return {
|
|
653
|
-
unsignedTransaction: unsignedPsbtBaseText,
|
|
654
|
-
outputs,
|
|
655
|
-
inputs: inputs.map((utx) => ({
|
|
656
|
-
hash: utx.hash,
|
|
657
|
-
value: Number(utx.value),
|
|
658
|
-
index: utx.index,
|
|
659
|
-
signerInfo: utx.signerInfo,
|
|
660
|
-
})),
|
|
661
|
-
fee,
|
|
662
|
-
change,
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
async processUnsignedTransaction({
|
|
667
|
-
extendedUtxo,
|
|
668
|
-
targets = [],
|
|
669
|
-
changeAddress = undefined,
|
|
670
|
-
fullAmount = false,
|
|
671
|
-
feeRate,
|
|
672
|
-
selectType = "normal",
|
|
673
|
-
}: {
|
|
674
|
-
extendedUtxo: ExtendedUtxo[]
|
|
675
|
-
targets: Target[]
|
|
676
|
-
feeRate: number
|
|
677
|
-
|
|
678
|
-
changeAddress?: string | SignerInfo
|
|
679
|
-
fullAmount?: boolean
|
|
680
|
-
selectType?: "normal" | "accumulative" | "full" | "inOrder"
|
|
681
|
-
}) {
|
|
682
|
-
if (!changeAddress && targets.length === 0) throw new Error("no target")
|
|
683
|
-
let changeObject =
|
|
684
|
-
typeof changeAddress === "string"
|
|
685
|
-
? {
|
|
686
|
-
address: changeAddress,
|
|
687
|
-
}
|
|
688
|
-
: changeAddress
|
|
689
|
-
const { inputs, outputs, change, fee } = await this.filterAndConvertTxDataToStandardFormat({
|
|
690
|
-
extendedUtxo,
|
|
691
|
-
targets,
|
|
692
|
-
changeObject,
|
|
693
|
-
feeRate,
|
|
694
|
-
selectType: fullAmount ? "full" : selectType,
|
|
695
|
-
})
|
|
696
|
-
let unsignedTransaction = this.createUnsignedTransaction({
|
|
697
|
-
inputs,
|
|
698
|
-
outputs,
|
|
699
|
-
change,
|
|
700
|
-
fee,
|
|
701
|
-
feeRate,
|
|
702
|
-
})
|
|
703
|
-
|
|
704
|
-
return unsignedTransaction
|
|
705
|
-
}
|
|
706
|
-
}
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
2
|
+
/* eslint-disable no-underscore-dangle */
|
|
3
|
+
import * as bitcoin from "bitcoinjs-lib"
|
|
4
|
+
|
|
5
|
+
import { createAddressObjectByPublicKey, getAddressType } from "../bitcoin-utils"
|
|
6
|
+
|
|
7
|
+
const coinselect = require("coinselect")
|
|
8
|
+
const coinselectSplit = require("coinselect/split")
|
|
9
|
+
const coinselectAccumulative = require("coinselect/accumulative")
|
|
10
|
+
|
|
11
|
+
// https://bitcoin.stackexchange.com/questions/84004/how-do-virtual-size-stripped-size-and-raw-size-compare-between-legacy-address-f
|
|
12
|
+
// export const componentBytes = {
|
|
13
|
+
// bytePerInput: {
|
|
14
|
+
// p2pkh: 148,
|
|
15
|
+
// p2wpkh: 70, // 68
|
|
16
|
+
// "p2sh-p2wpkh": 91,
|
|
17
|
+
// p2tr: 60, // actual 58
|
|
18
|
+
// },
|
|
19
|
+
// baseTxBytes: 10 + 5, // +5 extra bytes to be sure
|
|
20
|
+
// bytePerOutput: {
|
|
21
|
+
// p2pkh: 35, // 34
|
|
22
|
+
// p2wpkh: 35, // 31
|
|
23
|
+
// p2sh: 35, // 32
|
|
24
|
+
// p2tr: 45, // 43
|
|
25
|
+
// default: 35,
|
|
26
|
+
// },
|
|
27
|
+
//
|
|
28
|
+
// }
|
|
29
|
+
export const componentBytes = {
|
|
30
|
+
bytePerInput: {
|
|
31
|
+
p2pkh: 148,
|
|
32
|
+
p2wpkh: 68, // 68
|
|
33
|
+
"p2sh-p2wpkh": 91,
|
|
34
|
+
p2tr: 58, // actual 58
|
|
35
|
+
default: 100,
|
|
36
|
+
},
|
|
37
|
+
baseTxBytes: 10 + 5, // +5 extra bytes to be sure
|
|
38
|
+
bytePerOutput: {
|
|
39
|
+
p2pkh: 34, // 34
|
|
40
|
+
p2wpkh: 31, // 31
|
|
41
|
+
p2sh: 32, // 32
|
|
42
|
+
p2tr: 43, // 43
|
|
43
|
+
default: 35,
|
|
44
|
+
max: 45,
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
scriptExtraBytes: {
|
|
48
|
+
lessThan255: 12,
|
|
49
|
+
moreThan255: 15,
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const DUST = 1000
|
|
54
|
+
|
|
55
|
+
export type Utxo = {
|
|
56
|
+
hash: string
|
|
57
|
+
value: number
|
|
58
|
+
index: number
|
|
59
|
+
}
|
|
60
|
+
export type SignerInfo = {
|
|
61
|
+
address: string
|
|
62
|
+
publicKey: string
|
|
63
|
+
addressType: string
|
|
64
|
+
derivationPath?: string
|
|
65
|
+
masterFingerprint?: string
|
|
66
|
+
includeHex?: boolean
|
|
67
|
+
}
|
|
68
|
+
export type ExtendedUtxo = {
|
|
69
|
+
signerInfo: SignerInfo
|
|
70
|
+
hash: string
|
|
71
|
+
value: number
|
|
72
|
+
index: number
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type TargetAddress = {
|
|
76
|
+
address: string
|
|
77
|
+
value: number
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type TargetScript = {
|
|
81
|
+
script: Buffer
|
|
82
|
+
value: number
|
|
83
|
+
}
|
|
84
|
+
export type Target = TargetAddress | TargetScript
|
|
85
|
+
export type ChangeTarget = TargetAddress & {
|
|
86
|
+
bip32Derivation?: {
|
|
87
|
+
path: string
|
|
88
|
+
pubkey: Buffer
|
|
89
|
+
masterFingerprint: Buffer
|
|
90
|
+
}[]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type BitcoinJSInputInfo = ExtendedUtxo & {
|
|
94
|
+
bip32Derivation?: {
|
|
95
|
+
path: string
|
|
96
|
+
pubkey: Buffer
|
|
97
|
+
masterFingerprint: Buffer
|
|
98
|
+
}[]
|
|
99
|
+
nonWitnessUtxo?: Buffer
|
|
100
|
+
witnessUtxo?: {
|
|
101
|
+
script: Buffer
|
|
102
|
+
value: number
|
|
103
|
+
}
|
|
104
|
+
redeemScript?: Buffer
|
|
105
|
+
tapInternalKey?: Buffer
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type ExtendedUnsignedTransaction = {
|
|
109
|
+
unsignedTransaction: string
|
|
110
|
+
outputs: Target[]
|
|
111
|
+
inputs: {
|
|
112
|
+
hash: string
|
|
113
|
+
value: number
|
|
114
|
+
index: number
|
|
115
|
+
signerInfo: SignerInfo
|
|
116
|
+
}[]
|
|
117
|
+
fee: number
|
|
118
|
+
change: TargetAddress | undefined
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function coinSelectInOrder(
|
|
122
|
+
utxos: { hash: string; index: number; value: number }[],
|
|
123
|
+
outputs: {
|
|
124
|
+
address?: string
|
|
125
|
+
script?: Buffer
|
|
126
|
+
value: number
|
|
127
|
+
}[],
|
|
128
|
+
feeRate: number,
|
|
129
|
+
) {
|
|
130
|
+
let response = coinselectAccumulative(utxos, outputs, 1)
|
|
131
|
+
return {
|
|
132
|
+
...response,
|
|
133
|
+
fee: +(+response.fee * feeRate).toFixed(),
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class BaseTransactionBuilder {
|
|
138
|
+
testnet: boolean
|
|
139
|
+
network: bitcoin.Network
|
|
140
|
+
maximumNumberOfOutputsInTransaction: number
|
|
141
|
+
feeMin: number
|
|
142
|
+
dustLimit: number
|
|
143
|
+
// abstract
|
|
144
|
+
constructor({
|
|
145
|
+
network,
|
|
146
|
+
testnet,
|
|
147
|
+
feeMin = 0,
|
|
148
|
+
dustLimit,
|
|
149
|
+
maximumNumberOfOutputsInTransaction = 50,
|
|
150
|
+
}: {
|
|
151
|
+
network: bitcoin.Network
|
|
152
|
+
testnet: boolean
|
|
153
|
+
feeMin?: number
|
|
154
|
+
dustLimit?: number
|
|
155
|
+
maximumNumberOfOutputsInTransaction?: number
|
|
156
|
+
}) {
|
|
157
|
+
this.testnet = testnet
|
|
158
|
+
this.network = network
|
|
159
|
+
this.maximumNumberOfOutputsInTransaction = maximumNumberOfOutputsInTransaction
|
|
160
|
+
this.feeMin = feeMin
|
|
161
|
+
this.dustLimit = dustLimit || 1 * 2 * componentBytes.bytePerInput.p2pkh
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// eslint-disable-next-line no-unused-vars, class-methods-use-this
|
|
165
|
+
async _getUtxo(userAddress: string): Promise<Utxo[]> {
|
|
166
|
+
// The child has implemented this method.
|
|
167
|
+
throw new Error("Do not call abstract method directly")
|
|
168
|
+
// return utxo
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// eslint-disable-next-line no-unused-vars, class-methods-use-this
|
|
172
|
+
async _getTransactionHex(transactionId: string): Promise<string> {
|
|
173
|
+
// The child has implemented this method.
|
|
174
|
+
throw new Error("Do not call abstract method directly")
|
|
175
|
+
// return utxo
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// eslint-disable-next-line no-unused-vars, class-methods-use-this
|
|
179
|
+
createAddressObject(input: { addressType: string; publicKey: Buffer }) {
|
|
180
|
+
return createAddressObjectByPublicKey(input, this.network)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// methods
|
|
184
|
+
validateAddress(address: string) {
|
|
185
|
+
try {
|
|
186
|
+
getAddressType(address, this.network)
|
|
187
|
+
return true
|
|
188
|
+
} catch (error) {
|
|
189
|
+
return false
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
getOpReturnTarget(dataHex: string) {
|
|
194
|
+
if (!(dataHex.length > 0)) throw new Error("invalid data in hex")
|
|
195
|
+
const embed = bitcoin.payments.embed({
|
|
196
|
+
data: [Buffer.from(dataHex, "hex")],
|
|
197
|
+
network: this.network,
|
|
198
|
+
})
|
|
199
|
+
return {
|
|
200
|
+
script: embed.output!,
|
|
201
|
+
value: 0,
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async getExtendedUtxo(signerInfo: SignerInfo) {
|
|
206
|
+
let utxo = await this._getUtxo(signerInfo.address)
|
|
207
|
+
const extendedUtxo = utxo.map((input) => ({
|
|
208
|
+
...input,
|
|
209
|
+
signerInfo,
|
|
210
|
+
}))
|
|
211
|
+
if (!extendedUtxo || extendedUtxo.length === 0) {
|
|
212
|
+
return []
|
|
213
|
+
}
|
|
214
|
+
return extendedUtxo
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
calculateTxSize(
|
|
218
|
+
inputTypes: string[],
|
|
219
|
+
outputs: {
|
|
220
|
+
script?: Buffer
|
|
221
|
+
address?: string
|
|
222
|
+
value: number
|
|
223
|
+
}[],
|
|
224
|
+
changeAddressType = "default",
|
|
225
|
+
) {
|
|
226
|
+
const inputsSizes = inputTypes.map(
|
|
227
|
+
(addressType) =>
|
|
228
|
+
componentBytes.bytePerInput[addressType as keyof typeof componentBytes.bytePerInput],
|
|
229
|
+
)
|
|
230
|
+
const outputSizes = outputs.map((outP: any) => {
|
|
231
|
+
if (outP.address) {
|
|
232
|
+
let addressType = "default"
|
|
233
|
+
try {
|
|
234
|
+
addressType = getAddressType(outP.address, this.network)
|
|
235
|
+
} catch {
|
|
236
|
+
addressType = "default"
|
|
237
|
+
}
|
|
238
|
+
return componentBytes.bytePerOutput[
|
|
239
|
+
addressType as keyof typeof componentBytes.bytePerOutput
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (outP.script) {
|
|
244
|
+
if (outP.script.byteLength < 255) {
|
|
245
|
+
return outP.script.byteLength + componentBytes.scriptExtraBytes.lessThan255
|
|
246
|
+
}
|
|
247
|
+
return outP.script.byteLength + componentBytes.scriptExtraBytes.moreThan255
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return componentBytes.bytePerOutput[
|
|
251
|
+
changeAddressType as keyof typeof componentBytes.bytePerOutput
|
|
252
|
+
]
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
const txSize: number =
|
|
256
|
+
componentBytes.baseTxBytes +
|
|
257
|
+
inputsSizes.reduce((a, c) => a + c, 0) +
|
|
258
|
+
outputSizes.reduce((a, c) => a + c, 0)
|
|
259
|
+
|
|
260
|
+
return txSize
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
helperHandleInputsAndOutputs({
|
|
264
|
+
targets,
|
|
265
|
+
extendedUtxo,
|
|
266
|
+
feeRate,
|
|
267
|
+
changeObject,
|
|
268
|
+
selectType = "normal", // "accumulative" | "normal" | "full"
|
|
269
|
+
}: {
|
|
270
|
+
extendedUtxo: ExtendedUtxo[]
|
|
271
|
+
targets: Target[]
|
|
272
|
+
feeRate: number
|
|
273
|
+
changeObject?: {
|
|
274
|
+
address: string
|
|
275
|
+
publicKey?: string
|
|
276
|
+
addressType?: string
|
|
277
|
+
derivationPath?: string
|
|
278
|
+
masterFingerprint?: string
|
|
279
|
+
}
|
|
280
|
+
selectType?: "normal" | "accumulative" | "full" | "inOrder"
|
|
281
|
+
}) {
|
|
282
|
+
const filteredUtxo = extendedUtxo.filter(
|
|
283
|
+
(u) =>
|
|
284
|
+
u.value >
|
|
285
|
+
+feeRate *
|
|
286
|
+
componentBytes.bytePerInput[
|
|
287
|
+
u.signerInfo.addressType as keyof typeof componentBytes.bytePerInput
|
|
288
|
+
],
|
|
289
|
+
)
|
|
290
|
+
let selectResponse
|
|
291
|
+
switch (selectType) {
|
|
292
|
+
case "normal":
|
|
293
|
+
selectResponse = coinselect(filteredUtxo, targets, Math.round(feeRate))
|
|
294
|
+
break
|
|
295
|
+
case "accumulative":
|
|
296
|
+
selectResponse = coinselectAccumulative(filteredUtxo, targets, Math.round(feeRate))
|
|
297
|
+
break
|
|
298
|
+
case "inOrder":
|
|
299
|
+
selectResponse = coinSelectInOrder(filteredUtxo, targets, Math.round(feeRate))
|
|
300
|
+
break
|
|
301
|
+
case "full":
|
|
302
|
+
if (!(targets[0] as TargetAddress).address) {
|
|
303
|
+
throw new Error()
|
|
304
|
+
}
|
|
305
|
+
selectResponse = coinselectSplit(
|
|
306
|
+
filteredUtxo,
|
|
307
|
+
[{ address: (targets[0] as TargetAddress).address }],
|
|
308
|
+
Math.round(feeRate),
|
|
309
|
+
)
|
|
310
|
+
break
|
|
311
|
+
default:
|
|
312
|
+
break
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let {
|
|
316
|
+
inputs,
|
|
317
|
+
outputs,
|
|
318
|
+
fee,
|
|
319
|
+
}: {
|
|
320
|
+
inputs?: ExtendedUtxo[]
|
|
321
|
+
outputs?: {
|
|
322
|
+
script?: Buffer
|
|
323
|
+
address?: string
|
|
324
|
+
value: number
|
|
325
|
+
}[]
|
|
326
|
+
fee: number
|
|
327
|
+
} = selectResponse
|
|
328
|
+
|
|
329
|
+
if (!inputs || !outputs) {
|
|
330
|
+
inputs = filteredUtxo
|
|
331
|
+
outputs = targets
|
|
332
|
+
fee = inputs.reduce((a, b) => a + b.value, 0) - outputs.reduce((a, b) => a + b.value, 0)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let changeAddressType = "default"
|
|
336
|
+
try {
|
|
337
|
+
changeAddressType = getAddressType(changeObject?.address || "", this.network)
|
|
338
|
+
} catch {
|
|
339
|
+
changeAddressType = "default"
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const txSize =
|
|
343
|
+
this.calculateTxSize(
|
|
344
|
+
inputs.map((i) => i.signerInfo.addressType),
|
|
345
|
+
outputs,
|
|
346
|
+
changeAddressType,
|
|
347
|
+
) + componentBytes.bytePerOutput.default
|
|
348
|
+
|
|
349
|
+
let txFee = Math.round(txSize * feeRate)
|
|
350
|
+
if (Math.round(feeRate) === 1) {
|
|
351
|
+
txFee = Math.round(txFee + txFee * 0.1)
|
|
352
|
+
}
|
|
353
|
+
if (
|
|
354
|
+
inputs.reduce((a, b) => a + b.value, 0) -
|
|
355
|
+
outputs.filter((o) => o.address || o.script).reduce((a, b) => a + b.value, 0) -
|
|
356
|
+
txFee <
|
|
357
|
+
0
|
|
358
|
+
) {
|
|
359
|
+
let spendableBalance = inputs.reduce((a, b) => a + b.value, 0)
|
|
360
|
+
let totalOutputAmount = outputs
|
|
361
|
+
.filter((o) => o.address || o.script)
|
|
362
|
+
.reduce((a, b) => a + b.value, 0)
|
|
363
|
+
let need = spendableBalance - totalOutputAmount - txFee
|
|
364
|
+
throw new Error(
|
|
365
|
+
`not enough balance. details: ${JSON.stringify(
|
|
366
|
+
{ spendableBalance, totalOutputAmount, txFee, need },
|
|
367
|
+
null,
|
|
368
|
+
2,
|
|
369
|
+
)}`,
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
let diff = fee - txFee
|
|
373
|
+
let changeIndex = outputs.findIndex((x) => !x?.address && !x.script && (x.value || 0) > 0)
|
|
374
|
+
let change: ChangeTarget | undefined
|
|
375
|
+
if (changeIndex >= 0 || diff > DUST) {
|
|
376
|
+
if (changeIndex >= 0) {
|
|
377
|
+
diff = diff + componentBytes.bytePerOutput.default * Math.round(feeRate)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (diff < 0) {
|
|
381
|
+
diff = 0
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (selectType === "full") {
|
|
385
|
+
outputs[0].value = outputs[0].value + diff
|
|
386
|
+
fee = fee - diff
|
|
387
|
+
} else {
|
|
388
|
+
if (!changeObject) throw new Error("change not exist")
|
|
389
|
+
change = {
|
|
390
|
+
address: changeObject.address,
|
|
391
|
+
value: changeIndex >= 0 ? outputs[changeIndex].value + diff : diff,
|
|
392
|
+
}
|
|
393
|
+
fee = fee - diff
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (changeIndex >= 0) {
|
|
397
|
+
outputs.splice(changeIndex, 1)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
inputs,
|
|
403
|
+
fee,
|
|
404
|
+
outputs: outputs as Target[],
|
|
405
|
+
change,
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async filterAndConvertTxDataToStandardFormat({
|
|
410
|
+
extendedUtxo,
|
|
411
|
+
targets,
|
|
412
|
+
changeObject,
|
|
413
|
+
feeRate,
|
|
414
|
+
selectType,
|
|
415
|
+
}: {
|
|
416
|
+
extendedUtxo: ExtendedUtxo[]
|
|
417
|
+
targets: Target[]
|
|
418
|
+
feeRate: number
|
|
419
|
+
changeObject?: {
|
|
420
|
+
address: string
|
|
421
|
+
publicKey?: string
|
|
422
|
+
addressType?: string
|
|
423
|
+
derivationPath?: string
|
|
424
|
+
masterFingerprint?: string
|
|
425
|
+
}
|
|
426
|
+
selectType?: "normal" | "accumulative" | "full" | "inOrder"
|
|
427
|
+
}) {
|
|
428
|
+
let {
|
|
429
|
+
inputs: filteredInputs,
|
|
430
|
+
outputs,
|
|
431
|
+
change,
|
|
432
|
+
fee,
|
|
433
|
+
} = this.helperHandleInputsAndOutputs({
|
|
434
|
+
targets,
|
|
435
|
+
extendedUtxo,
|
|
436
|
+
feeRate,
|
|
437
|
+
changeObject,
|
|
438
|
+
selectType,
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
let inputs = await this.convertExtendedUtxoToInputs(filteredInputs)
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
inputs,
|
|
445
|
+
outputs,
|
|
446
|
+
change,
|
|
447
|
+
fee,
|
|
448
|
+
feeRate,
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ?note : we can extend this class and change this method for network other than bitcoin
|
|
453
|
+
async convertExtendedUtxoToInputs(baseInputs: ExtendedUtxo[] = []) {
|
|
454
|
+
let inputs: (BitcoinJSInputInfo & {
|
|
455
|
+
signerInfo: SignerInfo
|
|
456
|
+
})[] = baseInputs
|
|
457
|
+
let transactionId: string | null = null
|
|
458
|
+
let transactionHex: string | null = null
|
|
459
|
+
for (let i in inputs) {
|
|
460
|
+
let { address, publicKey, derivationPath, masterFingerprint, addressType } =
|
|
461
|
+
inputs[i].signerInfo
|
|
462
|
+
// todo : support without publicKey
|
|
463
|
+
let addressObject = this.createAddressObject({
|
|
464
|
+
publicKey: Buffer.from(publicKey, "hex"),
|
|
465
|
+
addressType,
|
|
466
|
+
})
|
|
467
|
+
if (derivationPath && masterFingerprint && addressObject.pubkey) {
|
|
468
|
+
inputs[i].bip32Derivation = [
|
|
469
|
+
{
|
|
470
|
+
path: derivationPath,
|
|
471
|
+
pubkey: addressObject.pubkey,
|
|
472
|
+
masterFingerprint: Buffer.from(masterFingerprint, "hex"),
|
|
473
|
+
},
|
|
474
|
+
]
|
|
475
|
+
}
|
|
476
|
+
if (addressType === "p2pkh") {
|
|
477
|
+
// add p2pkh data
|
|
478
|
+
if (transactionHex && transactionId === inputs[i].hash) {
|
|
479
|
+
inputs[i].nonWitnessUtxo = Buffer.from(transactionHex, "hex")
|
|
480
|
+
} else {
|
|
481
|
+
transactionHex = await this._getTransactionHex(inputs[i].hash)
|
|
482
|
+
transactionId = inputs[i].hash
|
|
483
|
+
inputs[i].nonWitnessUtxo = Buffer.from(transactionHex, "hex")
|
|
484
|
+
}
|
|
485
|
+
} else if (addressType === "p2wpkh") {
|
|
486
|
+
// add p2wpkh data
|
|
487
|
+
if (!addressObject.output) throw new Error("invalid signer info")
|
|
488
|
+
inputs[i].witnessUtxo = {
|
|
489
|
+
script: addressObject.output,
|
|
490
|
+
value: inputs[i].value,
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (inputs[i].signerInfo.includeHex) {
|
|
494
|
+
if (transactionHex && transactionId === inputs[i].hash) {
|
|
495
|
+
inputs[i].nonWitnessUtxo = Buffer.from(transactionHex, "hex")
|
|
496
|
+
} else {
|
|
497
|
+
transactionHex = await this._getTransactionHex(inputs[i].hash)
|
|
498
|
+
transactionId = inputs[i].hash
|
|
499
|
+
inputs[i].nonWitnessUtxo = Buffer.from(transactionHex, "hex")
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
} else if (addressType === "p2sh-p2wpkh") {
|
|
503
|
+
// add p2sh-p2wpkh data
|
|
504
|
+
if (!addressObject.output) throw new Error("invalid signer info")
|
|
505
|
+
inputs[i].witnessUtxo = {
|
|
506
|
+
script: addressObject.output,
|
|
507
|
+
value: inputs[i].value,
|
|
508
|
+
}
|
|
509
|
+
if (!addressObject?.redeem?.output) throw new Error("invalid signer info for p2sh address")
|
|
510
|
+
inputs[i].redeemScript = addressObject.redeem.output
|
|
511
|
+
|
|
512
|
+
if (inputs[i].signerInfo.includeHex) {
|
|
513
|
+
if (transactionHex && transactionId === inputs[i].hash) {
|
|
514
|
+
inputs[i].nonWitnessUtxo = Buffer.from(transactionHex, "hex")
|
|
515
|
+
} else {
|
|
516
|
+
transactionHex = await this._getTransactionHex(inputs[i].hash)
|
|
517
|
+
transactionId = inputs[i].hash
|
|
518
|
+
inputs[i].nonWitnessUtxo = Buffer.from(transactionHex, "hex")
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} else if (addressType === "p2tr") {
|
|
522
|
+
if (!addressObject.output) throw new Error("invalid signer info")
|
|
523
|
+
inputs[i].witnessUtxo = {
|
|
524
|
+
script: addressObject.output,
|
|
525
|
+
value: inputs[i].value,
|
|
526
|
+
}
|
|
527
|
+
if (!addressObject.pubkey) throw new Error("invalid signer info for p2tr address (pubkey)")
|
|
528
|
+
inputs[i].tapInternalKey = addressObject.internalPubkey
|
|
529
|
+
|
|
530
|
+
if (inputs[i].signerInfo.includeHex) {
|
|
531
|
+
if (transactionHex && transactionId === inputs[i].hash) {
|
|
532
|
+
inputs[i].nonWitnessUtxo = Buffer.from(transactionHex, "hex")
|
|
533
|
+
} else {
|
|
534
|
+
transactionHex = await this._getTransactionHex(inputs[i].hash)
|
|
535
|
+
transactionId = inputs[i].hash
|
|
536
|
+
inputs[i].nonWitnessUtxo = Buffer.from(transactionHex, "hex")
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return inputs
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// ?note : we can extend this class and change this method for network other than bitcoin
|
|
546
|
+
createUnsignedTransaction({
|
|
547
|
+
inputs,
|
|
548
|
+
outputs,
|
|
549
|
+
change,
|
|
550
|
+
fee, // not used in this section - just returned
|
|
551
|
+
feeRate,
|
|
552
|
+
}: {
|
|
553
|
+
inputs: BitcoinJSInputInfo[]
|
|
554
|
+
outputs: Target[]
|
|
555
|
+
change?: ChangeTarget
|
|
556
|
+
fee: number
|
|
557
|
+
feeRate: number
|
|
558
|
+
}) {
|
|
559
|
+
const sequence = 0xffffffff - 2
|
|
560
|
+
const { network } = this
|
|
561
|
+
const newPsbt = new bitcoin.Psbt({ network })
|
|
562
|
+
newPsbt.setMaximumFeeRate(+(feeRate + feeRate / 100).toFixed())
|
|
563
|
+
// add input
|
|
564
|
+
for (const input of inputs) {
|
|
565
|
+
let { addressType } = input.signerInfo
|
|
566
|
+
switch (addressType) {
|
|
567
|
+
case "p2pkh": {
|
|
568
|
+
let i = {
|
|
569
|
+
hash: input.hash,
|
|
570
|
+
index: Number(input.index),
|
|
571
|
+
nonWitnessUtxo: input.nonWitnessUtxo,
|
|
572
|
+
sequence,
|
|
573
|
+
bip32Derivation: input.bip32Derivation,
|
|
574
|
+
}
|
|
575
|
+
if (!i.bip32Derivation) delete i.bip32Derivation
|
|
576
|
+
newPsbt.addInput(i)
|
|
577
|
+
break
|
|
578
|
+
}
|
|
579
|
+
case "p2wpkh": {
|
|
580
|
+
let i = {
|
|
581
|
+
hash: input.hash,
|
|
582
|
+
index: Number(input.index),
|
|
583
|
+
witnessUtxo: input.witnessUtxo,
|
|
584
|
+
// we dont need nonWitnessUtxo. bud some application force nonWitnessUtxo
|
|
585
|
+
nonWitnessUtxo: input.nonWitnessUtxo,
|
|
586
|
+
sequence,
|
|
587
|
+
bip32Derivation: input.bip32Derivation,
|
|
588
|
+
}
|
|
589
|
+
if (!i.bip32Derivation) delete i.bip32Derivation
|
|
590
|
+
if (!i.nonWitnessUtxo) delete i.nonWitnessUtxo
|
|
591
|
+
newPsbt.addInput(i)
|
|
592
|
+
break
|
|
593
|
+
}
|
|
594
|
+
case "p2sh-p2wpkh": {
|
|
595
|
+
let i = {
|
|
596
|
+
hash: input.hash,
|
|
597
|
+
index: Number(input.index),
|
|
598
|
+
witnessUtxo: input.witnessUtxo,
|
|
599
|
+
// we dont need nonWitnessUtxo. bud some application force nonWitnessUtxo
|
|
600
|
+
nonWitnessUtxo: input.nonWitnessUtxo,
|
|
601
|
+
redeemScript: input.redeemScript,
|
|
602
|
+
sequence,
|
|
603
|
+
bip32Derivation: input.bip32Derivation,
|
|
604
|
+
}
|
|
605
|
+
if (!i.bip32Derivation) delete i.bip32Derivation
|
|
606
|
+
if (!i.nonWitnessUtxo) delete i.nonWitnessUtxo
|
|
607
|
+
newPsbt.addInput(i)
|
|
608
|
+
break
|
|
609
|
+
}
|
|
610
|
+
case "p2tr": {
|
|
611
|
+
let i = {
|
|
612
|
+
hash: input.hash,
|
|
613
|
+
index: Number(input.index),
|
|
614
|
+
witnessUtxo: input.witnessUtxo,
|
|
615
|
+
// we dont need nonWitnessUtxo. bud some application force nonWitnessUtxo
|
|
616
|
+
nonWitnessUtxo: input.nonWitnessUtxo,
|
|
617
|
+
tapInternalKey: input.tapInternalKey,
|
|
618
|
+
sequence,
|
|
619
|
+
bip32Derivation: input.bip32Derivation,
|
|
620
|
+
}
|
|
621
|
+
if (!i.bip32Derivation) delete i.bip32Derivation
|
|
622
|
+
if (!i.nonWitnessUtxo) delete i.nonWitnessUtxo
|
|
623
|
+
newPsbt.addInput(i)
|
|
624
|
+
break
|
|
625
|
+
}
|
|
626
|
+
default:
|
|
627
|
+
throw new Error("address type is incorrect")
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// add outputs
|
|
632
|
+
for (const target of outputs) {
|
|
633
|
+
newPsbt.addOutput(target)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// add changeAddress
|
|
637
|
+
if (change && Object.keys(change).length !== 0) {
|
|
638
|
+
newPsbt.addOutput(change)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// check created outputs with targets
|
|
642
|
+
if (change && Object.keys(change).length !== 0) {
|
|
643
|
+
if (newPsbt.txOutputs[outputs.length].address !== change.address) {
|
|
644
|
+
throw new Error("error change address")
|
|
645
|
+
}
|
|
646
|
+
// if (newPsbt.txOutputs[outputs.length].value !== change.value) {
|
|
647
|
+
// throw new Error("error change value")
|
|
648
|
+
// }
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const unsignedPsbtBaseText = newPsbt.toBase64()
|
|
652
|
+
return {
|
|
653
|
+
unsignedTransaction: unsignedPsbtBaseText,
|
|
654
|
+
outputs,
|
|
655
|
+
inputs: inputs.map((utx) => ({
|
|
656
|
+
hash: utx.hash,
|
|
657
|
+
value: Number(utx.value),
|
|
658
|
+
index: utx.index,
|
|
659
|
+
signerInfo: utx.signerInfo,
|
|
660
|
+
})),
|
|
661
|
+
fee,
|
|
662
|
+
change,
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async processUnsignedTransaction({
|
|
667
|
+
extendedUtxo,
|
|
668
|
+
targets = [],
|
|
669
|
+
changeAddress = undefined,
|
|
670
|
+
fullAmount = false,
|
|
671
|
+
feeRate,
|
|
672
|
+
selectType = "normal",
|
|
673
|
+
}: {
|
|
674
|
+
extendedUtxo: ExtendedUtxo[]
|
|
675
|
+
targets: Target[]
|
|
676
|
+
feeRate: number
|
|
677
|
+
|
|
678
|
+
changeAddress?: string | SignerInfo
|
|
679
|
+
fullAmount?: boolean
|
|
680
|
+
selectType?: "normal" | "accumulative" | "full" | "inOrder"
|
|
681
|
+
}) {
|
|
682
|
+
if (!changeAddress && targets.length === 0) throw new Error("no target")
|
|
683
|
+
let changeObject =
|
|
684
|
+
typeof changeAddress === "string"
|
|
685
|
+
? {
|
|
686
|
+
address: changeAddress,
|
|
687
|
+
}
|
|
688
|
+
: changeAddress
|
|
689
|
+
const { inputs, outputs, change, fee } = await this.filterAndConvertTxDataToStandardFormat({
|
|
690
|
+
extendedUtxo,
|
|
691
|
+
targets,
|
|
692
|
+
changeObject,
|
|
693
|
+
feeRate,
|
|
694
|
+
selectType: fullAmount ? "full" : selectType,
|
|
695
|
+
})
|
|
696
|
+
let unsignedTransaction = this.createUnsignedTransaction({
|
|
697
|
+
inputs,
|
|
698
|
+
outputs,
|
|
699
|
+
change,
|
|
700
|
+
fee,
|
|
701
|
+
feeRate,
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
return unsignedTransaction
|
|
705
|
+
}
|
|
706
|
+
}
|