@teleportdao/bitcoin 1.9.0 → 2.0.2

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