@exodus/bitcoin-api 2.14.1 → 2.15.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/bitcoin-api",
3
- "version": "2.14.1",
3
+ "version": "2.15.1",
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": "19c4a56008f41bfc7001a6b7b8d2e4849b345d93"
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,32 +1,40 @@
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) => {
24
28
  // The Taproot SIGHASH flag includes all previous outputs,
25
29
  // so signing is only done AFTER all inputs have been updated
30
+ const signingPromises = []
31
+
26
32
  for (let index = 0; index < psbt.inputCount; index++) {
27
33
  const inputInfo = inputsToSign[index]
28
34
  // dApps request to sign only specific transaction inputs.
29
35
  if (!inputInfo) continue
36
+
37
+ const input = psbt.data.inputs[index]
30
38
  const { address, sigHash } = inputInfo
31
39
  // The sighash value from the PSBT input itself will be used.
32
40
  // This list just represents possible sighash values the inputs can have.
@@ -34,12 +42,11 @@ export function createSignWithWallet({
34
42
  sigHash === undefined
35
43
  ? undefined // `SIGHASH_DEFAULT` is a default safe sig hash, always allow it.
36
44
  : [sigHash, Transaction.SIGHASH_ALL]
37
- const { key, purpose, publicKey } = getKeyAndPurpose(address)
45
+ const { key, purpose, keyId, publicKey } = await getKeyWithMetadata(address)
38
46
 
39
47
  const isP2SH = purpose === 49
40
- const hasTapLeafScript =
41
- psbt.data.inputs[index].tapLeafScript && psbt.data.inputs[index].tapLeafScript.length > 0
42
- const isTaprootKeySpend = purpose === 86 && !hasTapLeafScript
48
+ const hasTapLeafScript = input.tapLeafScript && input.tapLeafScript.length > 0
49
+ const isTaprootKeySpend = isTaprootPurpose(purpose) && !hasTapLeafScript
43
50
 
44
51
  if (isP2SH) {
45
52
  // If spending from a P2SH address, we assume the address is P2SH wrapping
@@ -50,7 +57,7 @@ export function createSignWithWallet({
50
57
  const p2sh = payments.p2sh({ redeem: p2wpkh })
51
58
  if (address === p2sh.address) {
52
59
  // Set the redeem script in the psbt in case it's missing.
53
- if (!Buffer.isBuffer(psbt.data.inputs[index].redeemScript)) {
60
+ if (!Buffer.isBuffer(input.redeemScript)) {
54
61
  psbt.updateInput(index, {
55
62
  redeemScript: p2sh.redeem.output,
56
63
  })
@@ -58,20 +65,20 @@ export function createSignWithWallet({
58
65
  } else {
59
66
  throw new Error('Expected P2SH script to be a nested segwit input')
60
67
  }
61
- } else if (isTaprootKeySpend && !Buffer.isBuffer(psbt.data.inputs[index].tapInternalKey)) {
68
+ } else if (isTaprootKeySpend && !Buffer.isBuffer(input.tapInternalKey)) {
62
69
  // tapInternalKey is metadata for signing and not part of the hash to sign.
63
70
  // so modifying it here is fine.
64
- psbt.updateInput(index, {
65
- tapInternalKey: toXOnly(publicKey),
66
- })
71
+ psbt.updateInput(index, { tapInternalKey: toXOnly(publicKey) })
67
72
  }
68
73
 
74
+ const asyncSigner = signer
75
+ ? await toAsyncBufferSigner({ signer, isTaprootKeySpend, purpose, keyId })
76
+ : toAsyncSigner({ keyPair: key, isTaprootKeySpend, network })
77
+
69
78
  // desktop / BE / mobile with bip-schnorr signing
70
- await psbt.signInputAsync(
71
- index,
72
- toAsyncSigner({ keyPair: key, isTaprootKeySpend, network }),
73
- allowedSigHashTypes
74
- )
79
+ signingPromises.push(psbt.signInputAsync(index, asyncSigner, allowedSigHashTypes))
75
80
  }
81
+
82
+ await Promise.all(signingPromises)
76
83
  }
77
84
  }
@@ -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
+ }