@exodus/bytes 1.0.0-rc.7 → 1.0.0-rc.8

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/README.md CHANGED
@@ -1,11 +1,27 @@
1
1
  # `@exodus/bytes`
2
2
 
3
- `Uint8Array` conversion to and from `base64`, `base32`, `hex` and `utf8`
3
+ `Uint8Array` conversion to and from `base64`, `base32`, `base58`, `hex`, and `utf8`
4
4
 
5
5
  [Fast](./Performance.md)
6
6
 
7
7
  Performs proper input validation
8
8
 
9
+ ## API
10
+
11
+ ### `@exodus/bytes/hex.js`
12
+
13
+ ### `@exodus/bytes/base64.js`
14
+
15
+ ### `@exodus/bytes/base32.js`
16
+
17
+ ### `@exodus/bytes/hex.js`
18
+
19
+ ### `@exodus/bytes/base58.js`
20
+
21
+ ### `@exodus/bytes/base58check.js`
22
+
23
+ ### `@exodus/bytes/wif.js`
24
+
9
25
  ## License
10
26
 
11
27
  [MIT](./LICENSE)
package/base58.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { typedView } from './array.js'
2
2
  import { assertUint8 } from './assert.js'
3
- import { nativeDecoder, nativeEncoder } from './fallback/_utils.js'
3
+ import { nativeDecoder, nativeEncoder, isHermes } from './fallback/_utils.js'
4
4
  import { encodeAscii, decodeAscii } from './fallback/latin1.js'
5
5
 
6
6
  const alphabet = [...'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz']
@@ -20,7 +20,7 @@ let fromMap
20
20
 
21
21
  const E_CHAR = 'Invalid character in base58 input'
22
22
 
23
- const shouldUseBigIntFrom = Boolean(globalThis.HermesInternal) // faster only on Hermes, numbers path beats it on normal engines
23
+ const shouldUseBigIntFrom = isHermes // faster only on Hermes, numbers path beats it on normal engines
24
24
 
25
25
  export function toBase58(arr) {
26
26
  assertUint8(arr)
@@ -134,7 +134,7 @@ export function fromBase58(str, format = 'uint8') {
134
134
  for (let i = 0; i < 58; i++) fromMap[alphabet[i].charCodeAt(0)] = i
135
135
  }
136
136
 
137
- const size = zeros + (((length - zeros) * 3 + 1) >> 2) // 3/4 rounded up, larger than ~0.73 coef to fit everything
137
+ const size = zeros + (((length - zeros + 1) * 3) >> 2) // 3/4 rounded up, larger than ~0.73 coef to fit everything
138
138
  const res = new Uint8Array(size)
139
139
  let at = size // where is the first significant byte written
140
140
 
package/base58check.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { typedView } from './array.js'
2
2
  import { assertUint8 } from './assert.js'
3
3
  import { toBase58, fromBase58 } from './base58.js'
4
- import { hashSync } from '@exodus/crypto/hash'
4
+ import { hashSync } from '@exodus/crypto/hash' // eslint-disable-line @exodus/import/no-deprecated
5
5
 
6
6
  // Note: while API is async, we use hashSync for now until we improve webcrypto perf for hash256
7
7
  // Inputs to base58 are typically very small, and that makes a difference
@@ -56,6 +56,7 @@ export const makeBase58check = (hashAlgo, hashAlgoSync) => {
56
56
  }
57
57
  }
58
58
 
59
+ // eslint-disable-next-line @exodus/import/no-deprecated
59
60
  const hash256sync = (x) => hashSync('sha256', hashSync('sha256', x, 'uint8'), 'uint8')
60
61
  const hash256 = hash256sync // See note at the top
61
62
  const {
package/base64.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { assertUint8, assertEmptyRest } from './assert.js'
2
2
  import { typedView } from './array.js'
3
+ import { isHermes, skipWeb } from './fallback/_utils.js'
3
4
  import { decodeLatin1, encodeLatin1 } from './fallback/latin1.js'
4
5
  import * as js from './fallback/base64.js'
5
6
 
@@ -15,8 +16,8 @@ const { toBase64: web64 } = Uint8Array.prototype // Modern engines have this
15
16
  const { E_CHAR, E_PADDING, E_LENGTH, E_LAST } = js
16
17
 
17
18
  // faster only on Hermes (and a little in old Chrome), js path beats it on normal engines
18
- const shouldUseBtoa = btoa && Boolean(globalThis.HermesInternal)
19
- const shouldUseAtob = atob && Boolean(globalThis.HermesInternal)
19
+ const shouldUseBtoa = btoa && isHermes
20
+ const shouldUseAtob = atob && isHermes
20
21
 
21
22
  // For native Buffer codepaths only
22
23
  const isBuffer = (x) => x.constructor === Buffer && Buffer.isBuffer(x)
@@ -33,8 +34,7 @@ function maybePad(res, padding) {
33
34
  }
34
35
 
35
36
  const toUrl = (x) => x.replaceAll('+', '-').replaceAll('/', '_')
36
- const fromUrl = (x) => x.replaceAll('-', '+').replaceAll('_', '/')
37
- const haveWeb = (x) => web64 && x.toBase64 === web64
37
+ const haveWeb = (x) => !skipWeb && web64 && x.toBase64 === web64
38
38
 
39
39
  export function toBase64(x, { padding = true } = {}) {
40
40
  assertUint8(x)
@@ -111,7 +111,7 @@ function noWhitespaceSeen(str, arr) {
111
111
  }
112
112
 
113
113
  let fromBase64impl
114
- if (Uint8Array.fromBase64) {
114
+ if (!skipWeb && Uint8Array.fromBase64) {
115
115
  // NOTICE: this is actually slower than our JS impl in older JavaScriptCore and (slightly) in SpiderMonkey, but faster on V8 and new JavaScriptCore
116
116
  fromBase64impl = (str, isBase64url, padding) => {
117
117
  const alphabet = isBase64url ? 'base64url' : 'base64'
@@ -137,40 +137,41 @@ if (Uint8Array.fromBase64) {
137
137
  if (!noWhitespaceSeen(str, arr)) throw new SyntaxError(E_CHAR)
138
138
  return arr
139
139
  }
140
- } else {
140
+ } else if (haveNativeBuffer) {
141
+ fromBase64impl = (str, isBase64url, padding) => {
142
+ const arr = Buffer.from(str, 'base64')
143
+ // Rechecking by re-encoding is cheaper than regexes on Node.js
144
+ const got = isBase64url ? maybeUnpad(str, padding === false) : maybePad(str, padding !== true)
145
+ const valid = isBase64url ? arr.base64urlSlice(0, arr.length) : arr.base64Slice(0, arr.length)
146
+ if (got !== valid) throw new SyntaxError(E_PADDING)
147
+ return arr // fully checked
148
+ }
149
+ } else if (shouldUseAtob) {
150
+ // atob is faster than manual parsing on Hermes
141
151
  fromBase64impl = (str, isBase64url, padding) => {
142
152
  let arr
143
- if (haveNativeBuffer) {
144
- arr = Buffer.from(str, 'base64')
145
- // Rechecking is cheaper than regexes on Node.js
146
- const r = isBase64url ? maybeUnpad(str, padding === false) : maybePad(str, padding !== true)
147
- if (r !== arr.toString(isBase64url ? 'base64url' : 'base64')) throw new SyntaxError(E_PADDING)
148
- } else if (shouldUseAtob) {
149
- // atob is faster than manual parsing on Hermes
150
- if (isBase64url) {
151
- if (/[\t\n\f\r +/]/.test(str)) throw new SyntaxError(E_CHAR) // atob verifies other invalid input
152
- str = fromUrl(str)
153
- }
154
-
155
- try {
156
- arr = encodeLatin1(atob(str))
157
- } catch {
158
- throw new SyntaxError(E_CHAR) // convert atob errors
159
- }
153
+ if (isBase64url) {
154
+ if (/[\t\n\f\r +/]/.test(str)) throw new SyntaxError(E_CHAR) // atob verifies other invalid input
155
+ str = str.replaceAll('-', '+').replaceAll('_', '/') // from url to normal
156
+ }
160
157
 
161
- if (!isBase64url && !noWhitespaceSeen(str, arr)) throw new SyntaxError(E_CHAR) // base64url checks input above
162
- } else {
163
- return js.fromBase64(str, isBase64url) // early return to skip last chunk verification, it's already validated in js
158
+ try {
159
+ arr = encodeLatin1(atob(str))
160
+ } catch {
161
+ throw new SyntaxError(E_CHAR) // convert atob errors
164
162
  }
165
163
 
164
+ if (!isBase64url && !noWhitespaceSeen(str, arr)) throw new SyntaxError(E_CHAR) // base64url checks input above
165
+
166
166
  if (arr.length % 3 !== 0) {
167
167
  // Check last chunk to be strict if it was incomplete
168
- const expected = toBase64(arr.subarray(-(arr.length % 3)))
168
+ const expected = toBase64(arr.subarray(-(arr.length % 3))) // str is normalized to non-url already
169
169
  const end = str.length % 4 === 0 ? str.slice(-4) : str.slice(-(str.length % 4)).padEnd(4, '=')
170
- const actual = isBase64url ? fromUrl(end) : end
171
- if (expected !== actual) throw new SyntaxError(E_LAST)
170
+ if (expected !== end) throw new SyntaxError(E_LAST)
172
171
  }
173
172
 
174
173
  return arr
175
174
  }
175
+ } else {
176
+ fromBase64impl = (str, isBase64url, padding) => js.fromBase64(str, isBase64url) // validated in js
176
177
  }
package/bech32.js ADDED
@@ -0,0 +1,254 @@
1
+ import { assertUint8 } from './assert.js'
2
+ import { nativeEncoder } from './fallback/_utils.js'
3
+ import { decodeAscii, encodeAscii, encodeLatin1 } from './fallback/latin1.js'
4
+
5
+ const alphabet = [...'qpzry9x8gf2tvdw0s3jn54khce6mua7l']
6
+ const BECH32 = 1
7
+ const BECH32M = 0x2b_c8_30_a3
8
+
9
+ const E_SIZE = 'Input length is out of range'
10
+ const E_PREFIX = 'Missing or invalid prefix'
11
+ const E_MIXED = 'Mixed-case string'
12
+ const E_PADDING = 'Padding is invalid'
13
+ const E_CHECKSUM = 'Invalid checksum'
14
+ const E_CHARACTER = 'Non-bech32 character'
15
+ const E_STRING = 'Input is not a string'
16
+
17
+ // nativeEncoder path uses encodeAscii which asserts ascii, otherwise we have 0-255 bytes from encodeLatin1
18
+ const c2x = new Int8Array(nativeEncoder ? 128 : 256).fill(-1)
19
+ const x2c = new Uint8Array(32)
20
+ for (let i = 0; i < alphabet.length; i++) {
21
+ const c = alphabet[i].charCodeAt(0)
22
+ c2x[c] = i
23
+ x2c[i] = c
24
+ }
25
+
26
+ // checksum size is 30 bits, 0x3f_ff_ff_ff
27
+ // The good thing about the checksum is that it's linear over every bit
28
+ const poly0 = new Uint32Array(32) // just precache all possible ones, it's only 1 KiB
29
+ const p = (x) => ((x & 0x1_ff_ff_ff) << 5) ^ poly0[x >> 25]
30
+ for (let i = 0; i < 32; i++) {
31
+ poly0[i] =
32
+ (i & 0b0_0001 ? 0x3b_6a_57_b2 : 0) ^
33
+ (i & 0b0_0010 ? 0x26_50_8e_6d : 0) ^
34
+ (i & 0b0_0100 ? 0x1e_a1_19_fa : 0) ^
35
+ (i & 0b0_1000 ? 0x3d_42_33_dd : 0) ^
36
+ (i & 0b1_0000 ? 0x2a_14_62_b3 : 0)
37
+ }
38
+
39
+ // 7 KiB more for faster p6/p8
40
+ const poly1 = new Uint32Array(32)
41
+ const poly2 = new Uint32Array(32)
42
+ const poly3 = new Uint32Array(32)
43
+ const poly4 = new Uint32Array(32)
44
+ const poly5 = new Uint32Array(32)
45
+ const poly6 = new Uint32Array(32)
46
+ const poly7 = new Uint32Array(32)
47
+ for (let i = 0; i < 32; i++) {
48
+ // poly0[i] === p(p(p(p(p(p(i))))))
49
+ poly1[i] = p(poly0[i]) // aka p(p(p(p(p(p(i << 5))))))
50
+ poly2[i] = p(poly1[i]) // aka p(p(p(p(p(p(i << 10))))))
51
+ poly3[i] = p(poly2[i]) // aka p(p(p(p(p(p(i << 15))))))
52
+ poly4[i] = p(poly3[i]) // aka p(p(p(p(p(p(i << 20))))))
53
+ poly5[i] = p(poly4[i]) // aka p(p(p(p(p(p(i << 25))))))
54
+ poly6[i] = p(poly5[i])
55
+ poly7[i] = p(poly6[i])
56
+ }
57
+
58
+ function p6(x) {
59
+ // Same as: return p(p(p(p(p(p(x))))))
60
+ const x0 = x & 0x1f
61
+ const x1 = (x >> 5) & 0x1f
62
+ const x2 = (x >> 10) & 0x1f
63
+ const x3 = (x >> 15) & 0x1f
64
+ const x4 = (x >> 20) & 0x1f
65
+ const x5 = (x >> 25) & 0x1f
66
+ return poly0[x0] ^ poly1[x1] ^ poly2[x2] ^ poly3[x3] ^ poly4[x4] ^ poly5[x5]
67
+ }
68
+
69
+ function p8(x) {
70
+ // Same as: return p(p(p(p(p(p(p(p(x))))))))
71
+ const x0 = x & 0x1f
72
+ const x1 = (x >> 5) & 0x1f
73
+ const x2 = (x >> 10) & 0x1f
74
+ const x3 = (x >> 15) & 0x1f
75
+ const x4 = (x >> 20) & 0x1f
76
+ const x5 = (x >> 25) & 0x1f
77
+ return poly2[x0] ^ poly3[x1] ^ poly4[x2] ^ poly5[x3] ^ poly6[x4] ^ poly7[x5]
78
+ }
79
+
80
+ // p(p(p(p(p(p(chk) ^ x0) ^ x1) ^ x2) ^ x3) ^ x4) ^ x5 === p6(chk) ^ merge(x0, x1, x2, x3, x4, x5)
81
+ const merge = (a, b, c, d, e, f) => f ^ (e << 5) ^ (d << 10) ^ (c << 15) ^ (b << 20) ^ (a << 25)
82
+
83
+ const prefixCache = new Map() // Cache 10 of them
84
+
85
+ function pPrefix(prefix) {
86
+ if (prefix === 'bc') return 0x2_31_80_43 // perf
87
+ const cached = prefixCache.get(prefix)
88
+ if (cached !== undefined) return cached
89
+
90
+ // bech32_hrp_expand(s): [ord(x) >> 5 for x in s] + [0] + [ord(x) & 31 for x in s]
91
+ // We can do this in a single scan due to linearity, but it's not very beneficial
92
+ let chk = 1 // it starts with one (see def bech32_polymod in BIP_0173)
93
+ const length = prefix.length
94
+ for (let i = 0; i < length; i++) {
95
+ const c = prefix.charCodeAt(i)
96
+ if (c < 33 || c > 126) throw new Error(E_PREFIX) // each character having a value in the range [33-126]
97
+ chk = p(chk) ^ (c >> 5)
98
+ }
99
+
100
+ chk = p(chk) // <= for + [0]
101
+ for (let i = 0; i < length; i++) {
102
+ const c = prefix.charCodeAt(i)
103
+ chk = p(chk) ^ (c & 0x1f)
104
+ }
105
+
106
+ if (prefixCache.size < 10) prefixCache.set(prefix, chk)
107
+ return chk
108
+ }
109
+
110
+ function toBech32enc(prefix, bytes, limit, encoding) {
111
+ if (typeof prefix !== 'string' || !prefix) throw new TypeError(E_PREFIX)
112
+ if (typeof limit !== 'number') throw new TypeError(E_SIZE)
113
+ assertUint8(bytes)
114
+ const bytesLength = bytes.length
115
+ const wordsLength = Math.ceil((bytesLength * 8) / 5)
116
+ if (!(prefix.length + 7 + wordsLength <= limit)) throw new TypeError(E_SIZE)
117
+ prefix = prefix.toLowerCase()
118
+ const out = new Uint8Array(wordsLength + 6)
119
+
120
+ let chk = pPrefix(prefix)
121
+ let i = 0, j = 0 // prettier-ignore
122
+
123
+ // This loop is just an optimization of the next one
124
+ for (const length4 = bytesLength - 4; i < length4; i += 5, j += 8) {
125
+ const b0 = bytes[i], b1 = bytes[i + 1], b2 = bytes[i + 2], b3 = bytes[i + 3], b4 = bytes[i + 4] // prettier-ignore
126
+ const x0 = b0 >> 3
127
+ const x1 = ((b0 << 2) & 0x1f) | (b1 >> 6)
128
+ const x2 = (b1 >> 1) & 0x1f
129
+ const x3 = ((b1 << 4) & 0x1f) | (b2 >> 4)
130
+ const x4 = ((b2 << 1) & 0x1f) | (b3 >> 7)
131
+ const x5 = (b3 >> 2) & 0x1f
132
+ const x6 = ((b3 << 3) & 0x1f) | (b4 >> 5)
133
+ const x7 = b4 & 0x1f
134
+ chk = merge(x2, x3, x4, x5, x6, x7) ^ poly0[x1] ^ poly1[x0] ^ p8(chk)
135
+ out[j] = x2c[x0]
136
+ out[j + 1] = x2c[x1]
137
+ out[j + 2] = x2c[x2]
138
+ out[j + 3] = x2c[x3]
139
+ out[j + 4] = x2c[x4]
140
+ out[j + 5] = x2c[x5]
141
+ out[j + 6] = x2c[x6]
142
+ out[j + 7] = x2c[x7]
143
+ }
144
+
145
+ let value = 0, bits = 0 // prettier-ignore
146
+ for (; i < bytesLength; i++) {
147
+ value = ((value & 0xf) << 8) | bytes[i]
148
+ bits += 3
149
+ const x = (value >> bits) & 0x1f
150
+ chk = p(chk) ^ x
151
+ out[j++] = x2c[x]
152
+ if (bits >= 5) {
153
+ bits -= 5
154
+ const x = (value >> bits) & 0x1f
155
+ chk = p(chk) ^ x
156
+ out[j++] = x2c[x]
157
+ }
158
+ }
159
+
160
+ if (bits > 0) {
161
+ const x = (value << (5 - bits)) & 0x1f
162
+ chk = p(chk) ^ x
163
+ out[j++] = x2c[x]
164
+ }
165
+
166
+ chk = encoding ^ p6(chk)
167
+ out[j++] = x2c[(chk >> 25) & 0x1f]
168
+ out[j++] = x2c[(chk >> 20) & 0x1f]
169
+ out[j++] = x2c[(chk >> 15) & 0x1f]
170
+ out[j++] = x2c[(chk >> 10) & 0x1f]
171
+ out[j++] = x2c[(chk >> 5) & 0x1f]
172
+ out[j++] = x2c[(chk >> 0) & 0x1f]
173
+
174
+ return prefix + '1' + decodeAscii(out) // suboptimal in barebones, but actually ok in Hermes for not to care atm
175
+ }
176
+
177
+ function assertDecodeArgs(str, limit) {
178
+ if (typeof str !== 'string') throw new TypeError(E_STRING)
179
+ if (typeof limit !== 'number' || str.length < 8 || !(str.length <= limit)) throw new Error(E_SIZE)
180
+ }
181
+
182
+ function fromBech32enc(str, limit, encoding) {
183
+ assertDecodeArgs(str, limit)
184
+ const lower = str.toLowerCase()
185
+ if (str !== lower) {
186
+ if (str !== str.toUpperCase()) throw new Error(E_MIXED)
187
+ str = lower
188
+ }
189
+
190
+ const split = str.lastIndexOf('1')
191
+ if (split <= 0) throw new Error(E_PREFIX)
192
+ const prefix = str.slice(0, split)
193
+ const charsLength = str.length - split - 1
194
+ const wordsLength = charsLength - 6
195
+ if (wordsLength < 0) throw new Error(E_SIZE)
196
+ const bytesLength = (wordsLength * 5) >> 3
197
+ const slice = str.slice(split + 1)
198
+ const c = nativeEncoder ? encodeAscii(slice, E_CHARACTER) : encodeLatin1(slice) // suboptimal, but only affects non-Hermes barebones
199
+ const bytes = new Uint8Array(bytesLength)
200
+
201
+ let chk = pPrefix(prefix)
202
+ let i = 0, j = 0 // prettier-ignore
203
+
204
+ // This loop is just an optimization of the next one
205
+ for (const length7 = wordsLength - 7; i < length7; i += 8, j += 5) {
206
+ const c0 = c[i], c1 = c[i + 1], c2 = c[i + 2], c3 = c[i + 3], c4 = c[i + 4], c5 = c[i + 5], c6 = c[i + 6], c7 = c[i + 7] // prettier-ignore
207
+ const x0 = c2x[c0], x1 = c2x[c1], x2 = c2x[c2], x3 = c2x[c3], x4 = c2x[c4], x5 = c2x[c5], x6 = c2x[c6], x7 = c2x[c7] // prettier-ignore
208
+ if (x0 < 0 || x1 < 0 || x2 < 0 || x3 < 0 || x4 < 0 || x5 < 0 || x6 < 0 || x7 < 0) throw new SyntaxError(E_CHARACTER) // prettier-ignore
209
+ chk = merge(x2, x3, x4, x5, x6, x7) ^ poly0[x1] ^ poly1[x0] ^ p8(chk)
210
+ bytes[j] = (x0 << 3) | (x1 >> 2)
211
+ bytes[j + 1] = (((x1 << 6) | (x2 << 1)) & 0xff) | (x3 >> 4)
212
+ bytes[j + 2] = ((x3 << 4) & 0xff) | (x4 >> 1)
213
+ bytes[j + 3] = ((((x4 << 5) | x5) << 2) & 0xff) | (x6 >> 3)
214
+ bytes[j + 4] = ((x6 << 5) & 0xff) | x7
215
+ }
216
+
217
+ let value = 0, bits = 0 // prettier-ignore
218
+ for (; i < wordsLength; i++) {
219
+ const x = c2x[c[i]]
220
+ if (x < 0) throw new SyntaxError(E_CHARACTER)
221
+ chk = p(chk) ^ x
222
+ value = (value << 5) | x
223
+ bits += 5
224
+ if (bits >= 8) {
225
+ bits -= 8
226
+ bytes[j++] = (value >> bits) & 0xff
227
+ }
228
+ }
229
+
230
+ if (bits >= 5 || (value << (8 - bits)) & 0xff) throw new Error(E_PADDING)
231
+
232
+ // Checksum
233
+ {
234
+ const c0 = c[i], c1 = c[i + 1], c2 = c[i + 2], c3 = c[i + 3], c4 = c[i + 4], c5 = c[i + 5] // prettier-ignore
235
+ const x0 = c2x[c0], x1 = c2x[c1], x2 = c2x[c2], x3 = c2x[c3], x4 = c2x[c4], x5 = c2x[c5] // prettier-ignore
236
+ if (x0 < 0 || x1 < 0 || x2 < 0 || x3 < 0 || x4 < 0 || x5 < 0) throw new SyntaxError(E_CHARACTER)
237
+ if ((merge(x0, x1, x2, x3, x4, x5) ^ p6(chk)) !== encoding) throw new Error(E_CHECKSUM)
238
+ }
239
+
240
+ return { prefix, bytes }
241
+ }
242
+
243
+ // This is designed to be a very quick check, skipping all other validation
244
+ export function getPrefix(str, limit = 90) {
245
+ assertDecodeArgs(str, limit)
246
+ const split = str.lastIndexOf('1')
247
+ if (split <= 0) throw new Error(E_PREFIX)
248
+ return str.slice(0, split).toLowerCase()
249
+ }
250
+
251
+ export const toBech32 = (prefix, bytes, limit = 90) => toBech32enc(prefix, bytes, limit, BECH32)
252
+ export const fromBech32 = (str, limit = 90) => fromBech32enc(str, limit, BECH32)
253
+ export const toBech32m = (prefix, bytes, limit = 90) => toBech32enc(prefix, bytes, limit, BECH32M)
254
+ export const fromBech32m = (str, limit = 90) => fromBech32enc(str, limit, BECH32M)
@@ -4,6 +4,7 @@ const isNative = (x) => x && (haveNativeBuffer || `${x}`.includes('[native code]
4
4
  const nativeEncoder = isNative(TextEncoder) ? new TextEncoder() : null
5
5
  const nativeDecoder = isNative(TextDecoder) ? new TextDecoder('utf8', { ignoreBOM: true }) : null
6
6
  const nativeBuffer = haveNativeBuffer ? Buffer : null
7
+ const isHermes = Boolean(globalThis.HermesInternal)
7
8
 
8
9
  // Actually windows-1252, compatible with ascii and latin1 decoding
9
10
  // Beware that on non-latin1, i.e. on windows-1252, this is broken in ~all Node.js versions released
@@ -12,4 +13,28 @@ const nativeDecoderLatin1 = isNative(TextDecoder)
12
13
  ? new TextDecoder('latin1', { ignoreBOM: true })
13
14
  : null
14
15
 
15
- export { nativeEncoder, nativeDecoder, nativeDecoderLatin1, nativeBuffer }
16
+ // Block Firefox < 146 specifically from using native hex/base64, as it's very slow there
17
+ // Refs: https://bugzilla.mozilla.org/show_bug.cgi?id=1994067 (and linked issues), fixed in 146
18
+ // Before that, all versions of Firefox >= 133 are slow
19
+ // TODO: this could be removed when < 146 usage diminishes (note ESR)
20
+ // We do not worry about false-negatives here but worry about false-positives!
21
+ function shouldSkipBuiltins() {
22
+ const g = globalThis
23
+ // First, attempt to exclude as many things as we can using trivial checks, just in case, and to not hit ua
24
+ if (haveNativeBuffer || isHermes || !g.window || g.chrome || !g.navigator) return false
25
+ try {
26
+ // This was fixed specifically in Firefox 146. Other engines except Hermes (already returned) get this right
27
+ new WeakSet().add(Symbol()) // eslint-disable-line symbol-description
28
+ return false
29
+ } catch {
30
+ // In catch and not after in case if something too smart optimizes out code in try. False-negative is acceptable in that case
31
+ if (!('onmozfullscreenerror' in g)) return false // Firefox has it (might remove in the future, but we don't care)
32
+ return /firefox/i.test(g.navigator.userAgent || '') // as simple as we can
33
+ }
34
+
35
+ return false // eslint-disable-line no-unreachable
36
+ }
37
+
38
+ const skipWeb = shouldSkipBuiltins()
39
+
40
+ export { nativeEncoder, nativeDecoder, nativeDecoderLatin1, nativeBuffer, isHermes, skipWeb }
@@ -1,5 +1,5 @@
1
1
  import { assertUint8 } from '../assert.js'
2
- import { nativeEncoder, nativeDecoder } from './_utils.js'
2
+ import { nativeEncoder, nativeDecoder, isHermes } from './_utils.js'
3
3
  import { encodeAscii, decodeAscii } from './latin1.js'
4
4
 
5
5
  // See https://datatracker.ietf.org/doc/html/rfc4648
@@ -14,7 +14,7 @@ export const E_PADDING = 'Invalid base32 padding'
14
14
  export const E_LENGTH = 'Invalid base32 length'
15
15
  export const E_LAST = 'Invalid last chunk'
16
16
 
17
- const useTemplates = Boolean(globalThis.HermesInternal) // Faster on Hermes and JSC, but we use it only on Hermes
17
+ const useTemplates = isHermes // Faster on Hermes and JSC, but we use it only on Hermes
18
18
 
19
19
  // We construct output by concatenating chars, this seems to be fine enough on modern JS engines
20
20
  export function toBase32(arr, isBase32Hex, padding) {
package/fallback/hex.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { assertUint8 } from '../assert.js'
2
- import { nativeDecoder, nativeEncoder } from './_utils.js'
2
+ import { nativeDecoder, nativeEncoder, isHermes } from './_utils.js'
3
3
  import { encodeAscii, decodeAscii } from './latin1.js'
4
4
 
5
5
  let hexArray // array of 256 bytes converted to two-char hex strings
@@ -61,7 +61,7 @@ function toHexPartTemplates(a, start, end) {
61
61
 
62
62
  // Using templates is significantly faster in Hermes and JSC
63
63
  // It's harder to detect JSC and not important anyway as it has native impl, so we detect only Hermes
64
- const toHexPart = globalThis.HermesInternal ? toHexPartTemplates : toHexPartAddition
64
+ const toHexPart = isHermes ? toHexPartTemplates : toHexPartAddition
65
65
 
66
66
  export function toHex(arr) {
67
67
  assertUint8(arr)
@@ -1,4 +1,10 @@
1
- import { nativeEncoder, nativeDecoder, nativeDecoderLatin1, nativeBuffer } from './_utils.js'
1
+ import {
2
+ nativeEncoder,
3
+ nativeDecoder,
4
+ nativeDecoderLatin1,
5
+ nativeBuffer,
6
+ isHermes,
7
+ } from './_utils.js'
2
8
 
3
9
  // See http://stackoverflow.com/a/22747272/680742, which says that lowest limit is in Chrome, with 0xffff args
4
10
  // On Hermes, actual max is 0x20_000 minus current stack depth, 1/16 of that should be safe
@@ -67,7 +73,7 @@ export const decodeAscii = nativeBuffer
67
73
 
68
74
  /* eslint-disable @exodus/mutable/no-param-reassign-prop-only */
69
75
 
70
- export const encodeCharcodes = globalThis.HermesInternal
76
+ export const encodeCharcodes = isHermes
71
77
  ? (str, arr) => {
72
78
  const length = str.length
73
79
  if (length > 64) {
@@ -91,7 +97,7 @@ export const encodeCharcodes = globalThis.HermesInternal
91
97
  export const encodeLatin1 = (str) => encodeCharcodes(str, new Uint8Array(str.length))
92
98
 
93
99
  // Expects nativeEncoder to be present
94
- export const encodeAscii = globalThis.HermesInternal
100
+ export const encodeAscii = isHermes
95
101
  ? (str, ERR) => {
96
102
  // Much faster in Hermes
97
103
  const codes = new Uint8Array(str.length + 4) // overshoot by a full utf8 char
package/hex.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { assertUint8 } from './assert.js'
2
2
  import { typedView } from './array.js'
3
+ import { skipWeb } from './fallback/_utils.js'
3
4
  import * as js from './fallback/hex.js'
4
5
 
5
6
  const { toHex: webHex } = Uint8Array.prototype // Modern engines have this
@@ -7,11 +8,12 @@ const { toHex: webHex } = Uint8Array.prototype // Modern engines have this
7
8
  export function toHex(arr) {
8
9
  assertUint8(arr)
9
10
  if (arr.length === 0) return ''
10
- if (webHex && arr.toHex === webHex) return arr.toHex()
11
+ if (!skipWeb && webHex && arr.toHex === webHex) return arr.toHex()
11
12
  return js.toHex(arr)
12
13
  }
13
14
 
14
15
  // Unlike Buffer.from(), throws on invalid input
15
- export const fromHex = Uint8Array.fromHex
16
- ? (str, format = 'uint8') => typedView(Uint8Array.fromHex(str), format)
17
- : (str, format = 'uint8') => typedView(js.fromHex(str), format)
16
+ export const fromHex =
17
+ !skipWeb && Uint8Array.fromHex
18
+ ? (str, format = 'uint8') => typedView(Uint8Array.fromHex(str), format)
19
+ : (str, format = 'uint8') => typedView(js.fromHex(str), format)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/bytes",
3
- "version": "1.0.0-rc.7",
3
+ "version": "1.0.0-rc.8",
4
4
  "description": "Various operations on Uint8Array data",
5
5
  "scripts": {
6
6
  "lint": "eslint .",
@@ -50,10 +50,12 @@
50
50
  "/base58.js",
51
51
  "/base58check.js",
52
52
  "/base64.js",
53
+ "/bech32.js",
53
54
  "/hex.node.js",
54
55
  "/hex.js",
55
56
  "/utf8.node.js",
56
- "/utf8.js"
57
+ "/utf8.js",
58
+ "/wif.js"
57
59
  ],
58
60
  "exports": {
59
61
  "./array.js": "./array.js",
@@ -61,6 +63,7 @@
61
63
  "./base58.js": "./base58.js",
62
64
  "./base58check.js": "./base58check.js",
63
65
  "./base64.js": "./base64.js",
66
+ "./bech32.js": "./bech32.js",
64
67
  "./hex.js": {
65
68
  "node": "./hex.node.js",
66
69
  "default": "./hex.js"
@@ -72,7 +75,8 @@
72
75
  "./utf8.js": {
73
76
  "node": "./utf8.node.js",
74
77
  "default": "./utf8.js"
75
- }
78
+ },
79
+ "./wif.js": "./wif.js"
76
80
  },
77
81
  "peerDependencies": {
78
82
  "@exodus/crypto": "^1.0.0-rc.4"
@@ -84,7 +88,7 @@
84
88
  },
85
89
  "devDependencies": {
86
90
  "@ethersproject/strings": "^5.8.0",
87
- "@exodus/crypto": "1.0.0-rc.29",
91
+ "@exodus/crypto": "^1.0.0-rc.30",
88
92
  "@exodus/eslint-config": "^5.24.0",
89
93
  "@exodus/prettier": "^1.0.0",
90
94
  "@exodus/test": "^1.0.0-rc.108",
@@ -96,11 +100,14 @@
96
100
  "base-x": "^5.0.1",
97
101
  "base32.js": "^0.1.0",
98
102
  "base64-js": "^1.5.1",
103
+ "bech32": "^2.0.0",
99
104
  "bs58": "^6.0.0",
100
105
  "bs58check": "^4.0.0",
101
106
  "bstring": "^0.3.9",
102
107
  "buffer": "^6.0.3",
108
+ "decode-utf8": "^1.0.1",
103
109
  "electron": "36.5.0",
110
+ "encode-utf8": "^2.0.0",
104
111
  "eslint": "^8.44.0",
105
112
  "fast-base64-decode": "^2.0.0",
106
113
  "fast-base64-encode": "^1.0.0",
@@ -109,7 +116,10 @@
109
116
  "iconv-lite": "^0.7.0",
110
117
  "jsvu": "^3.0.0",
111
118
  "text-encoding": "^0.7.0",
112
- "typescript": "^5.9.3"
119
+ "typescript": "^5.9.3",
120
+ "uint8array-tools": "^0.0.9",
121
+ "utf8": "^3.0.0",
122
+ "wif": "^5.0.0"
113
123
  },
114
124
  "prettier": "@exodus/prettier",
115
125
  "packageManager": "pnpm@10.12.1+sha256.889bac470ec93ccc3764488a19d6ba8f9c648ad5e50a9a6e4be3768a5de387a3"
package/utf8.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { assertUint8 } from './assert.js'
2
2
  import { typedView } from './array.js'
3
- import * as js from './fallback/utf8.js'
3
+ import { isHermes } from './fallback/_utils.js'
4
4
  import { asciiPrefix, decodeLatin1 } from './fallback/latin1.js'
5
+ import * as js from './fallback/utf8.js'
5
6
 
6
7
  const { Buffer, TextEncoder, TextDecoder, decodeURIComponent, escape } = globalThis // Buffer is optional
7
8
  const haveNativeBuffer = Buffer && !Buffer.TYPED_ARRAY_SUPPORT
@@ -16,7 +17,7 @@ const { isWellFormed } = String.prototype
16
17
 
17
18
  const { E_STRICT, E_STRICT_UNICODE } = js
18
19
 
19
- const shouldUseEscapePath = Boolean(globalThis.HermesInternal) // faster only on Hermes, js path beats it on normal engines
20
+ const shouldUseEscapePath = isHermes // faster only on Hermes, js path beats it on normal engines
20
21
 
21
22
  function deLoose(str, loose, res) {
22
23
  if (loose || str.length === res.length) return res // length is equal only for ascii, which is automatically fine
@@ -46,16 +47,16 @@ function deLoose(str, loose, res) {
46
47
 
47
48
  function encode(str, loose = false) {
48
49
  if (typeof str !== 'string') throw new TypeError('Input is not a string')
49
- if (haveNativeBuffer) return deLoose(str, loose, Buffer.from(str)) // faster on ascii on Node.js
50
- if (nativeEncoder) return deLoose(str, loose, nativeEncoder.encode(str)) // Node.js, browsers, and Hermes
50
+ if (str.length === 0) return new Uint8Array() // faster than Uint8Array.of
51
+ if (nativeEncoder) return deLoose(str, loose, nativeEncoder.encode(str))
51
52
  // No reason to use unescape + encodeURIComponent: it's slower than JS on normal engines, and modern Hermes already has TextEncoder
52
53
  return js.encode(str, loose)
53
54
  }
54
55
 
55
56
  function decode(arr, loose = false) {
56
57
  assertUint8(arr)
58
+ if (arr.byteLength === 0) return ''
57
59
  if (haveDecoder) return loose ? decoderLoose.decode(arr) : decoderFatal.decode(arr) // Node.js and browsers
58
- // No reason to use native Buffer: it's not faster than TextDecoder, needs rechecks in non-loose mode, and Node.js has TextDecoder
59
60
 
60
61
  // Fast path for ASCII prefix, this is faster than all alternatives below
61
62
  const prefix = decodeLatin1(arr, 0, asciiPrefix(arr))
package/utf8.node.js CHANGED
@@ -11,16 +11,34 @@ const { isWellFormed } = String.prototype
11
11
 
12
12
  function encode(str, loose = false) {
13
13
  if (typeof str !== 'string') throw new TypeError('Input is not a string')
14
- const res = Buffer.from(str)
15
- if (loose || str.length === res.length || isWellFormed.call(str)) return res // length is equal only for ascii, which is automatically fine
16
- throw new TypeError(E_STRICT_UNICODE)
14
+ const strLength = str.length
15
+ if (strLength === 0) return new Uint8Array() // faster than Uint8Array.of
16
+ let res
17
+ if (strLength > 0x4_00) {
18
+ // Faster for large strings
19
+ const byteLength = Buffer.byteLength(str)
20
+ res = Buffer.allocUnsafe(byteLength)
21
+ const ascii = byteLength === strLength
22
+ const written = ascii ? res.latin1Write(str) : res.utf8Write(str)
23
+ if (written !== byteLength) throw new Error('Failed to write all bytes') // safeguard just in case
24
+ if (ascii || loose) return res // no further checks needed
25
+ } else {
26
+ res = Buffer.from(str)
27
+ if (res.length === strLength || loose) return res
28
+ }
29
+
30
+ if (!isWellFormed.call(str)) throw new TypeError(E_STRICT_UNICODE)
31
+ return res
17
32
  }
18
33
 
19
34
  function decode(arr, loose = false) {
20
35
  assertUint8(arr)
21
- if (isAscii(arr)) {
36
+ const byteLength = arr.byteLength
37
+ if (byteLength === 0) return ''
38
+ if (byteLength > 0x6_00 && isAscii(arr)) {
22
39
  // On non-ascii strings, this loses ~10% * [relative position of the first non-ascii byte] (up to 10% total)
23
40
  // On ascii strings, this wins 1.5x on loose = false and 1.3x on loose = true
41
+ // Only makes sense for large enough strings
24
42
  return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength).latin1Slice(0, arr.byteLength) // .latin1Slice is faster than .asciiSlice
25
43
  }
26
44
 
package/wif.js ADDED
@@ -0,0 +1,42 @@
1
+ import { toBase58checkSync, fromBase58checkSync } from './base58check.js'
2
+ import { assertUint8 } from './assert.js'
3
+
4
+ // Mostly matches npmjs.com/wif, but with extra checks + using our base58check
5
+ // Also no inconsistent behavior on Buffer/Uint8Array input
6
+
7
+ function from(arr, expectedVersion) {
8
+ assertUint8(arr)
9
+ const version = arr[0]
10
+ if (expectedVersion !== undefined && version !== expectedVersion) {
11
+ throw new Error('Invalid network version')
12
+ }
13
+
14
+ // Makes a copy, regardless of input being a Buffer or a Uint8Array (unlike .slice)
15
+ const privateKey = Uint8Array.from(arr.subarray(1, 33))
16
+ if (arr.length === 33) return { version, privateKey, compressed: false }
17
+ if (arr.length !== 34) throw new Error('Invalid WIF length')
18
+ if (arr[33] !== 1) throw new Error('Invalid compression flag')
19
+ return { version, privateKey, compressed: true }
20
+ }
21
+
22
+ function to({ version: v, privateKey, compressed }) {
23
+ if (!Number.isSafeInteger(v) || v < 0 || v > 0xff) throw new Error('Missing or invalid version')
24
+ assertUint8(privateKey, { length: 32, name: 'privateKey' })
25
+ if (privateKey.length !== 32) throw new TypeError('Invalid privateKey length')
26
+ const out = new Uint8Array(compressed ? 34 : 33)
27
+ out[0] = v
28
+ out.set(privateKey, 1)
29
+ if (compressed) out[33] = 1
30
+ return out
31
+ }
32
+
33
+ // Async performance is worse here, so expose the same internal methods as sync for now
34
+ // ./base58check is sync internally anyway for now, so doesn't matter until that is changed
35
+
36
+ export const fromWifStringSync = (string, version) => from(fromBase58checkSync(string), version)
37
+ // export const fromWifString = async (string, version) => from(await fromBase58check(string), version)
38
+ export const fromWifString = async (string, version) => from(fromBase58checkSync(string), version)
39
+
40
+ export const toWifStringSync = (wif) => toBase58checkSync(to(wif))
41
+ // export const toWifString = async (wif) => toBase58check(to(wif))
42
+ export const toWifString = async (wif) => toBase58checkSync(to(wif))