@exodus/bitcoin-api 4.2.2 → 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 +22 -0
- package/package.json +2 -2
- package/src/psbt-builder.js +248 -0
- package/src/psbt-parser.js +502 -0
- package/src/psbt-proprietary-types.js +98 -0
- package/src/psbt-utils.js +68 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
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
|
+
|
|
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)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
* feat: add PSBT builder infrastructure (#6822)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
6
28
|
## [4.2.2](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.2.1...@exodus/bitcoin-api@4.2.2) (2025-11-10)
|
|
7
29
|
|
|
8
30
|
**Note:** Version bump only for package @exodus/bitcoin-api
|
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,248 @@
|
|
|
1
|
+
import { payments, Psbt, Transaction } from '@exodus/bitcoinjs'
|
|
2
|
+
import { publicKeyToX } from '@exodus/crypto/secp256k1'
|
|
3
|
+
import assert from 'minimalistic-assert'
|
|
4
|
+
|
|
5
|
+
import { SubType, writePsbtGlobalField, writePsbtOutputField } from './psbt-proprietary-types.js'
|
|
6
|
+
import { getAddressType, getPurposeXPubs, validatePurpose } from './psbt-utils.js'
|
|
7
|
+
|
|
8
|
+
function canParseTx(rawTxBuffer) {
|
|
9
|
+
try {
|
|
10
|
+
Transaction.fromBuffer(rawTxBuffer)
|
|
11
|
+
return true
|
|
12
|
+
} catch {
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getWrappedSegwitRedeemScript({ publicKey, address, context = '' }) {
|
|
18
|
+
const p2wpkh = payments.p2wpkh({ pubkey: publicKey })
|
|
19
|
+
const p2sh = payments.p2sh({ redeem: p2wpkh })
|
|
20
|
+
|
|
21
|
+
if (address !== p2sh.address) {
|
|
22
|
+
throw new Error(`Expected P2SH script to be a nested p2wpkh${context ? ' for ' + context : ''}`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return p2sh.redeem.output
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function addBip32Derivation({ isTaprootAddress, publicKey, path, masterFingerprint }) {
|
|
29
|
+
if (isTaprootAddress) {
|
|
30
|
+
const pubkey = publicKeyToX({ publicKey, format: 'buffer' })
|
|
31
|
+
return {
|
|
32
|
+
tapBip32Derivation: [
|
|
33
|
+
{
|
|
34
|
+
path,
|
|
35
|
+
leafHashes: [],
|
|
36
|
+
masterFingerprint,
|
|
37
|
+
pubkey,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
tapInternalKey: pubkey,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
bip32Derivation: [
|
|
46
|
+
{
|
|
47
|
+
path,
|
|
48
|
+
masterFingerprint,
|
|
49
|
+
pubkey: publicKey,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeGlobalMetadata(psbt, metadata) {
|
|
56
|
+
if (metadata.blockHeight !== undefined) {
|
|
57
|
+
writePsbtGlobalField(psbt, SubType.BlockHeight, metadata.blockHeight)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (metadata.rbfEnabled) {
|
|
61
|
+
writePsbtGlobalField(psbt, SubType.RbfEnabled, metadata.rbfEnabled)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (metadata.bumpTxId) {
|
|
65
|
+
writePsbtGlobalField(psbt, SubType.BumpTxId, metadata.bumpTxId)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (metadata.txType) {
|
|
69
|
+
writePsbtGlobalField(psbt, SubType.TxType, metadata.txType)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function createPsbtInput({
|
|
74
|
+
input,
|
|
75
|
+
asset,
|
|
76
|
+
addressPathsMap,
|
|
77
|
+
purposeXPubs,
|
|
78
|
+
nonWitnessTxs,
|
|
79
|
+
allowedPurposes,
|
|
80
|
+
}) {
|
|
81
|
+
const psbtInput = {
|
|
82
|
+
hash: input.txId,
|
|
83
|
+
index: input.vout,
|
|
84
|
+
sequence: input.sequence,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const purpose = asset.address.resolvePurpose(input.address)
|
|
88
|
+
validatePurpose(purpose, allowedPurposes, `address ${input.address}`)
|
|
89
|
+
|
|
90
|
+
const { isSegwitAddress, isTaprootAddress, isWrappedSegwitAddress } = getAddressType(purpose)
|
|
91
|
+
|
|
92
|
+
const path = addressPathsMap[input.address]
|
|
93
|
+
assert(path, `Derivation path is missing for address ${input.address}`)
|
|
94
|
+
|
|
95
|
+
const publicKey = purposeXPubs[purpose].hdkey.derive(path).publicKey
|
|
96
|
+
const masterFingerprint = purposeXPubs[purpose].masterFingerprint
|
|
97
|
+
|
|
98
|
+
if (isWrappedSegwitAddress) {
|
|
99
|
+
psbtInput.redeemScript = getWrappedSegwitRedeemScript({
|
|
100
|
+
publicKey,
|
|
101
|
+
address: input.address,
|
|
102
|
+
context: `input address ${input.address}`,
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (isWrappedSegwitAddress || isSegwitAddress || isTaprootAddress) {
|
|
107
|
+
psbtInput.witnessUtxo = {
|
|
108
|
+
value: input.value,
|
|
109
|
+
script: Buffer.from(input.script, 'hex'),
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!isTaprootAddress) {
|
|
114
|
+
const rawTx = (nonWitnessTxs || []).find((t) => t.txId === input.txId)
|
|
115
|
+
assert(
|
|
116
|
+
!!rawTx?.rawData,
|
|
117
|
+
`Non-taproot outputs require the full previous transaction for address ${input.address}`
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
const rawTxBuffer = Buffer.from(rawTx.rawData, 'hex')
|
|
121
|
+
if (canParseTx(rawTxBuffer)) {
|
|
122
|
+
psbtInput.nonWitnessUtxo = rawTxBuffer
|
|
123
|
+
} else {
|
|
124
|
+
// bitcoinjs can’t parse a handful of edge-case transactions (Litecoin MWEB, odd
|
|
125
|
+
// vendor forks, malformed historical data). When that happens we fall back to a
|
|
126
|
+
// witness-only record and rely on the signer to opt-in to __UNSAFE_SIGN_NONSEGWIT.
|
|
127
|
+
psbtInput.witnessUtxo = {
|
|
128
|
+
value: input.value,
|
|
129
|
+
script: Buffer.from(input.script, 'hex'),
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const derivationData = addBip32Derivation({
|
|
135
|
+
isTaprootAddress,
|
|
136
|
+
publicKey,
|
|
137
|
+
path,
|
|
138
|
+
masterFingerprint,
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
return { ...psbtInput, ...derivationData }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function createPsbtOutput({
|
|
145
|
+
address,
|
|
146
|
+
amount,
|
|
147
|
+
asset,
|
|
148
|
+
addressPathsMap,
|
|
149
|
+
purposeXPubs,
|
|
150
|
+
allowedPurposes,
|
|
151
|
+
}) {
|
|
152
|
+
const psbtOutput = {
|
|
153
|
+
address,
|
|
154
|
+
value: amount,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const path = addressPathsMap[address]
|
|
158
|
+
if (!path) {
|
|
159
|
+
return psbtOutput
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const purpose = asset.address.resolvePurpose(address)
|
|
163
|
+
validatePurpose(purpose, allowedPurposes, `output address ${address}`)
|
|
164
|
+
|
|
165
|
+
const { isTaprootAddress, isWrappedSegwitAddress } = getAddressType(purpose)
|
|
166
|
+
|
|
167
|
+
const publicKey = purposeXPubs[purpose].hdkey.derive(path).publicKey
|
|
168
|
+
const masterFingerprint = purposeXPubs[purpose].masterFingerprint
|
|
169
|
+
|
|
170
|
+
if (isWrappedSegwitAddress) {
|
|
171
|
+
psbtOutput.redeemScript = getWrappedSegwitRedeemScript({
|
|
172
|
+
publicKey,
|
|
173
|
+
address,
|
|
174
|
+
context: `output address ${address}`,
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const derivationData = addBip32Derivation({
|
|
179
|
+
isTaprootAddress,
|
|
180
|
+
publicKey,
|
|
181
|
+
path,
|
|
182
|
+
masterFingerprint,
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
return { ...psbtOutput, ...derivationData }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function createPsbtWithMetadata({
|
|
189
|
+
inputs,
|
|
190
|
+
outputs,
|
|
191
|
+
asset,
|
|
192
|
+
assetClientInterface,
|
|
193
|
+
walletAccount,
|
|
194
|
+
nonWitnessTxs,
|
|
195
|
+
addressPathsMap,
|
|
196
|
+
metadata,
|
|
197
|
+
allowedPurposes,
|
|
198
|
+
}) {
|
|
199
|
+
assert(inputs, 'inputs is required')
|
|
200
|
+
assert(outputs, 'outputs is required')
|
|
201
|
+
assert(asset, 'asset is required')
|
|
202
|
+
assert(assetClientInterface, 'assetClientInterface is required')
|
|
203
|
+
assert(walletAccount, 'walletAccount is required')
|
|
204
|
+
assert(addressPathsMap, 'addressPathsMap is required')
|
|
205
|
+
assert(metadata, 'metadata is required')
|
|
206
|
+
|
|
207
|
+
const psbt = new Psbt({ network: asset.coinInfo.toBitcoinJS() })
|
|
208
|
+
|
|
209
|
+
const purposeXPubs = await getPurposeXPubs({
|
|
210
|
+
assetClientInterface,
|
|
211
|
+
walletAccount,
|
|
212
|
+
asset,
|
|
213
|
+
allowedPurposes,
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
writeGlobalMetadata(psbt, metadata)
|
|
217
|
+
|
|
218
|
+
for (const input of inputs) {
|
|
219
|
+
const psbtInput = createPsbtInput({
|
|
220
|
+
input,
|
|
221
|
+
asset,
|
|
222
|
+
addressPathsMap,
|
|
223
|
+
purposeXPubs,
|
|
224
|
+
nonWitnessTxs,
|
|
225
|
+
allowedPurposes,
|
|
226
|
+
})
|
|
227
|
+
psbt.addInput(psbtInput)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
outputs.forEach(([address, amount]) => {
|
|
231
|
+
const psbtOutput = createPsbtOutput({
|
|
232
|
+
address,
|
|
233
|
+
amount,
|
|
234
|
+
asset,
|
|
235
|
+
addressPathsMap,
|
|
236
|
+
purposeXPubs,
|
|
237
|
+
allowedPurposes,
|
|
238
|
+
})
|
|
239
|
+
psbt.addOutput(psbtOutput)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
// sendOutputIndexes is optional (e.g., bump transactions).
|
|
243
|
+
for (const sendOutputIndex of metadata.sendOutputIndexes || []) {
|
|
244
|
+
writePsbtOutputField(psbt, sendOutputIndex, SubType.OutputRole, 'primary')
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return psbt.toBase64()
|
|
248
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -11,6 +11,10 @@ const PROP_TYPE_MARKER = 0xfc
|
|
|
11
11
|
|
|
12
12
|
export const SubType = Object.freeze({
|
|
13
13
|
BlockHeight: 0x01,
|
|
14
|
+
BumpTxId: 0x03,
|
|
15
|
+
RbfEnabled: 0x04,
|
|
16
|
+
TxType: 0x05,
|
|
17
|
+
OutputRole: 0x06,
|
|
14
18
|
})
|
|
15
19
|
|
|
16
20
|
const buildPropKey = (subType) => {
|
|
@@ -51,3 +55,97 @@ export const readPsbtBlockHeight = (psbt) => {
|
|
|
51
55
|
const kv = findProprietaryVal(psbt.data.globalMap.unknownKeyVals, SubType.BlockHeight)
|
|
52
56
|
return kv ? kv.value.readUInt32LE(0) : undefined
|
|
53
57
|
}
|
|
58
|
+
|
|
59
|
+
const encodeProprietaryValue = (subType, value) => {
|
|
60
|
+
switch (subType) {
|
|
61
|
+
case SubType.BlockHeight:
|
|
62
|
+
assert(
|
|
63
|
+
Number.isInteger(value) && value >= 0 && value <= 4_294_967_295,
|
|
64
|
+
'blockHeight must be a positive integer between 0 and 4294967295'
|
|
65
|
+
)
|
|
66
|
+
return u32LE(value)
|
|
67
|
+
case SubType.RbfEnabled:
|
|
68
|
+
return Buffer.from([value ? 1 : 0])
|
|
69
|
+
case SubType.BumpTxId:
|
|
70
|
+
case SubType.TxType:
|
|
71
|
+
case SubType.OutputRole:
|
|
72
|
+
return Buffer.from(value, 'utf8')
|
|
73
|
+
default:
|
|
74
|
+
throw new Error(`Unknown proprietary field subType: ${subType}`)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const decodeProprietaryValue = (subType, buffer) => {
|
|
79
|
+
switch (subType) {
|
|
80
|
+
case SubType.BlockHeight:
|
|
81
|
+
return buffer.readUInt32LE(0)
|
|
82
|
+
case SubType.RbfEnabled:
|
|
83
|
+
return buffer[0] === 1
|
|
84
|
+
case SubType.BumpTxId:
|
|
85
|
+
case SubType.TxType:
|
|
86
|
+
case SubType.OutputRole:
|
|
87
|
+
return buffer.toString('utf8')
|
|
88
|
+
default:
|
|
89
|
+
throw new Error(`Unknown proprietary field subType: ${subType}`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const writePsbtGlobalField = (psbt, subType, value) => {
|
|
94
|
+
assert(psbt, 'psbt is required')
|
|
95
|
+
assert(Number.isInteger(subType), 'subType must be an integer')
|
|
96
|
+
assert(value !== undefined, 'value is required')
|
|
97
|
+
|
|
98
|
+
const encodedValue = encodeProprietaryValue(subType, value)
|
|
99
|
+
psbt.addUnknownKeyValToGlobal({
|
|
100
|
+
key: buildPropKey(subType),
|
|
101
|
+
value: encodedValue,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const writePsbtOutputField = (psbt, outputIndex, subType, value) => {
|
|
106
|
+
assert(psbt, 'psbt is required')
|
|
107
|
+
assert(
|
|
108
|
+
Number.isInteger(outputIndex) && outputIndex >= 0,
|
|
109
|
+
'outputIndex must be a non-negative integer'
|
|
110
|
+
)
|
|
111
|
+
assert(Number.isInteger(subType), 'subType must be an integer')
|
|
112
|
+
assert(value !== undefined, 'value is required')
|
|
113
|
+
|
|
114
|
+
const encodedValue = encodeProprietaryValue(subType, value)
|
|
115
|
+
psbt.addUnknownKeyValToOutput(outputIndex, {
|
|
116
|
+
key: buildPropKey(subType),
|
|
117
|
+
value: encodedValue,
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const readPsbtGlobalField = (psbt, subType) => {
|
|
122
|
+
assert(psbt, 'psbt is required')
|
|
123
|
+
assert(Number.isInteger(subType), 'subType must be an integer')
|
|
124
|
+
|
|
125
|
+
const kv = findProprietaryVal(psbt.data.globalMap.unknownKeyVals, subType)
|
|
126
|
+
return kv ? decodeProprietaryValue(subType, kv.value) : undefined
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const readPsbtOutputField = (psbt, outputIndex) => {
|
|
130
|
+
assert(psbt, 'psbt is required')
|
|
131
|
+
assert(
|
|
132
|
+
Number.isInteger(outputIndex) && outputIndex >= 0,
|
|
133
|
+
'outputIndex must be a non-negative integer'
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
const output = psbt.data.outputs[outputIndex]
|
|
137
|
+
if (!output) return Object.create(null)
|
|
138
|
+
|
|
139
|
+
const result = Object.create(null)
|
|
140
|
+
for (const [key, subTypeVal] of Object.entries(SubType)) {
|
|
141
|
+
const kv = findProprietaryVal(output.unknownKeyVals, subTypeVal)
|
|
142
|
+
if (kv) {
|
|
143
|
+
result[key.charAt(0).toLowerCase() + key.slice(1)] = decodeProprietaryValue(
|
|
144
|
+
subTypeVal,
|
|
145
|
+
kv.value
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result
|
|
151
|
+
}
|
package/src/psbt-utils.js
CHANGED
|
@@ -1,10 +1,62 @@
|
|
|
1
|
+
import BIP32 from '@exodus/bip32'
|
|
1
2
|
import BipPath from 'bip32-path'
|
|
2
3
|
import lodash from 'lodash'
|
|
4
|
+
import assert from 'minimalistic-assert'
|
|
5
|
+
|
|
6
|
+
export const PURPOSE_TYPES = {
|
|
7
|
+
LEGACY: 44, // P2PKH
|
|
8
|
+
WRAPPED_SEGWIT: 49, // P2SH-P2WPKH
|
|
9
|
+
SEGWIT: 84, // P2WPKH
|
|
10
|
+
TAPROOT: 86, // P2TR
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getAddressType(purpose) {
|
|
14
|
+
return {
|
|
15
|
+
isLegacyAddress: purpose === PURPOSE_TYPES.LEGACY,
|
|
16
|
+
isWrappedSegwitAddress: purpose === PURPOSE_TYPES.WRAPPED_SEGWIT,
|
|
17
|
+
isSegwitAddress: purpose === PURPOSE_TYPES.SEGWIT,
|
|
18
|
+
isTaprootAddress: purpose === PURPOSE_TYPES.TAPROOT,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function getPurposeXPubs({
|
|
23
|
+
assetClientInterface,
|
|
24
|
+
walletAccount,
|
|
25
|
+
asset,
|
|
26
|
+
allowedPurposes,
|
|
27
|
+
}) {
|
|
28
|
+
assert(allowedPurposes, 'allowedPurposes is required')
|
|
29
|
+
const purposeXPubs = Object.create(null)
|
|
30
|
+
|
|
31
|
+
for (const purpose of allowedPurposes) {
|
|
32
|
+
const xpub = await assetClientInterface.getExtendedPublicKey({
|
|
33
|
+
walletAccount,
|
|
34
|
+
assetName: asset.name,
|
|
35
|
+
purpose,
|
|
36
|
+
})
|
|
37
|
+
const hdkey = BIP32.fromXPub(xpub)
|
|
38
|
+
const masterFingerprint = Buffer.alloc(4)
|
|
39
|
+
masterFingerprint.writeUint32BE(hdkey.fingerprint)
|
|
40
|
+
purposeXPubs[purpose] = {
|
|
41
|
+
hdkey,
|
|
42
|
+
masterFingerprint,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return purposeXPubs
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function validatePurpose(purpose, allowedPurposes, context = '') {
|
|
50
|
+
assert(allowedPurposes, 'allowedPurposes is required')
|
|
51
|
+
if (!allowedPurposes.includes(purpose)) {
|
|
52
|
+
throw new Error(`Purpose ${purpose} not found${context ? ' for ' + context : ''}`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
3
55
|
|
|
4
56
|
export const createPsbtToUnsignedTx =
|
|
5
57
|
({ assetClientInterface, assetName }) =>
|
|
6
58
|
async ({ psbt, walletAccount, purpose = 86 }) => {
|
|
7
|
-
const addressPathsMap =
|
|
59
|
+
const addressPathsMap = Object.create(null)
|
|
8
60
|
const inputsToSign = []
|
|
9
61
|
|
|
10
62
|
const addressOpts = {
|
|
@@ -59,3 +111,18 @@ export const createPsbtToUnsignedTx =
|
|
|
59
111
|
},
|
|
60
112
|
}
|
|
61
113
|
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Temporarily turns on __UNSAFE_SIGN_NONSEGWIT so we can sign or validate PSBTs
|
|
117
|
+
* whose legacy inputs are missing nonWitnessUtxo.
|
|
118
|
+
*/
|
|
119
|
+
export async function withUnsafeNonSegwit({ psbt, fn, unsafe = true }) {
|
|
120
|
+
const cache = psbt.__CACHE
|
|
121
|
+
const prevValue = cache.__UNSAFE_SIGN_NONSEGWIT
|
|
122
|
+
cache.__UNSAFE_SIGN_NONSEGWIT = unsafe
|
|
123
|
+
try {
|
|
124
|
+
return await fn()
|
|
125
|
+
} finally {
|
|
126
|
+
cache.__UNSAFE_SIGN_NONSEGWIT = prevValue
|
|
127
|
+
}
|
|
128
|
+
}
|