@exodus/bitcoin-api 4.4.0 → 4.5.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.
- package/CHANGELOG.md +14 -0
- package/package.json +2 -2
- package/src/psbt-builder.js +5 -19
- package/src/psbt-parser.js +10 -19
- package/src/psbt-utils.js +19 -14
- package/src/tx-create/create-tx.js +50 -2
- package/src/tx-send/index.js +82 -22
- package/src/tx-send/update-state.js +13 -14
- package/src/tx-sign/create-sign-with-wallet.js +6 -2
- package/src/tx-sign/default-create-tx.js +5 -1
- package/src/tx-sign/default-prepare-for-signing.js +2 -11
- package/src/tx-sign/default-sign-hardware.js +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [4.5.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.2.1...@exodus/bitcoin-api@4.5.0) (2025-11-12)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* feat: add PSBT builder infrastructure (#6822)
|
|
13
|
+
|
|
14
|
+
* feat: add PSBT parser functionality (#6823)
|
|
15
|
+
|
|
16
|
+
* feat: integrate PSBT support and legacy chain index (#6819)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
## [4.4.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.2.1...@exodus/bitcoin-api@4.4.0) (2025-11-12)
|
|
7
21
|
|
|
8
22
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"description": "Bitcoin transaction and fee monitors, RPC with the blockchain node, other networking code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"type": "git",
|
|
61
61
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
62
62
|
},
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "482aa1dda8d9273c4f1a477ffcef2310f1df9884"
|
|
64
64
|
}
|
package/src/psbt-builder.js
CHANGED
|
@@ -70,14 +70,7 @@ function writeGlobalMetadata(psbt, metadata) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
function createPsbtInput({
|
|
74
|
-
input,
|
|
75
|
-
asset,
|
|
76
|
-
addressPathsMap,
|
|
77
|
-
purposeXPubs,
|
|
78
|
-
nonWitnessTxs,
|
|
79
|
-
allowedPurposes,
|
|
80
|
-
}) {
|
|
73
|
+
function createPsbtInput({ input, asset, addressPathsMap, purposeXPubs, nonWitnessTxs }) {
|
|
81
74
|
const psbtInput = {
|
|
82
75
|
hash: input.txId,
|
|
83
76
|
index: input.vout,
|
|
@@ -85,7 +78,7 @@ function createPsbtInput({
|
|
|
85
78
|
}
|
|
86
79
|
|
|
87
80
|
const purpose = asset.address.resolvePurpose(input.address)
|
|
88
|
-
validatePurpose(purpose,
|
|
81
|
+
validatePurpose(purpose, purposeXPubs, `address ${input.address}`)
|
|
89
82
|
|
|
90
83
|
const { isSegwitAddress, isTaprootAddress, isWrappedSegwitAddress } = getAddressType(purpose)
|
|
91
84
|
|
|
@@ -141,14 +134,7 @@ function createPsbtInput({
|
|
|
141
134
|
return { ...psbtInput, ...derivationData }
|
|
142
135
|
}
|
|
143
136
|
|
|
144
|
-
function createPsbtOutput({
|
|
145
|
-
address,
|
|
146
|
-
amount,
|
|
147
|
-
asset,
|
|
148
|
-
addressPathsMap,
|
|
149
|
-
purposeXPubs,
|
|
150
|
-
allowedPurposes,
|
|
151
|
-
}) {
|
|
137
|
+
function createPsbtOutput({ address, amount, asset, addressPathsMap, purposeXPubs }) {
|
|
152
138
|
const psbtOutput = {
|
|
153
139
|
address,
|
|
154
140
|
value: amount,
|
|
@@ -160,7 +146,7 @@ function createPsbtOutput({
|
|
|
160
146
|
}
|
|
161
147
|
|
|
162
148
|
const purpose = asset.address.resolvePurpose(address)
|
|
163
|
-
validatePurpose(purpose,
|
|
149
|
+
validatePurpose(purpose, purposeXPubs, `output address ${address}`)
|
|
164
150
|
|
|
165
151
|
const { isTaprootAddress, isWrappedSegwitAddress } = getAddressType(purpose)
|
|
166
152
|
|
|
@@ -244,5 +230,5 @@ export async function createPsbtWithMetadata({
|
|
|
244
230
|
writePsbtOutputField(psbt, sendOutputIndex, SubType.OutputRole, 'primary')
|
|
245
231
|
}
|
|
246
232
|
|
|
247
|
-
return psbt.
|
|
233
|
+
return psbt.toBuffer()
|
|
248
234
|
}
|
package/src/psbt-parser.js
CHANGED
|
@@ -34,7 +34,7 @@ function extractInputUtxoData(psbtInput, txInput, index) {
|
|
|
34
34
|
throw new Error(`Input ${index} has no witnessUtxo or nonWitnessUtxo`)
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function parseSingleInput({ txInput, psbtInput, index, asset, purposeXPubs
|
|
37
|
+
function parseSingleInput({ txInput, psbtInput, index, asset, purposeXPubs }) {
|
|
38
38
|
const input = {
|
|
39
39
|
txId: Buffer.from(txInput.hash).reverse().toString('hex'),
|
|
40
40
|
vout: txInput.index,
|
|
@@ -53,7 +53,7 @@ function parseSingleInput({ txInput, psbtInput, index, asset, purposeXPubs, allo
|
|
|
53
53
|
|
|
54
54
|
const address = asset.address.fromScriptPubKey(inputUtxoData.scriptBuffer)
|
|
55
55
|
const purpose = asset.address.resolvePurpose(address)
|
|
56
|
-
validatePurpose(purpose,
|
|
56
|
+
validatePurpose(purpose, purposeXPubs, `input ${index}`)
|
|
57
57
|
|
|
58
58
|
const { isTaprootAddress } = getAddressType(purpose)
|
|
59
59
|
|
|
@@ -75,15 +75,7 @@ function parseSingleInput({ txInput, psbtInput, index, asset, purposeXPubs, allo
|
|
|
75
75
|
return input
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
function parseSingleOutput({
|
|
79
|
-
txOutput,
|
|
80
|
-
psbtOutput,
|
|
81
|
-
index,
|
|
82
|
-
asset,
|
|
83
|
-
purposeXPubs,
|
|
84
|
-
psbt,
|
|
85
|
-
allowedPurposes,
|
|
86
|
-
}) {
|
|
78
|
+
function parseSingleOutput({ txOutput, psbtOutput, index, asset, purposeXPubs, psbt }) {
|
|
87
79
|
const address = txOutput.address ?? asset.address.fromScriptPubKey(txOutput.script)
|
|
88
80
|
const output = { amount: txOutput.value }
|
|
89
81
|
|
|
@@ -94,7 +86,7 @@ function parseSingleOutput({
|
|
|
94
86
|
|
|
95
87
|
const purpose = asset.address.resolvePurpose(address)
|
|
96
88
|
try {
|
|
97
|
-
validatePurpose(purpose,
|
|
89
|
+
validatePurpose(purpose, purposeXPubs)
|
|
98
90
|
} catch {
|
|
99
91
|
output.address = { address }
|
|
100
92
|
return output
|
|
@@ -332,13 +324,13 @@ function extractRawTransactions(parsedInputs) {
|
|
|
332
324
|
}
|
|
333
325
|
|
|
334
326
|
export async function parsePsbt({
|
|
335
|
-
|
|
327
|
+
psbtBuffer,
|
|
336
328
|
asset,
|
|
337
329
|
assetClientInterface,
|
|
338
330
|
walletAccount,
|
|
339
331
|
allowedPurposes,
|
|
340
332
|
}) {
|
|
341
|
-
assert(
|
|
333
|
+
assert(psbtBuffer, 'psbtBuffer is required')
|
|
342
334
|
assert(asset, 'asset is required')
|
|
343
335
|
assert(assetClientInterface, 'assetClientInterface is required')
|
|
344
336
|
assert(walletAccount, 'walletAccount is required')
|
|
@@ -350,8 +342,7 @@ export async function parsePsbt({
|
|
|
350
342
|
allowedPurposes,
|
|
351
343
|
})
|
|
352
344
|
|
|
353
|
-
|
|
354
|
-
const psbt = Psbt.fromBase64(psbtBase64, { network: asset.coinInfo.toBitcoinJS() })
|
|
345
|
+
const psbt = Psbt.fromBuffer(psbtBuffer, { network: asset.coinInfo.toBitcoinJS() })
|
|
355
346
|
|
|
356
347
|
const inputs = []
|
|
357
348
|
for (let i = 0; i < psbt.inputCount; i++) {
|
|
@@ -392,13 +383,13 @@ export async function parsePsbt({
|
|
|
392
383
|
}
|
|
393
384
|
|
|
394
385
|
export async function extractTransactionContext({
|
|
395
|
-
|
|
386
|
+
psbtBuffer,
|
|
396
387
|
asset,
|
|
397
388
|
assetClientInterface,
|
|
398
389
|
walletAccount,
|
|
399
390
|
allowedPurposes,
|
|
400
391
|
}) {
|
|
401
|
-
assert(
|
|
392
|
+
assert(psbtBuffer, 'psbtBuffer is required')
|
|
402
393
|
assert(asset, 'asset is required')
|
|
403
394
|
assert(assetClientInterface, 'assetClientInterface is required')
|
|
404
395
|
assert(walletAccount, 'walletAccount is required')
|
|
@@ -409,7 +400,7 @@ export async function extractTransactionContext({
|
|
|
409
400
|
fee: calculatedFee,
|
|
410
401
|
globalMetadata,
|
|
411
402
|
} = await parsePsbt({
|
|
412
|
-
|
|
403
|
+
psbtBuffer,
|
|
413
404
|
asset,
|
|
414
405
|
assetClientInterface,
|
|
415
406
|
walletAccount,
|
package/src/psbt-utils.js
CHANGED
|
@@ -29,26 +29,31 @@ export async function getPurposeXPubs({
|
|
|
29
29
|
const purposeXPubs = Object.create(null)
|
|
30
30
|
|
|
31
31
|
for (const purpose of allowedPurposes) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
32
|
+
try {
|
|
33
|
+
const xpub = await assetClientInterface.getExtendedPublicKey({
|
|
34
|
+
walletAccount,
|
|
35
|
+
assetName: asset.name,
|
|
36
|
+
purpose,
|
|
37
|
+
})
|
|
38
|
+
const hdkey = BIP32.fromXPub(xpub)
|
|
39
|
+
const masterFingerprint = Buffer.alloc(4)
|
|
40
|
+
masterFingerprint.writeUint32BE(hdkey.fingerprint)
|
|
41
|
+
purposeXPubs[purpose] = {
|
|
42
|
+
hdkey,
|
|
43
|
+
masterFingerprint,
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// ignore any error that happened while we are getting the extended public key to handle cases where the extended public key is not available
|
|
47
|
+
// Eg. Ledger/Trezor doesn't support getting extended public keys for certain purposes
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
return purposeXPubs
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
export function validatePurpose(purpose,
|
|
50
|
-
assert(
|
|
51
|
-
if (!
|
|
54
|
+
export function validatePurpose(purpose, purposeXPubs, context = '') {
|
|
55
|
+
assert(purposeXPubs, 'purposeXPubs is required')
|
|
56
|
+
if (!purposeXPubs[purpose]) {
|
|
52
57
|
throw new Error(`Purpose ${purpose} not found${context ? ' for ' + context : ''}`)
|
|
53
58
|
}
|
|
54
59
|
}
|
|
@@ -5,6 +5,7 @@ import assert from 'minimalistic-assert'
|
|
|
5
5
|
import { getChangeDustValue } from '../dust.js'
|
|
6
6
|
import { parseCurrency, serializeCurrency } from '../fee/fee-utils.js'
|
|
7
7
|
import { selectUtxos } from '../fee/utxo-selector.js'
|
|
8
|
+
import { createPsbtWithMetadata } from '../psbt-builder.js'
|
|
8
9
|
import { findUnconfirmedSentRbfTxs } from '../tx-utils.js'
|
|
9
10
|
import { getUnconfirmedTxAncestorMap } from '../unconfirmed-ancestor-data.js'
|
|
10
11
|
import { getUsableUtxos, getUtxos } from '../utxos-utils.js'
|
|
@@ -267,10 +268,18 @@ async function createUnsignedTx({
|
|
|
267
268
|
asset,
|
|
268
269
|
selectedUtxos,
|
|
269
270
|
insightClient,
|
|
271
|
+
assetClientInterface,
|
|
272
|
+
walletAccount,
|
|
273
|
+
bumpTxId,
|
|
274
|
+
rbfEnabled,
|
|
275
|
+
txType = 'transfer',
|
|
276
|
+
sendOutputIndex,
|
|
277
|
+
changeOutputIndex,
|
|
278
|
+
allowedPurposes,
|
|
270
279
|
}) {
|
|
271
280
|
const nonWitnessTxs = await getNonWitnessTxs(asset, selectedUtxos, insightClient)
|
|
272
281
|
|
|
273
|
-
|
|
282
|
+
const result = {
|
|
274
283
|
txData: {
|
|
275
284
|
inputs,
|
|
276
285
|
outputs,
|
|
@@ -282,6 +291,34 @@ async function createUnsignedTx({
|
|
|
282
291
|
rawTxs: nonWitnessTxs,
|
|
283
292
|
},
|
|
284
293
|
}
|
|
294
|
+
|
|
295
|
+
// Only attach PSBT metadata for Bitcoin transfer flows for now; support for other coins and
|
|
296
|
+
// transaction types will come later.
|
|
297
|
+
const isBitcoin = ['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(asset.name)
|
|
298
|
+
if (isBitcoin) {
|
|
299
|
+
const psbtBuffer = await createPsbtWithMetadata({
|
|
300
|
+
inputs,
|
|
301
|
+
outputs,
|
|
302
|
+
asset,
|
|
303
|
+
assetClientInterface,
|
|
304
|
+
walletAccount,
|
|
305
|
+
nonWitnessTxs,
|
|
306
|
+
addressPathsMap,
|
|
307
|
+
allowedPurposes,
|
|
308
|
+
metadata: {
|
|
309
|
+
rbfEnabled,
|
|
310
|
+
txType,
|
|
311
|
+
sendOutputIndexes: [sendOutputIndex],
|
|
312
|
+
changeOutputIndex,
|
|
313
|
+
bumpTxId,
|
|
314
|
+
blockHeight,
|
|
315
|
+
},
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
result.txData.psbtBuffer = psbtBuffer
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return result
|
|
285
322
|
}
|
|
286
323
|
|
|
287
324
|
const getTxHandler = (type) => {
|
|
@@ -309,6 +346,7 @@ const transferHandler = {
|
|
|
309
346
|
utxosDescendingOrder,
|
|
310
347
|
assetClientInterface,
|
|
311
348
|
changeAddressType,
|
|
349
|
+
allowedPurposes,
|
|
312
350
|
}) => {
|
|
313
351
|
const assetName = asset.name
|
|
314
352
|
const insightClient = asset.baseAsset.insightClient
|
|
@@ -437,12 +475,20 @@ const transferHandler = {
|
|
|
437
475
|
asset,
|
|
438
476
|
selectedUtxos,
|
|
439
477
|
insightClient,
|
|
478
|
+
assetClientInterface,
|
|
479
|
+
walletAccount,
|
|
480
|
+
bumpTxId,
|
|
481
|
+
rbfEnabled: context.rbfEnabled,
|
|
482
|
+
txType: 'transfer',
|
|
483
|
+
sendOutputIndex: sendOutput ? outputs.indexOf(sendOutput) : undefined,
|
|
484
|
+
changeOutputIndex: changeOutput ? outputs.indexOf(changeOutput) : undefined,
|
|
485
|
+
allowedPurposes,
|
|
440
486
|
})
|
|
441
487
|
|
|
442
488
|
return {
|
|
443
489
|
unsignedTx,
|
|
444
|
-
fee: adjustedFee,
|
|
445
490
|
metadata: {
|
|
491
|
+
fee: adjustedFee,
|
|
446
492
|
amount,
|
|
447
493
|
change: selectedUtxos.value.sub(totalAmount).sub(adjustedFee),
|
|
448
494
|
totalAmount,
|
|
@@ -469,6 +515,7 @@ export const createTxFactory =
|
|
|
469
515
|
utxosDescendingOrder,
|
|
470
516
|
assetClientInterface,
|
|
471
517
|
changeAddressType,
|
|
518
|
+
allowedPurposes,
|
|
472
519
|
}) =>
|
|
473
520
|
async ({
|
|
474
521
|
asset,
|
|
@@ -514,5 +561,6 @@ export const createTxFactory =
|
|
|
514
561
|
utxosDescendingOrder,
|
|
515
562
|
assetClientInterface,
|
|
516
563
|
changeAddressType,
|
|
564
|
+
allowedPurposes,
|
|
517
565
|
})
|
|
518
566
|
}
|
package/src/tx-send/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as defaultBitcoinjsLib from '@exodus/bitcoinjs'
|
|
2
2
|
import assert from 'minimalistic-assert'
|
|
3
3
|
|
|
4
|
+
import { extractTransactionContext } from '../psbt-parser.js'
|
|
4
5
|
import { createTxFactory } from '../tx-create/create-tx.js'
|
|
5
6
|
import { broadcastTransaction } from './broadcast-tx.js'
|
|
6
7
|
import { updateAccountState, updateTransactionLog } from './update-state.js'
|
|
@@ -108,6 +109,7 @@ const getPrepareSendTransaction = async ({
|
|
|
108
109
|
options,
|
|
109
110
|
utxosDescendingOrder,
|
|
110
111
|
walletAccount,
|
|
112
|
+
allowedPurposes,
|
|
111
113
|
}) => {
|
|
112
114
|
const createTx = createTxFactory({
|
|
113
115
|
getFeeEstimator,
|
|
@@ -115,6 +117,7 @@ const getPrepareSendTransaction = async ({
|
|
|
115
117
|
utxosDescendingOrder,
|
|
116
118
|
assetClientInterface,
|
|
117
119
|
changeAddressType,
|
|
120
|
+
allowedPurposes,
|
|
118
121
|
})
|
|
119
122
|
|
|
120
123
|
// Set default values for options
|
|
@@ -131,6 +134,59 @@ const getPrepareSendTransaction = async ({
|
|
|
131
134
|
})
|
|
132
135
|
}
|
|
133
136
|
|
|
137
|
+
function toTransactionDescriptor(txContext, psbtBuffer) {
|
|
138
|
+
const {
|
|
139
|
+
inputs,
|
|
140
|
+
outputs,
|
|
141
|
+
addressPathsMap,
|
|
142
|
+
blockHeight,
|
|
143
|
+
rawTxs,
|
|
144
|
+
fee,
|
|
145
|
+
totalSendAmount,
|
|
146
|
+
changeAmount,
|
|
147
|
+
totalAmount,
|
|
148
|
+
primaryAddresses,
|
|
149
|
+
ourAddress,
|
|
150
|
+
usableUtxos,
|
|
151
|
+
selectedUtxos,
|
|
152
|
+
replaceTx,
|
|
153
|
+
sendOutputs,
|
|
154
|
+
changeOutput,
|
|
155
|
+
rbfEnabled,
|
|
156
|
+
} = txContext
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
unsignedTx: {
|
|
160
|
+
txData: {
|
|
161
|
+
inputs,
|
|
162
|
+
outputs,
|
|
163
|
+
psbtBuffer,
|
|
164
|
+
},
|
|
165
|
+
txMeta: {
|
|
166
|
+
addressPathsMap,
|
|
167
|
+
blockHeight,
|
|
168
|
+
rawTxs,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
metadata: {
|
|
172
|
+
fee,
|
|
173
|
+
amount: totalSendAmount.isZero ? undefined : totalSendAmount,
|
|
174
|
+
sendAmount: totalSendAmount,
|
|
175
|
+
change: changeAmount,
|
|
176
|
+
totalAmount,
|
|
177
|
+
address: primaryAddresses[0]?.address,
|
|
178
|
+
ourAddress,
|
|
179
|
+
usableUtxos,
|
|
180
|
+
selectedUtxos,
|
|
181
|
+
replaceTx,
|
|
182
|
+
sendOutput: sendOutputs[0],
|
|
183
|
+
changeOutput,
|
|
184
|
+
rbfEnabled,
|
|
185
|
+
blockHeight,
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
134
190
|
// not ported from Exodus; but this demos signing / broadcasting
|
|
135
191
|
// NOTE: this will be ripped out in the coming weeks
|
|
136
192
|
export const createAndBroadcastTXFactory =
|
|
@@ -141,6 +197,7 @@ export const createAndBroadcastTXFactory =
|
|
|
141
197
|
utxosDescendingOrder,
|
|
142
198
|
assetClientInterface,
|
|
143
199
|
changeAddressType,
|
|
200
|
+
allowedPurposes,
|
|
144
201
|
}) =>
|
|
145
202
|
async ({ asset, walletAccount, address, amount, options }) => {
|
|
146
203
|
// Prepare transaction
|
|
@@ -149,7 +206,7 @@ export const createAndBroadcastTXFactory =
|
|
|
149
206
|
const assetName = asset.name
|
|
150
207
|
const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
|
|
151
208
|
|
|
152
|
-
|
|
209
|
+
let transactionDescriptor = await getPrepareSendTransaction({
|
|
153
210
|
address,
|
|
154
211
|
allowUnconfirmedRbfEnabledUtxos,
|
|
155
212
|
amount,
|
|
@@ -160,22 +217,31 @@ export const createAndBroadcastTXFactory =
|
|
|
160
217
|
options,
|
|
161
218
|
utxosDescendingOrder,
|
|
162
219
|
walletAccount,
|
|
220
|
+
allowedPurposes,
|
|
163
221
|
})
|
|
164
222
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
223
|
+
// If we already created a PSBT for Bitcoin, hydrate the full transaction
|
|
224
|
+
// context from it before signing.
|
|
225
|
+
let unsignedTx, metadata
|
|
226
|
+
if (transactionDescriptor.unsignedTx?.txData?.psbtBuffer) {
|
|
227
|
+
const psbtBuffer = transactionDescriptor.unsignedTx.txData.psbtBuffer
|
|
228
|
+
const txContext = await extractTransactionContext({
|
|
229
|
+
psbtBuffer,
|
|
230
|
+
asset,
|
|
231
|
+
assetClientInterface,
|
|
232
|
+
walletAccount,
|
|
233
|
+
allowedPurposes,
|
|
234
|
+
})
|
|
235
|
+
transactionDescriptor = toTransactionDescriptor(txContext, psbtBuffer)
|
|
236
|
+
unsignedTx = transactionDescriptor.unsignedTx
|
|
237
|
+
metadata = transactionDescriptor.metadata
|
|
238
|
+
} else {
|
|
239
|
+
// Legacy/non-PSBT flows stick with the original descriptor shape.
|
|
240
|
+
unsignedTx = transactionDescriptor.unsignedTx
|
|
241
|
+
metadata = transactionDescriptor.metadata
|
|
242
|
+
}
|
|
177
243
|
|
|
178
|
-
|
|
244
|
+
const { sendAmount, usableUtxos, replaceTx, sendOutput, changeOutput } = metadata
|
|
179
245
|
|
|
180
246
|
// Sign transaction
|
|
181
247
|
const { rawTx, txId, tx } = await signTransaction({
|
|
@@ -192,7 +258,7 @@ export const createAndBroadcastTXFactory =
|
|
|
192
258
|
if (/insight broadcast http error.*missing inputs/i.test(err.message)) {
|
|
193
259
|
err.txInfo = JSON.stringify({
|
|
194
260
|
amount: sendAmount.toDefaultString({ unit: true }),
|
|
195
|
-
fee: ((fee && fee.toDefaultString({ unit: true })) || 0).toString(),
|
|
261
|
+
fee: ((metadata.fee && metadata.fee.toDefaultString({ unit: true })) || 0).toString(),
|
|
196
262
|
allUtxos: usableUtxos.toJSON(),
|
|
197
263
|
})
|
|
198
264
|
}
|
|
@@ -203,7 +269,7 @@ export const createAndBroadcastTXFactory =
|
|
|
203
269
|
function findUtxoIndex(output) {
|
|
204
270
|
let utxoIndex = -1
|
|
205
271
|
if (output) {
|
|
206
|
-
for (const [i, [address, amount]] of outputs.entries()) {
|
|
272
|
+
for (const [i, [address, amount]] of unsignedTx.txData.outputs.entries()) {
|
|
207
273
|
if (output[0] === address && output[1] === amount) {
|
|
208
274
|
utxoIndex = i
|
|
209
275
|
break
|
|
@@ -228,7 +294,6 @@ export const createAndBroadcastTXFactory =
|
|
|
228
294
|
rawTx,
|
|
229
295
|
changeUtxoIndex,
|
|
230
296
|
getSizeAndChangeScript,
|
|
231
|
-
rbfEnabled,
|
|
232
297
|
})
|
|
233
298
|
|
|
234
299
|
await updateTransactionLog({
|
|
@@ -236,14 +301,9 @@ export const createAndBroadcastTXFactory =
|
|
|
236
301
|
assetClientInterface,
|
|
237
302
|
walletAccount,
|
|
238
303
|
txId,
|
|
239
|
-
fee,
|
|
240
304
|
metadata,
|
|
241
|
-
address,
|
|
242
|
-
amount,
|
|
243
305
|
bumpTxId,
|
|
244
306
|
size,
|
|
245
|
-
blockHeight,
|
|
246
|
-
rbfEnabled,
|
|
247
307
|
})
|
|
248
308
|
|
|
249
309
|
return {
|
|
@@ -16,7 +16,6 @@ import { serializeCurrency } from '../fee/fee-utils.js'
|
|
|
16
16
|
* @param {number} params.changeUtxoIndex - Index of change output
|
|
17
17
|
* @param {Object} params.changeOutput - Change output details
|
|
18
18
|
* @param {Object} params.getSizeAndChangeScript - Function to get size and script
|
|
19
|
-
* @param {boolean} params.rbfEnabled - Whether RBF is enabled
|
|
20
19
|
*/
|
|
21
20
|
export async function updateAccountState({
|
|
22
21
|
assetClientInterface,
|
|
@@ -29,9 +28,8 @@ export async function updateAccountState({
|
|
|
29
28
|
rawTx,
|
|
30
29
|
changeUtxoIndex,
|
|
31
30
|
getSizeAndChangeScript,
|
|
32
|
-
rbfEnabled,
|
|
33
31
|
}) {
|
|
34
|
-
const { usableUtxos, selectedUtxos, replaceTx, change, ourAddress } = metadata
|
|
32
|
+
const { usableUtxos, selectedUtxos, replaceTx, change, ourAddress, rbfEnabled } = metadata
|
|
35
33
|
|
|
36
34
|
// Get change script and size
|
|
37
35
|
const { script, size } = getSizeAndChangeScript({
|
|
@@ -88,30 +86,31 @@ export async function updateAccountState({
|
|
|
88
86
|
* @param {Object} params.assetClientInterface - Asset client interface
|
|
89
87
|
* @param {Object} params.walletAccount - Wallet account
|
|
90
88
|
* @param {string} params.txId - Transaction ID
|
|
91
|
-
* @param {Object} params.fee - Transaction fee
|
|
92
89
|
* @param {Object} params.metadata - Transaction metadata
|
|
93
|
-
* @param {string} params.address - Recipient address
|
|
94
|
-
* @param {Object} params.amount - Transaction amount (for regular sends)
|
|
95
90
|
* @param {string} params.bumpTxId - ID of transaction being bumped (if applicable)
|
|
96
91
|
* @param {number} params.size - Transaction size
|
|
97
|
-
* @param {number} params.blockHeight - Block height
|
|
98
|
-
* @param {boolean} params.rbfEnabled - Whether RBF is enabled
|
|
99
92
|
*/
|
|
100
93
|
export async function updateTransactionLog({
|
|
101
94
|
asset,
|
|
102
95
|
assetClientInterface,
|
|
103
96
|
walletAccount,
|
|
104
97
|
txId,
|
|
105
|
-
fee,
|
|
106
98
|
metadata,
|
|
107
|
-
address,
|
|
108
|
-
amount,
|
|
109
99
|
bumpTxId,
|
|
110
100
|
size,
|
|
111
|
-
blockHeight,
|
|
112
|
-
rbfEnabled,
|
|
113
101
|
}) {
|
|
114
|
-
const {
|
|
102
|
+
const {
|
|
103
|
+
totalAmount,
|
|
104
|
+
selectedUtxos,
|
|
105
|
+
replaceTx,
|
|
106
|
+
changeOutput,
|
|
107
|
+
ourAddress,
|
|
108
|
+
fee,
|
|
109
|
+
blockHeight,
|
|
110
|
+
rbfEnabled,
|
|
111
|
+
address,
|
|
112
|
+
amount,
|
|
113
|
+
} = metadata
|
|
115
114
|
const assetName = asset.name
|
|
116
115
|
|
|
117
116
|
// Check if this is a self-send
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { bip371, payments, Transaction } from '@exodus/bitcoinjs'
|
|
2
2
|
import { publicKeyToX } from '@exodus/crypto/secp256k1'
|
|
3
3
|
|
|
4
|
+
import { withUnsafeNonSegwit } from '../psbt-utils.js'
|
|
4
5
|
import { createGetKeyWithMetadata } from './create-get-key-and-purpose.js'
|
|
5
6
|
import { toAsyncBufferSigner, toAsyncSigner } from './taproot.js'
|
|
6
7
|
|
|
@@ -82,9 +83,12 @@ export function createSignWithWallet({
|
|
|
82
83
|
: toAsyncSigner({ privateKey, publicKey, isTaprootKeySpend })
|
|
83
84
|
|
|
84
85
|
// desktop / BE / mobile with bip-schnorr signing
|
|
85
|
-
signingPromises.push(psbt.signInputAsync(index, asyncSigner, allowedSigHashTypes))
|
|
86
|
+
signingPromises.push(() => psbt.signInputAsync(index, asyncSigner, allowedSigHashTypes))
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
await
|
|
89
|
+
await withUnsafeNonSegwit({
|
|
90
|
+
psbt,
|
|
91
|
+
fn: () => Promise.all(signingPromises.map((sign) => sign())),
|
|
92
|
+
})
|
|
89
93
|
}
|
|
90
94
|
}
|
|
@@ -57,7 +57,11 @@ export const signTxFactory = ({
|
|
|
57
57
|
},
|
|
58
58
|
})
|
|
59
59
|
|
|
60
|
-
const
|
|
60
|
+
const isExternalPsbt =
|
|
61
|
+
unsignedTx.txData.psbtBuffer &&
|
|
62
|
+
unsignedTx.txMeta.addressPathsMap &&
|
|
63
|
+
unsignedTx.txMeta.inputsToSign
|
|
64
|
+
const skipFinalize = isExternalPsbt || unsignedTx.txMeta.returnPsbt
|
|
61
65
|
await signWithWallet(psbt, inputsToSign, skipFinalize)
|
|
62
66
|
return extractTransaction({ psbt, skipFinalize })
|
|
63
67
|
}
|
|
@@ -27,17 +27,8 @@ export function createPrepareForSigning({
|
|
|
27
27
|
return ({ unsignedTx }) => {
|
|
28
28
|
const networkInfo = coinInfo.toBitcoinJS()
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
unsignedTx.txData.psbtBuffer
|
|
32
|
-
unsignedTx.txMeta.addressPathsMap &&
|
|
33
|
-
unsignedTx.txMeta.inputsToSign
|
|
34
|
-
if (isPsbtBufferPassed) {
|
|
35
|
-
// PSBT created externally (Web3, etc..)
|
|
36
|
-
return createPsbtFromBuffer({
|
|
37
|
-
Psbt,
|
|
38
|
-
psbtBuffer: unsignedTx.txData.psbtBuffer,
|
|
39
|
-
networkInfo,
|
|
40
|
-
})
|
|
30
|
+
if (unsignedTx.txData.psbtBuffer) {
|
|
31
|
+
return createPsbtFromBuffer({ Psbt, psbtBuffer: unsignedTx.txData.psbtBuffer, networkInfo })
|
|
41
32
|
}
|
|
42
33
|
|
|
43
34
|
// Create PSBT based on internal Exodus data structure
|
|
@@ -40,7 +40,11 @@ export const signHardwareFactory = ({ assetName, resolvePurpose, keys, coinInfo
|
|
|
40
40
|
multisigData,
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
-
const
|
|
43
|
+
const isExternalPsbt =
|
|
44
|
+
unsignedTx.txData.psbtBuffer &&
|
|
45
|
+
unsignedTx.txMeta.addressPathsMap &&
|
|
46
|
+
unsignedTx.txMeta.inputsToSign
|
|
47
|
+
const skipFinalize = isExternalPsbt || unsignedTx.txMeta.returnPsbt
|
|
44
48
|
return extractTransaction({ psbt, skipFinalize })
|
|
45
49
|
}
|
|
46
50
|
}
|