@teleportdao/bitcoin 2.0.4 → 2.0.7

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 (78) hide show
  1. package/dist/bitcoin-interface-ordinal.d.ts +108 -108
  2. package/dist/bitcoin-interface-ordinal.js +140 -140
  3. package/dist/bitcoin-interface-teleswap.d.ts +101 -101
  4. package/dist/bitcoin-interface-teleswap.d.ts.map +1 -1
  5. package/dist/bitcoin-interface-teleswap.js +176 -165
  6. package/dist/bitcoin-interface-teleswap.js.map +1 -1
  7. package/dist/bitcoin-interface-utils.d.ts +20 -20
  8. package/dist/bitcoin-interface-utils.js +45 -45
  9. package/dist/bitcoin-interface-wallet.d.ts +28 -28
  10. package/dist/bitcoin-interface-wallet.js +125 -125
  11. package/dist/bitcoin-interface.d.ts +66 -66
  12. package/dist/bitcoin-interface.js +119 -119
  13. package/dist/bitcoin-utils.d.ts +96 -96
  14. package/dist/bitcoin-utils.js +514 -514
  15. package/dist/bitcoin-wallet-base.d.ts +111 -111
  16. package/dist/bitcoin-wallet-base.js +258 -258
  17. package/dist/helper/brc20-helper.d.ts +42 -42
  18. package/dist/helper/brc20-helper.js +127 -127
  19. package/dist/helper/index.d.ts +3 -3
  20. package/dist/helper/index.js +29 -29
  21. package/dist/helper/ordinal-helper.d.ts +12 -12
  22. package/dist/helper/ordinal-helper.js +129 -129
  23. package/dist/helper/teleswap-helper.d.ts +95 -95
  24. package/dist/helper/teleswap-helper.js +186 -186
  25. package/dist/index.d.ts +12 -12
  26. package/dist/index.js +41 -41
  27. package/dist/ordinal-wallet.d.ts +495 -495
  28. package/dist/ordinal-wallet.js +386 -386
  29. package/dist/sign/index.d.ts +1 -1
  30. package/dist/sign/index.js +8 -8
  31. package/dist/sign/sign-transaction.d.ts +12 -12
  32. package/dist/sign/sign-transaction.js +82 -82
  33. package/dist/teleswap-wallet.d.ts +45 -45
  34. package/dist/teleswap-wallet.js +68 -68
  35. package/dist/transaction-builder/bitcoin-transaction-builder.d.ts +9 -9
  36. package/dist/transaction-builder/bitcoin-transaction-builder.js +54 -54
  37. package/dist/transaction-builder/index.d.ts +3 -3
  38. package/dist/transaction-builder/index.js +19 -19
  39. package/dist/transaction-builder/ordinal-transaction-builder.d.ts +63 -63
  40. package/dist/transaction-builder/ordinal-transaction-builder.js +125 -125
  41. package/dist/transaction-builder/transaction-builder.d.ts +223 -223
  42. package/dist/transaction-builder/transaction-builder.js +447 -447
  43. package/dist/type.d.ts +61 -61
  44. package/dist/type.js +2 -2
  45. package/dist/utils/networks.d.ts +5 -5
  46. package/dist/utils/networks.js +53 -53
  47. package/dist/utils/tools.d.ts +18 -18
  48. package/dist/utils/tools.js +74 -74
  49. package/package.json +4 -4
  50. package/src/bitcoin-interface-ordinal.ts +185 -185
  51. package/src/bitcoin-interface-teleswap.ts +251 -237
  52. package/src/bitcoin-interface-utils.ts +60 -60
  53. package/src/bitcoin-interface-wallet.ts +114 -114
  54. package/src/bitcoin-interface.ts +156 -156
  55. package/src/bitcoin-utils.ts +591 -591
  56. package/src/bitcoin-wallet-base.ts +344 -344
  57. package/src/helper/brc20-helper.ts +179 -179
  58. package/src/helper/ordinal-helper.ts +118 -118
  59. package/src/index.ts +15 -15
  60. package/src/ordinal-wallet.ts +659 -659
  61. package/src/sign/index.ts +1 -1
  62. package/src/sign/sign-transaction.ts +108 -108
  63. package/src/teleswap-wallet.ts +133 -133
  64. package/src/transaction-builder/bitcoin-transaction-builder.ts +26 -26
  65. package/src/transaction-builder/index.ts +3 -3
  66. package/src/transaction-builder/ordinal-transaction-builder.ts +139 -139
  67. package/src/transaction-builder/transaction-builder.ts +690 -690
  68. package/src/type.ts +74 -74
  69. package/src/utils/networks.ts +33 -33
  70. package/src/utils/tools.ts +92 -92
  71. package/tsconfig.json +9 -9
  72. package/webpack.config.js +16 -16
  73. package/.tmp/block-parser.ts +0 -58
  74. package/.tmp/check.ts +0 -101
  75. package/.tmp/ordinal-helper.ts +0 -133
  76. package/.tmp/ordinal.ts +0 -25
  77. package/.tmp/psbt/sign-transaction.ts +0 -121
  78. package/.tmp/rbf.ts +0 -45
@@ -1,690 +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 _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
- }
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
+ }