@exodus/bitcoin-api 2.12.1 → 2.13.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.12.1",
3
+ "version": "2.13.0",
4
4
  "description": "Exodus bitcoin-api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -23,14 +23,13 @@
23
23
  "@exodus/bip322-js": "^1.1.0-exodus.4",
24
24
  "@exodus/bip44-constants": "^195.0.0",
25
25
  "@exodus/bitcoin-lib": "2.3.0",
26
+ "@exodus/bitcoinerlab-secp256k1": "^1.0.5-exodus.1",
26
27
  "@exodus/bitcoinjs-lib": "^6.1.5-exodus.1",
27
28
  "@exodus/currency": "^2.3.2",
28
29
  "@exodus/fetch": "^1.3.0",
29
30
  "@exodus/models": "^11.0.0",
30
- "@exodus/secp256k1": "4.0.2-exodus.0",
31
31
  "@exodus/simple-retry": "0.0.6",
32
32
  "@exodus/timer": "^1.0.0",
33
- "@noble/secp256k1": "~1.7.1",
34
33
  "bech32": "^1.1.3",
35
34
  "bip32-path": "^0.4.2",
36
35
  "bn.js": "4.12.0",
@@ -58,5 +57,5 @@
58
57
  "jest-when": "^3.5.1",
59
58
  "safe-buffer": "^5.2.1"
60
59
  },
61
- "gitHead": "646b7124b8d80c3e37ac8735fb218061ef24b745"
60
+ "gitHead": "9d296fc0875749545bc95b2a976e93739d63553b"
62
61
  }
@@ -1,3 +1,9 @@
1
- import { desktopEcc } from './desktop'
1
+ import { initEccLib } from '@exodus/bitcoinjs-lib'
2
2
 
3
- export const eccFactory = () => desktopEcc
3
+ import bitcoinerlab from '@exodus/bitcoinerlab-secp256k1'
4
+
5
+ export const ecc = bitcoinerlab
6
+
7
+ initEccLib(ecc)
8
+
9
+ export const eccFactory = () => ecc
package/src/index.js CHANGED
@@ -16,6 +16,7 @@ export * from './unconfirmed-ancestor-data'
16
16
  export * from './parse-unsigned-tx'
17
17
  export * from './insight-api-client/util'
18
18
  export * from './move-funds'
19
+ export { createEncodeMultisigContract } from './multisig-address'
19
20
  export { toAsyncSigner } from './tx-sign/taproot'
20
21
  export { toXOnly } from './bitcoinjs-lib/ecc-utils'
21
22
  export * from './ordinals-utils'
@@ -0,0 +1,81 @@
1
+ import * as defaultBitcoinjsLib from '@exodus/bitcoinjs-lib'
2
+ import { toXOnly } from './bitcoinjs-lib/ecc-utils'
3
+ import { eccFactory } from './bitcoinjs-lib/ecc'
4
+
5
+ // Key to use when key path spending is disabled https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs
6
+ const DUMMY_TAPROOT_PUBKEY = Buffer.from(
7
+ '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0',
8
+ 'hex'
9
+ )
10
+
11
+ // Leaf version for BIP342 is 0xc0 or 192 https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki#specification
12
+ const LEAF_VERSION_TAPSCRIPT = 192
13
+
14
+ // Limit multisig keys to 20 for now
15
+ const MAX_PUBKEYS = 20
16
+
17
+ export const createEncodeMultisigContract =
18
+ ({
19
+ bitcoinjsLib = defaultBitcoinjsLib,
20
+ network = bitcoinjsLib.Network.bitcoin,
21
+ ecc = eccFactory(),
22
+ }) =>
23
+ (publicKeys, { threshold = publicKeys.length, version = 0 } = Object.create(null)) => {
24
+ if (
25
+ !Array.isArray(publicKeys) ||
26
+ publicKeys.some((k) => !Buffer.isBuffer(k) || !ecc.isPointCompressed(k))
27
+ ) {
28
+ throw new Error('publicKeys must be an Array of Buffers representing compressed public keys')
29
+ }
30
+
31
+ if (publicKeys.length <= 0 || publicKeys.length > MAX_PUBKEYS) {
32
+ throw new Error(`asset.encodeMultisigContract supports from 1 to ${MAX_PUBKEYS} pubKeys`)
33
+ }
34
+
35
+ if (new Set(publicKeys.map((k) => k.toString('hex'))).size !== publicKeys.length) {
36
+ throw new Error('publicKeys must not contain any duplicates')
37
+ }
38
+
39
+ if (!Number.isSafeInteger(version)) {
40
+ throw new TypeError('asset.encodeMultisigContract requires meta.version to be an integer')
41
+ }
42
+
43
+ // Only support version 0 for now
44
+ if (version !== 0) {
45
+ throw new Error(`asset.encodeMultisigContract does not support version ${version}`)
46
+ }
47
+
48
+ if (!Number.isSafeInteger(threshold) || threshold <= 0 || threshold > MAX_PUBKEYS) {
49
+ throw new Error(
50
+ `asset.encodeMultisigContract requires meta.threshold to be an integer between 1 and ${MAX_PUBKEYS}`
51
+ )
52
+ }
53
+
54
+ if (threshold > publicKeys.length) {
55
+ throw new Error('threshold must be <= publicKeys.length')
56
+ }
57
+
58
+ // Sort according to BIP67 https://github.com/bitcoin/bips/blob/master/bip-0067.mediawiki
59
+ publicKeys.sort((a, b) => Buffer.compare(a, b))
60
+
61
+ // Create multisig redeem script https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki#cite_note-5
62
+ const OPS = bitcoinjsLib.script.OPS
63
+ const chunks = []
64
+ const keysIter = publicKeys[Symbol.iterator]()
65
+
66
+ chunks.push(toXOnly(keysIter.next().value), OPS.OP_CHECKSIG)
67
+ for (const key of keysIter) {
68
+ chunks.push(toXOnly(key), OPS.OP_CHECKSIGADD)
69
+ }
70
+
71
+ chunks.push(Buffer.from([threshold]), OPS.OP_NUMEQUAL)
72
+
73
+ const output = bitcoinjsLib.script.compile(chunks)
74
+
75
+ return bitcoinjsLib.payments.p2tr({
76
+ internalPubkey: DUMMY_TAPROOT_PUBKEY,
77
+ scriptTree: { output },
78
+ redeem: { output, redeemVersion: LEAF_VERSION_TAPSCRIPT },
79
+ network,
80
+ })
81
+ }
@@ -222,11 +222,9 @@ export const getPrepareSendTransaction =
222
222
  allowUnconfirmedRbfEnabledUtxos,
223
223
  utxosDescendingOrder,
224
224
  rbfEnabled: providedRbfEnabled,
225
+ assetClientInterface,
225
226
  }) =>
226
- async (
227
- { asset: maybeToken, walletAccount, address, amount: tokenAmount, options },
228
- { assetClientInterface }
229
- ) => {
227
+ async ({ asset: maybeToken, walletAccount, address, amount: tokenAmount, options }) => {
230
228
  const {
231
229
  multipleAddressesEnabled,
232
230
  feePerKB,
@@ -472,11 +470,9 @@ export const createAndBroadcastTXFactory =
472
470
  allowUnconfirmedRbfEnabledUtxos,
473
471
  ordinalsEnabled = false,
474
472
  utxosDescendingOrder,
473
+ assetClientInterface,
475
474
  }) =>
476
- async (
477
- { asset: maybeToken, walletAccount, address, amount: tokenAmount, options },
478
- { assetClientInterface }
479
- ) => {
475
+ async ({ asset: maybeToken, walletAccount, address, amount: tokenAmount, options }) => {
480
476
  // Prepare transaction
481
477
  const { bumpTxId, nft, isExchange, isBip70, isRbfAllowed = true, feeOpts } = options
482
478
 
@@ -501,10 +497,8 @@ export const createAndBroadcastTXFactory =
501
497
  allowUnconfirmedRbfEnabledUtxos,
502
498
  utxosDescendingOrder,
503
499
  rbfEnabled,
504
- })(
505
- { asset: maybeToken, walletAccount, address, amount: tokenAmount, options },
506
- { assetClientInterface }
507
- )
500
+ assetClientInterface,
501
+ })({ asset: maybeToken, walletAccount, address, amount: tokenAmount, options })
508
502
  const {
509
503
  amount,
510
504
  change,
@@ -37,7 +37,9 @@ export function createSignWithWallet({
37
37
  const { key, purpose, publicKey } = getKeyAndPurpose(address)
38
38
 
39
39
  const isP2SH = purpose === 49
40
- const isTaprootAddress = purpose === 86
40
+ const hasTapLeafScript =
41
+ psbt.data.inputs[index].tapLeafScript && psbt.data.inputs[index].tapLeafScript.length > 0
42
+ const isTaprootKeySpend = purpose === 86 && !hasTapLeafScript
41
43
 
42
44
  if (isP2SH) {
43
45
  // If spending from a P2SH address, we assume the address is P2SH wrapping
@@ -56,7 +58,7 @@ export function createSignWithWallet({
56
58
  } else {
57
59
  throw new Error('Expected P2SH script to be a nested segwit input')
58
60
  }
59
- } else if (isTaprootAddress && !Buffer.isBuffer(psbt.data.inputs[index].tapInternalKey)) {
61
+ } else if (isTaprootKeySpend && !Buffer.isBuffer(psbt.data.inputs[index].tapInternalKey)) {
60
62
  // tapInternalKey is metadata for signing and not part of the hash to sign.
61
63
  // so modifying it here is fine.
62
64
  psbt.updateInput(index, {
@@ -67,7 +69,7 @@ export function createSignWithWallet({
67
69
  // desktop / BE / mobile with bip-schnorr signing
68
70
  await psbt.signInputAsync(
69
71
  index,
70
- toAsyncSigner({ keyPair: key, isTaprootAddress, network }),
72
+ toAsyncSigner({ keyPair: key, isTaprootKeySpend, network }),
71
73
  allowedSigHashTypes
72
74
  )
73
75
  }
@@ -35,7 +35,7 @@ export const signTxFactory = ({ assetName, resolvePurpose, coinInfo, network })
35
35
 
36
36
  await signWithWallet(psbt, inputsToSign)
37
37
 
38
- const skipFinalize = !!unsignedTx.txData.psbtBuffer
38
+ const skipFinalize = !!unsignedTx.txData.psbtBuffer || unsignedTx.txMeta.returnPsbt
39
39
  return extractTransaction({ psbt, skipFinalize })
40
40
  }
41
41
  }
@@ -17,17 +17,21 @@ export function createPrepareForSigning({ assetName, resolvePurpose, coinInfo })
17
17
  assert(coinInfo, 'coinInfo is required')
18
18
 
19
19
  return ({ unsignedTx }) => {
20
+ const networkInfo = coinInfo.toBitcoinJS()
21
+
20
22
  const isPsbtBufferPassed =
21
23
  unsignedTx.txData.psbtBuffer &&
22
24
  unsignedTx.txMeta.addressPathsMap &&
23
25
  unsignedTx.txMeta.inputsToSign
24
26
  if (isPsbtBufferPassed) {
25
27
  // PSBT created externally (Web3, etc..)
26
- return createPsbtFromBuffer({ psbtBuffer: unsignedTx.txData.psbtBuffer })
28
+ return createPsbtFromBuffer({
29
+ psbtBuffer: unsignedTx.txData.psbtBuffer,
30
+ networkInfo,
31
+ })
27
32
  }
28
33
 
29
34
  // Create PSBT based on internal Exodus data structure
30
- const networkInfo = coinInfo.toBitcoinJS()
31
35
  const psbt = createPsbtFromTxData({
32
36
  ...unsignedTx.txData,
33
37
  ...unsignedTx.txMeta,
@@ -41,8 +45,8 @@ export function createPrepareForSigning({ assetName, resolvePurpose, coinInfo })
41
45
  }
42
46
 
43
47
  // Creates a PSBT instance from the passed transaction buffer provided by 3rd parties (e.g. dApps).
44
- function createPsbtFromBuffer({ psbtBuffer, ecc }) {
45
- return Psbt.fromBuffer(psbtBuffer, { eccLib: ecc })
48
+ function createPsbtFromBuffer({ psbtBuffer, ecc, networkInfo }) {
49
+ return Psbt.fromBuffer(psbtBuffer, { eccLib: ecc, network: networkInfo })
46
50
  }
47
51
 
48
52
  // Creates a PSBT instance from the passed inputs, outputs etc. The wallet itself provides this data.
@@ -54,7 +58,7 @@ function createPsbtFromTxData({ inputs, outputs, rawTxs, networkInfo, resolvePur
54
58
  const psbt = new Psbt({ maximumFeeRate, network: networkInfo })
55
59
 
56
60
  // Fill tx
57
- for (const { txId, vout, address, value, script, sequence } of inputs) {
61
+ for (const { txId, vout, address, value, script, sequence, tapLeafScript } of inputs) {
58
62
  // TODO: don't use the purpose as intermediate variable
59
63
  // see internals of `resolvePurposes`, just use `isP2TR, isP2SH etc directly
60
64
  const purpose = resolvePurpose(address)
@@ -64,6 +68,10 @@ function createPsbtFromTxData({ inputs, outputs, rawTxs, networkInfo, resolvePur
64
68
 
65
69
  const txIn = { hash: txId, index: vout, sequence }
66
70
  if (isSegwitAddress || isTaprootAddress) {
71
+ if (isTaprootAddress && tapLeafScript) {
72
+ txIn.tapLeafScript = tapLeafScript
73
+ }
74
+
67
75
  // witness outputs only require the value and the script, not the full transaction
68
76
  txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
69
77
  } else {
@@ -40,22 +40,22 @@ function tapTweakHash(pubKey, h) {
40
40
  /**
41
41
  * Take a sync signer and make it async.
42
42
  */
43
- export function toAsyncSigner({ keyPair, isTaprootAddress, network }) {
43
+ export function toAsyncSigner({ keyPair, isTaprootKeySpend, network }) {
44
44
  assert(keyPair, 'keyPair is required')
45
45
 
46
- if (isTaprootAddress) {
46
+ if (isTaprootKeySpend) {
47
47
  keyPair = tweakSigner({ signer: keyPair, ECPair, network })
48
48
  }
49
49
 
50
50
  // eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
51
51
  keyPair.sign = async (h) => {
52
- const sig = await ecc.signAsync(h, keyPair.privateKey)
52
+ const sig = ecc.sign(h, keyPair.privateKey)
53
53
  return Buffer.from(sig)
54
54
  }
55
55
 
56
56
  // eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
57
57
  keyPair.signSchnorr = async (h) => {
58
- const sig = await ecc.signSchnorrAsync(h, keyPair.privateKey, getSchnorrEntropy())
58
+ const sig = ecc.signSchnorr(h, keyPair.privateKey, getSchnorrEntropy())
59
59
  return Buffer.from(sig)
60
60
  }
61
61
 
@@ -1,81 +0,0 @@
1
- import secp256k1 from '@exodus/secp256k1'
2
- import { toXOnly } from '../ecc-utils'
3
-
4
- /**
5
- * Common ecc functions between mobile and desktop. Once mobile accepts @noble/secp256k1, we can unify both
6
- */
7
- export const common = {
8
- /**
9
- *
10
- * @param msg32 {Uint8Array} the message
11
- * @param seckey {Uint8Array} the private key
12
- * @param data {Uint8Array} the data to sign
13
- */
14
- sign: (msg32, seckey, data) => secp256k1.ecdsaSign(msg32, seckey, { data }).signature,
15
-
16
- /**
17
- *
18
- * @param msg32 {Uint8Array} the message to verify
19
- * @param publicKey {Uint8Array} the public key
20
- * @param signature {Uint8Array} the signature
21
- * @returns {boolean}
22
- */
23
- verify: (msg32, publicKey, signature) => secp256k1.ecdsaVerify(signature, msg32, publicKey),
24
-
25
- /**
26
- *
27
- * @param seckey {Uint8Array}
28
- * @param tweak {Uint8Array}
29
- */
30
- privateAdd: (seckey, tweak) =>
31
- // cloning input. secp256k1 modifies it and it cannot be reused/cached
32
- secp256k1.privateKeyTweakAdd(Buffer.from(seckey), tweak),
33
-
34
- /**
35
- * @param seckey {Uint8Array} the public key
36
- */
37
- privateNegate: (seckey) =>
38
- // cloning input. secp256k1 modifies it and it cannot be reused/cached
39
- secp256k1.privateKeyNegate(Buffer.from(seckey)),
40
-
41
- /**
42
- *
43
- * @param publicKey {Uint8Array}
44
- * @param tweak {Uint8Array}
45
- * @returns {{parity: (number), xOnlyPubkey}|null}
46
- */
47
- xOnlyPointAddTweak: (publicKey, tweak) => {
48
- try {
49
- const t = secp256k1.publicKeyTweakAdd(toPubKey(publicKey), tweak)
50
- return {
51
- parity: t[0] === 0x02 ? 0 : 1,
52
- xOnlyPubkey: toXOnly(t),
53
- }
54
- } catch {
55
- return null
56
- }
57
- },
58
-
59
- /**
60
- * @param seckey {Uint8Array}
61
- * @returns {boolean}
62
- */
63
- isPrivate: (seckey) => secp256k1.privateKeyVerify(seckey),
64
- /**
65
- * @param seckey {Uint8Array}
66
- * @param compressed {boolean}
67
- */
68
- pointFromScalar: (seckey, compressed) => secp256k1.publicKeyCreate(seckey, compressed),
69
- }
70
-
71
- /**
72
- *
73
- * @param xOnly {Uint8Array}
74
- * @returns {Uint8Array}
75
- */
76
- export const toPubKey = (xOnly) => {
77
- const p = new Uint8Array(33)
78
- p.set([0x02])
79
- p.set(xOnly, 1)
80
- return p
81
- }
@@ -1,83 +0,0 @@
1
- import { initEccLib } from '@exodus/bitcoinjs-lib'
2
- import { Point, schnorr, sign } from '@noble/secp256k1'
3
- import { common, toPubKey } from './common'
4
-
5
- /**
6
- * Wrapper around `secp256k1` in order to follow the bitcoinjs-lib `TinySecp256k1Interface`
7
- * Schnorr signatures are offered by @noble/secp256k1
8
- */
9
- export const desktopEcc = {
10
- ...common,
11
-
12
- /**
13
- * @param message {Uint8Array}
14
- * @param privateKey {Uint8Array}
15
- * @param extraEntropy {Uint8Array}
16
- * @returns {Promise<Uint8Array>}
17
- */
18
- signAsync: async (message, privateKey, extraEntropy) =>
19
- sign(message, privateKey, {
20
- extraEntropy,
21
- canonical: true,
22
- der: false,
23
- }),
24
-
25
- /**
26
- * @param message {Uint8Array}
27
- * @param privateKey {Uint8Array}
28
- * @param extraEntropy {Uint8Array}
29
- * @returns {Promise<Uint8Array>}
30
- */
31
- signSchnorrAsync: async (message, privateKey, extraEntropy) =>
32
- schnorr.sign(message, privateKey, extraEntropy),
33
-
34
- /**
35
- * @param message {Uint8Array}
36
- * @param publicKey {Uint8Array}
37
- * @param signature {Uint8Array}
38
- * @returns {Promise<boolean>}
39
- */
40
- verifySchnorrAsync: async (message, publicKey, signature) =>
41
- schnorr.verify(signature, message, publicKey),
42
-
43
- // The underlying library does not expose sync functions for Schnorr sign and verify.
44
- // These function are explicitly defined here as `null` for documentation purposes.
45
- // Update, latest version of https://github.com/paulmillr/noble-secp256k1 does support SYNC
46
- signSchnorr: null,
47
- verifySchnorr: null,
48
-
49
- /**
50
- * @param publicKey {Uint8Array}
51
- * @returns {boolean}
52
- */
53
- isPoint: (publicKey) => {
54
- try {
55
- Point.fromHex(publicKey).assertValidity()
56
- return true
57
- } catch {
58
- return false
59
- }
60
- },
61
-
62
- /**
63
- * @param publicKey {Uint8Array}
64
- * @returns {boolean}
65
- */
66
- isXOnlyPoint: (publicKey) => {
67
- try {
68
- Point.fromHex(toPubKey(publicKey)).assertValidity()
69
- return true
70
- } catch {
71
- return false
72
- }
73
- },
74
-
75
- /**
76
- * @param publicKey {Uint8Array}
77
- * @param compressed {boolean}
78
- * @returns {Uint8Array}
79
- */
80
- pointCompress: (publicKey, compressed) => Point.fromHex(publicKey).toRawBytes(compressed),
81
- }
82
-
83
- initEccLib(desktopEcc)