@exodus/bitcoin-api 2.5.0-alpha.0 → 2.5.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 +4 -2
- package/src/address-utils.js +0 -5
- package/src/bitcoinjs-lib/ecc/common.js +1 -1
- package/src/bitcoinjs-lib/ecc-utils.js +3 -0
- package/src/bitcoinjs-lib/script-classify/index.js +2 -4
- package/src/btc-like-address.js +2 -4
- package/src/btc-like-keys.js +4 -7
- package/src/fee/fee-estimator.js +4 -5
- package/src/fee/script-classifier.js +3 -3
- package/src/index.js +3 -0
- package/src/ordinals-utils.js +22 -0
- package/src/tx-log/bitcoin-monitor-scanner.js +15 -2
- package/src/tx-send/index.js +32 -18
- package/src/tx-sign/default-create-tx.js +86 -58
- package/src/tx-sign/taproot.js +6 -8
- package/src/utxos-utils.js +11 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.1",
|
|
4
4
|
"description": "Exodus bitcoin-api",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -39,7 +39,9 @@
|
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@exodus/bitcoin-meta": "^1.0.1",
|
|
42
|
+
"@scure/base": "^1.1.3",
|
|
43
|
+
"@scure/btc-signer": "^1.1.0",
|
|
42
44
|
"jest-when": "^3.5.1"
|
|
43
45
|
},
|
|
44
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "562c288cde1c1d7c7ab2c7bc6b1804ccca9b37f2"
|
|
45
47
|
}
|
package/src/address-utils.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import assert from 'minimalistic-assert'
|
|
2
2
|
|
|
3
|
-
export function isOrdinalAddress(address, ordinalChainIndex) {
|
|
4
|
-
assert(typeof ordinalChainIndex === 'number', `ordinalChainIndex must be a number`)
|
|
5
|
-
return parsePath(address)[0] === ordinalChainIndex
|
|
6
|
-
}
|
|
7
|
-
|
|
8
3
|
export function isReceiveAddress(address): boolean {
|
|
9
4
|
return parsePath(address)[0] === 0
|
|
10
5
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { payments } from 'bitcoinjs-lib'
|
|
2
|
-
import assert from 'minimalistic-assert'
|
|
3
2
|
|
|
4
3
|
function isPaymentFactory(payment: any): (script: Buffer, eccLib?: any) => boolean {
|
|
5
4
|
return (script: Buffer, eccLib?: any): boolean => {
|
|
@@ -31,10 +30,9 @@ const types = {
|
|
|
31
30
|
NONSTANDARD: 'nonstandard',
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
const outputFactory = (
|
|
35
|
-
assert(ecc, 'ecc is required')
|
|
33
|
+
const outputFactory = () => (script: Buffer) => {
|
|
36
34
|
if (isP2WPKH(script)) return types.P2WPKH
|
|
37
|
-
if (isP2TR(script
|
|
35
|
+
if (isP2TR(script)) return types.P2TR
|
|
38
36
|
if (isP2PKH(script)) return types.P2PKH
|
|
39
37
|
if (isP2MS(script)) return types.P2MS
|
|
40
38
|
if (isP2PK(script)) return types.P2PK
|
package/src/btc-like-address.js
CHANGED
|
@@ -8,14 +8,12 @@ export const createBtcLikeAddress = ({
|
|
|
8
8
|
versions,
|
|
9
9
|
coinInfo,
|
|
10
10
|
bitcoinjsLib: bitcoinjsLibFork,
|
|
11
|
-
ecc,
|
|
12
11
|
useBip86 = false,
|
|
13
12
|
validateFunctions = {},
|
|
14
13
|
extraFunctions = {},
|
|
15
14
|
}) => {
|
|
16
15
|
assert(versions, 'versions is required')
|
|
17
16
|
assert(coinInfo, 'coinInfo is required')
|
|
18
|
-
assert(ecc, 'ecc is required')
|
|
19
17
|
|
|
20
18
|
const bs58validateFactory = (version) =>
|
|
21
19
|
version === undefined
|
|
@@ -55,7 +53,7 @@ export const createBtcLikeAddress = ({
|
|
|
55
53
|
((addr) => {
|
|
56
54
|
try {
|
|
57
55
|
const network = coinInfo.toBitcoinJS()
|
|
58
|
-
bitcoinjsLibFork.payments.p2tr({ address: addr, network }
|
|
56
|
+
bitcoinjsLibFork.payments.p2tr({ address: addr, network })
|
|
59
57
|
return true
|
|
60
58
|
} catch (e) {
|
|
61
59
|
return false
|
|
@@ -91,7 +89,7 @@ export const createBtcLikeAddress = ({
|
|
|
91
89
|
|
|
92
90
|
const toScriptPubKey = (string) => {
|
|
93
91
|
const network = coinInfo.toBitcoinJS()
|
|
94
|
-
return bitcoinjsOriginal.address.toOutputScript(string, network
|
|
92
|
+
return bitcoinjsOriginal.address.toOutputScript(string, network)
|
|
95
93
|
}
|
|
96
94
|
|
|
97
95
|
const fromScriptPubKey = (scriptPubKey) => {
|
package/src/btc-like-keys.js
CHANGED
|
@@ -6,7 +6,8 @@ import { identity, pickBy } from 'lodash'
|
|
|
6
6
|
import * as defaultBitcoinjsLib from 'bitcoinjs-lib'
|
|
7
7
|
import secp256k1 from 'secp256k1'
|
|
8
8
|
import { hash160 } from './hash-utils'
|
|
9
|
-
import { toXOnly } from './
|
|
9
|
+
import { toXOnly } from './bitcoinjs-lib/ecc-utils'
|
|
10
|
+
import { eccFactory } from './bitcoinjs-lib/ecc'
|
|
10
11
|
|
|
11
12
|
export const publicKeyToHashFactory = (p2pkh) => (publicKey) => {
|
|
12
13
|
const payload = Buffer.concat([Buffer.from([p2pkh]), hash160(publicKey)])
|
|
@@ -16,14 +17,13 @@ export const publicKeyToHashFactory = (p2pkh) => (publicKey) => {
|
|
|
16
17
|
export const createBtcLikeKeys = ({
|
|
17
18
|
coinInfo,
|
|
18
19
|
versions,
|
|
19
|
-
ecc,
|
|
20
20
|
useBip86 = false,
|
|
21
21
|
bitcoinjsLib = defaultBitcoinjsLib,
|
|
22
22
|
extraFunctions = {},
|
|
23
23
|
}) => {
|
|
24
24
|
assert(coinInfo, 'coinInfo is required')
|
|
25
25
|
assert(versions, 'versions is required')
|
|
26
|
-
|
|
26
|
+
const ecc = eccFactory()
|
|
27
27
|
const {
|
|
28
28
|
encodePrivate: encodePrivateCustom,
|
|
29
29
|
encodePublic: encodePublicCustom,
|
|
@@ -91,10 +91,7 @@ export const createBtcLikeKeys = ({
|
|
|
91
91
|
(useBip86
|
|
92
92
|
? (publicKey: Buffer): string => {
|
|
93
93
|
const network = coinInfo.toBitcoinJS()
|
|
94
|
-
return bitcoinjsLib.payments.p2tr(
|
|
95
|
-
{ internalPubkey: toXOnly(publicKey), network },
|
|
96
|
-
{ eccLib: ecc }
|
|
97
|
-
).address
|
|
94
|
+
return bitcoinjsLib.payments.p2tr({ internalPubkey: toXOnly(publicKey), network }).address
|
|
98
95
|
}
|
|
99
96
|
: undefined)
|
|
100
97
|
|
package/src/fee/fee-estimator.js
CHANGED
|
@@ -53,12 +53,11 @@ const scriptPubKeyLengths = {
|
|
|
53
53
|
// 10 = version: 4, locktime: 4, inputs and outputs count: 1
|
|
54
54
|
// 148 = txId: 32, vout: 4, count: 1, script: 107 (max), sequence: 4
|
|
55
55
|
// 34 = value: 8, count: 1, scriptPubKey: 25 (P2PKH) and 23 (P2SH)
|
|
56
|
-
export const getSizeFactory = ({
|
|
57
|
-
assert(ecc, 'ecc is required')
|
|
56
|
+
export const getSizeFactory = ({ defaultOutputType, addressApi }) => {
|
|
58
57
|
assert(defaultOutputType, 'defaultOutputType is required')
|
|
59
58
|
assert(addressApi, 'addressApi is required')
|
|
60
59
|
|
|
61
|
-
const scriptClassifier = scriptClassifierFactory({
|
|
60
|
+
const scriptClassifier = scriptClassifierFactory({ addressApi })
|
|
62
61
|
|
|
63
62
|
return (
|
|
64
63
|
asset: Object,
|
|
@@ -166,8 +165,8 @@ export const getSizeFactory = ({ ecc, defaultOutputType, addressApi }) => {
|
|
|
166
165
|
}
|
|
167
166
|
}
|
|
168
167
|
|
|
169
|
-
const getFeeEstimatorFactory = ({
|
|
170
|
-
const getSize = getSizeFactory({
|
|
168
|
+
const getFeeEstimatorFactory = ({ defaultOutputType, addressApi }) => {
|
|
169
|
+
const getSize = getSizeFactory({ defaultOutputType, addressApi })
|
|
171
170
|
return createDefaultFeeEstimator(getSize)
|
|
172
171
|
}
|
|
173
172
|
export default getFeeEstimatorFactory
|
|
@@ -16,11 +16,10 @@ const hashStringIfTooBig = (str) =>
|
|
|
16
16
|
.slice(0, maxSize)
|
|
17
17
|
: str
|
|
18
18
|
|
|
19
|
-
export const scriptClassifierFactory = ({ addressApi
|
|
20
|
-
assert(ecc, 'ecc is required')
|
|
19
|
+
export const scriptClassifierFactory = ({ addressApi }) => {
|
|
21
20
|
assert(addressApi, 'addressApi is required')
|
|
22
21
|
|
|
23
|
-
const classifyOutput = scriptClassify.outputFactory(
|
|
22
|
+
const classifyOutput = scriptClassify.outputFactory()
|
|
24
23
|
|
|
25
24
|
const classifyScriptHex = memoizeLruCache(
|
|
26
25
|
({ assetName, script }) => {
|
|
@@ -42,6 +41,7 @@ export const scriptClassifierFactory = ({ addressApi, ecc }) => {
|
|
|
42
41
|
else if (addressApi.isP2TR && addressApi.isP2TR(address)) return P2TR
|
|
43
42
|
else if (addressApi.isP2WSH && addressApi.isP2WSH(address)) return P2WSH
|
|
44
43
|
return classifyScriptHex({
|
|
44
|
+
assetName,
|
|
45
45
|
classifyOutput,
|
|
46
46
|
script: addressApi.toScriptPubKey(address).toString('hex'),
|
|
47
47
|
})
|
package/src/index.js
CHANGED
|
@@ -16,3 +16,6 @@ export * from './unconfirmed-ancestor-data'
|
|
|
16
16
|
export * from './parse-unsigned-tx'
|
|
17
17
|
export * from './insight-api-client/util'
|
|
18
18
|
export * from './move-funds'
|
|
19
|
+
export { toAsyncSigner } from './tx-sign/taproot'
|
|
20
|
+
export { toXOnly } from './bitcoinjs-lib/ecc-utils'
|
|
21
|
+
export * from './ordinals-utils'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import assert from 'minimalistic-assert'
|
|
2
|
+
|
|
3
|
+
export function getOrdinalAddress({
|
|
4
|
+
asset,
|
|
5
|
+
assetClientInterface,
|
|
6
|
+
walletAccount,
|
|
7
|
+
ordinalChainIndex,
|
|
8
|
+
}) {
|
|
9
|
+
assert(asset, 'asset is required')
|
|
10
|
+
assert(assetClientInterface, 'assetClientInterface is required')
|
|
11
|
+
assert(walletAccount, 'walletAccount is required')
|
|
12
|
+
if (ordinalChainIndex === undefined) {
|
|
13
|
+
return undefined
|
|
14
|
+
}
|
|
15
|
+
return assetClientInterface.getAddress({
|
|
16
|
+
assetName: asset.name,
|
|
17
|
+
walletAccount,
|
|
18
|
+
purpose: 86,
|
|
19
|
+
chainIndex: ordinalChainIndex,
|
|
20
|
+
addressIndex: 0,
|
|
21
|
+
})
|
|
22
|
+
}
|
|
@@ -6,6 +6,7 @@ import ms from 'ms'
|
|
|
6
6
|
import assert from 'minimalistic-assert'
|
|
7
7
|
import { isChangeAddress, isReceiveAddress } from '../address-utils'
|
|
8
8
|
import { getOrdinalsUtxos, getUtxos, partitionUtxos } from '../utxos-utils'
|
|
9
|
+
import { getOrdinalAddress } from '../ordinals-utils'
|
|
9
10
|
|
|
10
11
|
// Time to check whether to drop a sent tx
|
|
11
12
|
const SENT_TIME_TO_DROP = ms('2m')
|
|
@@ -519,7 +520,10 @@ export class BitcoinMonitorScanner {
|
|
|
519
520
|
})
|
|
520
521
|
|
|
521
522
|
const utxosData = utxoCol
|
|
522
|
-
? partitionUtxos({
|
|
523
|
+
? partitionUtxos({
|
|
524
|
+
allUtxos: utxoCol,
|
|
525
|
+
ordinalAddress: await this.getOrdinalAddress({ walletAccount }),
|
|
526
|
+
})
|
|
523
527
|
: {}
|
|
524
528
|
|
|
525
529
|
return {
|
|
@@ -578,7 +582,7 @@ export class BitcoinMonitorScanner {
|
|
|
578
582
|
|
|
579
583
|
const { utxos, ordinalsUtxos } = partitionUtxos({
|
|
580
584
|
allUtxos: txConfirmedUtxos,
|
|
581
|
-
|
|
585
|
+
ordinalAddress: await this.getOrdinalAddress({ walletAccount }),
|
|
582
586
|
})
|
|
583
587
|
|
|
584
588
|
return {
|
|
@@ -587,4 +591,13 @@ export class BitcoinMonitorScanner {
|
|
|
587
591
|
txsToUpdate: updatedPropertiesTxs,
|
|
588
592
|
}
|
|
589
593
|
}
|
|
594
|
+
|
|
595
|
+
getOrdinalAddress({ walletAccount }) {
|
|
596
|
+
return getOrdinalAddress({
|
|
597
|
+
asset: this.#asset,
|
|
598
|
+
assetClientInterface: this.#assetClientInterface,
|
|
599
|
+
walletAccount,
|
|
600
|
+
ordinalChainIndex: this.#ordinalChainIndex,
|
|
601
|
+
})
|
|
602
|
+
}
|
|
590
603
|
}
|
package/src/tx-send/index.js
CHANGED
|
@@ -201,15 +201,15 @@ export const createAndBroadcastTXFactory = ({
|
|
|
201
201
|
`Expected ordinal utxos ${inscriptionIds.length}. Found: ${transferOrdinalsUtxos?.size || 0}`
|
|
202
202
|
)
|
|
203
203
|
|
|
204
|
-
const unconfirmedOrdinalUtxos = transferOrdinalsUtxos
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
assert(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
)
|
|
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
213
|
}
|
|
214
214
|
|
|
215
215
|
const insightClient = asset.baseAsset.insightClient
|
|
@@ -305,6 +305,8 @@ export const createAndBroadcastTXFactory = ({
|
|
|
305
305
|
} else {
|
|
306
306
|
outputs = []
|
|
307
307
|
}
|
|
308
|
+
|
|
309
|
+
let sendOutput
|
|
308
310
|
if (address) {
|
|
309
311
|
if (transferOrdinalsUtxos) {
|
|
310
312
|
outputs.push(
|
|
@@ -313,7 +315,8 @@ export const createAndBroadcastTXFactory = ({
|
|
|
313
315
|
.map((ordinalUtxo) => createOutput(assetName, address, ordinalUtxo.value))
|
|
314
316
|
)
|
|
315
317
|
} else {
|
|
316
|
-
|
|
318
|
+
sendOutput = createOutput(assetName, address, sendAmount)
|
|
319
|
+
outputs.push(sendOutput)
|
|
317
320
|
}
|
|
318
321
|
}
|
|
319
322
|
|
|
@@ -408,17 +411,23 @@ export const createAndBroadcastTXFactory = ({
|
|
|
408
411
|
}
|
|
409
412
|
}
|
|
410
413
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
414
|
+
function findUtxoIndex(output) {
|
|
415
|
+
let utxoIndex = -1
|
|
416
|
+
if (output) {
|
|
417
|
+
for (let i = 0; i < outputs.length; i++) {
|
|
418
|
+
const [address, amount] = outputs[i]
|
|
419
|
+
if (output[0] === address && output[1] === amount) {
|
|
420
|
+
utxoIndex = i
|
|
421
|
+
break
|
|
422
|
+
}
|
|
418
423
|
}
|
|
419
424
|
}
|
|
425
|
+
return utxoIndex
|
|
420
426
|
}
|
|
421
427
|
|
|
428
|
+
const changeUtxoIndex = findUtxoIndex(changeOutput)
|
|
429
|
+
const sendUtxoIndex = findUtxoIndex(sendOutput)
|
|
430
|
+
|
|
422
431
|
const { script, size } = getSizeAndChangeScript({ assetName, tx, rawTx, changeUtxoIndex, txId })
|
|
423
432
|
|
|
424
433
|
let remainingUtxos = usableUtxos.difference(selectedUtxos)
|
|
@@ -518,7 +527,12 @@ export const createAndBroadcastTXFactory = ({
|
|
|
518
527
|
})
|
|
519
528
|
}
|
|
520
529
|
|
|
521
|
-
return {
|
|
530
|
+
return {
|
|
531
|
+
txId,
|
|
532
|
+
sendUtxoIndex,
|
|
533
|
+
sendAmount: sendAmount.toBaseNumber(),
|
|
534
|
+
replacedTxId: replaceTx?.txId,
|
|
535
|
+
}
|
|
522
536
|
}
|
|
523
537
|
|
|
524
538
|
export function createInputs(assetName, ...rest) {
|
|
@@ -6,7 +6,9 @@ import { getOwnProperty } from '@exodus/basic-utils'
|
|
|
6
6
|
|
|
7
7
|
import secp256k1 from 'secp256k1'
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { toXOnly } from '../bitcoinjs-lib/ecc-utils'
|
|
10
|
+
import { toAsyncSigner, tweakSigner } from './taproot'
|
|
11
|
+
import { eccFactory } from '../bitcoinjs-lib/ecc'
|
|
10
12
|
|
|
11
13
|
let ECPair
|
|
12
14
|
|
|
@@ -34,28 +36,71 @@ export const serializeTx = ({ tx }) => {
|
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
// Creates a PSBT instance from the passed inputs, outputs etc. The wallet itself provides this data.
|
|
40
|
+
function createPSBT({ inputs, outputs, rawTxs, networkInfo, getKeyAndPurpose, assetName }) {
|
|
41
|
+
// use harcoded max fee rates for specific assets
|
|
42
|
+
// if undefined, will be set to default value by PSBT (2500)
|
|
43
|
+
const maximumFeeRate = _MAXIMUM_FEE_RATES[assetName]
|
|
44
|
+
|
|
45
|
+
const psbt = new Psbt({ maximumFeeRate, network: networkInfo })
|
|
46
|
+
|
|
47
|
+
// Fill tx
|
|
48
|
+
for (const { txId, vout, address, value, script, sequence } of inputs) {
|
|
49
|
+
const { purpose, publicKey } = getKeyAndPurpose(address)
|
|
50
|
+
|
|
51
|
+
const isSegwitAddress = purpose === 84
|
|
52
|
+
const isTaprootAddress = purpose === 86
|
|
53
|
+
const txIn = { hash: txId, index: vout, sequence }
|
|
54
|
+
if (isSegwitAddress || isTaprootAddress) {
|
|
55
|
+
// witness outputs only require the value and the script, not the full transaction
|
|
56
|
+
txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
|
|
57
|
+
if (isTaprootAddress) {
|
|
58
|
+
txIn.tapInternalKey = toXOnly(publicKey)
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
const rawTx = (rawTxs || []).find((t) => t.txId === txId)
|
|
62
|
+
// non-witness outptus require the full transaction
|
|
63
|
+
assert(!!rawTx, 'Non-witness outputs require the full previous transaction.')
|
|
64
|
+
const rawTxBuffer = Buffer.from(rawTx.rawData, 'hex')
|
|
65
|
+
if (canParseTx(rawTxBuffer)) {
|
|
66
|
+
txIn.nonWitnessUtxo = rawTxBuffer
|
|
67
|
+
} else {
|
|
68
|
+
// temp fix for https://exodusio.slack.com/archives/CP202D90Q/p1671014704829939 until bitcoinjs could parse a mweb tx without failing
|
|
69
|
+
console.warn(`Setting psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true for asset ${assetName}`)
|
|
70
|
+
psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true
|
|
71
|
+
txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
psbt.addInput(txIn)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const [address, amount] of outputs) {
|
|
78
|
+
psbt.addOutput({ value: amount, address })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return psbt
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Creates a PSBT instance from the passed transaction buffer provided by 3rd parties (e.g. dApps).
|
|
85
|
+
function createPSBTFromBuffer({ psbtBuffer, ecc }) {
|
|
86
|
+
const psbt = Psbt.fromBuffer(psbtBuffer, { eccLib: ecc })
|
|
87
|
+
|
|
88
|
+
return psbt
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, network }) => {
|
|
38
92
|
assert(assetName, 'assetName is required')
|
|
39
93
|
assert(resolvePurpose, 'resolvePurpose is required')
|
|
40
94
|
assert(keys, 'keys is required')
|
|
41
95
|
assert(coinInfo, 'coinInfo is required')
|
|
42
|
-
|
|
96
|
+
|
|
43
97
|
return async ({ unsignedTx, hdkeys, privateKeysAddressMap }): Object => {
|
|
44
98
|
assert(unsignedTx, 'unsignedTx is required')
|
|
45
99
|
assert(hdkeys || privateKeysAddressMap, 'hdkeys or privateKeysAddressMap is required')
|
|
46
|
-
const { addressPathsMap
|
|
47
|
-
const { inputs, outputs } = unsignedTx.txData
|
|
100
|
+
const { addressPathsMap } = unsignedTx.txMeta
|
|
48
101
|
const networkInfo = { ...coinInfo.toBitcoinJS(), messagePrefix: '' }
|
|
49
102
|
|
|
50
|
-
|
|
51
|
-
// if undefined, will be set to default value by PSBT (2500)
|
|
52
|
-
const maximumFeeRate = _MAXIMUM_FEE_RATES[assetName]
|
|
53
|
-
|
|
54
|
-
const psbt = new Psbt({ maximumFeeRate, eccLib: ecc, network: networkInfo })
|
|
55
|
-
|
|
56
|
-
if (!['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)) psbt.setVersion(1)
|
|
57
|
-
|
|
58
|
-
ECPair = ECPair || ECPairFactory(ecc)
|
|
103
|
+
ECPair = ECPair || ECPairFactory(eccFactory())
|
|
59
104
|
|
|
60
105
|
const getKeyAndPurpose = lodash.memoize((address) => {
|
|
61
106
|
const purpose = resolvePurpose(address)
|
|
@@ -78,44 +123,27 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
|
|
|
78
123
|
return { key, publicKey, purpose }
|
|
79
124
|
})
|
|
80
125
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
126
|
+
const isPsbtBufferPassed =
|
|
127
|
+
unsignedTx.txData.psbtBuffer &&
|
|
128
|
+
unsignedTx.txMeta.addressPathsMap &&
|
|
129
|
+
unsignedTx.txMeta.inputsToSign
|
|
130
|
+
const psbt = isPsbtBufferPassed
|
|
131
|
+
? createPSBTFromBuffer({ psbtBuffer: unsignedTx.txData.psbtBuffer })
|
|
132
|
+
: createPSBT({ ...unsignedTx.txData, ...unsignedTx.txMeta, getKeyAndPurpose, networkInfo })
|
|
133
|
+
const { inputs } = unsignedTx.txData
|
|
84
134
|
|
|
85
|
-
|
|
86
|
-
const isTaprootAddress = purpose === 86
|
|
87
|
-
const txIn = { hash: txId, index: vout, sequence }
|
|
88
|
-
if (isSegwitAddress || isTaprootAddress) {
|
|
89
|
-
// witness outputs only require the value and the script, not the full transaction
|
|
90
|
-
txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
|
|
91
|
-
if (isTaprootAddress) {
|
|
92
|
-
txIn.tapInternalKey = toXOnly(publicKey)
|
|
93
|
-
}
|
|
94
|
-
} else {
|
|
95
|
-
const rawTx = (rawTxs || []).find((t) => t.txId === txId)
|
|
96
|
-
// non-witness outptus require the full transaction
|
|
97
|
-
assert(!!rawTx, 'Non-witness outputs require the full previous transaction.')
|
|
98
|
-
const rawTxBuffer = Buffer.from(rawTx.rawData, 'hex')
|
|
99
|
-
if (canParseTx(rawTxBuffer)) {
|
|
100
|
-
txIn.nonWitnessUtxo = rawTxBuffer
|
|
101
|
-
} else {
|
|
102
|
-
// temp fix for https://exodusio.slack.com/archives/CP202D90Q/p1671014704829939 until bitcoinjs could parse a mweb tx without failing
|
|
103
|
-
console.warn(`Setting psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true for asset ${assetName}`)
|
|
104
|
-
psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true
|
|
105
|
-
txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
psbt.addInput(txIn)
|
|
109
|
-
}
|
|
135
|
+
if (!['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)) psbt.setVersion(1)
|
|
110
136
|
|
|
111
|
-
|
|
112
|
-
psbt.addOutput({ value: amount, address })
|
|
113
|
-
}
|
|
137
|
+
const inputsToSign = isPsbtBufferPassed ? unsignedTx.txMeta.inputsToSign : inputs
|
|
114
138
|
|
|
115
139
|
// The Taproot SIGHASH flag includes all previous outputs,
|
|
116
140
|
// so signing is only done AFTER all inputs have been updated
|
|
117
|
-
for (let index = 0; index <
|
|
118
|
-
const
|
|
141
|
+
for (let index = 0; index < psbt.inputCount; index++) {
|
|
142
|
+
const inputInfo = inputsToSign[index]
|
|
143
|
+
// dApps request to sign only specific transaction inputs.
|
|
144
|
+
if (!inputInfo) continue
|
|
145
|
+
const { address, sigHash } = inputInfo
|
|
146
|
+
const sigHashTypes = sigHash ? [sigHash] : undefined
|
|
119
147
|
const { key, purpose, publicKey } = getKeyAndPurpose(address)
|
|
120
148
|
|
|
121
149
|
if (purpose === 49) {
|
|
@@ -130,19 +158,19 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
|
|
|
130
158
|
})
|
|
131
159
|
}
|
|
132
160
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
? tweakSigner({ signer: key, ECPair, ecc, network })
|
|
138
|
-
: key
|
|
139
|
-
await psbt.signInputAsync(index, toAsyncSigner({ keyPair: signingKey, ecc }))
|
|
140
|
-
} else {
|
|
141
|
-
// mobile signing
|
|
142
|
-
psbt.signInput(index, key)
|
|
143
|
-
}
|
|
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)
|
|
144
165
|
}
|
|
145
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
|
+
}
|
|
146
174
|
// Serialize tx
|
|
147
175
|
psbt.finalizeAllInputs()
|
|
148
176
|
const tx = psbt.extractTransaction()
|
package/src/tx-sign/taproot.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { crypto } from 'bitcoinjs-lib'
|
|
2
2
|
import assert from 'minimalistic-assert'
|
|
3
3
|
import { getSchnorrEntropy } from './default-entropy'
|
|
4
|
+
import { toXOnly } from '../bitcoinjs-lib/ecc-utils'
|
|
5
|
+
import { eccFactory } from '../bitcoinjs-lib/ecc'
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
const ecc = eccFactory()
|
|
8
|
+
|
|
9
|
+
export function tweakSigner({ signer, ECPair, tweakHash, network }) {
|
|
6
10
|
assert(signer, 'signer is required')
|
|
7
11
|
assert(ECPair, 'ECPair is required')
|
|
8
|
-
assert(ecc, 'ecc is required')
|
|
9
12
|
let privateKey: Uint8Array | undefined = signer.privateKey
|
|
10
13
|
if (!privateKey) {
|
|
11
14
|
throw new Error('Private key is required for tweaking signer!')
|
|
@@ -34,9 +37,8 @@ function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer {
|
|
|
34
37
|
/**
|
|
35
38
|
* Take a sync signer and make it async.
|
|
36
39
|
*/
|
|
37
|
-
export function toAsyncSigner({ keyPair
|
|
40
|
+
export function toAsyncSigner({ keyPair }) {
|
|
38
41
|
assert(keyPair, 'keyPair is required')
|
|
39
|
-
assert(ecc, 'ecc is required')
|
|
40
42
|
keyPair.sign = async (h) => {
|
|
41
43
|
const sig = await ecc.signAsync(h, keyPair.privateKey)
|
|
42
44
|
return Buffer.from(sig)
|
|
@@ -48,7 +50,3 @@ export function toAsyncSigner({ keyPair, ecc }) {
|
|
|
48
50
|
}
|
|
49
51
|
return keyPair
|
|
50
52
|
}
|
|
51
|
-
|
|
52
|
-
export const toXOnly = (publicKey) => {
|
|
53
|
-
return publicKey.slice(1, 33)
|
|
54
|
-
}
|
package/src/utxos-utils.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { UtxoCollection } from '@exodus/models'
|
|
3
3
|
import { findLargeUnconfirmedTxs } from './tx-utils'
|
|
4
4
|
import assert from 'minimalistic-assert'
|
|
5
|
-
import { isOrdinalAddress } from './address-utils'
|
|
6
5
|
|
|
7
6
|
const MAX_ORDINAL_VALUE_POSTAGE = 10000
|
|
8
7
|
|
|
@@ -24,29 +23,31 @@ export function getOrdinalsUtxos({ accountState, asset }) {
|
|
|
24
23
|
)
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
function isOrdinalUtxo({ utxo,
|
|
26
|
+
function isOrdinalUtxo({ utxo, ordinalAddress }) {
|
|
28
27
|
if (utxo.inscriptionId) {
|
|
29
28
|
return true
|
|
30
29
|
}
|
|
31
|
-
if (
|
|
30
|
+
if (!ordinalAddress) {
|
|
32
31
|
// exclude utxos splitting
|
|
33
32
|
return false
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
if (utxo.address.toString() === ordinalAddress.toString()) {
|
|
36
|
+
return true // we assume any utxo to the ordinal address is a ordinal utxos just in case
|
|
37
|
+
}
|
|
38
|
+
|
|
38
39
|
if (utxo.confirmations) {
|
|
39
|
-
return
|
|
40
|
+
return false
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
return utxo.value.toBaseNumber() <= MAX_ORDINAL_VALUE_POSTAGE
|
|
43
|
+
return utxo.value.toBaseNumber() <= MAX_ORDINAL_VALUE_POSTAGE // while unconfirmed, put < 10000- sats in the ordinal utxos box just in case
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
export function partitionUtxos({ allUtxos,
|
|
46
|
+
export function partitionUtxos({ allUtxos, ordinalAddress }) {
|
|
46
47
|
assert(allUtxos, 'allUtxos is required')
|
|
47
48
|
return {
|
|
48
|
-
utxos: allUtxos.filter((utxo) => !isOrdinalUtxo({ utxo,
|
|
49
|
-
ordinalsUtxos: allUtxos.filter((utxo) => isOrdinalUtxo({ utxo,
|
|
49
|
+
utxos: allUtxos.filter((utxo) => !isOrdinalUtxo({ utxo, ordinalAddress })),
|
|
50
|
+
ordinalsUtxos: allUtxos.filter((utxo) => isOrdinalUtxo({ utxo, ordinalAddress })),
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
|