@exodus/bitcoin-api 4.3.0 → 4.4.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 +12 -0
- package/package.json +2 -2
- package/src/psbt-parser.js +502 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
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.4.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.2.1...@exodus/bitcoin-api@4.4.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
|
+
|
|
17
|
+
|
|
6
18
|
## [4.3.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.2.1...@exodus/bitcoin-api@4.3.0) (2025-11-11)
|
|
7
19
|
|
|
8
20
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.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": "5bc77cf38ec6fbd32d9e286d5bda25ea7366854d"
|
|
64
64
|
}
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { Psbt, Transaction } from '@exodus/bitcoinjs'
|
|
2
|
+
import { publicKeyToX } from '@exodus/crypto/secp256k1'
|
|
3
|
+
import { UtxoCollection } from '@exodus/models'
|
|
4
|
+
import BipPath from 'bip32-path'
|
|
5
|
+
import assert from 'minimalistic-assert'
|
|
6
|
+
|
|
7
|
+
import { readPsbtGlobalField, readPsbtOutputField, SubType } from './psbt-proprietary-types.js'
|
|
8
|
+
import { getAddressType, getPurposeXPubs, validatePurpose } from './psbt-utils.js'
|
|
9
|
+
import { createInputs, createOutput } from './tx-create/tx-create-utils.js'
|
|
10
|
+
import { findUnconfirmedSentRbfTxs } from './tx-utils.js'
|
|
11
|
+
import { getUnconfirmedTxAncestorMap } from './unconfirmed-ancestor-data.js'
|
|
12
|
+
import { getUsableUtxos, getUtxos } from './utxos-utils.js'
|
|
13
|
+
|
|
14
|
+
function extractInputUtxoData(psbtInput, txInput, index) {
|
|
15
|
+
if (psbtInput.nonWitnessUtxo) {
|
|
16
|
+
const prevTx = Transaction.fromBuffer(psbtInput.nonWitnessUtxo)
|
|
17
|
+
const prevOutput = prevTx.outs[txInput.index]
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
scriptBuffer: prevOutput.script,
|
|
21
|
+
value: prevOutput.value,
|
|
22
|
+
prevTxHex: prevTx.toHex(),
|
|
23
|
+
prevTxId: prevTx.getId(),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (psbtInput.witnessUtxo) {
|
|
28
|
+
return {
|
|
29
|
+
scriptBuffer: psbtInput.witnessUtxo.script,
|
|
30
|
+
value: psbtInput.witnessUtxo.value,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
throw new Error(`Input ${index} has no witnessUtxo or nonWitnessUtxo`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseSingleInput({ txInput, psbtInput, index, asset, purposeXPubs, allowedPurposes }) {
|
|
38
|
+
const input = {
|
|
39
|
+
txId: Buffer.from(txInput.hash).reverse().toString('hex'),
|
|
40
|
+
vout: txInput.index,
|
|
41
|
+
sequence: txInput.sequence,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const inputUtxoData = extractInputUtxoData(psbtInput, txInput, index)
|
|
45
|
+
|
|
46
|
+
input.value = inputUtxoData.value
|
|
47
|
+
input.script = Buffer.from(inputUtxoData.scriptBuffer).toString('hex')
|
|
48
|
+
|
|
49
|
+
if (inputUtxoData.prevTxHex) {
|
|
50
|
+
input.prevTxHex = inputUtxoData.prevTxHex
|
|
51
|
+
input.prevTxId = inputUtxoData.prevTxId
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const address = asset.address.fromScriptPubKey(inputUtxoData.scriptBuffer)
|
|
55
|
+
const purpose = asset.address.resolvePurpose(address)
|
|
56
|
+
validatePurpose(purpose, allowedPurposes, `input ${index}`)
|
|
57
|
+
|
|
58
|
+
const { isTaprootAddress } = getAddressType(purpose)
|
|
59
|
+
|
|
60
|
+
if (!isTaprootAddress && !psbtInput.nonWitnessUtxo) {
|
|
61
|
+
input.missingNonWitnessUtxo = true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const derivation = getBip32Derivation(
|
|
65
|
+
purposeXPubs[purpose].hdkey,
|
|
66
|
+
isTaprootAddress ? psbtInput.tapBip32Derivation : psbtInput.bip32Derivation,
|
|
67
|
+
isTaprootAddress
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if (!derivation) {
|
|
71
|
+
throw new Error(`Input ${index} has no derivation path`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
input.address = { address, meta: { path: derivation.path, purpose } }
|
|
75
|
+
return input
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseSingleOutput({
|
|
79
|
+
txOutput,
|
|
80
|
+
psbtOutput,
|
|
81
|
+
index,
|
|
82
|
+
asset,
|
|
83
|
+
purposeXPubs,
|
|
84
|
+
psbt,
|
|
85
|
+
allowedPurposes,
|
|
86
|
+
}) {
|
|
87
|
+
const address = txOutput.address ?? asset.address.fromScriptPubKey(txOutput.script)
|
|
88
|
+
const output = { amount: txOutput.value }
|
|
89
|
+
|
|
90
|
+
const outputMetadata = readPsbtOutputField(psbt, index)
|
|
91
|
+
if (outputMetadata) {
|
|
92
|
+
output.metadata = outputMetadata
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const purpose = asset.address.resolvePurpose(address)
|
|
96
|
+
try {
|
|
97
|
+
validatePurpose(purpose, allowedPurposes)
|
|
98
|
+
} catch {
|
|
99
|
+
output.address = { address }
|
|
100
|
+
return output
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const { isTaprootAddress } = getAddressType(purpose)
|
|
104
|
+
|
|
105
|
+
const derivation = getBip32Derivation(
|
|
106
|
+
purposeXPubs[purpose].hdkey,
|
|
107
|
+
isTaprootAddress ? psbtOutput.tapBip32Derivation : psbtOutput.bip32Derivation,
|
|
108
|
+
isTaprootAddress
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
output.address = derivation
|
|
112
|
+
? { address, meta: { path: derivation.path, purpose } }
|
|
113
|
+
: { address, meta: { purpose } }
|
|
114
|
+
|
|
115
|
+
return output
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function readGlobalMetadata(psbt) {
|
|
119
|
+
return {
|
|
120
|
+
blockHeight: readPsbtGlobalField(psbt, SubType.BlockHeight),
|
|
121
|
+
rbfEnabled: readPsbtGlobalField(psbt, SubType.RbfEnabled),
|
|
122
|
+
bumpTxId: readPsbtGlobalField(psbt, SubType.BumpTxId),
|
|
123
|
+
txType: readPsbtGlobalField(psbt, SubType.TxType),
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function calculateFee(inputs, outputs) {
|
|
128
|
+
const inputSum = inputs.reduce((sum, input) => sum + (input.value || 0), 0)
|
|
129
|
+
const outputSum = outputs.reduce((sum, output) => sum + (output.amount || 0), 0)
|
|
130
|
+
return inputSum - outputSum
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getBip32Derivation(hdkey, bip32Derivations, ignoreY) {
|
|
134
|
+
const fingerprintBuffer = Buffer.alloc(4)
|
|
135
|
+
fingerprintBuffer.writeUInt32BE(hdkey.fingerprint, 0)
|
|
136
|
+
|
|
137
|
+
const matchingDerivations = bip32Derivations?.filter((bipDv) => {
|
|
138
|
+
return bipDv.masterFingerprint.equals(fingerprintBuffer)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
if (!matchingDerivations?.length) {
|
|
142
|
+
// No derivation matched our fingerprint; the output isn’t ours.
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (matchingDerivations.length !== 1) {
|
|
147
|
+
// Multiple matches imply the input/output carries several keys from the same wallet xpub.
|
|
148
|
+
// That’s outside our wallet model, so we fail fast.
|
|
149
|
+
throw new Error(
|
|
150
|
+
`more than one matching derivation for fingerprint ${fingerprintBuffer.toString('hex')}: ${
|
|
151
|
+
matchingDerivations.length
|
|
152
|
+
}`
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const [derivation] = matchingDerivations
|
|
157
|
+
const publicKey = hdkey.derive(derivation.path).publicKey
|
|
158
|
+
|
|
159
|
+
const publicKeyToCompare = ignoreY ? publicKeyToX({ publicKey, format: 'buffer' }) : publicKey
|
|
160
|
+
|
|
161
|
+
if (publicKeyToCompare.equals(derivation.pubkey)) {
|
|
162
|
+
return derivation
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
throw new Error('pubkey did not match bip32Derivation')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getSelectedUtxos({ parsedInputs, utxos, asset, txSet }) {
|
|
169
|
+
const utxoId = (txId, vout) => txId + ':' + vout
|
|
170
|
+
const utxosArray = Array.isArray(utxos) ? utxos : utxos.toArray()
|
|
171
|
+
const utxosMap = new Map(utxosArray.map((u) => [utxoId(u.txId, u.vout), u]))
|
|
172
|
+
|
|
173
|
+
const inputIds = new Set(parsedInputs.map((inp) => utxoId(inp.txId, inp.vout)))
|
|
174
|
+
let detectedReplaceTx = null
|
|
175
|
+
|
|
176
|
+
const replaceableTxs = findUnconfirmedSentRbfTxs(txSet)
|
|
177
|
+
for (const replaceableTx of replaceableTxs) {
|
|
178
|
+
if (replaceableTx.data?.inputs) {
|
|
179
|
+
const txInputs = UtxoCollection.fromJSON(replaceableTx.data.inputs, {
|
|
180
|
+
currency: asset.currency,
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const txInputsArray = txInputs.toArray()
|
|
184
|
+
|
|
185
|
+
const txInputIds = txInputsArray.map((u) => utxoId(u.txId, u.vout))
|
|
186
|
+
const isReplacement = txInputIds.some((id) => inputIds.has(id))
|
|
187
|
+
|
|
188
|
+
if (isReplacement) {
|
|
189
|
+
detectedReplaceTx = replaceableTx
|
|
190
|
+
|
|
191
|
+
for (const utxo of txInputsArray) {
|
|
192
|
+
const id = utxoId(utxo.txId, utxo.vout)
|
|
193
|
+
if (!utxosMap.has(id)) {
|
|
194
|
+
utxosMap.set(id, utxo)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Exodus only handles a single replaceable transaction at a time, so bail once we’ve tagged one.
|
|
199
|
+
break
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const inputUtxos = []
|
|
205
|
+
const missingInputIds = []
|
|
206
|
+
|
|
207
|
+
for (const parsedInput of parsedInputs) {
|
|
208
|
+
const id = utxoId(parsedInput.txId, parsedInput.vout)
|
|
209
|
+
const ourUtxo = utxosMap.get(id)
|
|
210
|
+
|
|
211
|
+
if (ourUtxo) {
|
|
212
|
+
inputUtxos.push(ourUtxo)
|
|
213
|
+
} else {
|
|
214
|
+
missingInputIds.push(id)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
selectedUtxos: UtxoCollection.fromArray(inputUtxos, { currency: asset.currency }),
|
|
220
|
+
replaceTx: detectedReplaceTx,
|
|
221
|
+
missingInputIds,
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function getChangeOutputData({ outputs, assetClientInterface, walletAccount, asset }) {
|
|
226
|
+
let changeOutputData = null
|
|
227
|
+
for (const [i, output] of outputs.entries()) {
|
|
228
|
+
// Outputs marked “primary” are treated as sends even if they use one of our paths
|
|
229
|
+
// (common for self‑sends, consolidations, etc.), so skip them during change detection.
|
|
230
|
+
if (output.metadata?.outputRole === 'primary') {
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// We only treat addresses we can re-derive from the wallet xpub as potential
|
|
235
|
+
// change outputs.
|
|
236
|
+
if (!output.address?.meta?.path) {
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// At this point we already know the output was derived by our wallet and it is
|
|
241
|
+
// not explicitly flagged as a primary/send output, so we tentatively treat it as
|
|
242
|
+
// change and confirm by re-deriving the on-chain address.
|
|
243
|
+
if (changeOutputData) {
|
|
244
|
+
// Multiple change outputs are a smell. This can happen if an external PSBT
|
|
245
|
+
// marks a send output of the same wallet with its derivation path, so we fail fast instead of
|
|
246
|
+
// silently mislabelling funds.
|
|
247
|
+
throw new Error('Multiple change outputs are not allowed')
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const { path, purpose } = output.address.meta
|
|
251
|
+
const [chainIndex, addressIndex] = BipPath.fromString(path).toPathArray()
|
|
252
|
+
|
|
253
|
+
const verifyAddr = await assetClientInterface.getAddress({
|
|
254
|
+
assetName: asset.name,
|
|
255
|
+
walletAccount: walletAccount.toString(),
|
|
256
|
+
purpose,
|
|
257
|
+
chainIndex,
|
|
258
|
+
addressIndex,
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
if (verifyAddr.toString() === output.address.address) {
|
|
262
|
+
changeOutputData = {
|
|
263
|
+
address: verifyAddr,
|
|
264
|
+
amount: output.amount,
|
|
265
|
+
index: i,
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
throw new Error(`Invalid change output address ${output.address.address}`)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return changeOutputData
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function findPrimaryOutputIndexes(outputs) {
|
|
276
|
+
return outputs
|
|
277
|
+
.map((output, index) => (output.metadata?.outputRole === 'primary' ? index : null))
|
|
278
|
+
.filter((index) => index !== null)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function processReplacementTransaction(replaceTx, asset) {
|
|
282
|
+
if (!replaceTx) return null
|
|
283
|
+
|
|
284
|
+
const clonedTx = replaceTx.clone()
|
|
285
|
+
const updatedTx = clonedTx.update({ data: { ...clonedTx.data } })
|
|
286
|
+
|
|
287
|
+
updatedTx.data.sent = updatedTx?.data?.sent.map((to) => ({
|
|
288
|
+
...to,
|
|
289
|
+
amount: asset.currency.baseUnit(to.amount),
|
|
290
|
+
}))
|
|
291
|
+
|
|
292
|
+
return updatedTx
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function calculateAmounts({ primaryOutputs, changeOutputData, processedReplaceTx, asset }) {
|
|
296
|
+
const sendAmounts = primaryOutputs.map((primaryOutput) =>
|
|
297
|
+
asset.currency.baseUnit(primaryOutput.amount)
|
|
298
|
+
)
|
|
299
|
+
const totalSendAmount = sendAmounts.reduce((sum, amount) => sum.add(amount), asset.currency.ZERO)
|
|
300
|
+
|
|
301
|
+
const changeAmount = changeOutputData
|
|
302
|
+
? asset.currency.baseUnit(changeOutputData.amount)
|
|
303
|
+
: asset.currency.ZERO
|
|
304
|
+
|
|
305
|
+
const totalAmount = processedReplaceTx
|
|
306
|
+
? processedReplaceTx.data.sent.reduce((total, { amount }) => total.add(amount), totalSendAmount)
|
|
307
|
+
: totalSendAmount
|
|
308
|
+
|
|
309
|
+
return { sendAmounts, totalSendAmount, changeAmount, totalAmount }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function buildAddressPathsMap(selectedUtxos, changeOutputData) {
|
|
313
|
+
const addressPathsMap = selectedUtxos.getAddressPathsMap()
|
|
314
|
+
|
|
315
|
+
if (changeOutputData) {
|
|
316
|
+
addressPathsMap[changeOutputData.address.address] = changeOutputData.address.meta.path
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return addressPathsMap
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function extractRawTransactions(parsedInputs) {
|
|
323
|
+
const rawTxsData = parsedInputs
|
|
324
|
+
.filter((parsedInput) => parsedInput.prevTxId)
|
|
325
|
+
.map((parsedInput) => ({
|
|
326
|
+
txId: parsedInput.prevTxId,
|
|
327
|
+
rawData: parsedInput.prevTxHex,
|
|
328
|
+
}))
|
|
329
|
+
|
|
330
|
+
const rawTxsMap = new Map(rawTxsData.map((tx) => [tx.txId, tx.rawData]))
|
|
331
|
+
return [...rawTxsMap].map(([txId, rawData]) => ({ txId, rawData }))
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export async function parsePsbt({
|
|
335
|
+
psbtBase64,
|
|
336
|
+
asset,
|
|
337
|
+
assetClientInterface,
|
|
338
|
+
walletAccount,
|
|
339
|
+
allowedPurposes,
|
|
340
|
+
}) {
|
|
341
|
+
assert(psbtBase64, 'psbtBase64 is required')
|
|
342
|
+
assert(asset, 'asset is required')
|
|
343
|
+
assert(assetClientInterface, 'assetClientInterface is required')
|
|
344
|
+
assert(walletAccount, 'walletAccount is required')
|
|
345
|
+
|
|
346
|
+
const purposeXPubs = await getPurposeXPubs({
|
|
347
|
+
assetClientInterface,
|
|
348
|
+
walletAccount,
|
|
349
|
+
asset,
|
|
350
|
+
allowedPurposes,
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
// TBD: change it to fromBuffer
|
|
354
|
+
const psbt = Psbt.fromBase64(psbtBase64, { network: asset.coinInfo.toBitcoinJS() })
|
|
355
|
+
|
|
356
|
+
const inputs = []
|
|
357
|
+
for (let i = 0; i < psbt.inputCount; i++) {
|
|
358
|
+
const input = parseSingleInput({
|
|
359
|
+
txInput: psbt.txInputs[i],
|
|
360
|
+
psbtInput: psbt.data.inputs[i],
|
|
361
|
+
index: i,
|
|
362
|
+
asset,
|
|
363
|
+
purposeXPubs,
|
|
364
|
+
allowedPurposes,
|
|
365
|
+
})
|
|
366
|
+
inputs.push(input)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const outputs = []
|
|
370
|
+
for (let i = 0; i < psbt.data.outputs.length; i++) {
|
|
371
|
+
const output = parseSingleOutput({
|
|
372
|
+
txOutput: psbt.txOutputs[i],
|
|
373
|
+
psbtOutput: psbt.data.outputs[i],
|
|
374
|
+
index: i,
|
|
375
|
+
asset,
|
|
376
|
+
purposeXPubs,
|
|
377
|
+
psbt,
|
|
378
|
+
allowedPurposes,
|
|
379
|
+
})
|
|
380
|
+
outputs.push(output)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const globalMetadata = readGlobalMetadata(psbt)
|
|
384
|
+
const fee = calculateFee(inputs, outputs)
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
inputs,
|
|
388
|
+
outputs,
|
|
389
|
+
fee,
|
|
390
|
+
globalMetadata,
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export async function extractTransactionContext({
|
|
395
|
+
psbtBase64,
|
|
396
|
+
asset,
|
|
397
|
+
assetClientInterface,
|
|
398
|
+
walletAccount,
|
|
399
|
+
allowedPurposes,
|
|
400
|
+
}) {
|
|
401
|
+
assert(psbtBase64, 'psbtBase64 is required')
|
|
402
|
+
assert(asset, 'asset is required')
|
|
403
|
+
assert(assetClientInterface, 'assetClientInterface is required')
|
|
404
|
+
assert(walletAccount, 'walletAccount is required')
|
|
405
|
+
|
|
406
|
+
const {
|
|
407
|
+
inputs: parsedInputs,
|
|
408
|
+
outputs: parsedOutputs,
|
|
409
|
+
fee: calculatedFee,
|
|
410
|
+
globalMetadata,
|
|
411
|
+
} = await parsePsbt({
|
|
412
|
+
psbtBase64,
|
|
413
|
+
asset,
|
|
414
|
+
assetClientInterface,
|
|
415
|
+
walletAccount,
|
|
416
|
+
allowedPurposes,
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
const changeOutputData = await getChangeOutputData({
|
|
420
|
+
outputs: parsedOutputs,
|
|
421
|
+
assetClientInterface,
|
|
422
|
+
walletAccount,
|
|
423
|
+
asset,
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
const assetName = asset.name
|
|
427
|
+
const accountState = await assetClientInterface.getAccountState({
|
|
428
|
+
assetName,
|
|
429
|
+
walletAccount,
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
const utxos = getUtxos({ accountState, asset })
|
|
433
|
+
const txSet = await assetClientInterface.getTxLog({ assetName, walletAccount })
|
|
434
|
+
|
|
435
|
+
const { selectedUtxos, replaceTx, missingInputIds } = getSelectedUtxos({
|
|
436
|
+
parsedInputs,
|
|
437
|
+
utxos,
|
|
438
|
+
txSet,
|
|
439
|
+
asset,
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
if (missingInputIds.length > 0) {
|
|
443
|
+
// Missing inputs mean we no longer control some UTXOs referenced in the PSBT.
|
|
444
|
+
// Typical causes: they’ve already been spent from another device, or the RBF
|
|
445
|
+
// transaction we planned to replace has since confirmed and dropped out of the
|
|
446
|
+
// replaceable set.
|
|
447
|
+
throw new Error(`Unknown inputs: ${missingInputIds.join(', ')}`)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const processedReplaceTx = processReplacementTransaction(replaceTx, asset)
|
|
451
|
+
|
|
452
|
+
const primaryOutputIndexes = findPrimaryOutputIndexes(parsedOutputs)
|
|
453
|
+
const primaryOutputs = primaryOutputIndexes.map((v) => parsedOutputs[v])
|
|
454
|
+
|
|
455
|
+
const { sendAmounts, totalSendAmount, changeAmount, totalAmount } = calculateAmounts({
|
|
456
|
+
primaryOutputs,
|
|
457
|
+
changeOutputData,
|
|
458
|
+
processedReplaceTx,
|
|
459
|
+
asset,
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
const unconfirmedTxAncestor = getUnconfirmedTxAncestorMap({ accountState })
|
|
463
|
+
const feeData = await assetClientInterface.getFeeConfig({ assetName })
|
|
464
|
+
const usableUtxos = getUsableUtxos({
|
|
465
|
+
asset,
|
|
466
|
+
utxos,
|
|
467
|
+
feeData,
|
|
468
|
+
txSet,
|
|
469
|
+
unconfirmedTxAncestor,
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
const inputs = createInputs(assetName, selectedUtxos.toArray(), globalMetadata.rbfEnabled)
|
|
473
|
+
const outputs = parsedOutputs.map((output) =>
|
|
474
|
+
createOutput(assetName, output.address.address, asset.currency.baseUnit(output.amount))
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
const addressPathsMap = buildAddressPathsMap(selectedUtxos, changeOutputData)
|
|
478
|
+
const rawTxs = extractRawTransactions(parsedInputs)
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
inputs,
|
|
482
|
+
outputs,
|
|
483
|
+
addressPathsMap,
|
|
484
|
+
rawTxs,
|
|
485
|
+
fee: asset.currency.baseUnit(calculatedFee),
|
|
486
|
+
sendOutputIndexes: primaryOutputIndexes,
|
|
487
|
+
changeOutputIndex: changeOutputData?.index,
|
|
488
|
+
sendAmounts,
|
|
489
|
+
changeAmount,
|
|
490
|
+
totalSendAmount,
|
|
491
|
+
totalAmount,
|
|
492
|
+
primaryAddresses: primaryOutputs.map((primaryOutput) => primaryOutput.address),
|
|
493
|
+
ourAddress: changeOutputData?.address,
|
|
494
|
+
usableUtxos,
|
|
495
|
+
selectedUtxos,
|
|
496
|
+
replaceTx: processedReplaceTx,
|
|
497
|
+
sendOutputs: primaryOutputIndexes.map((primaryOutputIndex) => outputs[primaryOutputIndex]),
|
|
498
|
+
changeOutput: changeOutputData ? outputs[changeOutputData.index] : undefined,
|
|
499
|
+
rbfEnabled: globalMetadata.rbfEnabled,
|
|
500
|
+
blockHeight: globalMetadata.blockHeight,
|
|
501
|
+
}
|
|
502
|
+
}
|