@exodus/bitcoin-api 1.0.1 → 1.0.2

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": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Exodus bitcoin-api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -31,10 +31,12 @@
31
31
  "ecpair": "2.0.1",
32
32
  "querystring": "0.2.0",
33
33
  "socket.io-client": "2.1.1",
34
+ "tiny-secp256k1": "1.1.3",
34
35
  "url-join": "4.0.0"
35
36
  },
36
37
  "devDependencies": {
38
+ "@exodus/bip-schnorr": "0.6.6-fork-1",
37
39
  "@noble/secp256k1": "~1.5.3"
38
40
  },
39
- "gitHead": "16aa341d1ce1b6a86dba8516bf19b361a547d880"
41
+ "gitHead": "6bbd1f431656185196b2dbe74a9347a29e6bb3b1"
40
42
  }
@@ -23,6 +23,7 @@ export const desktopEcc: TinySecp256k1Interface = {
23
23
 
24
24
  // The underlying library does not expose sync functions for Schnorr sign and verify.
25
25
  // These function are explicitly defined here as `null` for documentation purposes.
26
+ // Update, latest version of https://github.com/paulmillr/noble-secp256k1 does support SYNC
26
27
  signSchnorr: null,
27
28
  verifySchnorr: null,
28
29
 
@@ -1,2 +1,9 @@
1
- export const eccFactory = (useDesktopEcc) =>
2
- useDesktopEcc ? require('./desktop').desktopEcc : require('./mobile').mobileEcc
1
+ export const eccFactory = (useDesktopEcc, useSchnorrEcc) => {
2
+ if (useDesktopEcc) {
3
+ return require('./desktop').desktopEcc
4
+ }
5
+ if (useSchnorrEcc) {
6
+ return require('./mobile-schnorr').mobileSchnorrEcc
7
+ }
8
+ return require('./mobile').mobileEcc
9
+ }
@@ -0,0 +1,9 @@
1
+ import assert from 'minimalistic-assert'
2
+
3
+ export const eccFactory = (useDesktopEcc, useSchnorrEcc) => {
4
+ assert(!useDesktopEcc, 'useDesktopEcc must be false on mobile!!')
5
+ if (useSchnorrEcc) {
6
+ return require('./mobile-schnorr').mobileSchnorrEcc
7
+ }
8
+ return require('./mobile').mobileEcc
9
+ }
@@ -0,0 +1,58 @@
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
+ }
@@ -1,7 +1,7 @@
1
1
  import { TinySecp256k1Interface } from '@exodus/bitcoinjs-lib'
2
2
  import secp256k1 from '@exodus/secp256k1'
3
3
  // TODO: temp import until '@noble/secp256k1' can be used
4
- import tinySecp256k1 from 'tiny-secp256k1'
4
+ import { isPoint } from 'tiny-secp256k1'
5
5
  import { common, toPubKey } from './common'
6
6
 
7
7
  /**
@@ -28,8 +28,8 @@ export const mobileEcc: TinySecp256k1Interface = {
28
28
  isPoint: (p: Uint8Array): boolean => {
29
29
  try {
30
30
  // temp solution secp256k1 does not actually verify the value range, only the data length
31
- return tinySecp256k1.isPoint(Buffer.from(p))
32
- } catch {
31
+ return isPoint(Buffer.from(p))
32
+ } catch (err) {
33
33
  return false
34
34
  }
35
35
  },
@@ -37,7 +37,7 @@ export const mobileEcc: TinySecp256k1Interface = {
37
37
  isXOnlyPoint: (p: Uint8Array): boolean => {
38
38
  try {
39
39
  // temp solution secp256k1 does not actually verify the value range, only the data length
40
- return tinySecp256k1.isPoint(Buffer.from(toPubKey(p)))
40
+ return isPoint(Buffer.from(toPubKey(p)))
41
41
  } catch (err) {
42
42
  return false
43
43
  }
package/src/index.js CHANGED
@@ -15,3 +15,4 @@ export * from './tx-log'
15
15
  export * from './unconfirmed-ancestor-data'
16
16
  export * from './parse-unsigned-tx'
17
17
  export * from './insight-api-client/util'
18
+ export * from './move-funds'
@@ -0,0 +1,172 @@
1
+ import wif from 'wif'
2
+ import { UtxoCollection, Address } from '@exodus/models'
3
+ import { createInputs, createOutput, getNonWitnessTxs } from './tx-send'
4
+ import assert from 'minimalistic-assert'
5
+ import assets from '@exodus/assets'
6
+
7
+ export const moveFundsFactory = ({ insightClient, getFeeEstimator, keys, signTx, address }) => {
8
+ assert(insightClient, 'insightClient is required')
9
+ assert(getFeeEstimator, 'getFeeEstimator is required')
10
+ assert(address, 'address is required')
11
+ assert(keys, 'keys is required')
12
+ assert(signTx, 'signTx is required')
13
+ async function prepareFunds(assetName, input, options = {}) {
14
+ const asset = assets[assetName]
15
+ const { toAddress, assetClientInterface, MoveFundsError, walletAccount } = options
16
+ assert(MoveFundsError, 'MoveFundsError is required') // should we move MoveFundsError to asset libs?
17
+ assert(toAddress, 'toAddress is required')
18
+ assert(assetClientInterface, 'assetClientInterface is required')
19
+ assert(walletAccount, 'walletAccount is required')
20
+
21
+ const formatProps = {
22
+ asset,
23
+ input,
24
+ }
25
+ const privateKey = input
26
+
27
+ if (!isValidPrivateKey(privateKey)) {
28
+ throw new MoveFundsError('private-key-invalid', formatProps)
29
+ }
30
+
31
+ const { compressed } = wif.decode(privateKey)
32
+ const addresses = getAllAddressesFromWIF(privateKey)
33
+
34
+ const receiveAddresses = await assetClientInterface.getReceiveAddresses({
35
+ walletAccount,
36
+ assetName,
37
+ multiAddressMode: true,
38
+ })
39
+
40
+ let found, address, utxos
41
+ for (address of addresses) {
42
+ const selfSend = receiveAddresses.some(
43
+ (receiveAddress) => String(receiveAddress) === String(address)
44
+ )
45
+ if (selfSend) {
46
+ throw new MoveFundsError('private-key-own-key', formatProps)
47
+ }
48
+
49
+ utxos = await getUtxos({ asset, address })
50
+
51
+ if (!utxos.value.isZero) {
52
+ found = true
53
+ break
54
+ }
55
+ }
56
+ if (!found) {
57
+ formatProps.fromAddress = addresses.join(' or ')
58
+ throw new MoveFundsError('balance-zero', formatProps)
59
+ }
60
+ const fromAddress = address
61
+ formatProps.fromAddress = fromAddress
62
+
63
+ const feeData = await assetClientInterface.getFeeConfig({ assetName })
64
+ const fee = getFee({ asset, feeData, utxos, compressed })
65
+
66
+ let amount = utxos.value.sub(fee)
67
+ if (amount.isNegative) {
68
+ throw new MoveFundsError('balance-negative', formatProps)
69
+ }
70
+
71
+ return { fromAddress, toAddress, amount, fee, utxos, privateKey }
72
+ }
73
+
74
+ const sendFunds = async (
75
+ assetName,
76
+ { fromAddress, toAddress, amount, fee, utxos, privateKey }
77
+ ) => {
78
+ assert(fromAddress, 'fromAddress is required')
79
+ assert(toAddress, 'toAddress is required')
80
+ assert(fee, 'fee is required')
81
+ assert(utxos, 'utxos is required')
82
+ assert(privateKey, 'privateKey is required')
83
+ const selected = utxos
84
+ const privateKeysAddressMap = {
85
+ [fromAddress]: privateKey,
86
+ }
87
+ const unsignedTx = {
88
+ txData: {
89
+ inputs: createInputs(assetName, selected.toArray()),
90
+ outputs: [createOutput(assetName, toAddress, amount)],
91
+ },
92
+ txMeta: {
93
+ addressPathsMap: selected.getAddressPathsMap(),
94
+ },
95
+ }
96
+ const nonWitnessTxs = await getNonWitnessTxs(
97
+ { name: assetName, address }, // pretty ugly hack!
98
+ selected,
99
+ insightClient
100
+ )
101
+ Object.assign(unsignedTx.txMeta, { rawTxs: nonWitnessTxs })
102
+
103
+ const { rawTx, txId } = await signTx({ unsignedTx, privateKeysAddressMap })
104
+
105
+ await insightClient.broadcastTx(rawTx.toString('hex'))
106
+
107
+ return { txId, fromAddress, toAddress, amount, fee }
108
+ }
109
+
110
+ function getAllAddressesFromWIF(privateKeyWIF) {
111
+ const { compressed } = wif.decode(privateKeyWIF)
112
+ const legacyAddress = keys.encodePublicFromWIF(privateKeyWIF)
113
+ // TODO: support nested segwit address, right now we don't support send
114
+ // const nestedSegwitAddress = encodeNestedSegwitFromWIF(privateKeyWIF, { asset })
115
+ const nativeSegwitAddress = keys.encodePublicBech32FromWIF(privateKeyWIF)
116
+
117
+ if (compressed) {
118
+ return [nativeSegwitAddress, legacyAddress]
119
+ } else {
120
+ return [legacyAddress]
121
+ }
122
+ }
123
+
124
+ /*
125
+ function getPublicFromWIF(privateKeyWIF, { asset }) {
126
+ const { versions } = asset.coinInfo
127
+ const { privateKey, compressed } = wif.decode(privateKeyWIF, versions.private)
128
+ return secp256k1.pointFromScalar(privateKey, compressed)
129
+ }
130
+
131
+ function encodeNestedSegwitFromWIF(privateKeyWIF, { asset }) {
132
+ const publicKey = getPublicFromWIF(privateKeyWIF, { asset })
133
+ const witnessProgram = bitcoinjs.payments.p2wpkh({ pubkey: publicKey }).output
134
+ const witnessProgramHash = bitcoinjs.crypto.hash160(witnessProgram)
135
+ return bitcoinjs.address.toBase58Check(witnessProgramHash, asset.coinInfo.versions.scripthash)
136
+ }
137
+ */
138
+
139
+ async function getUtxos({ asset, address }) {
140
+ const rawUtxos = await insightClient.fetchUTXOs([address])
141
+ return UtxoCollection.fromArray(
142
+ rawUtxos.map((utxo) => ({
143
+ txId: utxo.txId,
144
+ vout: utxo.vout,
145
+ value: asset.currency.defaultUnit(utxo.value),
146
+ address: Address.create(utxo.address, { path: 'm/0/0' }),
147
+ script: utxo.script,
148
+ })),
149
+ { currency: asset.currency }
150
+ )
151
+ }
152
+
153
+ function getFee({ asset, feeData, utxos, compressed }) {
154
+ const { feePerKB } = feeData
155
+ const feeEstimator = getFeeEstimator(asset, feePerKB, { compressed })
156
+ return feeEstimator({ inputs: utxos, outputs: [null] }).toDefault()
157
+ }
158
+
159
+ function isValidPrivateKey(privateKey) {
160
+ try {
161
+ wif.decode(privateKey)
162
+ return true
163
+ } catch (err) {
164
+ return false
165
+ }
166
+ }
167
+
168
+ return {
169
+ prepareFunds,
170
+ sendFunds,
171
+ }
172
+ }
@@ -18,9 +18,10 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
18
18
  assert(keys, 'keys is required')
19
19
  assert(coinInfo, 'coinInfo is required')
20
20
  assert(ecc, 'ecc is required')
21
- return async ({ unsignedTx, hdkeys }): Object => {
21
+ return async ({ unsignedTx, hdkeys, privateKeysAddressMap }): Object => {
22
22
  assert(unsignedTx, 'unsignedTx is required')
23
- const { privateKeysAddressMap, addressPathsMap, rawTxs } = unsignedTx.txMeta
23
+ assert(hdkeys || privateKeysAddressMap, 'hdkeys or privateKeysAddressMap is required')
24
+ const { addressPathsMap, rawTxs } = unsignedTx.txMeta
24
25
  const { inputs, outputs } = unsignedTx.txData
25
26
  const networkInfo = { ...coinInfo.toBitcoinJS(), messagePrefix: '' }
26
27
 
@@ -36,9 +37,12 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
36
37
 
37
38
  const getKeyAndPurpose = lodash.memoize((address) => {
38
39
  // TODO: Consider using privateKeysAddressMap for other assets
39
- if (privateKeysAddressMap) return ECPair.fromWIF(privateKeysAddressMap[address], networkInfo)
40
- const path = addressPathsMap[address]
41
40
  const purpose = resolvePurpose(address)
41
+ if (privateKeysAddressMap) {
42
+ const key = ECPair.fromWIF(privateKeysAddressMap[address], networkInfo)
43
+ return { key, purpose }
44
+ }
45
+ const path = addressPathsMap[address]
42
46
  assert(hdkeys, 'hdkeys must be provided')
43
47
  assert(purpose, `purpose for address ${address} could not be resolved`)
44
48
  const hdkey = hdkeys[purpose]
@@ -76,7 +80,7 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
76
80
  const { address } = inputs[index]
77
81
  const { key, purpose } = getKeyAndPurpose(address)
78
82
  if (ecc.signSchnorrAsync) {
79
- // desktop / BE signing
83
+ // desktop / BE / mobile with bip-schnorr signing
80
84
  const isTaprootAddress = purpose === 86
81
85
  const signingKey = isTaprootAddress
82
86
  ? tweakSigner({ signer: key, ECPair, ecc, network })