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