@exodus/bitcoin-api 4.5.0 → 4.7.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 +26 -0
- package/package.json +2 -2
- package/src/psbt-builder.js +37 -8
- package/src/psbt-parser.js +188 -37
- package/src/psbt-utils.js +4 -0
- package/src/tx-create/create-tx.js +56 -51
- package/src/tx-send/index.js +134 -117
- package/src/tx-send/update-state.js +40 -63
- package/src/tx-sign/common.js +9 -0
- package/src/tx-sign/default-create-tx.js +2 -6
- package/src/tx-sign/default-prepare-for-signing.js +29 -13
- package/src/tx-sign/default-sign-hardware.js +2 -6
- package/src/tx-sign/maximum-fee-rates.js +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,32 @@
|
|
|
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.7.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.6.0...@exodus/bitcoin-api@4.7.0) (2025-11-17)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* feat: remove giant metadata from createTx result (#6860)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [4.6.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.2.1...@exodus/bitcoin-api@4.6.0) (2025-11-14)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
* feat: add PSBT builder infrastructure (#6822)
|
|
23
|
+
|
|
24
|
+
* feat: add PSBT parser functionality (#6823)
|
|
25
|
+
|
|
26
|
+
* feat: integrate PSBT support and legacy chain index (#6819)
|
|
27
|
+
|
|
28
|
+
* feat: integrate PSBT support and legacy chain index (#6819) (#6829)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
6
32
|
## [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
33
|
|
|
8
34
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.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": "e281f3eae247487330a0ae16eb57372fa7982ca2"
|
|
64
64
|
}
|
package/src/psbt-builder.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import { payments
|
|
1
|
+
import { payments } from '@exodus/bitcoinjs'
|
|
2
2
|
import { publicKeyToX } from '@exodus/crypto/secp256k1'
|
|
3
3
|
import assert from 'minimalistic-assert'
|
|
4
4
|
|
|
5
5
|
import { SubType, writePsbtGlobalField, writePsbtOutputField } from './psbt-proprietary-types.js'
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import {
|
|
7
|
+
getAddressType,
|
|
8
|
+
getPurposeXPubs,
|
|
9
|
+
setPsbtVersionIfNotBitcoin,
|
|
10
|
+
validatePurpose,
|
|
11
|
+
} from './psbt-utils.js'
|
|
12
|
+
import { getMaximumFeeRate } from './tx-sign/maximum-fee-rates.js'
|
|
13
|
+
|
|
14
|
+
function canParseTx(rawTxBuffer, Transaction) {
|
|
9
15
|
try {
|
|
10
16
|
Transaction.fromBuffer(rawTxBuffer)
|
|
11
17
|
return true
|
|
@@ -70,7 +76,14 @@ function writeGlobalMetadata(psbt, metadata) {
|
|
|
70
76
|
}
|
|
71
77
|
}
|
|
72
78
|
|
|
73
|
-
function createPsbtInput({
|
|
79
|
+
function createPsbtInput({
|
|
80
|
+
input,
|
|
81
|
+
asset,
|
|
82
|
+
addressPathsMap,
|
|
83
|
+
purposeXPubs,
|
|
84
|
+
nonWitnessTxs,
|
|
85
|
+
Transaction,
|
|
86
|
+
}) {
|
|
74
87
|
const psbtInput = {
|
|
75
88
|
hash: input.txId,
|
|
76
89
|
index: input.vout,
|
|
@@ -111,7 +124,7 @@ function createPsbtInput({ input, asset, addressPathsMap, purposeXPubs, nonWitne
|
|
|
111
124
|
)
|
|
112
125
|
|
|
113
126
|
const rawTxBuffer = Buffer.from(rawTx.rawData, 'hex')
|
|
114
|
-
if (canParseTx(rawTxBuffer)) {
|
|
127
|
+
if (canParseTx(rawTxBuffer, Transaction)) {
|
|
115
128
|
psbtInput.nonWitnessUtxo = rawTxBuffer
|
|
116
129
|
} else {
|
|
117
130
|
// bitcoinjs can’t parse a handful of edge-case transactions (Litecoin MWEB, odd
|
|
@@ -181,6 +194,8 @@ export async function createPsbtWithMetadata({
|
|
|
181
194
|
addressPathsMap,
|
|
182
195
|
metadata,
|
|
183
196
|
allowedPurposes,
|
|
197
|
+
Psbt,
|
|
198
|
+
Transaction,
|
|
184
199
|
}) {
|
|
185
200
|
assert(inputs, 'inputs is required')
|
|
186
201
|
assert(outputs, 'outputs is required')
|
|
@@ -189,8 +204,16 @@ export async function createPsbtWithMetadata({
|
|
|
189
204
|
assert(walletAccount, 'walletAccount is required')
|
|
190
205
|
assert(addressPathsMap, 'addressPathsMap is required')
|
|
191
206
|
assert(metadata, 'metadata is required')
|
|
207
|
+
assert(Psbt, 'Psbt is required')
|
|
208
|
+
assert(Transaction, 'Transaction is required')
|
|
209
|
+
const psbtOptions = { network: asset.coinInfo.toBitcoinJS() }
|
|
210
|
+
const maximumFeeRate = getMaximumFeeRate(asset.name)
|
|
211
|
+
if (maximumFeeRate !== undefined) {
|
|
212
|
+
psbtOptions.maximumFeeRate = maximumFeeRate
|
|
213
|
+
}
|
|
192
214
|
|
|
193
|
-
const psbt = new Psbt(
|
|
215
|
+
const psbt = new Psbt(psbtOptions)
|
|
216
|
+
setPsbtVersionIfNotBitcoin(psbt, asset.name)
|
|
194
217
|
|
|
195
218
|
const purposeXPubs = await getPurposeXPubs({
|
|
196
219
|
assetClientInterface,
|
|
@@ -209,6 +232,7 @@ export async function createPsbtWithMetadata({
|
|
|
209
232
|
purposeXPubs,
|
|
210
233
|
nonWitnessTxs,
|
|
211
234
|
allowedPurposes,
|
|
235
|
+
Transaction,
|
|
212
236
|
})
|
|
213
237
|
psbt.addInput(psbtInput)
|
|
214
238
|
}
|
|
@@ -221,6 +245,7 @@ export async function createPsbtWithMetadata({
|
|
|
221
245
|
addressPathsMap,
|
|
222
246
|
purposeXPubs,
|
|
223
247
|
allowedPurposes,
|
|
248
|
+
changeOutputIndex: metadata.changeOutputIndex,
|
|
224
249
|
})
|
|
225
250
|
psbt.addOutput(psbtOutput)
|
|
226
251
|
})
|
|
@@ -230,5 +255,9 @@ export async function createPsbtWithMetadata({
|
|
|
230
255
|
writePsbtOutputField(psbt, sendOutputIndex, SubType.OutputRole, 'primary')
|
|
231
256
|
}
|
|
232
257
|
|
|
233
|
-
|
|
258
|
+
if (metadata.changeOutputIndex !== undefined) {
|
|
259
|
+
writePsbtOutputField(psbt, metadata.changeOutputIndex, SubType.OutputRole, 'change')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return psbt
|
|
234
263
|
}
|
package/src/psbt-parser.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { Psbt, Transaction } from '@exodus/bitcoinjs'
|
|
2
1
|
import { publicKeyToX } from '@exodus/crypto/secp256k1'
|
|
3
|
-
import { UtxoCollection } from '@exodus/models'
|
|
2
|
+
import { Address, UtxoCollection } from '@exodus/models'
|
|
4
3
|
import BipPath from 'bip32-path'
|
|
5
4
|
import assert from 'minimalistic-assert'
|
|
6
5
|
|
|
@@ -11,7 +10,7 @@ import { findUnconfirmedSentRbfTxs } from './tx-utils.js'
|
|
|
11
10
|
import { getUnconfirmedTxAncestorMap } from './unconfirmed-ancestor-data.js'
|
|
12
11
|
import { getUsableUtxos, getUtxos } from './utxos-utils.js'
|
|
13
12
|
|
|
14
|
-
function extractInputUtxoData(psbtInput, txInput, index) {
|
|
13
|
+
function extractInputUtxoData(psbtInput, txInput, index, Transaction) {
|
|
15
14
|
if (psbtInput.nonWitnessUtxo) {
|
|
16
15
|
const prevTx = Transaction.fromBuffer(psbtInput.nonWitnessUtxo)
|
|
17
16
|
const prevOutput = prevTx.outs[txInput.index]
|
|
@@ -34,14 +33,14 @@ function extractInputUtxoData(psbtInput, txInput, index) {
|
|
|
34
33
|
throw new Error(`Input ${index} has no witnessUtxo or nonWitnessUtxo`)
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
function parseSingleInput({ txInput, psbtInput, index, asset, purposeXPubs }) {
|
|
36
|
+
function parseSingleInput({ txInput, psbtInput, index, asset, purposeXPubs, Transaction }) {
|
|
38
37
|
const input = {
|
|
39
38
|
txId: Buffer.from(txInput.hash).reverse().toString('hex'),
|
|
40
39
|
vout: txInput.index,
|
|
41
40
|
sequence: txInput.sequence,
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
const inputUtxoData = extractInputUtxoData(psbtInput, txInput, index)
|
|
43
|
+
const inputUtxoData = extractInputUtxoData(psbtInput, txInput, index, Transaction)
|
|
45
44
|
|
|
46
45
|
input.value = inputUtxoData.value
|
|
47
46
|
input.script = Buffer.from(inputUtxoData.scriptBuffer).toString('hex')
|
|
@@ -71,7 +70,7 @@ function parseSingleInput({ txInput, psbtInput, index, asset, purposeXPubs }) {
|
|
|
71
70
|
throw new Error(`Input ${index} has no derivation path`)
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
input.address = { address, meta: { path: derivation.path
|
|
73
|
+
input.address = { address, meta: { path: derivation.path } }
|
|
75
74
|
return input
|
|
76
75
|
}
|
|
77
76
|
|
|
@@ -117,9 +116,15 @@ function readGlobalMetadata(psbt) {
|
|
|
117
116
|
}
|
|
118
117
|
|
|
119
118
|
function calculateFee(inputs, outputs) {
|
|
120
|
-
const inputSum = inputs.reduce((sum, input) =>
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
const inputSum = inputs.reduce((sum, input) => {
|
|
120
|
+
const value = BigInt(input.value || 0)
|
|
121
|
+
return sum + value
|
|
122
|
+
}, BigInt(0))
|
|
123
|
+
const outputSum = outputs.reduce((sum, output) => {
|
|
124
|
+
const amount = BigInt(output.amount || 0)
|
|
125
|
+
return sum + amount
|
|
126
|
+
}, BigInt(0))
|
|
127
|
+
return inputSum - outputSum // Return BigInt directly
|
|
123
128
|
}
|
|
124
129
|
|
|
125
130
|
function getBip32Derivation(hdkey, bip32Derivations, ignoreY) {
|
|
@@ -217,32 +222,22 @@ function getSelectedUtxos({ parsedInputs, utxos, asset, txSet }) {
|
|
|
217
222
|
async function getChangeOutputData({ outputs, assetClientInterface, walletAccount, asset }) {
|
|
218
223
|
let changeOutputData = null
|
|
219
224
|
for (const [i, output] of outputs.entries()) {
|
|
220
|
-
|
|
221
|
-
// (common for self‑sends, consolidations, etc.), so skip them during change detection.
|
|
222
|
-
if (output.metadata?.outputRole === 'primary') {
|
|
223
|
-
continue
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// We only treat addresses we can re-derive from the wallet xpub as potential
|
|
227
|
-
// change outputs.
|
|
228
|
-
if (!output.address?.meta?.path) {
|
|
225
|
+
if (output.metadata?.outputRole !== 'change') {
|
|
229
226
|
continue
|
|
230
227
|
}
|
|
231
228
|
|
|
232
|
-
// At this point we already know the output was derived by our wallet and it is
|
|
233
|
-
// not explicitly flagged as a primary/send output, so we tentatively treat it as
|
|
234
|
-
// change and confirm by re-deriving the on-chain address.
|
|
235
229
|
if (changeOutputData) {
|
|
236
|
-
// Multiple change outputs are a smell. This can happen if an external PSBT
|
|
237
|
-
// marks a send output of the same wallet with its derivation path, so we fail fast instead of
|
|
238
|
-
// silently mislabelling funds.
|
|
239
230
|
throw new Error('Multiple change outputs are not allowed')
|
|
240
231
|
}
|
|
241
232
|
|
|
233
|
+
if (!output.address?.meta?.path || !output.address?.meta?.purpose) {
|
|
234
|
+
throw new Error('Change output has no derivation path or purpose')
|
|
235
|
+
}
|
|
236
|
+
|
|
242
237
|
const { path, purpose } = output.address.meta
|
|
243
238
|
const [chainIndex, addressIndex] = BipPath.fromString(path).toPathArray()
|
|
244
239
|
|
|
245
|
-
|
|
240
|
+
let ourAddress = await assetClientInterface.getAddress({
|
|
246
241
|
assetName: asset.name,
|
|
247
242
|
walletAccount: walletAccount.toString(),
|
|
248
243
|
purpose,
|
|
@@ -250,10 +245,15 @@ async function getChangeOutputData({ outputs, assetClientInterface, walletAccoun
|
|
|
250
245
|
addressIndex,
|
|
251
246
|
})
|
|
252
247
|
|
|
253
|
-
if (
|
|
248
|
+
if (asset.address.toLegacyAddress) {
|
|
249
|
+
const legacyAddress = asset.address.toLegacyAddress(ourAddress.address)
|
|
250
|
+
ourAddress = Address.create(legacyAddress, ourAddress.meta)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (ourAddress.toString() === output.address.address) {
|
|
254
254
|
changeOutputData = {
|
|
255
|
-
address:
|
|
256
|
-
amount: output.amount,
|
|
255
|
+
address: ourAddress,
|
|
256
|
+
amount: toBigIntAmount(output.amount),
|
|
257
257
|
index: i,
|
|
258
258
|
}
|
|
259
259
|
} else {
|
|
@@ -311,6 +311,17 @@ function buildAddressPathsMap(selectedUtxos, changeOutputData) {
|
|
|
311
311
|
return addressPathsMap
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
function buildOutputAddressPurposesMap(outputs) {
|
|
315
|
+
const map = Object.create(null)
|
|
316
|
+
for (const output of outputs) {
|
|
317
|
+
if (output.address?.meta?.purpose) {
|
|
318
|
+
map[output.address.address] = output.address.meta.purpose
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return map
|
|
323
|
+
}
|
|
324
|
+
|
|
314
325
|
function extractRawTransactions(parsedInputs) {
|
|
315
326
|
const rawTxsData = parsedInputs
|
|
316
327
|
.filter((parsedInput) => parsedInput.prevTxId)
|
|
@@ -329,11 +340,15 @@ export async function parsePsbt({
|
|
|
329
340
|
assetClientInterface,
|
|
330
341
|
walletAccount,
|
|
331
342
|
allowedPurposes,
|
|
343
|
+
Psbt,
|
|
344
|
+
Transaction,
|
|
332
345
|
}) {
|
|
333
346
|
assert(psbtBuffer, 'psbtBuffer is required')
|
|
334
347
|
assert(asset, 'asset is required')
|
|
335
348
|
assert(assetClientInterface, 'assetClientInterface is required')
|
|
336
349
|
assert(walletAccount, 'walletAccount is required')
|
|
350
|
+
assert(Psbt, 'Psbt is required')
|
|
351
|
+
assert(Transaction, 'Transaction is required')
|
|
337
352
|
|
|
338
353
|
const purposeXPubs = await getPurposeXPubs({
|
|
339
354
|
assetClientInterface,
|
|
@@ -353,6 +368,7 @@ export async function parsePsbt({
|
|
|
353
368
|
asset,
|
|
354
369
|
purposeXPubs,
|
|
355
370
|
allowedPurposes,
|
|
371
|
+
Transaction,
|
|
356
372
|
})
|
|
357
373
|
inputs.push(input)
|
|
358
374
|
}
|
|
@@ -382,30 +398,159 @@ export async function parsePsbt({
|
|
|
382
398
|
}
|
|
383
399
|
}
|
|
384
400
|
|
|
401
|
+
function toBigIntAmount(value) {
|
|
402
|
+
return Buffer.isBuffer(value) ? value.readBigInt64LE(0) : BigInt(value)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function parseUnsignedTx({ unsignedTx }) {
|
|
406
|
+
const { txData, txMeta } = unsignedTx
|
|
407
|
+
assert(txData, 'txData is required')
|
|
408
|
+
assert(txMeta, 'txMeta is required')
|
|
409
|
+
|
|
410
|
+
const {
|
|
411
|
+
addressPathsMap = Object.create(null),
|
|
412
|
+
outputAddressPurposesMap = Object.create(null),
|
|
413
|
+
changeOutputIndex,
|
|
414
|
+
rawTxs = [],
|
|
415
|
+
blockHeight,
|
|
416
|
+
sendOutputIndexes = [],
|
|
417
|
+
rbfEnabled = false,
|
|
418
|
+
bumpTxId,
|
|
419
|
+
txType,
|
|
420
|
+
} = txMeta
|
|
421
|
+
|
|
422
|
+
const rawTxMap = new Map(rawTxs.map((tx) => [tx.txId, tx.rawData]))
|
|
423
|
+
|
|
424
|
+
const inputs = (txData.inputs || []).map((input) =>
|
|
425
|
+
parseUnsignedInput({
|
|
426
|
+
input,
|
|
427
|
+
addressPathsMap,
|
|
428
|
+
rawTxMap,
|
|
429
|
+
})
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
const outputs = (txData.outputs || []).map((output, index) =>
|
|
433
|
+
parseUnsignedOutput({
|
|
434
|
+
index,
|
|
435
|
+
output,
|
|
436
|
+
addressPathsMap,
|
|
437
|
+
outputAddressPurposesMap,
|
|
438
|
+
sendOutputIndexes,
|
|
439
|
+
changeOutputIndex,
|
|
440
|
+
})
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
const fee = computeFee(inputs, outputs)
|
|
444
|
+
|
|
445
|
+
const globalMetadata = {
|
|
446
|
+
blockHeight,
|
|
447
|
+
rbfEnabled,
|
|
448
|
+
bumpTxId,
|
|
449
|
+
txType,
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return { inputs, outputs, fee, globalMetadata }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function computeFee(inputs, outputs) {
|
|
456
|
+
const inputSum = inputs.reduce((sum, input) => sum + toBigIntAmount(input.value), BigInt(0))
|
|
457
|
+
const outputSum = outputs.reduce((sum, output) => sum + toBigIntAmount(output.amount), BigInt(0))
|
|
458
|
+
return inputSum - outputSum
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function parseUnsignedInput({ input, addressPathsMap, rawTxMap }) {
|
|
462
|
+
const { txId, vout, sequence, value, script, address: addressString } = input
|
|
463
|
+
|
|
464
|
+
const path = addressPathsMap[addressString]
|
|
465
|
+
|
|
466
|
+
const parsedInput = {
|
|
467
|
+
txId,
|
|
468
|
+
vout,
|
|
469
|
+
sequence,
|
|
470
|
+
value,
|
|
471
|
+
script,
|
|
472
|
+
address: { address: addressString, meta: { path } },
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const rawTxHex = rawTxMap.get(txId)
|
|
476
|
+
if (rawTxHex) {
|
|
477
|
+
parsedInput.prevTxHex = rawTxHex
|
|
478
|
+
parsedInput.prevTxId = txId
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return parsedInput
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function parseUnsignedOutput({
|
|
485
|
+
index,
|
|
486
|
+
output,
|
|
487
|
+
addressPathsMap,
|
|
488
|
+
outputAddressPurposesMap,
|
|
489
|
+
sendOutputIndexes,
|
|
490
|
+
changeOutputIndex,
|
|
491
|
+
}) {
|
|
492
|
+
const [addressString, amount] = output
|
|
493
|
+
|
|
494
|
+
const purpose = outputAddressPurposesMap[addressString]
|
|
495
|
+
const path = addressPathsMap[addressString]
|
|
496
|
+
|
|
497
|
+
const addressObject = { address: addressString }
|
|
498
|
+
if (purpose !== undefined) {
|
|
499
|
+
addressObject.meta = { purpose }
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (path !== undefined) {
|
|
503
|
+
addressObject.meta = { ...addressObject.meta, path }
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const parsedOutput = {
|
|
507
|
+
amount,
|
|
508
|
+
address: addressObject,
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (sendOutputIndexes.includes(index)) {
|
|
512
|
+
parsedOutput.metadata = { outputRole: 'primary' }
|
|
513
|
+
} else if (changeOutputIndex === index) {
|
|
514
|
+
parsedOutput.metadata = { outputRole: 'change' }
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return parsedOutput
|
|
518
|
+
}
|
|
519
|
+
|
|
385
520
|
export async function extractTransactionContext({
|
|
386
|
-
|
|
521
|
+
unsignedTx,
|
|
387
522
|
asset,
|
|
388
523
|
assetClientInterface,
|
|
389
524
|
walletAccount,
|
|
390
525
|
allowedPurposes,
|
|
526
|
+
Psbt,
|
|
527
|
+
Transaction,
|
|
391
528
|
}) {
|
|
392
|
-
assert(
|
|
529
|
+
assert(unsignedTx, 'unsignedTx is required')
|
|
393
530
|
assert(asset, 'asset is required')
|
|
394
531
|
assert(assetClientInterface, 'assetClientInterface is required')
|
|
395
532
|
assert(walletAccount, 'walletAccount is required')
|
|
396
533
|
|
|
534
|
+
const psbtBuffer = unsignedTx?.txData?.psbtBuffer
|
|
535
|
+
|
|
536
|
+
const parsed = psbtBuffer
|
|
537
|
+
? await parsePsbt({
|
|
538
|
+
psbtBuffer,
|
|
539
|
+
asset,
|
|
540
|
+
assetClientInterface,
|
|
541
|
+
walletAccount,
|
|
542
|
+
allowedPurposes,
|
|
543
|
+
Psbt,
|
|
544
|
+
Transaction,
|
|
545
|
+
})
|
|
546
|
+
: parseUnsignedTx({ unsignedTx })
|
|
547
|
+
|
|
397
548
|
const {
|
|
398
549
|
inputs: parsedInputs,
|
|
399
550
|
outputs: parsedOutputs,
|
|
400
551
|
fee: calculatedFee,
|
|
401
552
|
globalMetadata,
|
|
402
|
-
} =
|
|
403
|
-
psbtBuffer,
|
|
404
|
-
asset,
|
|
405
|
-
assetClientInterface,
|
|
406
|
-
walletAccount,
|
|
407
|
-
allowedPurposes,
|
|
408
|
-
})
|
|
553
|
+
} = parsed
|
|
409
554
|
|
|
410
555
|
const changeOutputData = await getChangeOutputData({
|
|
411
556
|
outputs: parsedOutputs,
|
|
@@ -466,6 +611,7 @@ export async function extractTransactionContext({
|
|
|
466
611
|
)
|
|
467
612
|
|
|
468
613
|
const addressPathsMap = buildAddressPathsMap(selectedUtxos, changeOutputData)
|
|
614
|
+
const outputAddressPurposesMap = buildOutputAddressPurposesMap(parsedOutputs)
|
|
469
615
|
const rawTxs = extractRawTransactions(parsedInputs)
|
|
470
616
|
|
|
471
617
|
return {
|
|
@@ -476,6 +622,7 @@ export async function extractTransactionContext({
|
|
|
476
622
|
fee: asset.currency.baseUnit(calculatedFee),
|
|
477
623
|
sendOutputIndexes: primaryOutputIndexes,
|
|
478
624
|
changeOutputIndex: changeOutputData?.index,
|
|
625
|
+
outputAddressPurposesMap,
|
|
479
626
|
sendAmounts,
|
|
480
627
|
changeAmount,
|
|
481
628
|
totalSendAmount,
|
|
@@ -489,5 +636,9 @@ export async function extractTransactionContext({
|
|
|
489
636
|
changeOutput: changeOutputData ? outputs[changeOutputData.index] : undefined,
|
|
490
637
|
rbfEnabled: globalMetadata.rbfEnabled,
|
|
491
638
|
blockHeight: globalMetadata.blockHeight,
|
|
639
|
+
bumpTxId: globalMetadata.bumpTxId,
|
|
640
|
+
txType: globalMetadata.txType,
|
|
641
|
+
useCashAddress: unsignedTx?.txMeta?.useCashAddress,
|
|
642
|
+
psbtBuffer,
|
|
492
643
|
}
|
|
493
644
|
}
|
package/src/psbt-utils.js
CHANGED
|
@@ -131,3 +131,7 @@ export async function withUnsafeNonSegwit({ psbt, fn, unsafe = true }) {
|
|
|
131
131
|
cache.__UNSAFE_SIGN_NONSEGWIT = prevValue
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
+
|
|
135
|
+
export function setPsbtVersionIfNotBitcoin(psbt, assetName) {
|
|
136
|
+
if (!['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)) psbt.setVersion(1)
|
|
137
|
+
}
|
|
@@ -174,6 +174,7 @@ function createTransactionOutputs({
|
|
|
174
174
|
const assetName = asset.name
|
|
175
175
|
let outputs = []
|
|
176
176
|
let sendOutput
|
|
177
|
+
let adjustedFee
|
|
177
178
|
|
|
178
179
|
// Add existing outputs from replacement transaction
|
|
179
180
|
if (replaceTx) {
|
|
@@ -196,8 +197,7 @@ function createTransactionOutputs({
|
|
|
196
197
|
)
|
|
197
198
|
: sendAmount
|
|
198
199
|
|
|
199
|
-
|
|
200
|
-
const { changeOutput, adjustedFee, changeAddressKeypath, ourAddress } = createChangeOutput({
|
|
200
|
+
const changeOutputData = createChangeOutput({
|
|
201
201
|
selectedUtxos,
|
|
202
202
|
totalAmount,
|
|
203
203
|
fee,
|
|
@@ -206,18 +206,23 @@ function createTransactionOutputs({
|
|
|
206
206
|
asset,
|
|
207
207
|
})
|
|
208
208
|
|
|
209
|
-
if (
|
|
210
|
-
outputs.push(changeOutput)
|
|
209
|
+
if (changeOutputData) {
|
|
210
|
+
outputs.push(changeOutputData.changeOutput)
|
|
211
|
+
adjustedFee = fee
|
|
212
|
+
} else {
|
|
213
|
+
// If we don't have enough for a change output, then all remaining dust is just added to fee
|
|
214
|
+
const changeAmount = selectedUtxos.value.sub(totalAmount).sub(fee)
|
|
215
|
+
adjustedFee = fee.add(changeAmount)
|
|
211
216
|
}
|
|
212
217
|
|
|
213
218
|
return {
|
|
214
219
|
outputs: replaceTx ? outputs : shuffle(outputs),
|
|
215
220
|
sendOutput,
|
|
216
|
-
|
|
217
|
-
|
|
221
|
+
ourAddress: changeOutputData?.ourAddress,
|
|
222
|
+
changeOutput: changeOutputData?.changeOutput,
|
|
223
|
+
changeAddressPath: changeOutputData?.changeAddressPath,
|
|
224
|
+
changeAddressPurpose: changeOutputData?.changeAddressPurpose,
|
|
218
225
|
adjustedFee,
|
|
219
|
-
changeAddressKeypath,
|
|
220
|
-
ourAddress,
|
|
221
226
|
}
|
|
222
227
|
}
|
|
223
228
|
|
|
@@ -233,7 +238,7 @@ function createChangeOutput({ selectedUtxos, totalAmount, fee, replaceTx, change
|
|
|
233
238
|
ourAddress = Address.create(legacyAddress, ourAddress.meta)
|
|
234
239
|
}
|
|
235
240
|
|
|
236
|
-
// Create change output if above dust threshold
|
|
241
|
+
// Create change output if above dust threshold.
|
|
237
242
|
if (change.gte(dust)) {
|
|
238
243
|
const changeOutput = createOutput(
|
|
239
244
|
asset.name,
|
|
@@ -241,22 +246,16 @@ function createChangeOutput({ selectedUtxos, totalAmount, fee, replaceTx, change
|
|
|
241
246
|
change
|
|
242
247
|
)
|
|
243
248
|
|
|
244
|
-
// Return the
|
|
249
|
+
// Return the path for hardware wallet change detection
|
|
245
250
|
return {
|
|
246
|
-
changeOutput,
|
|
247
|
-
adjustedFee: fee,
|
|
248
|
-
changeAddressKeypath: ourAddress.meta.path,
|
|
249
251
|
ourAddress,
|
|
252
|
+
changeOutput,
|
|
253
|
+
changeAddressPath: ourAddress.meta.path,
|
|
254
|
+
changeAddressPurpose: ourAddress.meta.purpose,
|
|
250
255
|
}
|
|
251
256
|
}
|
|
252
257
|
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
changeOutput: undefined,
|
|
256
|
-
adjustedFee: fee.add(change),
|
|
257
|
-
changeAddressKeypath: undefined,
|
|
258
|
-
ourAddress,
|
|
259
|
-
}
|
|
258
|
+
return null
|
|
260
259
|
}
|
|
261
260
|
|
|
262
261
|
async function createUnsignedTx({
|
|
@@ -275,7 +274,10 @@ async function createUnsignedTx({
|
|
|
275
274
|
txType = 'transfer',
|
|
276
275
|
sendOutputIndex,
|
|
277
276
|
changeOutputIndex,
|
|
277
|
+
outputAddressPurposesMap,
|
|
278
278
|
allowedPurposes,
|
|
279
|
+
Psbt,
|
|
280
|
+
Transaction,
|
|
279
281
|
}) {
|
|
280
282
|
const nonWitnessTxs = await getNonWitnessTxs(asset, selectedUtxos, insightClient)
|
|
281
283
|
|
|
@@ -289,14 +291,21 @@ async function createUnsignedTx({
|
|
|
289
291
|
addressPathsMap,
|
|
290
292
|
blockHeight,
|
|
291
293
|
rawTxs: nonWitnessTxs,
|
|
294
|
+
sendOutputIndexes:
|
|
295
|
+
sendOutputIndex === undefined || sendOutputIndex === null ? [] : [sendOutputIndex],
|
|
296
|
+
changeOutputIndex,
|
|
297
|
+
outputAddressPurposesMap,
|
|
298
|
+
rbfEnabled,
|
|
299
|
+
bumpTxId,
|
|
300
|
+
txType,
|
|
292
301
|
},
|
|
293
302
|
}
|
|
294
303
|
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
if (
|
|
299
|
-
const
|
|
304
|
+
// Create PSBT for all UTXO coins that support it
|
|
305
|
+
// Note: Some coins (like BCash) may not use PSBT in their signing flow,
|
|
306
|
+
// but creating it here allows for a unified transaction flow
|
|
307
|
+
if (Psbt) {
|
|
308
|
+
const psbt = await createPsbtWithMetadata({
|
|
300
309
|
inputs,
|
|
301
310
|
outputs,
|
|
302
311
|
asset,
|
|
@@ -313,9 +322,11 @@ async function createUnsignedTx({
|
|
|
313
322
|
bumpTxId,
|
|
314
323
|
blockHeight,
|
|
315
324
|
},
|
|
325
|
+
Psbt,
|
|
326
|
+
Transaction,
|
|
316
327
|
})
|
|
317
328
|
|
|
318
|
-
result.txData.psbtBuffer =
|
|
329
|
+
result.txData.psbtBuffer = psbt.toBuffer()
|
|
319
330
|
}
|
|
320
331
|
|
|
321
332
|
return result
|
|
@@ -347,6 +358,8 @@ const transferHandler = {
|
|
|
347
358
|
assetClientInterface,
|
|
348
359
|
changeAddressType,
|
|
349
360
|
allowedPurposes,
|
|
361
|
+
Psbt,
|
|
362
|
+
Transaction,
|
|
350
363
|
}) => {
|
|
351
364
|
const assetName = asset.name
|
|
352
365
|
const insightClient = asset.baseAsset.insightClient
|
|
@@ -444,10 +457,10 @@ const transferHandler = {
|
|
|
444
457
|
outputs,
|
|
445
458
|
sendOutput,
|
|
446
459
|
changeOutput,
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
changeAddressKeypath,
|
|
460
|
+
changeAddressPath,
|
|
461
|
+
changeAddressPurpose,
|
|
450
462
|
ourAddress,
|
|
463
|
+
adjustedFee,
|
|
451
464
|
} = createTransactionOutputs({
|
|
452
465
|
replaceTx: processedReplaceTx,
|
|
453
466
|
processedAddress,
|
|
@@ -458,11 +471,15 @@ const transferHandler = {
|
|
|
458
471
|
changeAddress: context.changeAddress,
|
|
459
472
|
})
|
|
460
473
|
|
|
474
|
+
// Create a map of wallet's own output addresses and their purposes.
|
|
475
|
+
const outputAddressPurposesMap = Object.create(null)
|
|
476
|
+
|
|
461
477
|
// Add the keypath of change address to support Trezor detect the change output.
|
|
462
478
|
// Output is change and does not need approval from user which shows the strange address that user never seen.
|
|
463
|
-
if (
|
|
479
|
+
if (changeAddressPath && changeAddressPurpose) {
|
|
464
480
|
const changeKey = ourAddress?.address ?? String(ourAddress)
|
|
465
|
-
addressPathsMap[changeKey] =
|
|
481
|
+
addressPathsMap[changeKey] = changeAddressPath
|
|
482
|
+
outputAddressPurposesMap[changeKey] = changeAddressPurpose
|
|
466
483
|
}
|
|
467
484
|
|
|
468
485
|
// Create unsigned transaction
|
|
@@ -471,6 +488,7 @@ const transferHandler = {
|
|
|
471
488
|
outputs,
|
|
472
489
|
useCashAddress,
|
|
473
490
|
addressPathsMap,
|
|
491
|
+
outputAddressPurposesMap,
|
|
474
492
|
blockHeight: context.blockHeight,
|
|
475
493
|
asset,
|
|
476
494
|
selectedUtxos,
|
|
@@ -483,28 +501,11 @@ const transferHandler = {
|
|
|
483
501
|
sendOutputIndex: sendOutput ? outputs.indexOf(sendOutput) : undefined,
|
|
484
502
|
changeOutputIndex: changeOutput ? outputs.indexOf(changeOutput) : undefined,
|
|
485
503
|
allowedPurposes,
|
|
504
|
+
Psbt,
|
|
505
|
+
Transaction,
|
|
486
506
|
})
|
|
487
507
|
|
|
488
|
-
return {
|
|
489
|
-
unsignedTx,
|
|
490
|
-
metadata: {
|
|
491
|
-
fee: adjustedFee,
|
|
492
|
-
amount,
|
|
493
|
-
change: selectedUtxos.value.sub(totalAmount).sub(adjustedFee),
|
|
494
|
-
totalAmount,
|
|
495
|
-
address: processedAddress,
|
|
496
|
-
ourAddress,
|
|
497
|
-
receiveAddress: utxoParams.receiveAddress,
|
|
498
|
-
sendAmount: utxoParams.sendAmount,
|
|
499
|
-
usableUtxos: context.usableUtxos,
|
|
500
|
-
selectedUtxos,
|
|
501
|
-
replaceTx: processedReplaceTx,
|
|
502
|
-
sendOutput,
|
|
503
|
-
changeOutput,
|
|
504
|
-
blockHeight: context.blockHeight,
|
|
505
|
-
rbfEnabled: context.rbfEnabled,
|
|
506
|
-
},
|
|
507
|
-
}
|
|
508
|
+
return { unsignedTx, fee: adjustedFee }
|
|
508
509
|
},
|
|
509
510
|
}
|
|
510
511
|
|
|
@@ -516,6 +517,8 @@ export const createTxFactory =
|
|
|
516
517
|
assetClientInterface,
|
|
517
518
|
changeAddressType,
|
|
518
519
|
allowedPurposes,
|
|
520
|
+
Psbt,
|
|
521
|
+
Transaction,
|
|
519
522
|
}) =>
|
|
520
523
|
async ({
|
|
521
524
|
asset,
|
|
@@ -562,5 +565,7 @@ export const createTxFactory =
|
|
|
562
565
|
assetClientInterface,
|
|
563
566
|
changeAddressType,
|
|
564
567
|
allowedPurposes,
|
|
568
|
+
Psbt,
|
|
569
|
+
Transaction,
|
|
565
570
|
})
|
|
566
571
|
}
|
package/src/tx-send/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as defaultBitcoinjsLib from '@exodus/bitcoinjs'
|
|
2
|
+
import { Psbt as DefaultPsbt, Transaction as DefaultTransaction } from '@exodus/bitcoinjs'
|
|
2
3
|
import assert from 'minimalistic-assert'
|
|
3
4
|
|
|
4
5
|
import { extractTransactionContext } from '../psbt-parser.js'
|
|
@@ -98,61 +99,21 @@ export async function signTransaction({
|
|
|
98
99
|
return { rawTx, txId, tx }
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
address,
|
|
103
|
-
allowUnconfirmedRbfEnabledUtxos,
|
|
104
|
-
amount,
|
|
105
|
-
asset,
|
|
106
|
-
assetClientInterface,
|
|
107
|
-
changeAddressType,
|
|
108
|
-
getFeeEstimator,
|
|
109
|
-
options,
|
|
110
|
-
utxosDescendingOrder,
|
|
111
|
-
walletAccount,
|
|
112
|
-
allowedPurposes,
|
|
113
|
-
}) => {
|
|
114
|
-
const createTx = createTxFactory({
|
|
115
|
-
getFeeEstimator,
|
|
116
|
-
allowUnconfirmedRbfEnabledUtxos,
|
|
117
|
-
utxosDescendingOrder,
|
|
118
|
-
assetClientInterface,
|
|
119
|
-
changeAddressType,
|
|
120
|
-
allowedPurposes,
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
// Set default values for options
|
|
124
|
-
const { isRbfAllowed = true, ...restOptions } = options || Object.create(null)
|
|
125
|
-
|
|
126
|
-
return createTx({
|
|
127
|
-
asset,
|
|
128
|
-
walletAccount,
|
|
129
|
-
type: 'transfer',
|
|
130
|
-
toAddress: address,
|
|
131
|
-
amount,
|
|
132
|
-
isRbfAllowed,
|
|
133
|
-
...restOptions,
|
|
134
|
-
})
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function toTransactionDescriptor(txContext, psbtBuffer) {
|
|
102
|
+
function getTransferUnsignedTx(txContext) {
|
|
138
103
|
const {
|
|
139
104
|
inputs,
|
|
140
105
|
outputs,
|
|
106
|
+
psbtBuffer,
|
|
107
|
+
useCashAddress,
|
|
141
108
|
addressPathsMap,
|
|
109
|
+
outputAddressPurposesMap,
|
|
142
110
|
blockHeight,
|
|
143
111
|
rawTxs,
|
|
144
|
-
|
|
145
|
-
totalSendAmount,
|
|
146
|
-
changeAmount,
|
|
147
|
-
totalAmount,
|
|
148
|
-
primaryAddresses,
|
|
149
|
-
ourAddress,
|
|
150
|
-
usableUtxos,
|
|
151
|
-
selectedUtxos,
|
|
152
|
-
replaceTx,
|
|
153
|
-
sendOutputs,
|
|
154
|
-
changeOutput,
|
|
112
|
+
txType,
|
|
155
113
|
rbfEnabled,
|
|
114
|
+
bumpTxId,
|
|
115
|
+
changeOutputIndex,
|
|
116
|
+
sendOutputIndexes,
|
|
156
117
|
} = txContext
|
|
157
118
|
|
|
158
119
|
return {
|
|
@@ -163,27 +124,47 @@ function toTransactionDescriptor(txContext, psbtBuffer) {
|
|
|
163
124
|
psbtBuffer,
|
|
164
125
|
},
|
|
165
126
|
txMeta: {
|
|
127
|
+
useCashAddress,
|
|
166
128
|
addressPathsMap,
|
|
129
|
+
outputAddressPurposesMap,
|
|
167
130
|
blockHeight,
|
|
168
131
|
rawTxs,
|
|
132
|
+
txType,
|
|
133
|
+
rbfEnabled,
|
|
134
|
+
bumpTxId,
|
|
135
|
+
changeOutputIndex,
|
|
136
|
+
sendOutputIndexes,
|
|
169
137
|
},
|
|
170
138
|
},
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Get additional transaction metadata that are not part of the unsignedTx.txMeta
|
|
143
|
+
function getExtendedTxMeta(txContext) {
|
|
144
|
+
const {
|
|
145
|
+
fee,
|
|
146
|
+
totalSendAmount,
|
|
147
|
+
changeAmount,
|
|
148
|
+
totalAmount,
|
|
149
|
+
primaryAddresses,
|
|
150
|
+
ourAddress,
|
|
151
|
+
usableUtxos,
|
|
152
|
+
selectedUtxos,
|
|
153
|
+
replaceTx,
|
|
154
|
+
changeOutput,
|
|
155
|
+
} = txContext
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
fee,
|
|
159
|
+
sendAmount: totalSendAmount,
|
|
160
|
+
changeAmount,
|
|
161
|
+
totalAmount,
|
|
162
|
+
toAddress: primaryAddresses[0]?.address,
|
|
163
|
+
ourAddress,
|
|
164
|
+
usableUtxos,
|
|
165
|
+
selectedUtxos,
|
|
166
|
+
replaceTx,
|
|
167
|
+
changeOutput,
|
|
187
168
|
}
|
|
188
169
|
}
|
|
189
170
|
|
|
@@ -198,50 +179,80 @@ export const createAndBroadcastTXFactory =
|
|
|
198
179
|
assetClientInterface,
|
|
199
180
|
changeAddressType,
|
|
200
181
|
allowedPurposes,
|
|
182
|
+
Psbt = DefaultPsbt,
|
|
183
|
+
Transaction = DefaultTransaction,
|
|
201
184
|
}) =>
|
|
202
|
-
async ({
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
185
|
+
async ({
|
|
186
|
+
asset,
|
|
187
|
+
walletAccount,
|
|
188
|
+
address,
|
|
189
|
+
amount,
|
|
190
|
+
multipleAddressesEnabled,
|
|
191
|
+
feePerKB,
|
|
192
|
+
customFee,
|
|
193
|
+
isSendAll,
|
|
194
|
+
bumpTxId: bumpTxIdProvided,
|
|
195
|
+
isExchange,
|
|
196
|
+
isRbfAllowed = true,
|
|
197
|
+
taprootInputWitnessSize,
|
|
198
|
+
}) => {
|
|
206
199
|
const assetName = asset.name
|
|
207
200
|
const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
|
|
208
201
|
|
|
209
|
-
|
|
210
|
-
|
|
202
|
+
const createTx = createTxFactory({
|
|
203
|
+
getFeeEstimator,
|
|
211
204
|
allowUnconfirmedRbfEnabledUtxos,
|
|
205
|
+
utxosDescendingOrder,
|
|
206
|
+
assetClientInterface,
|
|
207
|
+
changeAddressType,
|
|
208
|
+
allowedPurposes,
|
|
209
|
+
Psbt,
|
|
210
|
+
Transaction,
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const { unsignedTx: unsignedTxByCreateTx } = await createTx({
|
|
214
|
+
asset,
|
|
215
|
+
walletAccount,
|
|
216
|
+
type: 'transfer',
|
|
217
|
+
toAddress: address,
|
|
212
218
|
amount,
|
|
219
|
+
isRbfAllowed,
|
|
220
|
+
multipleAddressesEnabled,
|
|
221
|
+
feePerKB,
|
|
222
|
+
customFee,
|
|
223
|
+
isSendAll,
|
|
224
|
+
bumpTxId: bumpTxIdProvided,
|
|
225
|
+
isExchange,
|
|
226
|
+
taprootInputWitnessSize,
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const txContext = await extractTransactionContext({
|
|
230
|
+
unsignedTx: unsignedTxByCreateTx,
|
|
213
231
|
asset,
|
|
214
232
|
assetClientInterface,
|
|
215
|
-
changeAddressType,
|
|
216
|
-
getFeeEstimator,
|
|
217
|
-
options,
|
|
218
|
-
utxosDescendingOrder,
|
|
219
233
|
walletAccount,
|
|
220
234
|
allowedPurposes,
|
|
235
|
+
Psbt,
|
|
236
|
+
Transaction,
|
|
221
237
|
})
|
|
222
238
|
|
|
223
|
-
|
|
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
|
-
}
|
|
239
|
+
const { unsignedTx } = getTransferUnsignedTx(txContext)
|
|
243
240
|
|
|
244
|
-
const {
|
|
241
|
+
const {
|
|
242
|
+
fee,
|
|
243
|
+
sendAmount,
|
|
244
|
+
changeAmount,
|
|
245
|
+
totalAmount,
|
|
246
|
+
toAddress,
|
|
247
|
+
ourAddress,
|
|
248
|
+
usableUtxos,
|
|
249
|
+
selectedUtxos,
|
|
250
|
+
replaceTx,
|
|
251
|
+
changeOutput,
|
|
252
|
+
} = getExtendedTxMeta(txContext)
|
|
253
|
+
|
|
254
|
+
const { sendOutputIndexes, changeOutputIndex, bumpTxId, rbfEnabled, blockHeight } =
|
|
255
|
+
unsignedTx.txMeta
|
|
245
256
|
|
|
246
257
|
// Sign transaction
|
|
247
258
|
const { rawTx, txId, tx } = await signTransaction({
|
|
@@ -258,7 +269,7 @@ export const createAndBroadcastTXFactory =
|
|
|
258
269
|
if (/insight broadcast http error.*missing inputs/i.test(err.message)) {
|
|
259
270
|
err.txInfo = JSON.stringify({
|
|
260
271
|
amount: sendAmount.toDefaultString({ unit: true }),
|
|
261
|
-
fee: ((
|
|
272
|
+
fee: ((fee && fee.toDefaultString({ unit: true })) || 0).toString(),
|
|
262
273
|
allUtxos: usableUtxos.toJSON(),
|
|
263
274
|
})
|
|
264
275
|
}
|
|
@@ -266,34 +277,31 @@ export const createAndBroadcastTXFactory =
|
|
|
266
277
|
throw err
|
|
267
278
|
}
|
|
268
279
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (output) {
|
|
272
|
-
for (const [i, [address, amount]] of unsignedTx.txData.outputs.entries()) {
|
|
273
|
-
if (output[0] === address && output[1] === amount) {
|
|
274
|
-
utxoIndex = i
|
|
275
|
-
break
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return utxoIndex
|
|
281
|
-
}
|
|
280
|
+
const changeUtxoIndex = changeOutputIndex ?? -1
|
|
281
|
+
const sendUtxoIndex = sendOutputIndexes?.[0] ?? -1
|
|
282
282
|
|
|
283
|
-
const
|
|
284
|
-
|
|
283
|
+
const { script, size } = getSizeAndChangeScript({
|
|
284
|
+
assetName,
|
|
285
|
+
tx,
|
|
286
|
+
rawTx,
|
|
287
|
+
changeUtxoIndex,
|
|
288
|
+
txId,
|
|
289
|
+
})
|
|
285
290
|
|
|
286
|
-
|
|
291
|
+
await updateAccountState({
|
|
287
292
|
assetClientInterface,
|
|
288
293
|
assetName,
|
|
289
294
|
walletAccount,
|
|
290
295
|
accountState,
|
|
291
296
|
txId,
|
|
292
|
-
metadata,
|
|
293
|
-
tx,
|
|
294
|
-
rawTx,
|
|
295
297
|
changeUtxoIndex,
|
|
296
|
-
|
|
298
|
+
script,
|
|
299
|
+
usableUtxos,
|
|
300
|
+
selectedUtxos,
|
|
301
|
+
replaceTx,
|
|
302
|
+
changeAmount,
|
|
303
|
+
ourAddress,
|
|
304
|
+
rbfEnabled,
|
|
297
305
|
})
|
|
298
306
|
|
|
299
307
|
await updateTransactionLog({
|
|
@@ -301,7 +309,16 @@ export const createAndBroadcastTXFactory =
|
|
|
301
309
|
assetClientInterface,
|
|
302
310
|
walletAccount,
|
|
303
311
|
txId,
|
|
304
|
-
|
|
312
|
+
totalAmount,
|
|
313
|
+
selectedUtxos,
|
|
314
|
+
replaceTx,
|
|
315
|
+
changeOutput,
|
|
316
|
+
ourAddress,
|
|
317
|
+
fee,
|
|
318
|
+
blockHeight,
|
|
319
|
+
rbfEnabled,
|
|
320
|
+
toAddress,
|
|
321
|
+
sendAmount,
|
|
305
322
|
bumpTxId,
|
|
306
323
|
size,
|
|
307
324
|
})
|
|
@@ -2,44 +2,21 @@ import { Address } from '@exodus/models'
|
|
|
2
2
|
|
|
3
3
|
import { serializeCurrency } from '../fee/fee-utils.js'
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Update account state after transaction is broadcast
|
|
7
|
-
* @param {Object} params
|
|
8
|
-
* @param {Object} params.assetClientInterface - Asset client interface
|
|
9
|
-
* @param {string} params.assetName - Name of the asset
|
|
10
|
-
* @param {Object} params.walletAccount - Wallet account
|
|
11
|
-
* @param {Object} params.accountState - Current account state
|
|
12
|
-
* @param {string} params.txId - Transaction ID
|
|
13
|
-
* @param {Object} params.metadata - Transaction metadata
|
|
14
|
-
* @param {Object} params.tx - Signed transaction object
|
|
15
|
-
* @param {Buffer} params.rawTx - Raw transaction
|
|
16
|
-
* @param {number} params.changeUtxoIndex - Index of change output
|
|
17
|
-
* @param {Object} params.changeOutput - Change output details
|
|
18
|
-
* @param {Object} params.getSizeAndChangeScript - Function to get size and script
|
|
19
|
-
*/
|
|
20
5
|
export async function updateAccountState({
|
|
21
6
|
assetClientInterface,
|
|
22
7
|
assetName,
|
|
23
8
|
walletAccount,
|
|
24
9
|
accountState,
|
|
25
10
|
txId,
|
|
26
|
-
metadata,
|
|
27
|
-
tx,
|
|
28
|
-
rawTx,
|
|
29
11
|
changeUtxoIndex,
|
|
30
|
-
|
|
12
|
+
script,
|
|
13
|
+
usableUtxos,
|
|
14
|
+
selectedUtxos,
|
|
15
|
+
replaceTx,
|
|
16
|
+
changeAmount,
|
|
17
|
+
ourAddress,
|
|
18
|
+
rbfEnabled,
|
|
31
19
|
}) {
|
|
32
|
-
const { usableUtxos, selectedUtxos, replaceTx, change, ourAddress, rbfEnabled } = metadata
|
|
33
|
-
|
|
34
|
-
// Get change script and size
|
|
35
|
-
const { script, size } = getSizeAndChangeScript({
|
|
36
|
-
assetName,
|
|
37
|
-
tx,
|
|
38
|
-
rawTx,
|
|
39
|
-
changeUtxoIndex,
|
|
40
|
-
txId,
|
|
41
|
-
})
|
|
42
|
-
|
|
43
20
|
// Update remaining UTXOs
|
|
44
21
|
const knownBalanceUtxoIds = accountState.knownBalanceUtxoIds || []
|
|
45
22
|
let remainingUtxos = usableUtxos.difference(selectedUtxos)
|
|
@@ -52,7 +29,7 @@ export async function updateAccountState({
|
|
|
52
29
|
address,
|
|
53
30
|
vout: changeUtxoIndex,
|
|
54
31
|
script,
|
|
55
|
-
value:
|
|
32
|
+
value: changeAmount,
|
|
56
33
|
confirmations: 0,
|
|
57
34
|
rbfEnabled,
|
|
58
35
|
}
|
|
@@ -75,42 +52,26 @@ export async function updateAccountState({
|
|
|
75
52
|
knownBalanceUtxoIds,
|
|
76
53
|
},
|
|
77
54
|
})
|
|
78
|
-
|
|
79
|
-
return { size }
|
|
80
55
|
}
|
|
81
56
|
|
|
82
|
-
/**
|
|
83
|
-
* Update transaction log with new transaction
|
|
84
|
-
* @param {Object} params
|
|
85
|
-
* @param {Object} params.asset - Asset object
|
|
86
|
-
* @param {Object} params.assetClientInterface - Asset client interface
|
|
87
|
-
* @param {Object} params.walletAccount - Wallet account
|
|
88
|
-
* @param {string} params.txId - Transaction ID
|
|
89
|
-
* @param {Object} params.metadata - Transaction metadata
|
|
90
|
-
* @param {string} params.bumpTxId - ID of transaction being bumped (if applicable)
|
|
91
|
-
* @param {number} params.size - Transaction size
|
|
92
|
-
*/
|
|
93
57
|
export async function updateTransactionLog({
|
|
94
58
|
asset,
|
|
95
59
|
assetClientInterface,
|
|
96
60
|
walletAccount,
|
|
97
61
|
txId,
|
|
98
|
-
metadata,
|
|
99
62
|
bumpTxId,
|
|
100
63
|
size,
|
|
64
|
+
totalAmount,
|
|
65
|
+
selectedUtxos,
|
|
66
|
+
replaceTx,
|
|
67
|
+
changeOutput,
|
|
68
|
+
ourAddress,
|
|
69
|
+
fee,
|
|
70
|
+
blockHeight,
|
|
71
|
+
rbfEnabled,
|
|
72
|
+
toAddress,
|
|
73
|
+
sendAmount,
|
|
101
74
|
}) {
|
|
102
|
-
const {
|
|
103
|
-
totalAmount,
|
|
104
|
-
selectedUtxos,
|
|
105
|
-
replaceTx,
|
|
106
|
-
changeOutput,
|
|
107
|
-
ourAddress,
|
|
108
|
-
fee,
|
|
109
|
-
blockHeight,
|
|
110
|
-
rbfEnabled,
|
|
111
|
-
address,
|
|
112
|
-
amount,
|
|
113
|
-
} = metadata
|
|
114
75
|
const assetName = asset.name
|
|
115
76
|
|
|
116
77
|
// Check if this is a self-send
|
|
@@ -129,9 +90,11 @@ export async function updateTransactionLog({
|
|
|
129
90
|
// If we have a bumpTxId, but we aren't replacing, then it is a self-send.
|
|
130
91
|
const selfSend = bumpTxId
|
|
131
92
|
? !replaceTx
|
|
132
|
-
: walletAddressObjects.some((receiveAddress) => String(receiveAddress) === String(
|
|
93
|
+
: walletAddressObjects.some((receiveAddress) => String(receiveAddress) === String(toAddress))
|
|
133
94
|
|
|
134
|
-
const displayReceiveAddress = asset.address.displayAddress?.(
|
|
95
|
+
const displayReceiveAddress = asset.address.displayAddress?.(toAddress) || toAddress
|
|
96
|
+
|
|
97
|
+
const amountToSerialize = sendAmount.isZero ? undefined : sendAmount
|
|
135
98
|
|
|
136
99
|
// Build receivers list
|
|
137
100
|
const receivers = bumpTxId
|
|
@@ -141,9 +104,17 @@ export async function updateTransactionLog({
|
|
|
141
104
|
: replaceTx
|
|
142
105
|
? [
|
|
143
106
|
...replaceTx.data.sent,
|
|
144
|
-
{
|
|
107
|
+
{
|
|
108
|
+
address: displayReceiveAddress,
|
|
109
|
+
amount: serializeCurrency(amountToSerialize, asset.currency),
|
|
110
|
+
},
|
|
111
|
+
]
|
|
112
|
+
: [
|
|
113
|
+
{
|
|
114
|
+
address: displayReceiveAddress,
|
|
115
|
+
amount: serializeCurrency(amountToSerialize, asset.currency),
|
|
116
|
+
},
|
|
145
117
|
]
|
|
146
|
-
: [{ address: displayReceiveAddress, amount: serializeCurrency(amount, asset.currency) }]
|
|
147
118
|
|
|
148
119
|
// Calculate coin amount
|
|
149
120
|
const coinAmount = selfSend ? asset.currency.ZERO : totalAmount.abs().negate()
|
|
@@ -177,11 +148,17 @@ export async function updateTransactionLog({
|
|
|
177
148
|
|
|
178
149
|
// If replacing a transaction, update the old one
|
|
179
150
|
if (replaceTx) {
|
|
180
|
-
|
|
151
|
+
const updatedReplaceTx = {
|
|
152
|
+
...replaceTx,
|
|
153
|
+
data: {
|
|
154
|
+
...replaceTx.data,
|
|
155
|
+
replacedBy: txId,
|
|
156
|
+
},
|
|
157
|
+
}
|
|
181
158
|
await assetClientInterface.updateTxLogAndNotify({
|
|
182
159
|
assetName,
|
|
183
160
|
walletAccount,
|
|
184
|
-
txs: [
|
|
161
|
+
txs: [updatedReplaceTx],
|
|
185
162
|
})
|
|
186
163
|
}
|
|
187
164
|
}
|
package/src/tx-sign/common.js
CHANGED
|
@@ -27,3 +27,12 @@ export const serializeTx = ({ tx }) => {
|
|
|
27
27
|
})),
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
export function shouldSkipFinalize(unsignedTx) {
|
|
32
|
+
const isExternalPsbt =
|
|
33
|
+
unsignedTx.txData.psbtBuffer &&
|
|
34
|
+
unsignedTx.txMeta.addressPathsMap &&
|
|
35
|
+
unsignedTx.txMeta.inputsToSign
|
|
36
|
+
|
|
37
|
+
return isExternalPsbt || unsignedTx.txMeta.returnPsbt
|
|
38
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Psbt as DefaultPsbt, Transaction as DefaultTransaction } from '@exodus/bitcoinjs'
|
|
2
2
|
import assert from 'minimalistic-assert'
|
|
3
3
|
|
|
4
|
-
import { extractTransaction } from './common.js'
|
|
4
|
+
import { extractTransaction, shouldSkipFinalize } from './common.js'
|
|
5
5
|
import { createSignWithWallet } from './create-sign-with-wallet.js'
|
|
6
6
|
import { createPrepareForSigning } from './default-prepare-for-signing.js'
|
|
7
7
|
|
|
@@ -57,11 +57,7 @@ export const signTxFactory = ({
|
|
|
57
57
|
},
|
|
58
58
|
})
|
|
59
59
|
|
|
60
|
-
const
|
|
61
|
-
unsignedTx.txData.psbtBuffer &&
|
|
62
|
-
unsignedTx.txMeta.addressPathsMap &&
|
|
63
|
-
unsignedTx.txMeta.inputsToSign
|
|
64
|
-
const skipFinalize = isExternalPsbt || unsignedTx.txMeta.returnPsbt
|
|
60
|
+
const skipFinalize = shouldSkipFinalize(unsignedTx)
|
|
65
61
|
await signWithWallet(psbt, inputsToSign, skipFinalize)
|
|
66
62
|
return extractTransaction({ psbt, skipFinalize })
|
|
67
63
|
}
|
|
@@ -2,11 +2,8 @@ import { Psbt as DefaultPsbt, Transaction as DefaultTransaction } from '@exodus/
|
|
|
2
2
|
import assert from 'minimalistic-assert'
|
|
3
3
|
|
|
4
4
|
import { writePsbtBlockHeight } from '../psbt-proprietary-types.js'
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
qtumignition: 25_000,
|
|
8
|
-
ravencoin: 1_000_000,
|
|
9
|
-
}
|
|
5
|
+
import { setPsbtVersionIfNotBitcoin } from '../psbt-utils.js'
|
|
6
|
+
import { getMaximumFeeRate } from './maximum-fee-rates.js'
|
|
10
7
|
|
|
11
8
|
/**
|
|
12
9
|
* Factory function that create the prepareForSigning function for a bitcoin-like asset.
|
|
@@ -26,9 +23,19 @@ export function createPrepareForSigning({
|
|
|
26
23
|
|
|
27
24
|
return ({ unsignedTx }) => {
|
|
28
25
|
const networkInfo = coinInfo.toBitcoinJS()
|
|
26
|
+
const maximumFeeRate = getMaximumFeeRate(assetName)
|
|
27
|
+
|
|
28
|
+
const psbtOptions = { network: networkInfo }
|
|
29
|
+
if (maximumFeeRate !== undefined) {
|
|
30
|
+
psbtOptions.maximumFeeRate = maximumFeeRate
|
|
31
|
+
}
|
|
29
32
|
|
|
30
33
|
if (unsignedTx.txData.psbtBuffer) {
|
|
31
|
-
return createPsbtFromBuffer({
|
|
34
|
+
return createPsbtFromBuffer({
|
|
35
|
+
Psbt,
|
|
36
|
+
psbtBuffer: unsignedTx.txData.psbtBuffer,
|
|
37
|
+
psbtOptions,
|
|
38
|
+
})
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
// Create PSBT based on internal Exodus data structure
|
|
@@ -41,15 +48,17 @@ export function createPrepareForSigning({
|
|
|
41
48
|
Psbt,
|
|
42
49
|
Transaction,
|
|
43
50
|
})
|
|
44
|
-
|
|
51
|
+
setPsbtVersionIfNotBitcoin(psbt, assetName)
|
|
45
52
|
|
|
46
53
|
return psbt
|
|
47
54
|
}
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
// Creates a PSBT instance from the passed transaction buffer provided by 3rd parties (e.g. dApps).
|
|
51
|
-
function createPsbtFromBuffer({ Psbt, psbtBuffer, ecc,
|
|
52
|
-
|
|
58
|
+
function createPsbtFromBuffer({ Psbt, psbtBuffer, ecc, psbtOptions = {} }) {
|
|
59
|
+
const fromBufferOptions = { ...psbtOptions }
|
|
60
|
+
if (ecc) fromBufferOptions.eccLib = ecc
|
|
61
|
+
return Psbt.fromBuffer(psbtBuffer, fromBufferOptions)
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
// Creates a PSBT instance from the passed inputs, outputs etc. The wallet itself provides this data.
|
|
@@ -64,11 +73,18 @@ function createPsbtFromTxData({
|
|
|
64
73
|
Transaction,
|
|
65
74
|
blockHeight,
|
|
66
75
|
}) {
|
|
67
|
-
// use
|
|
68
|
-
// if undefined,
|
|
69
|
-
|
|
76
|
+
// use hardcoded max fee rates for specific assets
|
|
77
|
+
// if undefined, the PSBT default (5000 sat/vB) is used. Passing `undefined`
|
|
78
|
+
// directly into the constructor used to clobber the default (buggy behavior),
|
|
79
|
+
// so we now only set the override when we actually have one.
|
|
80
|
+
const maximumFeeRate = getMaximumFeeRate(assetName)
|
|
81
|
+
|
|
82
|
+
const psbtOptions = { network: networkInfo }
|
|
83
|
+
if (maximumFeeRate !== undefined) {
|
|
84
|
+
psbtOptions.maximumFeeRate = maximumFeeRate
|
|
85
|
+
}
|
|
70
86
|
|
|
71
|
-
const psbt = new Psbt(
|
|
87
|
+
const psbt = new Psbt(psbtOptions)
|
|
72
88
|
|
|
73
89
|
// If present, add blockHeight as a proprietary field
|
|
74
90
|
if (blockHeight !== undefined) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'minimalistic-assert'
|
|
2
2
|
|
|
3
|
-
import { extractTransaction } from './common.js'
|
|
3
|
+
import { extractTransaction, shouldSkipFinalize } from './common.js'
|
|
4
4
|
import { createPrepareForSigning } from './default-prepare-for-signing.js'
|
|
5
5
|
|
|
6
6
|
export const signHardwareFactory = ({ assetName, resolvePurpose, keys, coinInfo }) => {
|
|
@@ -40,11 +40,7 @@ export const signHardwareFactory = ({ assetName, resolvePurpose, keys, coinInfo
|
|
|
40
40
|
multisigData,
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
-
const
|
|
44
|
-
unsignedTx.txData.psbtBuffer &&
|
|
45
|
-
unsignedTx.txMeta.addressPathsMap &&
|
|
46
|
-
unsignedTx.txMeta.inputsToSign
|
|
47
|
-
const skipFinalize = isExternalPsbt || unsignedTx.txMeta.returnPsbt
|
|
43
|
+
const skipFinalize = shouldSkipFinalize(unsignedTx)
|
|
48
44
|
return extractTransaction({ psbt, skipFinalize })
|
|
49
45
|
}
|
|
50
46
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// We keep hardcoded fee caps here so both signing and PSBT builders reuse the
|
|
2
|
+
// same per-asset overrides (e.g. Dogecoin needs a higher ceiling than the
|
|
3
|
+
// bitcoinjs default). If an asset is missing, the library fallback (5000 sat/vB)
|
|
4
|
+
// will apply instead.
|
|
5
|
+
const MAXIMUM_FEE_RATES = {
|
|
6
|
+
dogecoin: 12_000,
|
|
7
|
+
qtumignition: 25_000,
|
|
8
|
+
ravencoin: 1_000_000,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getMaximumFeeRate(assetName) {
|
|
12
|
+
return MAXIMUM_FEE_RATES[assetName]
|
|
13
|
+
}
|