@exodus/bitcoin-api 2.4.1 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -3
- package/src/bitcoinjs-lib/ecc/common.js +2 -1
- package/src/bitcoinjs-lib/ecc/desktop.js +3 -1
- package/src/bitcoinjs-lib/ecc/mobile.js +3 -1
- package/src/bitcoinjs-lib/ecc-utils.js +3 -0
- package/src/bitcoinjs-lib/script-classify/index.js +3 -5
- package/src/btc-like-address.js +3 -5
- package/src/btc-like-keys.js +5 -7
- package/src/fee/fee-estimator.js +4 -5
- package/src/fee/script-classifier.js +3 -3
- package/src/index.js +2 -0
- package/src/parse-unsigned-tx.js +1 -1
- package/src/tx-send/index.js +24 -10
- package/src/tx-sign/default-create-tx.js +93 -59
- package/src/tx-sign/taproot.js +8 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Exodus bitcoin-api",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -22,13 +22,13 @@
|
|
|
22
22
|
"@exodus/basic-utils": "^2.0.1",
|
|
23
23
|
"@exodus/bip-schnorr": "0.6.6-fork-1",
|
|
24
24
|
"@exodus/bip44-constants": "^195.0.0",
|
|
25
|
-
"@exodus/bitcoinjs-lib": "6.0.2-beta.5",
|
|
26
25
|
"@exodus/models": "^8.10.4",
|
|
27
26
|
"@exodus/secp256k1": "4.0.2-exodus.0",
|
|
28
27
|
"@exodus/simple-retry": "0.0.6",
|
|
29
28
|
"@exodus/timer": "^1.0.0",
|
|
30
29
|
"@noble/secp256k1": "~1.5.3",
|
|
31
30
|
"bech32": "^1.1.3",
|
|
31
|
+
"bitcoinjs-lib": "^6.1.5",
|
|
32
32
|
"coininfo": "5.1.0",
|
|
33
33
|
"delay": "4.0.1",
|
|
34
34
|
"ecpair": "2.0.1",
|
|
@@ -39,6 +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
|
+
},
|
|
46
|
+
"gitHead": "d7b87308c1645a96ba4451f0b40b08ae3c1c29e5"
|
|
44
47
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import secp256k1 from '@exodus/secp256k1'
|
|
2
|
+
import { toXOnly } from '../ecc-utils'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Common ecc functions between mobile and desktop. Once mobile accepts @noble/secp256k1, we can unify both
|
|
@@ -24,7 +25,7 @@ export const common = {
|
|
|
24
25
|
const t = secp256k1.publicKeyTweakAdd(toPubKey(p), tweak)
|
|
25
26
|
return {
|
|
26
27
|
parity: t[0] === 0x02 ? 0 : 1,
|
|
27
|
-
xOnlyPubkey: t
|
|
28
|
+
xOnlyPubkey: toXOnly(t),
|
|
28
29
|
}
|
|
29
30
|
} catch (err) {
|
|
30
31
|
return null
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TinySecp256k1Interface } from '
|
|
1
|
+
import { TinySecp256k1Interface, initEccLib } from 'bitcoinjs-lib'
|
|
2
2
|
import { Point, schnorr, sign } from '@noble/secp256k1'
|
|
3
3
|
import { common, toPubKey } from './common'
|
|
4
4
|
|
|
@@ -48,3 +48,5 @@ export const desktopEcc: TinySecp256k1Interface = {
|
|
|
48
48
|
pointCompress: (p: Uint8Array, compressed?: boolean): Uint8Array =>
|
|
49
49
|
Point.fromHex(p).toRawBytes(compressed),
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
initEccLib(desktopEcc)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TinySecp256k1Interface } from '
|
|
1
|
+
import { TinySecp256k1Interface, initEccLib } from 'bitcoinjs-lib'
|
|
2
2
|
import secp256k1 from '@exodus/secp256k1'
|
|
3
3
|
// TODO: temp import until '@noble/secp256k1' can be used
|
|
4
4
|
import { isPoint } from 'tiny-secp256k1'
|
|
@@ -56,3 +56,5 @@ export const mobileEcc: TinySecp256k1Interface = {
|
|
|
56
56
|
pointCompress: (p: Uint8Array, compressed?: boolean): Uint8Array =>
|
|
57
57
|
secp256k1.publicKeyConvert(p, compressed),
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
initEccLib(mobileEcc)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { payments } from '
|
|
2
|
-
import assert from 'minimalistic-assert'
|
|
1
|
+
import { payments } from 'bitcoinjs-lib'
|
|
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
|
@@ -2,20 +2,18 @@ import bs58check from 'bs58check'
|
|
|
2
2
|
import * as bech32 from 'bech32'
|
|
3
3
|
import assert from 'minimalistic-assert'
|
|
4
4
|
import { identity, pickBy } from 'lodash'
|
|
5
|
-
import * as bitcoinjsOriginal from '
|
|
5
|
+
import * as bitcoinjsOriginal from 'bitcoinjs-lib'
|
|
6
6
|
|
|
7
7
|
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
|
@@ -3,9 +3,11 @@ import wif from 'wif'
|
|
|
3
3
|
import * as bech32 from 'bech32'
|
|
4
4
|
import assert from 'minimalistic-assert'
|
|
5
5
|
import { identity, pickBy } from 'lodash'
|
|
6
|
-
import * as defaultBitcoinjsLib from '
|
|
6
|
+
import * as defaultBitcoinjsLib from 'bitcoinjs-lib'
|
|
7
7
|
import secp256k1 from 'secp256k1'
|
|
8
8
|
import { hash160 } from './hash-utils'
|
|
9
|
+
import { toXOnly } from './bitcoinjs-lib/ecc-utils'
|
|
10
|
+
import { eccFactory } from './bitcoinjs-lib/ecc'
|
|
9
11
|
|
|
10
12
|
export const publicKeyToHashFactory = (p2pkh) => (publicKey) => {
|
|
11
13
|
const payload = Buffer.concat([Buffer.from([p2pkh]), hash160(publicKey)])
|
|
@@ -15,14 +17,13 @@ export const publicKeyToHashFactory = (p2pkh) => (publicKey) => {
|
|
|
15
17
|
export const createBtcLikeKeys = ({
|
|
16
18
|
coinInfo,
|
|
17
19
|
versions,
|
|
18
|
-
ecc,
|
|
19
20
|
useBip86 = false,
|
|
20
21
|
bitcoinjsLib = defaultBitcoinjsLib,
|
|
21
22
|
extraFunctions = {},
|
|
22
23
|
}) => {
|
|
23
24
|
assert(coinInfo, 'coinInfo is required')
|
|
24
25
|
assert(versions, 'versions is required')
|
|
25
|
-
|
|
26
|
+
const ecc = eccFactory()
|
|
26
27
|
const {
|
|
27
28
|
encodePrivate: encodePrivateCustom,
|
|
28
29
|
encodePublic: encodePublicCustom,
|
|
@@ -90,10 +91,7 @@ export const createBtcLikeKeys = ({
|
|
|
90
91
|
(useBip86
|
|
91
92
|
? (publicKey: Buffer): string => {
|
|
92
93
|
const network = coinInfo.toBitcoinJS()
|
|
93
|
-
return bitcoinjsLib.payments.p2tr(
|
|
94
|
-
{ internalPubkey: publicKey.slice(1, 33), network },
|
|
95
|
-
{ eccLib: ecc }
|
|
96
|
-
).address
|
|
94
|
+
return bitcoinjsLib.payments.p2tr({ internalPubkey: toXOnly(publicKey), network }).address
|
|
97
95
|
}
|
|
98
96
|
: undefined)
|
|
99
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
package/src/parse-unsigned-tx.js
CHANGED
|
@@ -2,7 +2,7 @@ import assert from 'minimalistic-assert'
|
|
|
2
2
|
import BIPPath from 'bip32-path'
|
|
3
3
|
import BN from 'bn.js'
|
|
4
4
|
import lodash from 'lodash'
|
|
5
|
-
import { Transaction as BitcoinTransactionClass } from '
|
|
5
|
+
import { Transaction as BitcoinTransactionClass } from 'bitcoinjs-lib'
|
|
6
6
|
|
|
7
7
|
export const parseUnsignedTxFactory = ({ Transaction = BitcoinTransactionClass } = {}) => async ({
|
|
8
8
|
asset,
|
package/src/tx-send/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import { findUnconfirmedSentRbfTxs } from '../tx-utils'
|
|
17
17
|
import { getOrdinalsUtxos, getUsableUtxos, getUtxos } from '../utxos-utils'
|
|
18
18
|
|
|
19
|
-
import * as defaultBitcoinjsLib from '
|
|
19
|
+
import * as defaultBitcoinjsLib from 'bitcoinjs-lib'
|
|
20
20
|
|
|
21
21
|
const ASSETS_SUPPORTED_BIP_174 = [
|
|
22
22
|
'bitcoin',
|
|
@@ -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) {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import assert from 'minimalistic-assert'
|
|
2
2
|
import lodash from 'lodash'
|
|
3
3
|
import ECPairFactory from 'ecpair'
|
|
4
|
-
import { payments, Psbt, Transaction } from '
|
|
4
|
+
import { payments, Psbt, Transaction } from 'bitcoinjs-lib'
|
|
5
5
|
import { getOwnProperty } from '@exodus/basic-utils'
|
|
6
6
|
|
|
7
7
|
import secp256k1 from 'secp256k1'
|
|
8
8
|
|
|
9
|
+
import { toXOnly } from '../bitcoinjs-lib/ecc-utils'
|
|
9
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)
|
|
@@ -63,7 +108,8 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
|
|
|
63
108
|
const privateKey = getOwnProperty(privateKeysAddressMap, address, 'string')
|
|
64
109
|
assert(privateKey, `there is no private key for address ${address}`)
|
|
65
110
|
const key = ECPair.fromWIF(privateKey, networkInfo)
|
|
66
|
-
|
|
111
|
+
const publicKey = secp256k1.publicKeyCreate(key.privateKey, true)
|
|
112
|
+
return { key, purpose, publicKey }
|
|
67
113
|
}
|
|
68
114
|
const path = addressPathsMap[address]
|
|
69
115
|
assert(hdkeys, 'hdkeys must be provided')
|
|
@@ -72,51 +118,39 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
|
|
|
72
118
|
assert(hdkey, `hdkey for purpose for ${purpose} and address ${address} could not be resolved`)
|
|
73
119
|
const derivedhdkey = hdkey.derive(path)
|
|
74
120
|
const privateEncoded = keys.encodePrivate(derivedhdkey.privateKey)
|
|
75
|
-
|
|
121
|
+
const key = ECPair.fromWIF(privateEncoded, networkInfo)
|
|
122
|
+
const publicKey = derivedhdkey.publicKey
|
|
123
|
+
return { key, publicKey, purpose }
|
|
76
124
|
})
|
|
77
125
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
|
|
87
|
-
} else {
|
|
88
|
-
const rawTx = (rawTxs || []).find((t) => t.txId === txId)
|
|
89
|
-
// non-witness outptus require the full transaction
|
|
90
|
-
assert(!!rawTx, 'Non-witness outputs require the full previous transaction.')
|
|
91
|
-
const rawTxBuffer = Buffer.from(rawTx.rawData, 'hex')
|
|
92
|
-
if (canParseTx(rawTxBuffer)) {
|
|
93
|
-
txIn.nonWitnessUtxo = rawTxBuffer
|
|
94
|
-
} else {
|
|
95
|
-
// temp fix for https://exodusio.slack.com/archives/CP202D90Q/p1671014704829939 until bitcoinjs could parse a mweb tx without failing
|
|
96
|
-
console.warn(`Setting psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true for asset ${assetName}`)
|
|
97
|
-
psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true
|
|
98
|
-
txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
psbt.addInput(txIn)
|
|
102
|
-
}
|
|
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
|
|
103
134
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
135
|
+
if (!['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)) psbt.setVersion(1)
|
|
136
|
+
|
|
137
|
+
const inputsToSign = isPsbtBufferPassed ? unsignedTx.txMeta.inputsToSign : inputs
|
|
107
138
|
|
|
108
139
|
// The Taproot SIGHASH flag includes all previous outputs,
|
|
109
140
|
// so signing is only done AFTER all inputs have been updated
|
|
110
|
-
for (let index = 0; index <
|
|
111
|
-
const
|
|
112
|
-
|
|
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
|
|
147
|
+
const { key, purpose, publicKey } = getKeyAndPurpose(address)
|
|
113
148
|
|
|
114
149
|
if (purpose === 49) {
|
|
115
150
|
// If spending from a P2SH address, we assume the address is P2SH wrapping
|
|
116
151
|
// P2WPKH. Exodus doesn't use P2SH addresses so we should only ever be
|
|
117
152
|
// signing a P2SH input if we are importing a private key
|
|
118
153
|
// BIP143: As a default policy, only compressed public keys are accepted in P2WPKH and P2WSH
|
|
119
|
-
const publicKey = secp256k1.publicKeyCreate(key.privateKey, true)
|
|
120
154
|
const p2wpkh = payments.p2wpkh({ pubkey: publicKey })
|
|
121
155
|
const p2sh = payments.p2sh({ redeem: p2wpkh })
|
|
122
156
|
psbt.updateInput(index, {
|
|
@@ -124,19 +158,19 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
|
|
|
124
158
|
})
|
|
125
159
|
}
|
|
126
160
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
? tweakSigner({ signer: key, ECPair, ecc, network })
|
|
132
|
-
: key
|
|
133
|
-
await psbt.signInputAsync(index, toAsyncSigner({ keyPair: signingKey, ecc }))
|
|
134
|
-
} else {
|
|
135
|
-
// mobile signing
|
|
136
|
-
psbt.signInput(index, key)
|
|
137
|
-
}
|
|
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)
|
|
138
165
|
}
|
|
139
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
|
+
}
|
|
140
174
|
// Serialize tx
|
|
141
175
|
psbt.finalizeAllInputs()
|
|
142
176
|
const tx = psbt.extractTransaction()
|
package/src/tx-sign/taproot.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { crypto } from '
|
|
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!')
|
|
@@ -16,7 +19,7 @@ export function tweakSigner({ signer, ECPair, ecc, tweakHash, network }) {
|
|
|
16
19
|
|
|
17
20
|
const tweakedPrivateKey = ecc.privateAdd(
|
|
18
21
|
privateKey,
|
|
19
|
-
tapTweakHash(signer.publicKey
|
|
22
|
+
tapTweakHash(toXOnly(signer.publicKey), tweakHash)
|
|
20
23
|
)
|
|
21
24
|
if (!tweakedPrivateKey) {
|
|
22
25
|
throw new Error('Invalid tweaked private key!')
|
|
@@ -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)
|