@teleportdao/bitcoin 1.7.6 → 1.7.8

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