@exodus/bitcoin-api 1.0.4 → 2.0.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 +6 -5
- package/src/btc-like-address.js +1 -1
- package/src/constants/bip44.js +3 -27
- package/src/fee/fee-estimator.js +104 -102
- package/src/fee/script-classifier.js +53 -0
- package/src/index.js +0 -1
- package/src/key-identifier.js +0 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Exodus bitcoin-api",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -19,14 +19,15 @@
|
|
|
19
19
|
"lint:fix": "yarn lint --fix"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@exodus/asset-lib": "^3.7.
|
|
23
|
-
"@exodus/
|
|
22
|
+
"@exodus/asset-lib": "^3.7.2",
|
|
23
|
+
"@exodus/bip44-constants": "^195.0.0",
|
|
24
|
+
"@exodus/bitcoinjs-lib": "6.0.2-beta.5",
|
|
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",
|
|
29
|
+
"@exodus/timer": "^1.0.0",
|
|
28
30
|
"bech32": "^1.1.3",
|
|
29
|
-
"bip44-constants": "55.0.0",
|
|
30
31
|
"coininfo": "5.1.0",
|
|
31
32
|
"delay": "4.0.1",
|
|
32
33
|
"ecpair": "2.0.1",
|
|
@@ -39,5 +40,5 @@
|
|
|
39
40
|
"@exodus/bip-schnorr": "0.6.6-fork-1",
|
|
40
41
|
"@noble/secp256k1": "~1.5.3"
|
|
41
42
|
},
|
|
42
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "53103ac1ac2c6fd96111869a13893b6c13bbcb3a"
|
|
43
44
|
}
|
package/src/btc-like-address.js
CHANGED
|
@@ -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) =>
|
package/src/constants/bip44.js
CHANGED
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
package/src/fee/fee-estimator.js
CHANGED
|
@@ -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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
+
}
|
package/src/index.js
CHANGED
|
@@ -6,7 +6,6 @@ export * from './bitcoinjs-lib'
|
|
|
6
6
|
export { default as InsightAPIClient } from './insight-api-client'
|
|
7
7
|
export { default as InsightWSClient } from './insight-api-client/ws'
|
|
8
8
|
export { default as bip44Constants } from './constants/bip44'
|
|
9
|
-
export { default as createGetKeyIdentifier } from './key-identifier'
|
|
10
9
|
export * from './tx-send'
|
|
11
10
|
export * from './tx-sign'
|
|
12
11
|
export * from './fee'
|
package/src/key-identifier.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { buildBip32Path } from '@exodus/keychain'
|
|
2
|
-
import assert from 'minimalistic-assert'
|
|
3
|
-
|
|
4
|
-
export default ({ bip44 }) => ({ purpose, accountIndex, chainIndex, addressIndex }) => {
|
|
5
|
-
assert(typeof bip44 === 'number', 'bip44 must be a number')
|
|
6
|
-
// TODO, move key-utils.js hydra/keychain to asset libs.
|
|
7
|
-
// https://github.com/ExodusMovement/assets/pull/410#discussion_r1026331735
|
|
8
|
-
const derivationPath = buildBip32Path({
|
|
9
|
-
asset: { bip44 }, // buildBip32Path only requires bip44. Change asset with bip44 and validate!
|
|
10
|
-
purpose,
|
|
11
|
-
accountIndex,
|
|
12
|
-
chainIndex,
|
|
13
|
-
addressIndex,
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
return {
|
|
17
|
-
derivationAlgorithm: 'BIP32',
|
|
18
|
-
derivationPath,
|
|
19
|
-
keyType: 'secp256k1',
|
|
20
|
-
}
|
|
21
|
-
}
|