@exodus/bitcoin-api 2.14.1 → 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.14.1",
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": "2af27ab114d53d02eb1c324150a847e894cdcc20"
61
+ "gitHead": "364ffb79577938023d87e78e7d24a8e6413af060"
61
62
  }
@@ -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 createGetKeyAndPurpose = ({
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 { createGetKeyAndPurpose } from './create-get-key-and-purpose'
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 getKeyAndPurpose = createGetKeyAndPurpose({
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 } = getKeyAndPurpose(address)
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 === 86 && !hasTapLeafScript
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 = ({ assetName, resolvePurpose, coinInfo, network }) => {
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(hdkeys || privateKeysAddressMap, 'hdkeys or privateKeysAddressMap is required')
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)
@@ -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
- function tapTweakHash(pubKey, h) {
37
- return crypto.taggedHash('TapTweak', Buffer.concat(h ? [pubKey, h] : [pubKey]))
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, ECPair, network })
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
+ }