@exodus/bitcoin-api 2.0.0 → 2.1.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.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Exodus bitcoin-api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -40,5 +40,5 @@
40
40
  "@exodus/bip-schnorr": "0.6.6-fork-1",
41
41
  "@noble/secp256k1": "~1.5.3"
42
42
  },
43
- "gitHead": "53103ac1ac2c6fd96111869a13893b6c13bbcb3a"
43
+ "gitHead": "667523b6a37cb6d9755082df52c812a86cda594d"
44
44
  }
@@ -1,6 +1,3 @@
1
- export const eccFactory = (useSchnorrEcc) => {
2
- if (useSchnorrEcc) {
3
- return require('./mobile-schnorr').mobileSchnorrEcc
4
- }
5
- return require('./mobile').mobileEcc
6
- }
1
+ import { mobileEcc } from './mobile'
2
+
3
+ export const eccFactory = () => mobileEcc
@@ -3,27 +3,37 @@ import secp256k1 from '@exodus/secp256k1'
3
3
  // TODO: temp import until '@noble/secp256k1' can be used
4
4
  import { isPoint } from 'tiny-secp256k1'
5
5
  import { common, toPubKey } from './common'
6
+ import schnorr from '@exodus/bip-schnorr'
6
7
 
7
8
  /**
8
9
  * Wrapper around `secp256k1` in order to follow the bitcoinjs-lib `TinySecp256k1Interface`
9
- * Schnorr signatures are offered by @noble/secp256k1
10
+ * Schnorr signatures are offered by @exodus/bip-schnorr
11
+ *
10
12
  */
13
+
11
14
  export const mobileEcc: TinySecp256k1Interface = {
12
15
  ...common,
13
16
 
14
17
  signAsync: async (h: Uint8Array, d: Uint8Array, extraEntropy?: Uint8Array): Uint8Array =>
15
18
  secp256k1.ecdsaSign(h, d, { data: extraEntropy }).signature,
16
19
 
17
- // TODO: waiting for the '@noble/secp256k1' lib
18
- // signSchnorrAsync: async (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array =>
19
- // schnorr.sign(h, d, e),
20
- // verifySchnorrAsync: async (h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean =>
21
- // schnorr.verify(signature, h, Q),
20
+ signSchnorr: (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array =>
21
+ schnorr.sign(d.toString('hex'), h, e),
22
+
23
+ signSchnorrAsync: async (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array =>
24
+ mobileEcc.signSchnorr(h, d, e),
25
+
26
+ verifySchnorr: (h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean => {
27
+ try {
28
+ schnorr.verify(Q, h, signature)
29
+ return true
30
+ } catch (e) {
31
+ return false
32
+ }
33
+ },
22
34
 
23
- // The underlying library does not expose sync functions for Schnorr sign and verify.
24
- // These function are explicitly defined here as `null` for documentation purposes.
25
- signSchnorr: null,
26
- verifySchnorr: null,
35
+ verifySchnorrAsync: async (h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean =>
36
+ mobileEcc.verifySchnorr(h, Q, signature),
27
37
 
28
38
  isPoint: (p: Uint8Array): boolean => {
29
39
  try {
@@ -6,6 +6,17 @@ import assert from 'minimalistic-assert'
6
6
  import { identity, pickBy } from 'lodash'
7
7
  import * as defaultBitcoinjsLib from '@exodus/bitcoinjs-lib'
8
8
 
9
+ export const publicKeyToHashFactory = (p2pkh) => (publicKey) => {
10
+ const sha = createHash('sha256')
11
+ .update(publicKey)
12
+ .digest()
13
+ const pubKeyHash = createHash('rmd160')
14
+ .update(sha)
15
+ .digest()
16
+ const payload = Buffer.concat([Buffer.from([p2pkh]), pubKeyHash])
17
+ return bs58check.encode(payload)
18
+ }
19
+
9
20
  export const createBtcLikeKeys = ({
10
21
  coinInfo,
11
22
  versions,
@@ -32,18 +43,7 @@ export const createBtcLikeKeys = ({
32
43
  ((privateKey, compressed = true) => {
33
44
  return wif.encode(coinInfo.versions.private, privateKey, compressed)
34
45
  })
35
- const encodePublicPurpose44 =
36
- encodePublicCustom ||
37
- ((publicKey) => {
38
- const sha = createHash('sha256')
39
- .update(publicKey)
40
- .digest()
41
- const pubKeyHash = createHash('rmd160')
42
- .update(sha)
43
- .digest()
44
- const payload = Buffer.concat([Buffer.from([versions.p2pkh]), pubKeyHash])
45
- return bs58check.encode(payload)
46
- })
46
+ const encodePublicPurpose44 = encodePublicCustom || publicKeyToHashFactory(versions.p2pkh)
47
47
  const encodePublicFromWIF =
48
48
  encodePublicFromWIFCustom ||
49
49
  ((privateKeyWIF) => {
@@ -15,7 +15,6 @@ const _canBumpTx = ({
15
15
  accountState,
16
16
  feeData,
17
17
  getFeeEstimator,
18
- taprootEnabled,
19
18
  allowUnconfirmedRbfEnabledUtxos,
20
19
  }) => {
21
20
  assert(asset, 'asset must be provided')
@@ -58,7 +57,6 @@ const _canBumpTx = ({
58
57
  utxos,
59
58
  feeData,
60
59
  txSet,
61
- taprootEnabled,
62
60
  allowUnconfirmedRbfEnabledUtxos,
63
61
  })
64
62
  if (usableUtxos.value.isZero) return { errorMessage: 'insufficient funds' }
@@ -6,13 +6,11 @@ import { canBumpTx } from './can-bump-tx'
6
6
 
7
7
  export class GetFeeResolver {
8
8
  #getFeeEstimator
9
- #taprootEnabled
10
9
  #allowUnconfirmedRbfEnabledUtxos
11
10
 
12
- constructor({ getFeeEstimator, taprootEnabled, allowUnconfirmedRbfEnabledUtxos }) {
11
+ constructor({ getFeeEstimator, allowUnconfirmedRbfEnabledUtxos }) {
13
12
  assert(getFeeEstimator, 'getFeeEstimator must be provided')
14
13
  this.#getFeeEstimator = (asset, { feePerKB }) => getFeeEstimator(asset, feePerKB)
15
- this.#taprootEnabled = taprootEnabled
16
14
  this.#allowUnconfirmedRbfEnabledUtxos = allowUnconfirmedRbfEnabledUtxos
17
15
  }
18
16
 
@@ -48,7 +46,6 @@ export class GetFeeResolver {
48
46
  utxos,
49
47
  feeData,
50
48
  txSet,
51
- taprootEnabled: this.#taprootEnabled,
52
49
  allowUnconfirmedRbfEnabledUtxos: this.#allowUnconfirmedRbfEnabledUtxos,
53
50
  })
54
51
  return spendableUtxos.value
@@ -67,7 +64,6 @@ export class GetFeeResolver {
67
64
  utxos,
68
65
  feeData,
69
66
  txSet,
70
- taprootEnabled: this.#taprootEnabled,
71
67
  })
72
68
  const replaceableTxs = findUnconfirmedSentRbfTxs(txSet)
73
69
 
@@ -97,7 +93,7 @@ export class GetFeeResolver {
97
93
  accountState,
98
94
  feeData,
99
95
  getFeeEstimator: this.#getFeeEstimator,
100
- taprootEnabled: this.#taprootEnabled,
96
+
101
97
  allowUnconfirmedRbfEnabledUtxos: this.#allowUnconfirmedRbfEnabledUtxos,
102
98
  })
103
99
  }
@@ -144,7 +144,17 @@ export class BitcoinMonitorScanner {
144
144
  chainIndex,
145
145
  addressIndex,
146
146
  })
147
- .then((address) => ({ address, purpose }))
147
+ .then((address) => {
148
+ return {
149
+ address: this.#asset.address.toLegacyAddress
150
+ ? Address.create(
151
+ this.#asset.address.toLegacyAddress(String(address)),
152
+ address.meta
153
+ )
154
+ : address,
155
+ purpose,
156
+ }
157
+ })
148
158
  )
149
159
  }
150
160
  }
@@ -365,15 +375,17 @@ export class BitcoinMonitorScanner {
365
375
  // this is an array because legacy multisig has multiple addresses
366
376
  if (!Array.isArray(vout.scriptPubKey.addresses)) return
367
377
  if (vout.scriptPubKey.addresses.length === 0) return
368
- if (!addrMap[vout.scriptPubKey.addresses[0]]) {
378
+ const sentAddress = vout.scriptPubKey.addresses[0]
379
+ if (!addrMap[sentAddress]) {
369
380
  if (isSent && !txLogItem.to) {
370
381
  const val = currency.defaultUnit(vout.value)
371
- txLogItem.data.sent.push({ address: vout.scriptPubKey.addresses[0], amount: val })
382
+ const sentDisplayAddress = asset.address.displayAddress?.(sentAddress) || sentAddress
383
+ txLogItem.data.sent.push({ address: sentDisplayAddress, amount: val })
372
384
  }
373
385
  return
374
386
  }
375
387
 
376
- const address = addrMap[vout.scriptPubKey.addresses[0]]
388
+ const address = addrMap[sentAddress]
377
389
  if (isReceiveAddress(address)) {
378
390
  txLogItem.addresses.push(address)
379
391
  }
@@ -403,13 +415,11 @@ export class BitcoinMonitorScanner {
403
415
  utxos.push(output) // but save the unspent ones for state.utxos
404
416
  })
405
417
 
406
- // BCH: convert to cash format
407
- if (assetName === 'bcash') {
418
+ if (this.#asset.address.displayAddress) {
408
419
  if (txLogItem.to) {
409
- txLogItem.to = asset.address.toCashAddress(txLogItem.to)
420
+ txLogItem.to = asset.address.displayAddress(txLogItem.to)
410
421
  }
411
-
412
- from = from.map(asset.address.toCashAddress)
422
+ from = from.map(asset.address.displayAddress)
413
423
  }
414
424
 
415
425
  if (isSent) {
@@ -84,7 +84,7 @@ export async function getNonWitnessTxs(asset, utxos, insightClient) {
84
84
 
85
85
  export const createAndBroadcastTXFactory = ({
86
86
  getFeeEstimator,
87
- taprootEnabled,
87
+
88
88
  allowUnconfirmedRbfEnabledUtxos,
89
89
  }) => async ({ asset, walletAccount, address, amount, options }, { assetClientInterface }) => {
90
90
  const {
@@ -126,7 +126,6 @@ export const createAndBroadcastTXFactory = ({
126
126
  utxos: getUtxos({ accountState, asset }),
127
127
  feeData,
128
128
  txSet,
129
- taprootEnabled,
130
129
  })
131
130
 
132
131
  let replaceableTxs = findUnconfirmedSentRbfTxs(txSet)
@@ -339,16 +338,15 @@ export const createAndBroadcastTXFactory = ({
339
338
  ? !replaceTx
340
339
  : receiveAddresses.some((receiveAddress) => String(receiveAddress) === String(address))
341
340
 
342
- if (['bcash'].includes(assetName)) {
343
- receiveAddress = asset.address.toCashAddress(receiveAddress)
344
- }
341
+ const displayReceiveAddress = asset.address.displayAddress?.(receiveAddress) || receiveAddress
342
+
345
343
  const receivers = bumpTxId
346
344
  ? replaceTx
347
345
  ? replaceTx.data.sent
348
346
  : []
349
347
  : replaceTx
350
- ? replaceTx.data.sent.concat([{ address: receiveAddress, amount }])
351
- : [{ address: receiveAddress, amount }]
348
+ ? replaceTx.data.sent.concat([{ address: displayReceiveAddress, amount }])
349
+ : [{ address: displayReceiveAddress, amount }]
352
350
 
353
351
  await assetClientInterface.updateTxLogAndNotify({
354
352
  assetName,
@@ -366,7 +364,6 @@ export const createAndBroadcastTXFactory = ({
366
364
  feePerKB: ['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)
367
365
  ? fee.div(tx.virtualSize() / 1000).toBaseNumber()
368
366
  : undefined,
369
- additionalTo: bumpTxId ? undefined : [{ address: receiveAddress, amount }],
370
367
  changeAddress: changeOutput ? ourAddress : undefined,
371
368
  blockHeight,
372
369
  blocksSeen: 0,
@@ -12,11 +12,7 @@ export function getUtxos({ accountState, asset }) {
12
12
  )
13
13
  }
14
14
 
15
- export const getBalancesFactory = ({
16
- taprootEnabled,
17
- feeData,
18
- allowUnconfirmedRbfEnabledUtxos,
19
- }) => {
15
+ export const getBalancesFactory = ({ feeData, allowUnconfirmedRbfEnabledUtxos }) => {
20
16
  assert(feeData, 'feeData is required')
21
17
  return ({ asset, accountState, txLog }) => {
22
18
  assert(asset, 'asset is required')
@@ -29,15 +25,13 @@ export const getBalancesFactory = ({
29
25
  utxos,
30
26
  txSet: txLog,
31
27
  feeData,
32
- taprootEnabled,
28
+
33
29
  allowUnconfirmedRbfEnabledUtxos,
34
30
  }).value
35
31
  return { balance, spendableBalance }
36
32
  }
37
33
  }
38
34
 
39
- const isTaprootUtxo = ({ utxo }) => String(utxo.address).length === 62
40
-
41
35
  export function getConfirmedUtxos({ asset, utxos }) {
42
36
  assert(asset, 'asset is required')
43
37
  assert(utxos, 'utxos is required')
@@ -65,18 +59,12 @@ export function getConfirmedOrRfbDisabledUtxos({ asset, utxos, allowUnconfirmedR
65
59
  )
66
60
  }
67
61
 
68
- export function getUsableUtxos({ asset, utxos, feeData, txSet, taprootEnabled }) {
62
+ export function getUsableUtxos({ asset, utxos, feeData, txSet }) {
69
63
  assert(asset, 'asset is required')
70
64
  assert(utxos, 'utxos is required')
71
65
  assert(feeData, 'feeData is required')
72
66
  assert(txSet, 'txSet is required')
73
67
  if (!['bitcoin', 'bitcointestnet', 'bitcoinregtest'].includes(asset.name)) return utxos
74
- if (!taprootEnabled) {
75
- utxos = UtxoCollection.fromArray(
76
- utxos.toArray().filter((utxo) => !isTaprootUtxo({ utxo })),
77
- { currency: asset.currency }
78
- )
79
- }
80
68
  const { fastestFee } = feeData
81
69
  const feeRate = fastestFee.toBaseNumber()
82
70
  const maxFee = feeData.maxExtraCpfpFee
@@ -101,10 +89,10 @@ export function getSpendableUtxos({
101
89
  utxos,
102
90
  feeData,
103
91
  txSet,
104
- taprootEnabled,
92
+
105
93
  allowUnconfirmedRbfEnabledUtxos,
106
94
  }) {
107
- const usableUtxos = getUsableUtxos({ asset, utxos, feeData, txSet, taprootEnabled })
95
+ const usableUtxos = getUsableUtxos({ asset, utxos, feeData, txSet })
108
96
  return getConfirmedOrRfbDisabledUtxos({
109
97
  asset,
110
98
  utxos: usableUtxos,
@@ -1,58 +0,0 @@
1
- import { TinySecp256k1Interface } from '@exodus/bitcoinjs-lib'
2
- import secp256k1 from '@exodus/secp256k1'
3
- // TODO: temp import until '@noble/secp256k1' can be used
4
- import { isPoint } from 'tiny-secp256k1'
5
- import { common, toPubKey } from './common'
6
- import schnorr from '@exodus/bip-schnorr'
7
-
8
- /**
9
- * Wrapper around `secp256k1` in order to follow the bitcoinjs-lib `TinySecp256k1Interface`
10
- * Schnorr signatures are offered by @exodus/bip-schnorr
11
- *
12
- */
13
-
14
- export const mobileSchnorrEcc: TinySecp256k1Interface = {
15
- ...common,
16
-
17
- signAsync: async (h: Uint8Array, d: Uint8Array, extraEntropy?: Uint8Array): Uint8Array =>
18
- secp256k1.ecdsaSign(h, d, { data: extraEntropy }).signature,
19
-
20
- signSchnorr: (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array =>
21
- schnorr.sign(d.toString('hex'), h, e),
22
-
23
- signSchnorrAsync: async (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array =>
24
- mobileSchnorrEcc.signSchnorr(h, d, e),
25
-
26
- verifySchnorr: (h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean => {
27
- try {
28
- schnorr.verify(Q, h, signature)
29
- return true
30
- } catch (e) {
31
- return false
32
- }
33
- },
34
-
35
- verifySchnorrAsync: async (h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean =>
36
- mobileSchnorrEcc.verifySchnorr(h, Q, signature),
37
-
38
- isPoint: (p: Uint8Array): boolean => {
39
- try {
40
- // temp solution secp256k1 does not actually verify the value range, only the data length
41
- return isPoint(Buffer.from(p))
42
- } catch (err) {
43
- return false
44
- }
45
- },
46
-
47
- isXOnlyPoint: (p: Uint8Array): boolean => {
48
- try {
49
- // temp solution secp256k1 does not actually verify the value range, only the data length
50
- return isPoint(Buffer.from(toPubKey(p)))
51
- } catch (err) {
52
- return false
53
- }
54
- },
55
-
56
- pointCompress: (p: Uint8Array, compressed?: boolean): Uint8Array =>
57
- secp256k1.publicKeyConvert(p, compressed),
58
- }