@exodus/bitcoin-api 2.14.0 → 2.15.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
4
4
|
"description": "Exodus bitcoin-api",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"@exodus/bitcoinjs-lib": "^6.1.5-exodus.1",
|
|
28
28
|
"@exodus/currency": "^2.3.2",
|
|
29
29
|
"@exodus/fetch": "^1.3.0",
|
|
30
|
+
"@exodus/key-identifier": "^1.1.1",
|
|
30
31
|
"@exodus/models": "^11.0.0",
|
|
31
32
|
"@exodus/simple-retry": "0.0.6",
|
|
32
33
|
"@exodus/timer": "^1.0.0",
|
|
@@ -57,5 +58,5 @@
|
|
|
57
58
|
"jest-when": "^3.5.1",
|
|
58
59
|
"safe-buffer": "^5.2.1"
|
|
59
60
|
},
|
|
60
|
-
"gitHead": "
|
|
61
|
+
"gitHead": "364ffb79577938023d87e78e7d24a8e6413af060"
|
|
61
62
|
}
|
|
@@ -14,8 +14,13 @@ const getTextFromResponse = async (response) => {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const fetchJson = async (url, fetchOptions) => {
|
|
17
|
+
const fetchJson = async (url, fetchOptions, nullWhen404) => {
|
|
18
18
|
const response = await fetch(url, fetchOptions)
|
|
19
|
+
|
|
20
|
+
if (nullWhen404 && response.status === 404) {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
if (!response.ok)
|
|
20
25
|
throw new Error(
|
|
21
26
|
`${url} returned ${response.status}: ${
|
|
@@ -93,19 +98,13 @@ export default class InsightAPIClient {
|
|
|
93
98
|
async fetchTx(txId) {
|
|
94
99
|
const encodedTxId = encodeURIComponent(txId)
|
|
95
100
|
const url = urlJoin(this._baseURL, `/tx/${encodedTxId}`)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (response.status === 404) return null
|
|
99
|
-
return response.json()
|
|
101
|
+
return fetchJson(url, undefined, true)
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
async fetchTxObject(txId) {
|
|
103
105
|
const url = urlJoin(this._baseURL, `/fulltx?${new URLSearchParams({ hash: txId })}`)
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
if (response.status === 404) return null
|
|
107
|
-
const object = await response.json()
|
|
108
|
-
if (isEmpty(object)) {
|
|
106
|
+
const object = await fetchJson(url, undefined, true)
|
|
107
|
+
if (!object || isEmpty(object)) {
|
|
109
108
|
return null
|
|
110
109
|
}
|
|
111
110
|
|
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
import lodash from 'lodash'
|
|
2
|
+
import BipPath from 'bip32-path'
|
|
2
3
|
import assert from 'minimalistic-assert'
|
|
3
4
|
import { getOwnProperty } from '@exodus/basic-utils'
|
|
5
|
+
import KeyIdentifier from '@exodus/key-identifier'
|
|
6
|
+
|
|
4
7
|
import { getECPair } from '../bitcoinjs-lib'
|
|
5
8
|
|
|
6
9
|
import secp256k1 from 'secp256k1'
|
|
7
10
|
|
|
8
11
|
const ECPair = getECPair()
|
|
9
12
|
|
|
10
|
-
export const
|
|
13
|
+
export const createGetKeyWithMetadata = ({
|
|
14
|
+
signer,
|
|
11
15
|
hdkeys,
|
|
12
16
|
resolvePurpose,
|
|
13
17
|
addressPathsMap,
|
|
14
18
|
privateKeysAddressMap,
|
|
15
19
|
coinInfo,
|
|
20
|
+
getKeyIdentifier,
|
|
16
21
|
}) =>
|
|
17
22
|
lodash.memoize((address) => {
|
|
18
23
|
const purpose = resolvePurpose(address)
|
|
19
24
|
const networkInfo = coinInfo.toBitcoinJS()
|
|
25
|
+
|
|
26
|
+
if (signer) {
|
|
27
|
+
return getPublicKeyFromSigner(signer, addressPathsMap, purpose, address, getKeyIdentifier)
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
if (privateKeysAddressMap) {
|
|
21
31
|
return getPrivateKeyFromMap(privateKeysAddressMap, networkInfo, purpose, address)
|
|
22
32
|
}
|
|
@@ -43,3 +53,12 @@ function getPrivateKeyFromHDKeys(hdkeys, addressPathsMap, networkInfo, purpose,
|
|
|
43
53
|
const publicKey = derivedhdkey.publicKey
|
|
44
54
|
return { key, publicKey, purpose }
|
|
45
55
|
}
|
|
56
|
+
|
|
57
|
+
async function getPublicKeyFromSigner(signer, addressPathsMap, purpose, address, getKeyIdentifier) {
|
|
58
|
+
assert(purpose, `purpose for address ${address} could not be resolved`)
|
|
59
|
+
const addressPath = getOwnProperty(addressPathsMap, address, 'string')
|
|
60
|
+
const [chainIndex, addressIndex] = BipPath.fromString(addressPath).toPathArray()
|
|
61
|
+
const keyId = new KeyIdentifier(getKeyIdentifier({ purpose, chainIndex, addressIndex }))
|
|
62
|
+
const publicKey = await signer.getPublicKey({ keyId })
|
|
63
|
+
return { purpose, keyId, publicKey }
|
|
64
|
+
}
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
import { Transaction, payments } from '@exodus/bitcoinjs-lib'
|
|
2
2
|
|
|
3
3
|
import { toXOnly } from '../bitcoinjs-lib/ecc-utils'
|
|
4
|
-
import {
|
|
5
|
-
import { toAsyncSigner } from './taproot'
|
|
4
|
+
import { createGetKeyWithMetadata } from './create-get-key-and-purpose'
|
|
5
|
+
import { toAsyncSigner, toAsyncBufferSigner, isTaprootPurpose } from './taproot'
|
|
6
6
|
|
|
7
7
|
export function createSignWithWallet({
|
|
8
|
+
signer,
|
|
8
9
|
hdkeys,
|
|
9
10
|
resolvePurpose,
|
|
10
11
|
privateKeysAddressMap,
|
|
11
12
|
addressPathsMap,
|
|
12
13
|
coinInfo,
|
|
13
14
|
network,
|
|
15
|
+
getKeyIdentifier,
|
|
14
16
|
}) {
|
|
15
|
-
const
|
|
17
|
+
const getKeyWithMetadata = createGetKeyWithMetadata({
|
|
18
|
+
signer,
|
|
16
19
|
hdkeys,
|
|
17
20
|
resolvePurpose,
|
|
18
21
|
privateKeysAddressMap,
|
|
19
22
|
addressPathsMap,
|
|
20
23
|
coinInfo,
|
|
24
|
+
getKeyIdentifier,
|
|
21
25
|
})
|
|
22
26
|
|
|
23
27
|
return async (psbt, inputsToSign) => {
|
|
@@ -34,12 +38,12 @@ export function createSignWithWallet({
|
|
|
34
38
|
sigHash === undefined
|
|
35
39
|
? undefined // `SIGHASH_DEFAULT` is a default safe sig hash, always allow it.
|
|
36
40
|
: [sigHash, Transaction.SIGHASH_ALL]
|
|
37
|
-
const { key, purpose, publicKey } =
|
|
41
|
+
const { key, purpose, keyId, publicKey } = await getKeyWithMetadata(address)
|
|
38
42
|
|
|
39
43
|
const isP2SH = purpose === 49
|
|
40
44
|
const hasTapLeafScript =
|
|
41
45
|
psbt.data.inputs[index].tapLeafScript && psbt.data.inputs[index].tapLeafScript.length > 0
|
|
42
|
-
const isTaprootKeySpend = purpose
|
|
46
|
+
const isTaprootKeySpend = isTaprootPurpose(purpose) && !hasTapLeafScript
|
|
43
47
|
|
|
44
48
|
if (isP2SH) {
|
|
45
49
|
// If spending from a P2SH address, we assume the address is P2SH wrapping
|
|
@@ -61,17 +65,15 @@ export function createSignWithWallet({
|
|
|
61
65
|
} else if (isTaprootKeySpend && !Buffer.isBuffer(psbt.data.inputs[index].tapInternalKey)) {
|
|
62
66
|
// tapInternalKey is metadata for signing and not part of the hash to sign.
|
|
63
67
|
// so modifying it here is fine.
|
|
64
|
-
psbt.updateInput(index, {
|
|
65
|
-
tapInternalKey: toXOnly(publicKey),
|
|
66
|
-
})
|
|
68
|
+
psbt.updateInput(index, { tapInternalKey: toXOnly(publicKey) })
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
const asyncSigner = signer
|
|
72
|
+
? await toAsyncBufferSigner({ signer, isTaprootKeySpend, purpose, keyId })
|
|
73
|
+
: toAsyncSigner({ keyPair: key, isTaprootKeySpend, network })
|
|
74
|
+
|
|
69
75
|
// desktop / BE / mobile with bip-schnorr signing
|
|
70
|
-
await psbt.signInputAsync(
|
|
71
|
-
index,
|
|
72
|
-
toAsyncSigner({ keyPair: key, isTaprootKeySpend, network }),
|
|
73
|
-
allowedSigHashTypes
|
|
74
|
-
)
|
|
76
|
+
await psbt.signInputAsync(index, asyncSigner, allowedSigHashTypes)
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
}
|
|
@@ -4,7 +4,13 @@ import { createPrepareForSigning } from './default-prepare-for-signing'
|
|
|
4
4
|
import { createSignWithWallet } from './create-sign-with-wallet'
|
|
5
5
|
import { extractTransaction } from './common'
|
|
6
6
|
|
|
7
|
-
export const signTxFactory = ({
|
|
7
|
+
export const signTxFactory = ({
|
|
8
|
+
assetName,
|
|
9
|
+
resolvePurpose,
|
|
10
|
+
coinInfo,
|
|
11
|
+
network,
|
|
12
|
+
getKeyIdentifier,
|
|
13
|
+
}) => {
|
|
8
14
|
assert(assetName, 'assetName is required')
|
|
9
15
|
assert(resolvePurpose, 'resolvePurpose is required')
|
|
10
16
|
assert(coinInfo, 'coinInfo is required')
|
|
@@ -15,22 +21,33 @@ export const signTxFactory = ({ assetName, resolvePurpose, coinInfo, network })
|
|
|
15
21
|
coinInfo,
|
|
16
22
|
})
|
|
17
23
|
|
|
18
|
-
return async ({ unsignedTx, hdkeys, privateKeysAddressMap }) => {
|
|
24
|
+
return async ({ unsignedTx, hdkeys, privateKeysAddressMap, signer }) => {
|
|
19
25
|
assert(unsignedTx, 'unsignedTx is required')
|
|
20
|
-
assert(
|
|
26
|
+
assert(
|
|
27
|
+
hdkeys || privateKeysAddressMap || signer,
|
|
28
|
+
'hdkeys or privateKeysAddressMap or signer is required'
|
|
29
|
+
)
|
|
21
30
|
|
|
22
|
-
const { addressPathsMap } = unsignedTx.txMeta
|
|
31
|
+
const { addressPathsMap, accountIndex } = unsignedTx.txMeta
|
|
23
32
|
|
|
24
33
|
const psbt = prepareForSigning({ unsignedTx })
|
|
25
34
|
|
|
26
35
|
const inputsToSign = unsignedTx.txMeta.inputsToSign || unsignedTx.txData.inputs
|
|
27
36
|
const signWithWallet = createSignWithWallet({
|
|
37
|
+
signer,
|
|
28
38
|
hdkeys,
|
|
29
39
|
resolvePurpose,
|
|
30
40
|
privateKeysAddressMap,
|
|
31
41
|
addressPathsMap,
|
|
32
42
|
coinInfo,
|
|
33
43
|
network,
|
|
44
|
+
getKeyIdentifier: (args) => {
|
|
45
|
+
assert(
|
|
46
|
+
!('accountIndex' in args) || args.accountIndex === accountIndex,
|
|
47
|
+
'`accountIndex` mismatch'
|
|
48
|
+
)
|
|
49
|
+
return getKeyIdentifier({ ...args, accountIndex })
|
|
50
|
+
},
|
|
34
51
|
})
|
|
35
52
|
|
|
36
53
|
await signWithWallet(psbt, inputsToSign)
|
package/src/tx-sign/taproot.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { crypto } from '@exodus/bitcoinjs-lib'
|
|
2
2
|
import assert from 'minimalistic-assert'
|
|
3
|
+
|
|
3
4
|
import { getSchnorrEntropy } from './default-entropy'
|
|
4
|
-
import { toXOnly } from '../bitcoinjs-lib/ecc-utils'
|
|
5
5
|
import { eccFactory } from '../bitcoinjs-lib/ecc'
|
|
6
6
|
import { getECPair } from '../bitcoinjs-lib'
|
|
7
7
|
|
|
@@ -20,10 +20,7 @@ function tweakSigner({ signer, tweakHash, network }) {
|
|
|
20
20
|
privateKey = ecc.privateNegate(privateKey)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const tweakedPrivateKey = ecc.privateAdd(
|
|
24
|
-
privateKey,
|
|
25
|
-
tapTweakHash(toXOnly(signer.publicKey), tweakHash)
|
|
26
|
-
)
|
|
23
|
+
const tweakedPrivateKey = ecc.privateAdd(privateKey, tapTweakHash(signer.publicKey, tweakHash))
|
|
27
24
|
if (!tweakedPrivateKey) {
|
|
28
25
|
throw new Error('Invalid tweaked private key!')
|
|
29
26
|
}
|
|
@@ -33,8 +30,20 @@ function tweakSigner({ signer, tweakHash, network }) {
|
|
|
33
30
|
})
|
|
34
31
|
}
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
export const tweakPublicKey = ({ publicKey, tweak }) => {
|
|
34
|
+
const xOnlyPub = ecc.xOnlyPointFromPoint(publicKey)
|
|
35
|
+
const { parity, xOnlyPubkey } = ecc.xOnlyPointAddTweak(xOnlyPub, tweak)
|
|
36
|
+
|
|
37
|
+
return Buffer.from([parity ? 0x03 : 0x02, ...xOnlyPubkey])
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const tapTweakHash = (publicKey, h) => {
|
|
41
|
+
const xOnlyPoint = ecc.xOnlyPointFromPoint(publicKey)
|
|
42
|
+
return crypto.taggedHash('TapTweak', Buffer.concat(h ? [xOnlyPoint, h] : [xOnlyPoint]))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function isTaprootPurpose(purpose) {
|
|
46
|
+
return purpose === 86
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
/**
|
|
@@ -44,7 +53,7 @@ export function toAsyncSigner({ keyPair, isTaprootKeySpend, network }) {
|
|
|
44
53
|
assert(keyPair, 'keyPair is required')
|
|
45
54
|
|
|
46
55
|
if (isTaprootKeySpend) {
|
|
47
|
-
keyPair = tweakSigner({ signer: keyPair,
|
|
56
|
+
keyPair = tweakSigner({ signer: keyPair, network })
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
// eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
|
|
@@ -61,3 +70,42 @@ export function toAsyncSigner({ keyPair, isTaprootKeySpend, network }) {
|
|
|
61
70
|
|
|
62
71
|
return keyPair
|
|
63
72
|
}
|
|
73
|
+
|
|
74
|
+
// signer: {
|
|
75
|
+
// sign: ({ data, ecOptions, enc, purpose, keyId, signatureType, tweak, extraEntropy }: KeychainSignerParams): Promise<any>
|
|
76
|
+
// getPublicKey: ({ keyId }) => Promise<Buffer>
|
|
77
|
+
// }
|
|
78
|
+
//
|
|
79
|
+
export async function toAsyncBufferSigner({ signer, purpose, keyId, isTaprootKeySpend }) {
|
|
80
|
+
let tweak
|
|
81
|
+
let publicKey = await signer.getPublicKey({ keyId })
|
|
82
|
+
if (isTaprootKeySpend) {
|
|
83
|
+
tweak = tapTweakHash(publicKey)
|
|
84
|
+
publicKey = tweakPublicKey({ publicKey, tweak })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
sign: async (data) => {
|
|
89
|
+
const ecOptions = { canonical: true }
|
|
90
|
+
const sig = await signer.sign({ data, keyId, ecOptions, enc: 'raw', signatureType: 'ecdsa' })
|
|
91
|
+
const signature = new Uint8Array(64)
|
|
92
|
+
signature.set(sig.r.toArrayLike(Uint8Array, 'be', 32), 0)
|
|
93
|
+
signature.set(sig.s.toArrayLike(Uint8Array, 'be', 32), 32)
|
|
94
|
+
return Buffer.from(signature)
|
|
95
|
+
},
|
|
96
|
+
signSchnorr: async (data) => {
|
|
97
|
+
assert(
|
|
98
|
+
isTaprootPurpose(purpose),
|
|
99
|
+
`signSchnorr: invalid purpose for schnorr signing: ${purpose}`
|
|
100
|
+
)
|
|
101
|
+
return signer.sign({
|
|
102
|
+
data,
|
|
103
|
+
keyId,
|
|
104
|
+
signatureType: 'schnorr',
|
|
105
|
+
tweak,
|
|
106
|
+
extraEntropy: getSchnorrEntropy(),
|
|
107
|
+
})
|
|
108
|
+
},
|
|
109
|
+
publicKey,
|
|
110
|
+
}
|
|
111
|
+
}
|