@exodus/bitcoin-api 2.6.8-hiro.2 → 2.7.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 +3 -2
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "Exodus bitcoin-api",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -42,5 +42,6 @@
|
|
|
42
42
|
"@scure/base": "^1.1.3",
|
|
43
43
|
"@scure/btc-signer": "^1.1.0",
|
|
44
44
|
"jest-when": "^3.5.1"
|
|
45
|
-
}
|
|
45
|
+
},
|
|
46
|
+
"gitHead": "5c306496cb4086ff823640e8eadb0d9750ac3683"
|
|
46
47
|
}
|
|
@@ -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!')
|