@exodus/bitcoin-api 1.0.3 → 1.0.5

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.3",
3
+ "version": "1.0.5",
4
4
  "description": "Exodus bitcoin-api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -19,13 +19,15 @@
19
19
  "lint:fix": "yarn lint --fix"
20
20
  },
21
21
  "dependencies": {
22
- "@exodus/asset-lib": "^3.7.1",
22
+ "@exodus/asset-lib": "^3.7.2",
23
+ "@exodus/bip44-constants": "^195.0.0",
23
24
  "@exodus/bitcoinjs-lib": "6.0.2-beta.4",
24
25
  "@exodus/keychain": "^3.0.0",
25
26
  "@exodus/models": "^8.10.4",
26
27
  "@exodus/secp256k1": "4.0.2-exodus.0",
27
28
  "@exodus/simple-retry": "0.0.6",
28
- "bip44-constants": "55.0.0",
29
+ "@exodus/timer": "^1.0.0",
30
+ "bech32": "^1.1.3",
29
31
  "coininfo": "5.1.0",
30
32
  "delay": "4.0.1",
31
33
  "ecpair": "2.0.1",
@@ -38,5 +40,5 @@
38
40
  "@exodus/bip-schnorr": "0.6.6-fork-1",
39
41
  "@noble/secp256k1": "~1.5.3"
40
42
  },
41
- "gitHead": "6da3cc2b9aaba76c1e3523d9598e465c4b4f45eb"
43
+ "gitHead": "0be2bccacf297e2b4fcb65cf758f415f1b2df50e"
42
44
  }
@@ -21,7 +21,7 @@ export const createBtcLikeAddress = ({
21
21
  ? undefined
22
22
  : (string) => {
23
23
  const payload = bs58check.decodeUnsafe(string)
24
- return payload && payload.length === 21 && payload[0] === version
24
+ return !!payload && payload.length === 21 && payload[0] === version
25
25
  }
26
26
 
27
27
  const bech32ValidateFactory = (version, length) =>
@@ -1,27 +1,3 @@
1
- import bip44Constants from 'bip44-constants'
2
-
3
- // MOVE TO SHARED LIB!!!
4
-
5
- // Translate from
6
- // array of [bip44, symbol, coin]
7
- // to
8
- // map of symbol ---> bip44
9
- //
10
- // Careful: for duplicate symbols only the first definition gets stored.
11
- // Duplicate symbols: LTBC, DST, SAFE, BCO, RYO, XRD
12
- // Run tests to find all differences between existing and new constants.
13
-
14
- const _bip44Constants = {}
15
- for (let [c, t] of bip44Constants) {
16
- if (!(t in _bip44Constants)) {
17
- _bip44Constants[t] = c
18
- }
19
- }
20
-
21
- // force NANO to 'Bitcoin Nano'
22
- _bip44Constants['NANO'] = 0x80000100
23
- _bip44Constants['HBAR'] = _bip44Constants['XHB']
24
- _bip44Constants['FLR'] = 0x8000022a
25
- _bip44Constants['EGLD'] = 0x800001fc
26
-
27
- export default _bip44Constants
1
+ // back compatibility. Use @exodus/bip44-constants/by-ticker instead
2
+ import bip44Constants from '@exodus/bip44-constants/by-ticker'
3
+ export default bip44Constants
@@ -2,9 +2,10 @@
2
2
  import assert from 'minimalistic-assert'
3
3
  import * as varuint from 'varuint-bitcoin'
4
4
  import { UtxoCollection } from '@exodus/models'
5
-
6
5
  import { scriptClassify } from '../bitcoinjs-lib'
7
6
 
7
+ import { scriptClassifierFactory } from './script-classifier'
8
+
8
9
  import createDefaultFeeEstimator, { isHex } from './fee-utils'
9
10
 
10
11
  const { P2PKH, P2SH, P2WPKH, P2WSH, P2TR } = scriptClassify.types
@@ -52,116 +53,117 @@ const scriptPubKeyLengths = {
52
53
  // 10 = version: 4, locktime: 4, inputs and outputs count: 1
53
54
  // 148 = txId: 32, vout: 4, count: 1, script: 107 (max), sequence: 4
54
55
  // 34 = value: 8, count: 1, scriptPubKey: 25 (P2PKH) and 23 (P2SH)
55
- export const getSizeFactory = ({ ecc, defaultOutputType, addressApi }) => (
56
- asset: Object,
57
- inputs: Array | UtxoCollection,
58
- outputs: Array,
59
- { compressed = true } = {}
60
- ) => {
56
+ export const getSizeFactory = ({ ecc, defaultOutputType, addressApi }) => {
61
57
  assert(ecc, 'ecc is required')
62
58
  assert(defaultOutputType, 'defaultOutputType is required')
63
59
  assert(addressApi, 'addressApi is required')
64
- const assetName = asset.name
65
- if (inputs instanceof UtxoCollection) {
66
- inputs = Array.from(inputs).map((utxo) => utxo.script || null)
67
- }
68
60
 
69
- // other bitcoin-like assets
70
- const classifyOutput = scriptClassify.outputFactory({ ecc })
71
- const baseSize =
72
- 4 + // n_version
73
- 4 + // n_locktime
74
- varuint.encodingLength(inputs.length) + // inputs_len
75
- // input[]
76
- inputs.reduce((t, script) => {
77
- if (script === null) script = '76a914000000000000000000000000000000000000000088ac' // P2PKH
78
- assert(isHex(script), 'script must be hex string')
79
-
80
- const scriptBuffer = Buffer.from(script, 'hex')
81
- const scriptType = classifyOutput(scriptBuffer)
82
-
83
- const supportedTypes = supportedInputTypes[assetName] || supportedInputTypes.default
84
- assert(
85
- supportedTypes.includes(scriptType),
86
- `Only ${supportedTypes.join(', ')} inputs supported right now`
87
- )
88
-
89
- const scriptSigLengths = compressed
90
- ? scriptSigCompressedLengths
91
- : scriptSigUncompressedLengths
92
- const scriptSigLength = scriptSigLengths[scriptType]
93
- return t + 32 + 4 + varuint.encodingLength(scriptSigLength) + scriptSigLength + 4
94
- }, 0) +
95
- varuint.encodingLength(outputs.length) + // outputs_len
96
- // output[]
97
- outputs.reduce((t, output) => {
98
- // if (output === null) output = get(asset, 'address.versions.bech32') ? 'P2WSH' : 'P2PKH'
99
-
100
- if (output === null) output = defaultOutputType
101
-
102
- let scriptType = scriptClassify.types[output]
103
- const supportedTypes = supportedOutputTypes[assetName] || supportedOutputTypes.default
104
-
105
- if (!supportedTypes.includes(scriptType)) {
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
111
- else {
112
- scriptType = classifyOutput(addressApi.toScriptPubKey(output))
113
- }
114
- }
115
- assert(
116
- supportedTypes.includes(scriptType),
117
- `Only ${supportedTypes.join(', ')} outputs supported right now`
118
- )
119
-
120
- const scriptPubKeyLength = scriptPubKeyLengths[scriptType]
121
- return t + 8 + varuint.encodingLength(scriptPubKeyLength) + scriptPubKeyLength
122
- }, 0)
123
-
124
- const witnessSize =
125
- 1 + // marker
126
- 1 + // flag
127
- // witnesses
128
- inputs.reduce((t, script) => {
129
- if (!script) return t + 1
130
- const utxoScriptType = classifyOutput(Buffer.from(script, 'hex'))
131
- if ([P2SH, P2WPKH].includes(utxoScriptType)) {
132
- const pubKeyLength = 33
133
- const signatureLength = 73 // maximum possible length
134
- // Need to encode the witness item count, which is 2 for for P2WPKH as a var_int
135
- return (
136
- t +
137
- varuint.encodingLength(2) +
138
- varuint.encodingLength(pubKeyLength) +
139
- pubKeyLength +
140
- varuint.encodingLength(signatureLength) +
141
- signatureLength
61
+ const scriptClassifier = scriptClassifierFactory({ ecc, addressApi })
62
+
63
+ return (
64
+ asset: Object,
65
+ inputs: Array | UtxoCollection,
66
+ outputs: Array,
67
+ { compressed = true } = {}
68
+ ) => {
69
+ if (inputs instanceof UtxoCollection) {
70
+ inputs = Array.from(inputs).map((utxo) => utxo.script || null)
71
+ }
72
+ const assetName = asset.name
73
+ // other bitcoin-like assets
74
+ const baseSize =
75
+ 4 + // n_version
76
+ 4 + // n_locktime
77
+ varuint.encodingLength(inputs.length) + // inputs_len
78
+ // input[]
79
+ inputs.reduce((t, script) => {
80
+ if (script === null) script = '76a914000000000000000000000000000000000000000088ac' // P2PKH
81
+ assert(isHex(script), 'script must be hex string')
82
+
83
+ const scriptType = scriptClassifier.classifyScriptHex({ assetName, script })
84
+
85
+ const supportedTypes = supportedInputTypes[assetName] || supportedInputTypes.default
86
+ assert(
87
+ supportedTypes.includes(scriptType),
88
+ `Only ${supportedTypes.join(', ')} inputs supported right now`
142
89
  )
143
- }
144
- if ([P2TR].includes(utxoScriptType)) {
145
- // Only the 64 byte Schnorr signature is present for Taproot Key-Path spend
146
- const signatureLength = 64
147
- return (
148
- t + varuint.encodingLength(1) + varuint.encodingLength(signatureLength) + signatureLength
90
+
91
+ const scriptSigLengths = compressed
92
+ ? scriptSigCompressedLengths
93
+ : scriptSigUncompressedLengths
94
+ const scriptSigLength = scriptSigLengths[scriptType]
95
+ return t + 32 + 4 + varuint.encodingLength(scriptSigLength) + scriptSigLength + 4
96
+ }, 0) +
97
+ varuint.encodingLength(outputs.length) + // outputs_len
98
+ // output[]
99
+ outputs.reduce((t, output) => {
100
+ // if (output === null) output = get(asset, 'address.versions.bech32') ? 'P2WSH' : 'P2PKH'
101
+
102
+ if (output === null) output = defaultOutputType
103
+
104
+ let scriptType = scriptClassify.types[output]
105
+ const supportedTypes = supportedOutputTypes[assetName] || supportedOutputTypes.default
106
+
107
+ if (!supportedTypes.includes(scriptType)) {
108
+ scriptType = scriptClassifier.classifyAddress({
109
+ assetName,
110
+ address: output,
111
+ })
112
+ }
113
+ assert(
114
+ supportedTypes.includes(scriptType),
115
+ `Only ${supportedTypes.join(', ')} outputs supported right now`
149
116
  )
150
- }
151
117
 
152
- // Non-witness inputs get a placeholder zero byte
153
- return t + 1
154
- }, 0)
118
+ const scriptPubKeyLength = scriptPubKeyLengths[scriptType]
119
+ return t + 8 + varuint.encodingLength(scriptPubKeyLength) + scriptPubKeyLength
120
+ }, 0)
121
+
122
+ const witnessSize =
123
+ 1 + // marker
124
+ 1 + // flag
125
+ // witnesses
126
+ inputs.reduce((t, script) => {
127
+ if (!script) return t + 1
128
+ const utxoScriptType = scriptClassifier.classifyScriptHex({ assetName, script })
129
+ if ([P2SH, P2WPKH].includes(utxoScriptType)) {
130
+ const pubKeyLength = 33
131
+ const signatureLength = 73 // maximum possible length
132
+ // Need to encode the witness item count, which is 2 for for P2WPKH as a var_int
133
+ return (
134
+ t +
135
+ varuint.encodingLength(2) +
136
+ varuint.encodingLength(pubKeyLength) +
137
+ pubKeyLength +
138
+ varuint.encodingLength(signatureLength) +
139
+ signatureLength
140
+ )
141
+ }
142
+ if ([P2TR].includes(utxoScriptType)) {
143
+ // Only the 64 byte Schnorr signature is present for Taproot Key-Path spend
144
+ const signatureLength = 64
145
+ return (
146
+ t +
147
+ varuint.encodingLength(1) +
148
+ varuint.encodingLength(signatureLength) +
149
+ signatureLength
150
+ )
151
+ }
155
152
 
156
- // If witness is all placeholder bytes, that means we have no witness inputs
157
- // In that case, we're going to use the legacy serialization format, so just
158
- // return baseSize
159
- if (witnessSize === 1 + 1 + inputs.length) return baseSize
153
+ // Non-witness inputs get a placeholder zero byte
154
+ return t + 1
155
+ }, 0)
160
156
 
161
- const totalSize = baseSize + witnessSize
162
- const weight = baseSize * 3 + totalSize
163
- // Return vbytes
164
- return Math.ceil(weight / 4)
157
+ // If witness is all placeholder bytes, that means we have no witness inputs
158
+ // In that case, we're going to use the legacy serialization format, so just
159
+ // return baseSize
160
+ if (witnessSize === 1 + 1 + inputs.length) return baseSize
161
+
162
+ const totalSize = baseSize + witnessSize
163
+ const weight = baseSize * 3 + totalSize
164
+ // Return vbytes
165
+ return Math.ceil(weight / 4)
166
+ }
165
167
  }
166
168
 
167
169
  const getFeeEstimatorFactory = ({ ecc, defaultOutputType, addressApi }) => {
@@ -0,0 +1,53 @@
1
+ import { scriptClassify } from '../bitcoinjs-lib'
2
+ import createHash from 'create-hash'
3
+
4
+ import { memoizeLruCache } from '@exodus/asset-lib'
5
+ import assert from 'minimalistic-assert'
6
+
7
+ const { P2PKH, P2SH, P2WPKH, P2WSH, P2TR } = scriptClassify.types
8
+
9
+ const cacheSize = 1000
10
+ const maxSize = 30
11
+ const hashStringIfTooBig = (str) =>
12
+ str.length > maxSize
13
+ ? createHash('sha256')
14
+ .update(str)
15
+ .digest('hex')
16
+ .slice(0, maxSize)
17
+ : str
18
+
19
+ export const scriptClassifierFactory = ({ addressApi, ecc }) => {
20
+ assert(ecc, 'ecc is required')
21
+ assert(addressApi, 'addressApi is required')
22
+
23
+ const classifyOutput = scriptClassify.outputFactory({ ecc })
24
+
25
+ const classifyScriptHex = memoizeLruCache(
26
+ ({ assetName, script }) => {
27
+ assert(assetName, 'assetName is required')
28
+ assert(script, 'script is required')
29
+ return classifyOutput(Buffer.from(script, 'hex'))
30
+ },
31
+ ({ assetName, script }) => `${assetName}_${hashStringIfTooBig(script)}`, // hashing the script in case the script is really long
32
+ cacheSize
33
+ )
34
+
35
+ const classifyAddress = memoizeLruCache(
36
+ ({ assetName, address }) => {
37
+ assert(assetName, 'assetName is required')
38
+ assert(assetName, address, 'address is required')
39
+ if (addressApi.isP2PKH(address)) return P2PKH
40
+ else if (addressApi.isP2SH(address)) return P2SH
41
+ else if (addressApi.isP2WPKH && addressApi.isP2WPKH(address)) return P2WPKH
42
+ else if (addressApi.isP2TR && addressApi.isP2TR(address)) return P2TR
43
+ else if (addressApi.isP2WSH && addressApi.isP2WSH(address)) return P2WSH
44
+ return classifyScriptHex({
45
+ classifyOutput,
46
+ script: addressApi.toScriptPubKey(address).toString('hex'),
47
+ })
48
+ },
49
+ ({ assetName, address }) => `${assetName}_${address}`,
50
+ cacheSize
51
+ )
52
+ return { classifyScriptHex, classifyAddress }
53
+ }
@@ -4,6 +4,25 @@ import urlJoin from 'url-join'
4
4
  import qs from 'querystring'
5
5
  import delay from 'delay'
6
6
 
7
+ const getTextFromResponse = async (response) => {
8
+ try {
9
+ const responseBody = await response.text()
10
+ return responseBody.substring(0, 100)
11
+ } catch (e) {
12
+ return ''
13
+ }
14
+ }
15
+
16
+ const fetchJson = async (url, fetchOptions) => {
17
+ const response = await fetch(url, fetchOptions)
18
+ if (!response.ok)
19
+ throw new Error(
20
+ `${url} returned ${response.status}: ${response.statusText ||
21
+ 'Unknown Status Text'}. Body: ${await getTextFromResponse(response)}`
22
+ )
23
+ return response.json()
24
+ }
25
+
7
26
  // TODO: use p-retry
8
27
  async function fetchJsonRetry(url, fetchOptions) {
9
28
  const waitTimes = [5, 10, 20, 30].map((t) => t * 1000)
@@ -22,12 +41,6 @@ async function fetchJsonRetry(url, fetchOptions) {
22
41
  }
23
42
  }
24
43
  }
25
-
26
- async function fetchJson(url, fetchOptions) {
27
- const response = await fetch(url, fetchOptions)
28
- if (!response.ok) throw new Error(`${url} returned ${response.status}: ${response.statusText}`)
29
- return response.json()
30
- }
31
44
  }
32
45
 
33
46
  export default class InsightAPIClient {
@@ -41,9 +54,7 @@ export default class InsightAPIClient {
41
54
 
42
55
  async isNetworkConnected() {
43
56
  const url = urlJoin(this._baseURL, '/peer')
44
- const resp = await fetch(url, { timeout: 10000 })
45
- const peerStatus = await resp.json()
46
-
57
+ const peerStatus = await fetchJson(url, { timeout: 10000 })
47
58
  return !!peerStatus.connected
48
59
  }
49
60
 
@@ -56,9 +67,7 @@ export default class InsightAPIClient {
56
67
 
57
68
  async fetchBlockHeight() {
58
69
  const url = urlJoin(this._baseURL, '/status')
59
- const resp = await fetch(url, { timeout: 10000 })
60
- const status = await resp.json()
61
-
70
+ const status = await fetchJson(url, { timeout: 10000 })
62
71
  return status.info.blocks
63
72
  }
64
73
 
@@ -68,9 +77,7 @@ export default class InsightAPIClient {
68
77
  this._baseURL,
69
78
  opts.includeTxs ? `/addr/${encodedAddress}` : `/addr/${encodedAddress}?noTxList=1`
70
79
  )
71
- const response = await fetch(url)
72
-
73
- return response.json()
80
+ return fetchJson(url)
74
81
  }
75
82
 
76
83
  async fetchUTXOs(addresses, { assetNames = [] } = {}) {
@@ -81,8 +88,7 @@ export default class InsightAPIClient {
81
88
  }
82
89
  const encodedAddresses = encodeURIComponent(addresses)
83
90
  const url = urlJoin(this._baseURL, `/addrs/${encodedAddresses}/utxo?${query}`)
84
- const response = await fetch(url)
85
- const utxos = await response.json()
91
+ const utxos = await fetchJson(url)
86
92
 
87
93
  return utxos.map((utxo) => ({
88
94
  address: utxo.address,
@@ -111,8 +117,7 @@ export default class InsightAPIClient {
111
117
  async fetchRawTx(txId: string) {
112
118
  const encodedTxId = encodeURIComponent(txId)
113
119
  const url = urlJoin(this._baseURL, `/rawtx/${encodedTxId}`)
114
- const response = await fetch(url)
115
- const { rawtx } = await response.json()
120
+ const { rawtx } = await fetchJson(url)
116
121
  return rawtx
117
122
  }
118
123
 
@@ -155,14 +160,12 @@ export default class InsightAPIClient {
155
160
  async fetchUnconfirmedAncestorData(txId: string) {
156
161
  const encodedTxId = encodeURIComponent(txId)
157
162
  const url = urlJoin(this._baseURL, `/unconfirmed_ancestor/${encodedTxId}`)
158
- const response = await fetch(url)
159
- return response.json()
163
+ return fetchJson(url)
160
164
  }
161
165
 
162
166
  async fetchFeeRate() {
163
167
  const url = urlJoin(this._baseURL, '/v2/fees')
164
- const response = await fetch(url)
165
- return response.json()
168
+ return fetchJson(url)
166
169
  }
167
170
 
168
171
  async broadcastTx(rawTx) {
@@ -200,16 +203,12 @@ export default class InsightAPIClient {
200
203
  async getClaimable(address) {
201
204
  const encodedAddress = encodeURIComponent(address)
202
205
  const url = urlJoin(this._baseURL, `/addr/${encodedAddress}/claimable`)
203
- const response = await fetch(url)
204
- if (!response.ok) throw new Error(`${url} returned ${response.status}: ${response.statusText}`)
205
- return response.json()
206
+ return fetchJson(url)
206
207
  }
207
208
 
208
209
  async getUnclaimed(address) {
209
210
  const encodedAddress = encodeURIComponent(address)
210
211
  const url = urlJoin(this._baseURL, `/addr/${encodedAddress}/unclaimed`)
211
- const response = await fetch(url)
212
- if (!response.ok) throw new Error(`${url} returned ${response.status}: ${response.statusText}`)
213
- return response.json()
212
+ return fetchJson(url)
214
213
  }
215
214
  }
@@ -1,7 +1,7 @@
1
1
  import assert from 'minimalistic-assert'
2
2
  import lodash from 'lodash'
3
3
  import ECPairFactory from 'ecpair'
4
- import { Psbt } from '@exodus/bitcoinjs-lib'
4
+ import { Psbt, Transaction } from '@exodus/bitcoinjs-lib'
5
5
 
6
6
  import { toAsyncSigner, tweakSigner } from './taproot'
7
7
 
@@ -12,6 +12,15 @@ const _MAXIMUM_FEE_RATES = {
12
12
  ravencoin: 1000000,
13
13
  }
14
14
 
15
+ const canParseTx = (rawTxBuffer) => {
16
+ try {
17
+ Transaction.fromBuffer(rawTxBuffer)
18
+ return true
19
+ } catch (e) {
20
+ return false
21
+ }
22
+ }
23
+
15
24
  export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, network, ecc }) => {
16
25
  assert(assetName, 'assetName is required')
17
26
  assert(resolvePurpose, 'resolvePurpose is required')
@@ -62,10 +71,18 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
62
71
  // witness outputs only require the value and the script, not the full transaction
63
72
  txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
64
73
  } else {
65
- // non-witness outptus require the full transaction
66
74
  const rawTx = (rawTxs || []).find((t) => t.txId === txId)
75
+ // non-witness outptus require the full transaction
67
76
  assert(!!rawTx, 'Non-witness outputs require the full previous transaction.')
68
- txIn.nonWitnessUtxo = Buffer.from(rawTx.rawData, 'hex')
77
+ const rawTxBuffer = Buffer.from(rawTx.rawData, 'hex')
78
+ if (canParseTx(rawTxBuffer)) {
79
+ txIn.nonWitnessUtxo = rawTxBuffer
80
+ } else {
81
+ // temp fix for https://exodusio.slack.com/archives/CP202D90Q/p1671014704829939 until bitcoinjs could parse a mweb tx without failing
82
+ console.warn(`Setting psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true for asset ${assetName}`)
83
+ psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true
84
+ txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
85
+ }
69
86
  }
70
87
  psbt.addInput(txIn)
71
88
  }