@exodus/bitcoin-api 2.6.8 → 2.7.1
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/package.json +2 -2
- package/src/account-state.js +1 -0
- package/src/insight-api-client/index.js +13 -3
- package/src/tx-log/bitcoin-monitor-scanner.js +62 -9
- package/src/tx-log/ordinals-indexer-utils.js +53 -0
- package/src/tx-send/index.js +29 -21
- package/src/tx-sign/common.js +29 -0
- package/src/tx-sign/create-get-key-and-purpose.js +47 -0
- package/src/tx-sign/create-sign-with-wallet.js +70 -0
- package/src/tx-sign/default-create-tx.js +24 -164
- package/src/tx-sign/default-prepare-for-signing.js +103 -0
- package/src/tx-sign/default-sign-hardware.js +98 -0
- package/src/tx-sign/index.js +3 -1
- package/src/tx-sign/taproot.js +4 -2
- package/src/utxos-utils.js +78 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.1",
|
|
4
4
|
"description": "Exodus bitcoin-api",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -43,5 +43,5 @@
|
|
|
43
43
|
"@scure/btc-signer": "^1.1.0",
|
|
44
44
|
"jest-when": "^3.5.1"
|
|
45
45
|
},
|
|
46
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "9c993eed3a3bcfc1c49d86377fdc5c7cb731d632"
|
|
47
47
|
}
|
package/src/account-state.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import urlJoin from 'url-join'
|
|
3
3
|
import qs from 'querystring'
|
|
4
4
|
import delay from 'delay'
|
|
5
|
+
import { isEmpty } from 'lodash'
|
|
5
6
|
|
|
6
7
|
const getTextFromResponse = async (response) => {
|
|
7
8
|
try {
|
|
@@ -105,13 +106,22 @@ export default class InsightAPIClient {
|
|
|
105
106
|
const url = urlJoin(this._baseURL, `/tx/${encodedTxId}`)
|
|
106
107
|
const response = await fetch(url)
|
|
107
108
|
|
|
108
|
-
// change in https://github.com/jprichardson/exodus-rn/pull/4336 may break Insight compatibility
|
|
109
|
-
// we're probably past the point of just spinning up an Insight server and have it plug n' play for Magnifier
|
|
110
|
-
|
|
111
109
|
if (response.status === 404) return null
|
|
112
110
|
return response.json()
|
|
113
111
|
}
|
|
114
112
|
|
|
113
|
+
async fetchTxObject(txId) {
|
|
114
|
+
const url = urlJoin(this._baseURL, `/fulltx?${new URLSearchParams({ hash: txId })}`)
|
|
115
|
+
const response = await fetch(url)
|
|
116
|
+
|
|
117
|
+
if (response.status === 404) return null
|
|
118
|
+
const object = await response.json()
|
|
119
|
+
if (isEmpty(object)) {
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
return object
|
|
123
|
+
}
|
|
124
|
+
|
|
115
125
|
async fetchRawTx(txId) {
|
|
116
126
|
const encodedTxId = encodeURIComponent(txId)
|
|
117
127
|
const url = urlJoin(this._baseURL, `/rawtx/${encodedTxId}`)
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { orderTxs } from '../insight-api-client/util'
|
|
2
2
|
import { Address, UtxoCollection } from '@exodus/models'
|
|
3
|
-
import {
|
|
3
|
+
import { compact, isEqual, uniq } from 'lodash'
|
|
4
4
|
import ms from 'ms'
|
|
5
5
|
import assert from 'minimalistic-assert'
|
|
6
6
|
import { isChangeAddress, isReceiveAddress } from '../address-utils'
|
|
7
7
|
import { getOrdinalsUtxos, getUtxos, partitionUtxos } from '../utxos-utils'
|
|
8
8
|
import { getOrdinalAddress } from '../ordinals-utils'
|
|
9
|
+
import { indexOrdinalUnconfirmedTx } from './ordinals-indexer-utils'
|
|
9
10
|
|
|
10
11
|
// Time to check whether to drop a sent tx
|
|
11
12
|
const SENT_TIME_TO_DROP = ms('2m')
|
|
@@ -67,7 +68,6 @@ export class BitcoinMonitorScanner {
|
|
|
67
68
|
|
|
68
69
|
const storedUtxos = getUtxos({ asset, accountState })
|
|
69
70
|
const storedOrdinalUtxos = getOrdinalsUtxos({ asset, accountState })
|
|
70
|
-
const ordinalAddress = await this.getOrdinalAddress({ walletAccount })
|
|
71
71
|
|
|
72
72
|
const currentStoredUtxos = storedUtxos.union(storedOrdinalUtxos)
|
|
73
73
|
|
|
@@ -90,7 +90,9 @@ export class BitcoinMonitorScanner {
|
|
|
90
90
|
: (txs) =>
|
|
91
91
|
txs.filter((tx) => {
|
|
92
92
|
const txItem = currentTxs.get(tx.txid)
|
|
93
|
-
|
|
93
|
+
const confirmed = txItem && txItem.confirmed
|
|
94
|
+
const inscriptionsIndexed = txItem?.data?.inscriptionsIndexed
|
|
95
|
+
return confirmed && (!this.#ordinalsEnabled || inscriptionsIndexed)
|
|
94
96
|
}).length >= txs.length
|
|
95
97
|
|
|
96
98
|
const unusedAddressIndexes = await assetClientInterface.getUnusedAddressIndexes({
|
|
@@ -190,8 +192,30 @@ export class BitcoinMonitorScanner {
|
|
|
190
192
|
promises.push(promise)
|
|
191
193
|
}
|
|
192
194
|
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
+
const insightTxs = (await Promise.all(promises)).reduce(
|
|
196
|
+
(total, some) => total.concat(some),
|
|
197
|
+
[]
|
|
198
|
+
)
|
|
199
|
+
if (!this.#ordinalsEnabled) {
|
|
200
|
+
return insightTxs
|
|
201
|
+
}
|
|
202
|
+
return Promise.all(
|
|
203
|
+
insightTxs.map((tx) => {
|
|
204
|
+
try {
|
|
205
|
+
return indexOrdinalUnconfirmedTx({
|
|
206
|
+
tx,
|
|
207
|
+
currency: this.#asset.currency,
|
|
208
|
+
insightClient,
|
|
209
|
+
})
|
|
210
|
+
} catch (e) {
|
|
211
|
+
console.warn(
|
|
212
|
+
`Could not index ${asset.name} ordinal tx ${tx.txid} for wallet account ${walletAccount}. message: ${e.message}`,
|
|
213
|
+
e
|
|
214
|
+
)
|
|
215
|
+
return tx
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
)
|
|
195
219
|
}
|
|
196
220
|
|
|
197
221
|
const gapSearchParameters = newChains.map(({ purpose, chain }) => {
|
|
@@ -344,6 +368,15 @@ export class BitcoinMonitorScanner {
|
|
|
344
368
|
currencies: { [assetName]: currency },
|
|
345
369
|
}
|
|
346
370
|
|
|
371
|
+
if (this.#ordinalsEnabled) {
|
|
372
|
+
txLogItem.data = {
|
|
373
|
+
...txLogItem.data,
|
|
374
|
+
inscriptionsIndexed: txItem.inscriptionsIndexed,
|
|
375
|
+
sentInscriptions: [],
|
|
376
|
+
receivedInscriptions: [],
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
347
380
|
let from = []
|
|
348
381
|
|
|
349
382
|
// if txItem.vin has an address that matches ours, means we've spent this tx
|
|
@@ -361,8 +394,13 @@ export class BitcoinMonitorScanner {
|
|
|
361
394
|
txLogItem.coinAmount = txLogItem.coinAmount.sub(currency.defaultUnit(vin.value))
|
|
362
395
|
isSent = true
|
|
363
396
|
txLogItem.data.sent = []
|
|
364
|
-
if (
|
|
365
|
-
txLogItem.data.
|
|
397
|
+
if (this.#ordinalsEnabled && vin.inscriptions) {
|
|
398
|
+
txLogItem.data.sentInscriptions.push(
|
|
399
|
+
...vin.inscriptions.map((i) => ({
|
|
400
|
+
...i,
|
|
401
|
+
value: currency.defaultUnit(vin.value).toBaseNumber(),
|
|
402
|
+
}))
|
|
403
|
+
)
|
|
366
404
|
}
|
|
367
405
|
|
|
368
406
|
// this is only used to exclude the utxos in the reducer which is why we don't care about the other fields
|
|
@@ -432,8 +470,13 @@ export class BitcoinMonitorScanner {
|
|
|
432
470
|
txLogItem.data.changeAddress = address
|
|
433
471
|
}
|
|
434
472
|
|
|
435
|
-
if (
|
|
436
|
-
txLogItem.data.
|
|
473
|
+
if (this.#ordinalsEnabled && vout.inscriptions) {
|
|
474
|
+
txLogItem.data.receivedInscriptions.push(
|
|
475
|
+
...vout.inscriptions.map((i) => ({
|
|
476
|
+
...i,
|
|
477
|
+
value: currency.defaultUnit(vout.value).toBaseNumber(),
|
|
478
|
+
}))
|
|
479
|
+
)
|
|
437
480
|
}
|
|
438
481
|
|
|
439
482
|
// it was sent to us...
|
|
@@ -450,6 +493,11 @@ export class BitcoinMonitorScanner {
|
|
|
450
493
|
rbfEnabled: txItem.rbf,
|
|
451
494
|
}
|
|
452
495
|
|
|
496
|
+
if (this.#ordinalsEnabled) {
|
|
497
|
+
output.inscriptionsIndexed = txItem.inscriptionsIndexed
|
|
498
|
+
output.inscriptions = vout.inscriptions || []
|
|
499
|
+
}
|
|
500
|
+
|
|
453
501
|
if (this.#shouldExcludeVoutUtxo({ asset, output, txItem, vout })) {
|
|
454
502
|
return
|
|
455
503
|
}
|
|
@@ -540,10 +588,13 @@ export class BitcoinMonitorScanner {
|
|
|
540
588
|
return !isEqual(chain, originalChain.chain)
|
|
541
589
|
})
|
|
542
590
|
|
|
591
|
+
const ordinalAddress = await this.getOrdinalAddress({ walletAccount })
|
|
543
592
|
const utxosData = utxoCol
|
|
544
593
|
? partitionUtxos({
|
|
545
594
|
allUtxos: utxoCol,
|
|
595
|
+
ordinalsEnabled: this.#ordinalsEnabled,
|
|
546
596
|
ordinalAddress,
|
|
597
|
+
knownBalanceUtxoIds: accountState.knownBalanceUtxoIds,
|
|
547
598
|
})
|
|
548
599
|
: {}
|
|
549
600
|
|
|
@@ -603,7 +654,9 @@ export class BitcoinMonitorScanner {
|
|
|
603
654
|
|
|
604
655
|
const { utxos, ordinalsUtxos } = partitionUtxos({
|
|
605
656
|
allUtxos: txConfirmedUtxos,
|
|
657
|
+
ordinalsEnabled: this.#ordinalsEnabled,
|
|
606
658
|
ordinalAddress: await this.getOrdinalAddress({ walletAccount }),
|
|
659
|
+
knownBalanceUtxoIds: accountState.knownBalanceUtxoIds,
|
|
607
660
|
})
|
|
608
661
|
|
|
609
662
|
return {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { memoizeLruCache } from '@exodus/asset-lib'
|
|
2
|
+
import { cloneDeep } from 'lodash'
|
|
3
|
+
|
|
4
|
+
export const indexOutputs = ({ tx, currency }) => {
|
|
5
|
+
const inscriptions = []
|
|
6
|
+
|
|
7
|
+
let inputOffset = 0
|
|
8
|
+
for (let i = 0; i < tx.vin.length; i++) {
|
|
9
|
+
const vin = tx.vin[i]
|
|
10
|
+
const value = currency.defaultUnit(vin.value).toBaseNumber()
|
|
11
|
+
inscriptions.push(
|
|
12
|
+
...(vin.inscriptions || []).map((i) => ({ ...i, offset: i.offset + inputOffset }))
|
|
13
|
+
)
|
|
14
|
+
inputOffset = value
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let outputOffset = 0
|
|
18
|
+
for (let i = 0; i < tx.vout.length; i++) {
|
|
19
|
+
const vout = tx.vout[i]
|
|
20
|
+
const value = currency.defaultUnit(vout.value).toBaseNumber()
|
|
21
|
+
vout.inscriptions = inscriptions
|
|
22
|
+
.map((i) => ({ ...i, offset: i.offset - outputOffset }))
|
|
23
|
+
.filter((i) => i.offset >= 0 && i.offset < value)
|
|
24
|
+
outputOffset = value
|
|
25
|
+
}
|
|
26
|
+
tx.inscriptionsMemoryIndexed = true // avoids btc being spent even when the mempool index was done in memory
|
|
27
|
+
return tx
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const indexOrdinalUnconfirmedTx = memoizeLruCache(
|
|
31
|
+
async ({ insightClient, currency, tx }) => {
|
|
32
|
+
if (tx.inscriptionsIndexed || tx.inscriptionsMemoryIndexed) {
|
|
33
|
+
return tx
|
|
34
|
+
}
|
|
35
|
+
const copyTx = cloneDeep(tx)
|
|
36
|
+
await Promise.all(
|
|
37
|
+
copyTx.vin.map(async (vin) => {
|
|
38
|
+
const outputTx = await indexOrdinalUnconfirmedTx({
|
|
39
|
+
insightClient,
|
|
40
|
+
currency,
|
|
41
|
+
tx: await insightClient.fetchTxObject(vin.txid),
|
|
42
|
+
})
|
|
43
|
+
if (!outputTx.inscriptionsIndexed && !outputTx.inscriptionsMemoryIndexed) {
|
|
44
|
+
throw new Error(`Cannot index ${tx.txid}. Input tx ${outputTx.txid} is not indexed. `)
|
|
45
|
+
}
|
|
46
|
+
vin.inscriptions = outputTx.vout[vin.vout].inscriptions
|
|
47
|
+
})
|
|
48
|
+
)
|
|
49
|
+
return indexOutputs({ tx: copyTx, currency })
|
|
50
|
+
},
|
|
51
|
+
({ tx }) => tx.txid,
|
|
52
|
+
100
|
|
53
|
+
)
|
package/src/tx-send/index.js
CHANGED
|
@@ -14,7 +14,12 @@ import {
|
|
|
14
14
|
createOutput as dogecoinCreateOutput,
|
|
15
15
|
} from './dogecoin'
|
|
16
16
|
import { findUnconfirmedSentRbfTxs } from '../tx-utils'
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
getOrdinalsUtxos,
|
|
19
|
+
getTransferOrdinalsUtxos,
|
|
20
|
+
getUsableUtxos,
|
|
21
|
+
getUtxos,
|
|
22
|
+
} from '../utxos-utils'
|
|
18
23
|
|
|
19
24
|
import * as defaultBitcoinjsLib from '@exodus/bitcoinjs-lib'
|
|
20
25
|
|
|
@@ -132,6 +137,7 @@ export const createAndBroadcastTXFactory = ({
|
|
|
132
137
|
getFeeEstimator,
|
|
133
138
|
getSizeAndChangeScript = getSizeAndChangeScriptFactory(), // for decred customizations
|
|
134
139
|
allowUnconfirmedRbfEnabledUtxos,
|
|
140
|
+
ordinalsEnabled = false,
|
|
135
141
|
}) => async (
|
|
136
142
|
{ asset: maybeToken, walletAccount, address, amount: tokenAmount, options },
|
|
137
143
|
{ assetClientInterface }
|
|
@@ -167,6 +173,11 @@ export const createAndBroadcastTXFactory = ({
|
|
|
167
173
|
|
|
168
174
|
const inscriptionIds = nft?.tokenId ? [nft?.tokenId] : brc20 ? brc20.inscriptionIds : undefined
|
|
169
175
|
|
|
176
|
+
assert(
|
|
177
|
+
ordinalsEnabled || !inscriptionIds,
|
|
178
|
+
'inscriptions cannot be sent when ordinalsEnabled=false '
|
|
179
|
+
)
|
|
180
|
+
|
|
170
181
|
const shuffle = (list) => {
|
|
171
182
|
return inscriptionIds ? list : doShuffle(list) // don't shuffle when sending ordinal!!!!
|
|
172
183
|
}
|
|
@@ -192,26 +203,9 @@ export const createAndBroadcastTXFactory = ({
|
|
|
192
203
|
|
|
193
204
|
const currentOrdinalsUtxos = getOrdinalsUtxos({ accountState, asset })
|
|
194
205
|
const transferOrdinalsUtxos = inscriptionIds
|
|
195
|
-
?
|
|
206
|
+
? getTransferOrdinalsUtxos({ inscriptionIds, ordinalsUtxos: currentOrdinalsUtxos })
|
|
196
207
|
: undefined
|
|
197
208
|
|
|
198
|
-
if (inscriptionIds) {
|
|
199
|
-
assert(
|
|
200
|
-
transferOrdinalsUtxos?.size === inscriptionIds.length,
|
|
201
|
-
`Expected ordinal utxos ${inscriptionIds.length}. Found: ${transferOrdinalsUtxos?.size || 0}`
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
// const unconfirmedOrdinalUtxos = transferOrdinalsUtxos
|
|
205
|
-
// .toArray()
|
|
206
|
-
// .filter((ordinalUtxo) => !(ordinalUtxo.confirmations > 0))
|
|
207
|
-
// assert(
|
|
208
|
-
// !unconfirmedOrdinalUtxos.length,
|
|
209
|
-
// `OrdinalUtxo with inscription ids ${unconfirmedOrdinalUtxos
|
|
210
|
-
// .map((utxo) => utxo.inscriptionId)
|
|
211
|
-
// .join(', ')} have not confirmed yet`
|
|
212
|
-
// )
|
|
213
|
-
}
|
|
214
|
-
|
|
215
209
|
const insightClient = asset.baseAsset.insightClient
|
|
216
210
|
const currency = asset.currency
|
|
217
211
|
const feeData = await assetClientInterface.getFeeConfig({ assetName })
|
|
@@ -434,6 +428,8 @@ export const createAndBroadcastTXFactory = ({
|
|
|
434
428
|
|
|
435
429
|
const { script, size } = getSizeAndChangeScript({ assetName, tx, rawTx, changeUtxoIndex, txId })
|
|
436
430
|
|
|
431
|
+
// for ordinals, used to allow users spending change utxos even when unconfirmed and ordinals are unknown
|
|
432
|
+
const knownBalanceUtxoIds = accountState.knownBalanceUtxoIds || []
|
|
437
433
|
let remainingUtxos = usableUtxos.difference(selectedUtxos)
|
|
438
434
|
if (changeUtxoIndex !== -1) {
|
|
439
435
|
const address = Address.create(ourAddress.address, ourAddress.meta)
|
|
@@ -446,6 +442,8 @@ export const createAndBroadcastTXFactory = ({
|
|
|
446
442
|
confirmations: 0,
|
|
447
443
|
rbfEnabled,
|
|
448
444
|
}
|
|
445
|
+
|
|
446
|
+
knownBalanceUtxoIds.push(`${changeUtxo.txId}:${changeUtxo.vout}`.toLowerCase())
|
|
449
447
|
remainingUtxos = remainingUtxos.addUtxo(changeUtxo)
|
|
450
448
|
}
|
|
451
449
|
if (replaceTx) {
|
|
@@ -454,7 +452,7 @@ export const createAndBroadcastTXFactory = ({
|
|
|
454
452
|
|
|
455
453
|
const remainingOrdinalsUtxos = transferOrdinalsUtxos
|
|
456
454
|
? currentOrdinalsUtxos.difference(transferOrdinalsUtxos)
|
|
457
|
-
:
|
|
455
|
+
: currentOrdinalsUtxos
|
|
458
456
|
|
|
459
457
|
await assetClientInterface.updateAccountState({
|
|
460
458
|
assetName,
|
|
@@ -462,6 +460,7 @@ export const createAndBroadcastTXFactory = ({
|
|
|
462
460
|
newData: {
|
|
463
461
|
utxos: remainingUtxos,
|
|
464
462
|
ordinalsUtxos: remainingOrdinalsUtxos,
|
|
463
|
+
knownBalanceUtxoIds,
|
|
465
464
|
},
|
|
466
465
|
})
|
|
467
466
|
|
|
@@ -522,7 +521,16 @@ export const createAndBroadcastTXFactory = ({
|
|
|
522
521
|
inputs: selectedUtxos.toJSON(),
|
|
523
522
|
replacedTxId: replaceTx ? replaceTx.txId : undefined,
|
|
524
523
|
nftId: nft ? `${assetName}:${nft.tokenId}` : undefined, // it allows BE to load the nft info while the nft is in transit
|
|
525
|
-
|
|
524
|
+
inscriptionsIndexed: ordinalsEnabled ? true : undefined,
|
|
525
|
+
sentInscriptions: inscriptionIds
|
|
526
|
+
? inscriptionIds.map((inscriptionId) => {
|
|
527
|
+
return {
|
|
528
|
+
inscriptionId,
|
|
529
|
+
offset: 0,
|
|
530
|
+
value: 0,
|
|
531
|
+
}
|
|
532
|
+
})
|
|
533
|
+
: undefined,
|
|
526
534
|
},
|
|
527
535
|
},
|
|
528
536
|
],
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function extractTransaction({ psbt, skipFinalize }) {
|
|
2
|
+
// If a dapp authored the TX, it expects a serialized PSBT response.
|
|
3
|
+
// Note: we wouldn't be able to finalise inputs in some cases that's why we serialize before finalizing inputs.
|
|
4
|
+
|
|
5
|
+
if (skipFinalize) {
|
|
6
|
+
const rawPSBT = psbt.toBuffer()
|
|
7
|
+
|
|
8
|
+
return { plainTx: { rawPSBT } }
|
|
9
|
+
} else {
|
|
10
|
+
// Serialize tx
|
|
11
|
+
psbt.finalizeAllInputs()
|
|
12
|
+
const tx = psbt.extractTransaction()
|
|
13
|
+
const rawTx = tx.toBuffer()
|
|
14
|
+
const txId = tx.getId()
|
|
15
|
+
|
|
16
|
+
// tx needs to be serializable for desktop RPC send => sign communication
|
|
17
|
+
return { rawTx, txId, tx: serializeTx({ tx }) }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const serializeTx = ({ tx }) => {
|
|
22
|
+
// for desktop compatibility
|
|
23
|
+
return {
|
|
24
|
+
virtualSize: tx.virtualSize?.(),
|
|
25
|
+
outs: tx.outs?.map((out) => ({
|
|
26
|
+
script: out.script.toString('hex'),
|
|
27
|
+
})),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import lodash from 'lodash'
|
|
2
|
+
import assert from 'minimalistic-assert'
|
|
3
|
+
import { getOwnProperty } from '@exodus/basic-utils'
|
|
4
|
+
import { getECPair } from '../bitcoinjs-lib'
|
|
5
|
+
|
|
6
|
+
import secp256k1 from 'secp256k1'
|
|
7
|
+
|
|
8
|
+
const ECPair = getECPair()
|
|
9
|
+
|
|
10
|
+
export const createGetKeyAndPurpose = ({
|
|
11
|
+
keys,
|
|
12
|
+
hdkeys,
|
|
13
|
+
resolvePurpose,
|
|
14
|
+
addressPathsMap,
|
|
15
|
+
privateKeysAddressMap,
|
|
16
|
+
coinInfo,
|
|
17
|
+
}) =>
|
|
18
|
+
lodash.memoize((address) => {
|
|
19
|
+
const purpose = resolvePurpose(address)
|
|
20
|
+
const networkInfo = coinInfo.toBitcoinJS()
|
|
21
|
+
if (privateKeysAddressMap) {
|
|
22
|
+
return getPrivateKeyFromMap(privateKeysAddressMap, networkInfo, purpose, address)
|
|
23
|
+
} else {
|
|
24
|
+
return getPrivateKeyFromHDKeys(hdkeys, addressPathsMap, keys, networkInfo, purpose, address)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
function getPrivateKeyFromMap(privateKeysAddressMap, networkInfo, purpose, address) {
|
|
29
|
+
const privateKey = getOwnProperty(privateKeysAddressMap, address, 'string')
|
|
30
|
+
assert(privateKey, `there is no private key for address ${address}`)
|
|
31
|
+
const key = ECPair.fromWIF(privateKey, networkInfo)
|
|
32
|
+
const publicKey = secp256k1.publicKeyCreate(key.privateKey, true)
|
|
33
|
+
return { key, purpose, publicKey }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getPrivateKeyFromHDKeys(hdkeys, addressPathsMap, keys, networkInfo, purpose, address) {
|
|
37
|
+
const path = getOwnProperty(addressPathsMap, address, 'string')
|
|
38
|
+
assert(hdkeys, 'hdkeys must be provided')
|
|
39
|
+
assert(purpose, `purpose for address ${address} could not be resolved`)
|
|
40
|
+
const hdkey = hdkeys[purpose]
|
|
41
|
+
assert(hdkey, `hdkey for purpose for ${purpose} and address ${address} could not be resolved`)
|
|
42
|
+
const derivedhdkey = hdkey.derive(path)
|
|
43
|
+
const privateEncoded = keys.encodePrivate(derivedhdkey.privateKey)
|
|
44
|
+
const key = ECPair.fromWIF(privateEncoded, networkInfo)
|
|
45
|
+
const publicKey = derivedhdkey.publicKey
|
|
46
|
+
return { key, publicKey, purpose }
|
|
47
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Transaction, payments } from '@exodus/bitcoinjs-lib'
|
|
2
|
+
|
|
3
|
+
import { getECPair } from '../bitcoinjs-lib'
|
|
4
|
+
import { toXOnly } from '../bitcoinjs-lib/ecc-utils'
|
|
5
|
+
import { createGetKeyAndPurpose } from './create-get-key-and-purpose'
|
|
6
|
+
import { toAsyncSigner, tweakSigner } from './taproot'
|
|
7
|
+
|
|
8
|
+
const ECPair = getECPair()
|
|
9
|
+
|
|
10
|
+
export function createSignWithWallet({
|
|
11
|
+
keys,
|
|
12
|
+
hdkeys,
|
|
13
|
+
resolvePurpose,
|
|
14
|
+
privateKeysAddressMap,
|
|
15
|
+
addressPathsMap,
|
|
16
|
+
coinInfo,
|
|
17
|
+
network,
|
|
18
|
+
}) {
|
|
19
|
+
const getKeyAndPurpose = createGetKeyAndPurpose({
|
|
20
|
+
keys,
|
|
21
|
+
hdkeys,
|
|
22
|
+
resolvePurpose,
|
|
23
|
+
privateKeysAddressMap,
|
|
24
|
+
addressPathsMap,
|
|
25
|
+
coinInfo,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
return async (psbt, inputsToSign) => {
|
|
29
|
+
// The Taproot SIGHASH flag includes all previous outputs,
|
|
30
|
+
// so signing is only done AFTER all inputs have been updated
|
|
31
|
+
for (let index = 0; index < psbt.inputCount; index++) {
|
|
32
|
+
const inputInfo = inputsToSign[index]
|
|
33
|
+
// dApps request to sign only specific transaction inputs.
|
|
34
|
+
if (!inputInfo) continue
|
|
35
|
+
const { address, sigHash } = inputInfo
|
|
36
|
+
// TODO: we can remove SIGHASH_ALL, it is default.
|
|
37
|
+
const sigHashTypes = sigHash !== undefined ? [sigHash || Transaction.SIGHASH_ALL] : undefined
|
|
38
|
+
const { key, purpose, publicKey } = getKeyAndPurpose(address)
|
|
39
|
+
|
|
40
|
+
const isP2SH = purpose === 49
|
|
41
|
+
const isTaprootAddress = purpose === 86
|
|
42
|
+
|
|
43
|
+
if (isP2SH) {
|
|
44
|
+
// If spending from a P2SH address, we assume the address is P2SH wrapping
|
|
45
|
+
// P2WPKH. Exodus doesn't use P2SH addresses so we should only ever be
|
|
46
|
+
// signing a P2SH input if we are importing a private key
|
|
47
|
+
// BIP143: As a default policy, only compressed public keys are accepted in P2WPKH and P2WSH
|
|
48
|
+
const p2wpkh = payments.p2wpkh({ pubkey: publicKey })
|
|
49
|
+
const p2sh = payments.p2sh({ redeem: p2wpkh })
|
|
50
|
+
if (address === p2sh.address && !Buffer.isBuffer(psbt.data.inputs[index].redeemScript)) {
|
|
51
|
+
psbt.updateInput(index, {
|
|
52
|
+
redeemScript: p2sh.redeem.output,
|
|
53
|
+
})
|
|
54
|
+
} else {
|
|
55
|
+
throw new Error('Expected P2SH script to be a nested segwit input')
|
|
56
|
+
}
|
|
57
|
+
} else if (isTaprootAddress && !Buffer.isBuffer(psbt.data.inputs[index].tapInternalKey)) {
|
|
58
|
+
// tapInternalKey is metadata for signing and not part of the hash to sign.
|
|
59
|
+
// so modifying it here is fine.
|
|
60
|
+
psbt.updateInput(index, {
|
|
61
|
+
tapInternalKey: toXOnly(publicKey),
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// desktop / BE / mobile with bip-schnorr signing
|
|
66
|
+
const signingKey = isTaprootAddress ? tweakSigner({ signer: key, ECPair, network }) : key
|
|
67
|
+
await psbt.signInputAsync(index, toAsyncSigner({ keyPair: signingKey }), sigHashTypes)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -1,91 +1,8 @@
|
|
|
1
1
|
import assert from 'minimalistic-assert'
|
|
2
|
-
import lodash from 'lodash'
|
|
3
|
-
import { payments, Psbt, Transaction } from '@exodus/bitcoinjs-lib'
|
|
4
|
-
import { getOwnProperty } from '@exodus/basic-utils'
|
|
5
2
|
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
import { toAsyncSigner, tweakSigner } from './taproot'
|
|
10
|
-
import { getECPair } from '../bitcoinjs-lib'
|
|
11
|
-
|
|
12
|
-
let ECPair
|
|
13
|
-
|
|
14
|
-
const _MAXIMUM_FEE_RATES = {
|
|
15
|
-
qtumignition: 25000,
|
|
16
|
-
ravencoin: 1000000,
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const canParseTx = (rawTxBuffer) => {
|
|
20
|
-
try {
|
|
21
|
-
Transaction.fromBuffer(rawTxBuffer)
|
|
22
|
-
return true
|
|
23
|
-
} catch (e) {
|
|
24
|
-
return false
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export const serializeTx = ({ tx }) => {
|
|
29
|
-
// for desktop compatibility
|
|
30
|
-
return {
|
|
31
|
-
virtualSize: tx.virtualSize?.(),
|
|
32
|
-
outs: tx.outs?.map((out) => ({
|
|
33
|
-
script: out.script.toString('hex'),
|
|
34
|
-
})),
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Creates a PSBT instance from the passed inputs, outputs etc. The wallet itself provides this data.
|
|
39
|
-
function createPSBT({ inputs, outputs, rawTxs, networkInfo, getKeyAndPurpose, assetName }) {
|
|
40
|
-
// use harcoded max fee rates for specific assets
|
|
41
|
-
// if undefined, will be set to default value by PSBT (2500)
|
|
42
|
-
const maximumFeeRate = _MAXIMUM_FEE_RATES[assetName]
|
|
43
|
-
|
|
44
|
-
const psbt = new Psbt({ maximumFeeRate, network: networkInfo })
|
|
45
|
-
|
|
46
|
-
// Fill tx
|
|
47
|
-
for (const { txId, vout, address, value, script, sequence } of inputs) {
|
|
48
|
-
const { purpose, publicKey } = getKeyAndPurpose(address)
|
|
49
|
-
|
|
50
|
-
const isSegwitAddress = purpose === 84
|
|
51
|
-
const isTaprootAddress = purpose === 86
|
|
52
|
-
const txIn = { hash: txId, index: vout, sequence }
|
|
53
|
-
if (isSegwitAddress || isTaprootAddress) {
|
|
54
|
-
// witness outputs only require the value and the script, not the full transaction
|
|
55
|
-
txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
|
|
56
|
-
if (isTaprootAddress) {
|
|
57
|
-
txIn.tapInternalKey = toXOnly(publicKey)
|
|
58
|
-
}
|
|
59
|
-
} else {
|
|
60
|
-
const rawTx = (rawTxs || []).find((t) => t.txId === txId)
|
|
61
|
-
// non-witness outptus require the full transaction
|
|
62
|
-
assert(!!rawTx, 'Non-witness outputs require the full previous transaction.')
|
|
63
|
-
const rawTxBuffer = Buffer.from(rawTx.rawData, 'hex')
|
|
64
|
-
if (canParseTx(rawTxBuffer)) {
|
|
65
|
-
txIn.nonWitnessUtxo = rawTxBuffer
|
|
66
|
-
} else {
|
|
67
|
-
// temp fix for https://exodusio.slack.com/archives/CP202D90Q/p1671014704829939 until bitcoinjs could parse a mweb tx without failing
|
|
68
|
-
console.warn(`Setting psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true for asset ${assetName}`)
|
|
69
|
-
psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true
|
|
70
|
-
txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
psbt.addInput(txIn)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
for (const [address, amount] of outputs) {
|
|
77
|
-
psbt.addOutput({ value: amount, address })
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return psbt
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Creates a PSBT instance from the passed transaction buffer provided by 3rd parties (e.g. dApps).
|
|
84
|
-
function createPSBTFromBuffer({ psbtBuffer, ecc }) {
|
|
85
|
-
const psbt = Psbt.fromBuffer(psbtBuffer, { eccLib: ecc })
|
|
86
|
-
|
|
87
|
-
return psbt
|
|
88
|
-
}
|
|
3
|
+
import { createPrepareForSigning } from './default-prepare-for-signing'
|
|
4
|
+
import { createSignWithWallet } from './create-sign-with-wallet'
|
|
5
|
+
import { extractTransaction } from './common'
|
|
89
6
|
|
|
90
7
|
export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, network }) => {
|
|
91
8
|
assert(assetName, 'assetName is required')
|
|
@@ -93,91 +10,34 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
|
|
|
93
10
|
assert(keys, 'keys is required')
|
|
94
11
|
assert(coinInfo, 'coinInfo is required')
|
|
95
12
|
|
|
13
|
+
const prepareForSigning = createPrepareForSigning({
|
|
14
|
+
assetName,
|
|
15
|
+
resolvePurpose,
|
|
16
|
+
coinInfo,
|
|
17
|
+
})
|
|
18
|
+
|
|
96
19
|
return async ({ unsignedTx, hdkeys, privateKeysAddressMap }) => {
|
|
97
20
|
assert(unsignedTx, 'unsignedTx is required')
|
|
98
21
|
assert(hdkeys || privateKeysAddressMap, 'hdkeys or privateKeysAddressMap is required')
|
|
99
|
-
const { addressPathsMap } = unsignedTx.txMeta
|
|
100
|
-
const networkInfo = { ...coinInfo.toBitcoinJS(), messagePrefix: '' }
|
|
101
22
|
|
|
102
|
-
|
|
23
|
+
const { addressPathsMap } = unsignedTx.txMeta
|
|
103
24
|
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
assert(purpose, `purpose for address ${address} could not be resolved`)
|
|
116
|
-
const hdkey = hdkeys[purpose]
|
|
117
|
-
assert(hdkey, `hdkey for purpose for ${purpose} and address ${address} could not be resolved`)
|
|
118
|
-
const derivedhdkey = hdkey.derive(path)
|
|
119
|
-
const privateEncoded = keys.encodePrivate(derivedhdkey.privateKey)
|
|
120
|
-
const key = ECPair.fromWIF(privateEncoded, networkInfo)
|
|
121
|
-
const publicKey = derivedhdkey.publicKey
|
|
122
|
-
return { key, publicKey, purpose }
|
|
25
|
+
const psbt = prepareForSigning({ unsignedTx })
|
|
26
|
+
|
|
27
|
+
const inputsToSign = unsignedTx.txMeta.inputsToSign || unsignedTx.txData.inputs
|
|
28
|
+
const signWithWallet = createSignWithWallet({
|
|
29
|
+
keys,
|
|
30
|
+
hdkeys,
|
|
31
|
+
resolvePurpose,
|
|
32
|
+
privateKeysAddressMap,
|
|
33
|
+
addressPathsMap,
|
|
34
|
+
coinInfo,
|
|
35
|
+
network,
|
|
123
36
|
})
|
|
124
37
|
|
|
125
|
-
|
|
126
|
-
unsignedTx.txData.psbtBuffer &&
|
|
127
|
-
unsignedTx.txMeta.addressPathsMap &&
|
|
128
|
-
unsignedTx.txMeta.inputsToSign
|
|
129
|
-
const psbt = isPsbtBufferPassed
|
|
130
|
-
? createPSBTFromBuffer({ psbtBuffer: unsignedTx.txData.psbtBuffer })
|
|
131
|
-
: createPSBT({ ...unsignedTx.txData, ...unsignedTx.txMeta, getKeyAndPurpose, networkInfo })
|
|
132
|
-
const { inputs } = unsignedTx.txData
|
|
133
|
-
|
|
134
|
-
if (!['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)) psbt.setVersion(1)
|
|
135
|
-
|
|
136
|
-
const inputsToSign = isPsbtBufferPassed ? unsignedTx.txMeta.inputsToSign : inputs
|
|
137
|
-
|
|
138
|
-
// The Taproot SIGHASH flag includes all previous outputs,
|
|
139
|
-
// so signing is only done AFTER all inputs have been updated
|
|
140
|
-
for (let index = 0; index < psbt.inputCount; index++) {
|
|
141
|
-
const inputInfo = inputsToSign[index]
|
|
142
|
-
// dApps request to sign only specific transaction inputs.
|
|
143
|
-
if (!inputInfo) continue
|
|
144
|
-
const { address, sigHash } = inputInfo
|
|
145
|
-
const sigHashTypes = sigHash !== undefined ? [sigHash || Transaction.SIGHASH_ALL] : undefined
|
|
146
|
-
const { key, purpose, publicKey } = getKeyAndPurpose(address)
|
|
147
|
-
|
|
148
|
-
// We shouldn't modify dApp PSBTs.
|
|
149
|
-
if (purpose === 49 && !isPsbtBufferPassed) {
|
|
150
|
-
// If spending from a P2SH address, we assume the address is P2SH wrapping
|
|
151
|
-
// P2WPKH. Exodus doesn't use P2SH addresses so we should only ever be
|
|
152
|
-
// signing a P2SH input if we are importing a private key
|
|
153
|
-
// BIP143: As a default policy, only compressed public keys are accepted in P2WPKH and P2WSH
|
|
154
|
-
const p2wpkh = payments.p2wpkh({ pubkey: publicKey })
|
|
155
|
-
const p2sh = payments.p2sh({ redeem: p2wpkh })
|
|
156
|
-
psbt.updateInput(index, {
|
|
157
|
-
redeemScript: p2sh.redeem.output,
|
|
158
|
-
})
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// desktop / BE / mobile with bip-schnorr signing
|
|
162
|
-
const isTaprootAddress = purpose === 86
|
|
163
|
-
const signingKey = isTaprootAddress ? tweakSigner({ signer: key, ECPair, network }) : key
|
|
164
|
-
await psbt.signInputAsync(index, toAsyncSigner({ keyPair: signingKey }), sigHashTypes)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// If a dapp authored the TX, it expects a serialized PSBT response.
|
|
168
|
-
// Note: we wouldn't be able to finalise inputs in some cases that's why we serialize before finalizing inputs.
|
|
169
|
-
if (isPsbtBufferPassed) {
|
|
170
|
-
const rawPSBT = psbt.toBuffer()
|
|
171
|
-
|
|
172
|
-
return { plainTx: { rawPSBT } }
|
|
173
|
-
}
|
|
174
|
-
// Serialize tx
|
|
175
|
-
psbt.finalizeAllInputs()
|
|
176
|
-
const tx = psbt.extractTransaction()
|
|
177
|
-
const rawTx = tx.toBuffer()
|
|
178
|
-
const txId = tx.getId()
|
|
38
|
+
await signWithWallet(psbt, inputsToSign)
|
|
179
39
|
|
|
180
|
-
|
|
181
|
-
return {
|
|
40
|
+
const skipFinalize = !!unsignedTx.txData.psbtBuffer
|
|
41
|
+
return extractTransaction({ psbt, skipFinalize })
|
|
182
42
|
}
|
|
183
43
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import assert from 'minimalistic-assert'
|
|
2
|
+
import { Psbt, Transaction } from '@exodus/bitcoinjs-lib'
|
|
3
|
+
|
|
4
|
+
const _MAXIMUM_FEE_RATES = {
|
|
5
|
+
qtumignition: 25000,
|
|
6
|
+
ravencoin: 1000000,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Factory function that create the prepareForSigning function for a bitcoin-like asset.
|
|
11
|
+
* @param { assetName, resolvePurpose, coinInfo} dependencies
|
|
12
|
+
* @returns A prepareForSigning function that returns a PSBTv1 as buffer
|
|
13
|
+
*/
|
|
14
|
+
export function createPrepareForSigning({ assetName, resolvePurpose, coinInfo }) {
|
|
15
|
+
assert(assetName, 'assetName is required')
|
|
16
|
+
assert(resolvePurpose, 'resolvePurpose is required')
|
|
17
|
+
assert(coinInfo, 'coinInfo is required')
|
|
18
|
+
|
|
19
|
+
return ({ unsignedTx }) => {
|
|
20
|
+
const isPsbtBufferPassed =
|
|
21
|
+
unsignedTx.txData.psbtBuffer &&
|
|
22
|
+
unsignedTx.txMeta.addressPathsMap &&
|
|
23
|
+
unsignedTx.txMeta.inputsToSign
|
|
24
|
+
if (isPsbtBufferPassed) {
|
|
25
|
+
// PSBT created externally (Web3, etc..)
|
|
26
|
+
return createPsbtFromBuffer({ psbtBuffer: unsignedTx.txData.psbtBuffer })
|
|
27
|
+
} else {
|
|
28
|
+
// Create PSBT based on internal Exodus data structure
|
|
29
|
+
const networkInfo = coinInfo.toBitcoinJS()
|
|
30
|
+
const psbt = createPsbtFromTxData({
|
|
31
|
+
...unsignedTx.txData,
|
|
32
|
+
...unsignedTx.txMeta,
|
|
33
|
+
resolvePurpose,
|
|
34
|
+
networkInfo,
|
|
35
|
+
})
|
|
36
|
+
if (!['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)) psbt.setVersion(1)
|
|
37
|
+
|
|
38
|
+
return psbt
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Creates a PSBT instance from the passed transaction buffer provided by 3rd parties (e.g. dApps).
|
|
44
|
+
function createPsbtFromBuffer({ psbtBuffer, ecc }) {
|
|
45
|
+
const psbt = Psbt.fromBuffer(psbtBuffer, { eccLib: ecc })
|
|
46
|
+
|
|
47
|
+
return psbt
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Creates a PSBT instance from the passed inputs, outputs etc. The wallet itself provides this data.
|
|
51
|
+
function createPsbtFromTxData({ inputs, outputs, rawTxs, networkInfo, resolvePurpose, assetName }) {
|
|
52
|
+
// use harcoded max fee rates for specific assets
|
|
53
|
+
// if undefined, will be set to default value by PSBT (2500)
|
|
54
|
+
const maximumFeeRate = _MAXIMUM_FEE_RATES[assetName]
|
|
55
|
+
|
|
56
|
+
const psbt = new Psbt({ maximumFeeRate, network: networkInfo })
|
|
57
|
+
|
|
58
|
+
// Fill tx
|
|
59
|
+
for (const { txId, vout, address, value, script, sequence } of inputs) {
|
|
60
|
+
// TODO: don't use the purpose as intermediate variable
|
|
61
|
+
// see internals of `resolvePurposes`, just use `isP2TR, isP2SH etc directly
|
|
62
|
+
const purpose = resolvePurpose(address)
|
|
63
|
+
|
|
64
|
+
const isSegwitAddress = purpose === 84
|
|
65
|
+
const isTaprootAddress = purpose === 86
|
|
66
|
+
|
|
67
|
+
const txIn = { hash: txId, index: vout, sequence }
|
|
68
|
+
if (isSegwitAddress || isTaprootAddress) {
|
|
69
|
+
// witness outputs only require the value and the script, not the full transaction
|
|
70
|
+
txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
|
|
71
|
+
} else {
|
|
72
|
+
const rawTx = (rawTxs || []).find((t) => t.txId === txId)
|
|
73
|
+
// non-witness outptus require the full transaction
|
|
74
|
+
assert(!!rawTx, 'Non-witness outputs require the full previous transaction.')
|
|
75
|
+
const rawTxBuffer = Buffer.from(rawTx.rawData, 'hex')
|
|
76
|
+
if (canParseTx(rawTxBuffer)) {
|
|
77
|
+
txIn.nonWitnessUtxo = rawTxBuffer
|
|
78
|
+
} else {
|
|
79
|
+
// temp fix for https://exodusio.slack.com/archives/CP202D90Q/p1671014704829939 until bitcoinjs could parse a mweb tx without failing
|
|
80
|
+
console.warn(`Setting psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true for asset ${assetName}`)
|
|
81
|
+
psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true
|
|
82
|
+
txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
psbt.addInput(txIn)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const [address, amount] of outputs) {
|
|
90
|
+
psbt.addOutput({ value: amount, address })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return psbt
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const canParseTx = (rawTxBuffer) => {
|
|
97
|
+
try {
|
|
98
|
+
Transaction.fromBuffer(rawTxBuffer)
|
|
99
|
+
return true
|
|
100
|
+
} catch (e) {
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import assert from 'minimalistic-assert'
|
|
2
|
+
|
|
3
|
+
import { createPrepareForSigning } from './default-prepare-for-signing'
|
|
4
|
+
import { extractTransaction } from './common'
|
|
5
|
+
|
|
6
|
+
export const signHardwareFactory = ({ assetName, resolvePurpose, keys, coinInfo }) => {
|
|
7
|
+
assert(assetName, 'assetName is required')
|
|
8
|
+
assert(resolvePurpose, 'resolvePurpose is required')
|
|
9
|
+
assert(keys, 'keys is required')
|
|
10
|
+
assert(coinInfo, 'coinInfo is required')
|
|
11
|
+
|
|
12
|
+
const prepareForSigning = createPrepareForSigning({
|
|
13
|
+
assetName,
|
|
14
|
+
resolvePurpose,
|
|
15
|
+
coinInfo,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const signWithHardwareWallet = createSignWithHardwareWallet({
|
|
19
|
+
assetName,
|
|
20
|
+
resolvePurpose,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
return async ({ unsignedTx, hardwareDevice, accountIndex }) => {
|
|
24
|
+
assert(unsignedTx, 'unsignedTx is required')
|
|
25
|
+
assert(hardwareDevice, 'hardwareDevice is required')
|
|
26
|
+
assert(Number.isInteger(accountIndex), 'accountIndex must be integer')
|
|
27
|
+
|
|
28
|
+
const { addressPathsMap } = unsignedTx.txMeta
|
|
29
|
+
|
|
30
|
+
const psbt = prepareForSigning({ unsignedTx })
|
|
31
|
+
|
|
32
|
+
const inputsToSign = unsignedTx.txMeta.inputsToSign || unsignedTx.txData.inputs
|
|
33
|
+
|
|
34
|
+
await signWithHardwareWallet({
|
|
35
|
+
psbt,
|
|
36
|
+
inputsToSign,
|
|
37
|
+
addressPathsMap,
|
|
38
|
+
hardwareDevice,
|
|
39
|
+
accountIndex,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const skipFinalize = !!unsignedTx.txData.psbtBuffer
|
|
43
|
+
return extractTransaction({ psbt, skipFinalize })
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createSignWithHardwareWallet({ assetName, resolvePurpose }) {
|
|
48
|
+
return async ({ psbt, inputsToSign, addressPathsMap, accountIndex, hardwareDevice }) => {
|
|
49
|
+
const derivationPaths = getDerivationPaths({ resolvePurpose, addressPathsMap, accountIndex })
|
|
50
|
+
const signatures = await hardwareDevice.signTransaction({
|
|
51
|
+
assetName,
|
|
52
|
+
signableTransaction: psbt.toBuffer(),
|
|
53
|
+
derivationPaths,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
applySignatures(psbt, signatures, inputsToSign)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getDerivationPaths({ resolvePurpose, accountIndex, addressPathsMap }) {
|
|
61
|
+
let derivationPaths = []
|
|
62
|
+
for (const [address, path] of Object.entries(addressPathsMap)) {
|
|
63
|
+
const purpose = resolvePurpose(address)
|
|
64
|
+
const derivationPath = `m/${purpose}'/0'/${accountIndex}'/${path.slice(2)}` // TODO: coinindex
|
|
65
|
+
derivationPaths.push(derivationPath)
|
|
66
|
+
}
|
|
67
|
+
return derivationPaths
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function applySignatures(psbt, signatures, inputsToSign) {
|
|
71
|
+
for (let inputIndex = 0; inputIndex < psbt.inputCount; inputIndex++) {
|
|
72
|
+
const shouldSign = !!inputsToSign[inputIndex]
|
|
73
|
+
if (shouldSign) {
|
|
74
|
+
const signature = signatures.find((signature) => signature.inputIndex === inputIndex)
|
|
75
|
+
if (signature) {
|
|
76
|
+
const isTaprootSig = signature.publicKey.length === 32
|
|
77
|
+
if (isTaprootSig) {
|
|
78
|
+
psbt.data.updateInput(inputIndex, {
|
|
79
|
+
tapKeySig: signature.signature,
|
|
80
|
+
})
|
|
81
|
+
} else {
|
|
82
|
+
psbt.data.updateInput(inputIndex, {
|
|
83
|
+
partialSig: [
|
|
84
|
+
{
|
|
85
|
+
pubkey: signature.publicKey,
|
|
86
|
+
signature: signature.signature,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`expected to sign for inputIndex ${inputIndex} but no signature was produced`
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
package/src/tx-sign/index.js
CHANGED
package/src/tx-sign/taproot.js
CHANGED
|
@@ -3,12 +3,14 @@ import assert from 'minimalistic-assert'
|
|
|
3
3
|
import { getSchnorrEntropy } from './default-entropy'
|
|
4
4
|
import { toXOnly } from '../bitcoinjs-lib/ecc-utils'
|
|
5
5
|
import { eccFactory } from '../bitcoinjs-lib/ecc'
|
|
6
|
+
import { getECPair } from '../bitcoinjs-lib'
|
|
6
7
|
|
|
7
8
|
const ecc = eccFactory()
|
|
9
|
+
const ECPair = getECPair()
|
|
8
10
|
|
|
9
|
-
export function tweakSigner({ signer,
|
|
11
|
+
export function tweakSigner({ signer, tweakHash, network }) {
|
|
10
12
|
assert(signer, 'signer is required')
|
|
11
|
-
|
|
13
|
+
|
|
12
14
|
let privateKey = signer.privateKey
|
|
13
15
|
if (!privateKey) {
|
|
14
16
|
throw new Error('Private key is required for tweaking signer!')
|
package/src/utxos-utils.js
CHANGED
|
@@ -5,6 +5,46 @@ import assert from 'minimalistic-assert'
|
|
|
5
5
|
|
|
6
6
|
const MAX_ORDINAL_VALUE_POSTAGE = 10000
|
|
7
7
|
|
|
8
|
+
export function getTransferOrdinalsUtxos({ inscriptionIds, ordinalsUtxos }) {
|
|
9
|
+
const transferOrdinalsUtxos = ordinalsUtxos.filter((utxo) =>
|
|
10
|
+
utxo.inscriptions?.some((i) => inscriptionIds.includes(i.inscriptionId))
|
|
11
|
+
)
|
|
12
|
+
const unsafeInscriptions = transferOrdinalsUtxos.toArray().flatMap(
|
|
13
|
+
(utxo) =>
|
|
14
|
+
utxo.inscriptions?.filter((i) => {
|
|
15
|
+
const validInscription = isValidInscription({
|
|
16
|
+
value: utxo.value.toBaseNumber(),
|
|
17
|
+
offset: i.offset,
|
|
18
|
+
})
|
|
19
|
+
return validInscription && !inscriptionIds.includes(i.inscriptionId)
|
|
20
|
+
}) || []
|
|
21
|
+
)
|
|
22
|
+
assert(
|
|
23
|
+
!unsafeInscriptions.length,
|
|
24
|
+
`The following inscriptions are unsafe ${unsafeInscriptions.map(
|
|
25
|
+
(i) => i.inscriptionId
|
|
26
|
+
)} when ${inscriptionIds} should be spent`
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const transferInscriptionIds = transferOrdinalsUtxos
|
|
30
|
+
.toArray()
|
|
31
|
+
.flatMap((utxo) => utxo.inscriptions)
|
|
32
|
+
.filter(({ inscriptionId }) => inscriptionIds.includes(inscriptionId))
|
|
33
|
+
|
|
34
|
+
assert(
|
|
35
|
+
transferInscriptionIds.length === inscriptionIds.length,
|
|
36
|
+
`Expected inscriptions ${inscriptionIds.length}. Found: ${transferInscriptionIds.length}`
|
|
37
|
+
)
|
|
38
|
+
return transferOrdinalsUtxos
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isValidInscription({ value, offset }) {
|
|
42
|
+
assert(typeof value === 'number', 'value must be a number')
|
|
43
|
+
assert(typeof offset === 'number', 'offset must be a number')
|
|
44
|
+
// value >= 0 in case offset, alternatively convert to string/ln
|
|
45
|
+
return (value >= 0 && value <= MAX_ORDINAL_VALUE_POSTAGE) || offset === 0
|
|
46
|
+
}
|
|
47
|
+
|
|
8
48
|
export function getUtxos({ accountState, asset }) {
|
|
9
49
|
return (
|
|
10
50
|
accountState?.utxos ||
|
|
@@ -23,31 +63,41 @@ export function getOrdinalsUtxos({ accountState, asset }) {
|
|
|
23
63
|
)
|
|
24
64
|
}
|
|
25
65
|
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
66
|
+
export function getValidInscriptions({ utxo }) {
|
|
67
|
+
return (utxo.inscriptions || []).filter((i) =>
|
|
68
|
+
isValidInscription({ value: utxo.value.toBaseNumber(), offset: i.offset })
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isOrdinalUtxo({ utxo, ordinalsEnabled, knownBalanceUtxoIds }) {
|
|
73
|
+
if (!ordinalsEnabled) {
|
|
32
74
|
return false
|
|
33
75
|
}
|
|
34
76
|
|
|
35
|
-
|
|
36
|
-
|
|
77
|
+
const utxoId = `${utxo.txId}:${utxo.vout}`.toLowerCase()
|
|
78
|
+
|
|
79
|
+
if (knownBalanceUtxoIds?.includes(utxoId) && !utxo.inscriptionsIndexed) {
|
|
80
|
+
return false // this allows users see and spend change balance after sending before hiro confirmation
|
|
37
81
|
}
|
|
38
82
|
|
|
39
|
-
if (utxo.
|
|
40
|
-
return
|
|
83
|
+
if (!utxo.inscriptionsIndexed) {
|
|
84
|
+
return true
|
|
41
85
|
}
|
|
42
86
|
|
|
43
|
-
|
|
87
|
+
const validInscriptions = getValidInscriptions({ utxo })
|
|
88
|
+
return validInscriptions.length > 0
|
|
44
89
|
}
|
|
45
90
|
|
|
46
|
-
export function partitionUtxos({ allUtxos,
|
|
91
|
+
export function partitionUtxos({ allUtxos, ordinalsEnabled, knownBalanceUtxoIds }) {
|
|
47
92
|
assert(allUtxos, 'allUtxos is required')
|
|
93
|
+
// assert(ordinalAddress, 'ordinalAddress is required') // not used atm we may need to tune by ordinalAddress when unconfirmed or rubbish inscriptions
|
|
48
94
|
return {
|
|
49
|
-
utxos: allUtxos.filter(
|
|
50
|
-
|
|
95
|
+
utxos: allUtxos.filter(
|
|
96
|
+
(utxo) => !isOrdinalUtxo({ utxo, ordinalsEnabled, knownBalanceUtxoIds })
|
|
97
|
+
),
|
|
98
|
+
ordinalsUtxos: allUtxos.filter((utxo) =>
|
|
99
|
+
isOrdinalUtxo({ utxo, ordinalsEnabled, knownBalanceUtxoIds })
|
|
100
|
+
),
|
|
51
101
|
}
|
|
52
102
|
}
|
|
53
103
|
|
|
@@ -68,12 +118,20 @@ export function getConfirmedOrRfbDisabledUtxos({ utxos, allowUnconfirmedRbfEnabl
|
|
|
68
118
|
return utxos.filter((utxo) => utxo.confirmations > 0 || !utxo.rbfEnabled)
|
|
69
119
|
}
|
|
70
120
|
|
|
121
|
+
function filterDustUtxos({ utxos, feeData }) {
|
|
122
|
+
if (feeData.utxoDustValue) {
|
|
123
|
+
return utxos.filter((utxo) => utxo.value.toBaseNumber() > feeData.utxoDustValue)
|
|
124
|
+
}
|
|
125
|
+
return utxos
|
|
126
|
+
}
|
|
127
|
+
|
|
71
128
|
export function getUsableUtxos({ asset, utxos, feeData, txSet }) {
|
|
72
129
|
assert(asset, 'asset is required')
|
|
73
130
|
assert(utxos, 'utxos is required')
|
|
74
131
|
assert(feeData, 'feeData is required')
|
|
75
132
|
assert(txSet, 'txSet is required')
|
|
76
|
-
if (!['bitcoin', 'bitcointestnet', 'bitcoinregtest'].includes(asset.name))
|
|
133
|
+
if (!['bitcoin', 'bitcointestnet', 'bitcoinregtest'].includes(asset.name))
|
|
134
|
+
return filterDustUtxos({ utxos, feeData })
|
|
77
135
|
const { fastestFee } = feeData
|
|
78
136
|
const feeRate = fastestFee.toBaseNumber()
|
|
79
137
|
const maxFee = feeData.maxExtraCpfpFee
|
|
@@ -85,7 +143,9 @@ export function getUsableUtxos({ asset, utxos, feeData, txSet }) {
|
|
|
85
143
|
feeRate,
|
|
86
144
|
maxFee,
|
|
87
145
|
})
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
146
|
+
const confirmedAndSmallUtxos =
|
|
147
|
+
largeUnconfirmedTxs.size === 0
|
|
148
|
+
? utxos
|
|
149
|
+
: utxos.filter((utxo) => !largeUnconfirmedTxs.has(utxo.txId))
|
|
150
|
+
return filterDustUtxos({ utxos: confirmedAndSmallUtxos, feeData })
|
|
91
151
|
}
|