@exodus/bitcoin-api 1.0.0 → 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.0",
3
+ "version": "1.0.2",
4
4
  "description": "Exodus bitcoin-api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -31,9 +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
+ },
41
+ "gitHead": "6bbd1f431656185196b2dbe74a9347a29e6bb3b1"
39
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
  }
@@ -1,6 +1,5 @@
1
1
  // @flow
2
2
  import assert from 'minimalistic-assert'
3
- import { get } from 'lodash'
4
3
  import * as varuint from 'varuint-bitcoin'
5
4
  import { UtxoCollection } from '@exodus/models'
6
5
 
@@ -53,13 +52,16 @@ const scriptPubKeyLengths = {
53
52
  // 10 = version: 4, locktime: 4, inputs and outputs count: 1
54
53
  // 148 = txId: 32, vout: 4, count: 1, script: 107 (max), sequence: 4
55
54
  // 34 = value: 8, count: 1, scriptPubKey: 25 (P2PKH) and 23 (P2SH)
56
- export const getSizeFactory = ({ ecc }) => (
55
+ export const getSizeFactory = ({ ecc, defaultOutputType, addressApi }) => (
57
56
  asset: Object,
58
57
  inputs: Array | UtxoCollection,
59
58
  outputs: Array,
60
59
  { compressed = true } = {}
61
60
  ) => {
62
61
  assert(ecc, 'ecc is required')
62
+ assert(defaultOutputType, 'defaultOutputType is required')
63
+ assert(addressApi, 'addressApi is required')
64
+ const assetName = asset.name
63
65
  if (inputs instanceof UtxoCollection) {
64
66
  inputs = Array.from(inputs).map((utxo) => utxo.script || null)
65
67
  }
@@ -78,7 +80,7 @@ export const getSizeFactory = ({ ecc }) => (
78
80
  const scriptBuffer = Buffer.from(script, 'hex')
79
81
  const scriptType = classifyOutput(scriptBuffer)
80
82
 
81
- const supportedTypes = supportedInputTypes[asset.name] || supportedInputTypes.default
83
+ const supportedTypes = supportedInputTypes[assetName] || supportedInputTypes.default
82
84
  assert(
83
85
  supportedTypes.includes(scriptType),
84
86
  `Only ${supportedTypes.join(', ')} inputs supported right now`
@@ -93,19 +95,21 @@ export const getSizeFactory = ({ ecc }) => (
93
95
  varuint.encodingLength(outputs.length) + // outputs_len
94
96
  // output[]
95
97
  outputs.reduce((t, output) => {
96
- if (output === null) output = get(asset, 'address.versions.bech32') ? 'P2WSH' : 'P2PKH'
98
+ // if (output === null) output = get(asset, 'address.versions.bech32') ? 'P2WSH' : 'P2PKH'
99
+
100
+ if (output === null) output = defaultOutputType
97
101
 
98
102
  let scriptType = scriptClassify.types[output]
99
- const supportedTypes = supportedOutputTypes[asset.name] || supportedOutputTypes.default
103
+ const supportedTypes = supportedOutputTypes[assetName] || supportedOutputTypes.default
100
104
 
101
105
  if (!supportedTypes.includes(scriptType)) {
102
- if (asset.address.isP2PKH(output)) scriptType = P2PKH
103
- else if (asset.address.isP2SH(output)) scriptType = P2SH
104
- else if (asset.address.isP2WPKH && asset.address.isP2WPKH(output)) scriptType = P2WPKH
105
- else if (asset.address.isP2TR && asset.address.isP2TR(output)) scriptType = P2TR
106
- else if (asset.address.isP2WSH && asset.address.isP2WSH(output)) scriptType = P2WSH
106
+ if (addressApi.isP2PKH(output)) scriptType = P2PKH
107
+ else if (addressApi.isP2SH(output)) scriptType = P2SH
108
+ else if (addressApi.isP2WPKH && addressApi.isP2WPKH(output)) scriptType = P2WPKH
109
+ else if (addressApi.isP2TR && addressApi.isP2TR(output)) scriptType = P2TR
110
+ else if (addressApi.isP2WSH && addressApi.isP2WSH(output)) scriptType = P2WSH
107
111
  else {
108
- scriptType = classifyOutput(asset.address.toScriptPubKey(output))
112
+ scriptType = classifyOutput(addressApi.toScriptPubKey(output))
109
113
  }
110
114
  }
111
115
  assert(
@@ -160,5 +164,8 @@ export const getSizeFactory = ({ ecc }) => (
160
164
  return Math.ceil(weight / 4)
161
165
  }
162
166
 
163
- const getFeeEstimatorFactory = ({ ecc }) => createDefaultFeeEstimator(getSizeFactory({ ecc }))
167
+ const getFeeEstimatorFactory = ({ ecc, defaultOutputType, addressApi }) => {
168
+ const getSize = getSizeFactory({ ecc, defaultOutputType, addressApi })
169
+ return createDefaultFeeEstimator(getSize)
170
+ }
164
171
  export default getFeeEstimatorFactory
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 })
@@ -2,7 +2,6 @@
2
2
  import { UtxoCollection } from '@exodus/models'
3
3
  import { findLargeUnconfirmedTxs } from './tx-utils'
4
4
  import assert from 'minimalistic-assert'
5
- import { mapValues } from '@exodus/basic-utils'
6
5
 
7
6
  export function getUtxos({ accountState, asset }) {
8
7
  return (
@@ -28,19 +27,18 @@ export const getBalancesFactory = ({ taprootEnabled, feeData }) => {
28
27
  feeData,
29
28
  taprootEnabled,
30
29
  }).value
31
- return mapValues({ balance, spendableBalance }, (balance) => (balance.isZero ? null : balance))
30
+ return { balance, spendableBalance }
32
31
  }
33
32
  }
34
33
 
35
- const isTaprootUtxo = (asset, utxo) =>
36
- String(utxo.address).startsWith(asset.address.versions.taproot)
34
+ const isTaprootUtxo = ({ utxo }) => String(utxo.address).length === 62
37
35
 
38
36
  export function getSpendableUtxos({ asset, utxos, feeData, txSet, taprootEnabled }) {
39
37
  if (!['bitcoin', 'bitcointestnet', 'bitcoinregtest'].includes(asset.name)) return utxos
40
38
 
41
39
  if (!taprootEnabled) {
42
40
  utxos = UtxoCollection.fromArray(
43
- utxos.toArray().filter((utxo) => !isTaprootUtxo(asset, utxo)),
41
+ utxos.toArray().filter((utxo) => !isTaprootUtxo({ utxo })),
44
42
  { currency: asset.currency }
45
43
  )
46
44
  }