@teleportdao/bitcoin 1.7.21 → 1.8.0

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