@exodus/bytes 1.0.0-rc.4 → 1.0.0-rc.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/assert.js CHANGED
@@ -1,7 +1,3 @@
1
- export function assert(x, msg) {
2
- if (!x) throw new Error(msg || 'Assertion failed')
3
- }
4
-
5
1
  export function assertEmptyRest(rest) {
6
2
  if (Object.keys(rest).length > 0) throw new TypeError('Unexpected extra options')
7
3
  }
package/base58.js ADDED
@@ -0,0 +1,212 @@
1
+ import { typedView } from './array.js'
2
+ import { assertUint8 } from './assert.js'
3
+ import { nativeDecoder, nativeEncoder } from './fallback/_utils.js'
4
+
5
+ const alphabet = [...'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz']
6
+ const codes = new Uint8Array(alphabet.map((x) => x.charCodeAt(0)))
7
+ const ZERO = alphabet[0]
8
+ const zeroC = codes[0]
9
+
10
+ const _0n = BigInt(0)
11
+ const _1n = BigInt(1)
12
+ const _8n = BigInt(8)
13
+ const _32n = BigInt(32)
14
+ const _58n = BigInt(58)
15
+ const _0xffffffffn = BigInt(0xff_ff_ff_ff)
16
+
17
+ let table // 15 * 82, diagonal, <1kb
18
+ let fromMap
19
+
20
+ const E_CHAR = 'Invalid character in base58 input'
21
+
22
+ const shouldUseBigIntFrom = Boolean(globalThis.HermesInternal) // faster only on Hermes, numbers path beats it on normal engines
23
+
24
+ export function toBase58(arr) {
25
+ assertUint8(arr)
26
+ const length = arr.length
27
+ if (length === 0) return ''
28
+
29
+ let zeros = 0
30
+ while (zeros < length && arr[zeros] === 0) zeros++
31
+
32
+ if (length > 60) {
33
+ // Slow path. Can be optimized ~10%, but the main factor is /58n division anyway, so doesn't matter much
34
+ let x = _0n
35
+ for (let i = 0; i < arr.length; i++) x = (x << _8n) | BigInt(arr[i])
36
+
37
+ let out = ''
38
+ while (x) {
39
+ const d = x / _58n
40
+ out = alphabet[Number(x - _58n * d)] + out
41
+ x = d
42
+ }
43
+
44
+ return ZERO.repeat(zeros) + out
45
+ }
46
+
47
+ // We run fast mode operations only on short (<=60 bytes) inputs, via precomputation table
48
+ if (!table) {
49
+ table = []
50
+ let x = _1n
51
+ for (let i = 0; i < 15; i++) {
52
+ // Convert x to base 58 digits
53
+ const in58 = []
54
+ let y = x
55
+ while (y) {
56
+ const d = y / _58n
57
+ in58.push(Number(y - _58n * d))
58
+ y = d
59
+ }
60
+
61
+ table.push(new Uint8Array(in58))
62
+ x <<= _32n
63
+ }
64
+ }
65
+
66
+ const res = []
67
+ {
68
+ let j = 0
69
+ // We group each 4 bytes into 32-bit chunks
70
+ // Not using u32arr to not deal with remainder + BE/LE differences
71
+ for (let i = length - 1; i >= 0; i -= 4) {
72
+ let c
73
+ if (i > 2) {
74
+ c = (arr[i] | (arr[i - 1] << 8) | (arr[i - 2] << 16) | (arr[i - 3] << 24)) >>> 0
75
+ } else if (i > 1) {
76
+ c = arr[i] | (arr[i - 1] << 8) | (arr[i - 2] << 16)
77
+ } else {
78
+ c = i === 1 ? arr[i] | (arr[i - 1] << 8) : arr[i]
79
+ }
80
+
81
+ const row = table[j++]
82
+ if (c === 0) continue
83
+ const olen = res.length
84
+ const nlen = row.length
85
+ let k = 0
86
+ for (; k < olen; k++) res[k] += c * row[k]
87
+ while (k < nlen) res.push(c * row[k++])
88
+ }
89
+ }
90
+
91
+ // We can now do a single scan over regular numbers under MAX_SAFE_INTEGER
92
+ // Note: can't use int32 operations on them, as they are outside of 2**32 range
93
+ // This is faster though
94
+ {
95
+ let carry = 0
96
+ let i = 0
97
+ while (i < res.length) {
98
+ const c = res[i] + carry
99
+ carry = Math.floor(c / 58)
100
+ res[i++] = c - carry * 58
101
+ }
102
+
103
+ while (carry) {
104
+ const c = carry
105
+ carry = Math.floor(c / 58)
106
+ res[i++] = c - carry * 58
107
+ }
108
+ }
109
+
110
+ if (nativeDecoder) {
111
+ const oa = new Uint8Array(res.length)
112
+ let j = 0
113
+ for (let i = res.length - 1; i >= 0; i--) oa[j++] = codes[res[i]]
114
+ return ZERO.repeat(zeros) + nativeDecoder.decode(oa)
115
+ }
116
+
117
+ let out = ''
118
+ for (let i = res.length - 1; i >= 0; i--) out += alphabet[res[i]]
119
+ return ZERO.repeat(zeros) + out
120
+ }
121
+
122
+ // TODO: test on 'z'.repeat(from 1 to smth)
123
+ export function fromBase58(str, format = 'uint8') {
124
+ if (typeof str !== 'string') throw new TypeError('Input is not a string')
125
+ const length = str.length
126
+ if (length === 0) return typedView(new Uint8Array(), format)
127
+
128
+ let zeros = 0
129
+ while (zeros < length && str.charCodeAt(zeros) === zeroC) zeros++
130
+
131
+ if (!fromMap) {
132
+ fromMap = new Int8Array(256).fill(-1)
133
+ for (let i = 0; i < 58; i++) fromMap[alphabet[i].charCodeAt(0)] = i
134
+ }
135
+
136
+ const size = zeros + (((length - zeros) * 3 + 1) >> 2) // 3/4 rounded up, larger than ~0.73 coef to fit everything
137
+ const res = new Uint8Array(size)
138
+ let at = size // where is the first significant byte written
139
+
140
+ if (shouldUseBigIntFrom) {
141
+ let x = _0n
142
+
143
+ // nativeEncoder gives a benefit here
144
+ if (nativeEncoder) {
145
+ const codes = nativeEncoder.encode(str)
146
+ if (codes.length !== str.length) throw new SyntaxError(E_CHAR) // non-ascii
147
+ for (let i = zeros; i < length; i++) {
148
+ const c = fromMap[codes[i]]
149
+ if (c < 0) throw new SyntaxError(E_CHAR)
150
+ x = x * _58n + BigInt(c)
151
+ }
152
+ } else {
153
+ for (let i = zeros; i < length; i++) {
154
+ const charCode = str.charCodeAt(i)
155
+ const c = fromMap[charCode]
156
+ if (charCode > 255 || c < 0) throw new SyntaxError(E_CHAR)
157
+ x = x * _58n + BigInt(c)
158
+ }
159
+ }
160
+
161
+ while (x) {
162
+ let y = Number(x & _0xffffffffn)
163
+ x >>= 32n
164
+ res[--at] = y & 0xff
165
+ y >>>= 8
166
+ if (!x && !y) break
167
+ res[--at] = y & 0xff
168
+ y >>>= 8
169
+ if (!x && !y) break
170
+ res[--at] = y & 0xff
171
+ y >>>= 8
172
+ if (!x && !y) break
173
+ res[--at] = y & 0xff
174
+ }
175
+ } else {
176
+ for (let i = zeros; i < length; i++) {
177
+ const charCode = str.charCodeAt(i)
178
+ let c = fromMap[charCode]
179
+ if (charCode > 255 || c < 0) throw new SyntaxError(E_CHAR)
180
+
181
+ let k = size - 1
182
+ for (;;) {
183
+ if (c === 0 && k < at) break
184
+ c += 58 * res[k]
185
+ res[k] = c & 0xff
186
+ c >>>= 8
187
+ k--
188
+ // unroll a bit
189
+ if (c === 0 && k < at) break
190
+ c += 58 * res[k]
191
+ res[k] = c & 0xff
192
+ c >>>= 8
193
+ k--
194
+ if (c === 0 && k < at) break
195
+ c += 58 * res[k]
196
+ res[k] = c & 0xff
197
+ c >>>= 8
198
+ k--
199
+ if (c === 0 && k < at) break
200
+ c += 58 * res[k]
201
+ res[k] = c & 0xff
202
+ c >>>= 8
203
+ k--
204
+ }
205
+
206
+ at = k + 1
207
+ if (c !== 0 || at < zeros) throw new Error('Unexpected') // unreachable
208
+ }
209
+ }
210
+
211
+ return typedView(res.slice(at - zeros), format) // slice is faster for small sizes than subarray
212
+ }
package/base58check.js ADDED
@@ -0,0 +1,30 @@
1
+ import { typedView } from './array.js'
2
+ import { assertUint8 } from './assert.js'
3
+ import { toBase58, fromBase58 } from './base58.js'
4
+ import { hashSync } from '@exodus/crypto/hash'
5
+
6
+ // Note: while API is async, we use hashSync for now until we improve webcrypto perf for hash256
7
+ // Inputs to base58 are typically very small, and that makes a difference
8
+
9
+ const hash256 = (x) => hashSync('sha256', hashSync('sha256', x, 'uint8'), 'uint8')
10
+
11
+ const E_CHECKSUM = 'Invalid checksum'
12
+
13
+ export async function toBase58check(arr) {
14
+ assertUint8(arr)
15
+ const checksum = hash256(arr)
16
+ const res = new Uint8Array(arr.length + 4)
17
+ res.set(arr, 0)
18
+ res.set(checksum.subarray(0, 4), arr.length)
19
+ return toBase58(res)
20
+ }
21
+
22
+ export async function fromBase58check(str, format = 'uint8') {
23
+ const arr = fromBase58(str) // checks input
24
+ const len4 = arr.length - 4
25
+ const payload = arr.subarray(0, len4)
26
+ const c = arr.subarray(len4)
27
+ const r = hash256(payload)
28
+ if ((c[0] ^ r[0]) | (c[1] ^ r[1]) | (c[2] ^ r[2]) | (c[3] ^ r[3])) throw new Error(E_CHECKSUM)
29
+ return typedView(payload, format)
30
+ }
package/fallback/hex.js CHANGED
@@ -58,9 +58,6 @@ export function fromHex(str) {
58
58
  if (typeof str !== 'string') throw new TypeError('Input is not a string')
59
59
  if (str.length % 2 !== 0) throw new SyntaxError(E_HEX)
60
60
 
61
- // We don't use native Buffer impl, as rechecking input make it slower than pure js
62
- // This path is used only on older engines though
63
-
64
61
  if (!dehexArray) {
65
62
  dehexArray = new Int8Array(mapSize).fill(-1) // no regex input validation here, so we map all other bytes to -1 and recheck sign
66
63
  for (let i = 0; i < 16; i++) {
package/hex.js CHANGED
@@ -6,6 +6,8 @@ const { Buffer } = globalThis // Buffer is optional, only used when native
6
6
  const haveNativeBuffer = Buffer && !Buffer.TYPED_ARRAY_SUPPORT
7
7
  const { toHex: webHex } = Uint8Array.prototype // Modern engines have this
8
8
 
9
+ const { E_HEX } = js
10
+
9
11
  export function toHex(arr) {
10
12
  assertUint8(arr)
11
13
  if (arr.length === 0) return ''
@@ -18,4 +20,12 @@ export function toHex(arr) {
18
20
  // Unlike Buffer.from(), throws on invalid input
19
21
  export const fromHex = Uint8Array.fromHex
20
22
  ? (str, format = 'uint8') => typedView(Uint8Array.fromHex(str), format)
21
- : (str, format = 'uint8') => typedView(js.fromHex(str), format)
23
+ : haveNativeBuffer
24
+ ? (str, format = 'uint8') => {
25
+ if (typeof str !== 'string') throw new TypeError('Input is not a string')
26
+ if (str.length % 2 !== 0) throw new SyntaxError(E_HEX)
27
+ const buf = Buffer.from(str, 'hex') // will stop on first non-hex character, so we can just validate length
28
+ if (buf.length * 2 !== str.length) throw new SyntaxError(E_HEX)
29
+ return typedView(buf, format)
30
+ }
31
+ : (str, format = 'uint8') => typedView(js.fromHex(str), format)
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "@exodus/bytes",
3
- "version": "1.0.0-rc.4",
3
+ "version": "1.0.0-rc.5",
4
4
  "description": "Various operations on Uint8Array data",
5
5
  "scripts": {
6
6
  "lint": "eslint .",
7
- "test:v8": "npm run test:d8 --",
8
7
  "test:javascriptcore": "npm run test:jsc --",
9
- "test:d8": "exodus-test --engine=d8:bundle",
8
+ "test:v8": "exodus-test --engine=v8:bundle",
10
9
  "test:jsc": "exodus-test --engine=jsc:bundle",
11
10
  "test:spidermonkey": "exodus-test --engine=spidermonkey:bundle",
12
11
  "test:hermes": "exodus-test --engine=hermes:bundle",
@@ -47,6 +46,8 @@
47
46
  "/array.js",
48
47
  "/assert.js",
49
48
  "/base32.js",
49
+ "/base58.js",
50
+ "/base58check.js",
50
51
  "/base64.js",
51
52
  "/hex.js",
52
53
  "/utf8.js"
@@ -54,23 +55,42 @@
54
55
  "exports": {
55
56
  "./array.js": "./array.js",
56
57
  "./base32.js": "./base32.js",
58
+ "./base58.js": "./base58.js",
59
+ "./base58check.js": "./base58check.js",
57
60
  "./base64.js": "./base64.js",
58
61
  "./hex.js": "./hex.js",
59
62
  "./utf8.js": "./utf8.js"
60
63
  },
64
+ "peerDependencies": {
65
+ "@exodus/crypto": "^1.0.0-rc.4"
66
+ },
67
+ "peerDependenciesMeta": {
68
+ "@exodus/crypto": {
69
+ "optional": true
70
+ }
71
+ },
61
72
  "devDependencies": {
73
+ "@ethersproject/strings": "^5.8.0",
74
+ "@exodus/crypto": "1.0.0-rc.29",
62
75
  "@exodus/eslint-config": "^5.24.0",
63
76
  "@exodus/prettier": "^1.0.0",
64
- "@exodus/test": "^1.0.0-rc.105",
77
+ "@exodus/test": "^1.0.0-rc.107",
78
+ "@noble/hashes": "^2.0.1",
65
79
  "@scure/base": "^1.2.6",
80
+ "@stablelib/base64": "^2.0.1",
81
+ "@stablelib/hex": "^2.0.1",
66
82
  "@types/node": "^24.0.10",
67
83
  "base-x": "^5.0.1",
68
84
  "base32.js": "^0.1.0",
69
85
  "base64-js": "^1.5.1",
86
+ "bs58": "^6.0.0",
87
+ "bs58check": "^4.0.0",
88
+ "bstring": "^0.3.9",
70
89
  "buffer": "^6.0.3",
71
90
  "electron": "36.5.0",
72
91
  "eslint": "^8.44.0",
73
92
  "fast-base64-decode": "^2.0.0",
93
+ "fast-base64-encode": "^1.0.0",
74
94
  "hi-base32": "^0.5.1",
75
95
  "jsvu": "^3.0.0",
76
96
  "text-encoding": "^0.7.0"
package/utf8.js CHANGED
@@ -1,4 +1,4 @@
1
- import { assert, assertUint8 } from './assert.js'
1
+ import { assertUint8 } from './assert.js'
2
2
  import { typedView } from './array.js'
3
3
  import * as js from './fallback/utf8.js'
4
4
 
@@ -11,6 +11,7 @@ const nativeEncoder = isNative(TextEncoder) ? new TextEncoder() : null
11
11
  // We don't want to strip anything unexpectedly
12
12
  const decoderFatal = haveDecoder ? new TextDecoder('utf8', { ignoreBOM: true, fatal: true }) : null
13
13
  const decoderLoose = haveDecoder ? new TextDecoder('utf8', { ignoreBOM: true }) : null
14
+ const { isWellFormed } = String.prototype
14
15
 
15
16
  const { E_STRICT, E_STRICT_UNICODE } = js
16
17
 
@@ -18,6 +19,12 @@ const shouldUseEscapePath = Boolean(globalThis.HermesInternal) // faster only on
18
19
 
19
20
  function deLoose(str, loose, res) {
20
21
  if (loose) return res
22
+ if (isWellFormed) {
23
+ // We have a fast native method
24
+ if (isWellFormed.call(str)) return res
25
+ throw new TypeError(E_STRICT_UNICODE)
26
+ }
27
+
21
28
  // Recheck if the string was encoded correctly
22
29
  let start = 0
23
30
  const last = res.length - 2
@@ -28,7 +35,7 @@ function deLoose(str, loose, res) {
28
35
  start = pos + 1
29
36
  if (res[pos + 1] === 0xbf && res[pos + 2] === 0xbd) {
30
37
  // Found a replacement char in output, need to recheck if we encoded the input correctly
31
- assert(str === decode(res), E_STRICT_UNICODE)
38
+ if (str !== decode(res)) throw new TypeError(E_STRICT_UNICODE)
32
39
  return res
33
40
  }
34
41
  }
@@ -37,7 +44,7 @@ function deLoose(str, loose, res) {
37
44
  }
38
45
 
39
46
  function encode(str, loose = false) {
40
- assert(typeof str === 'string')
47
+ if (typeof str !== 'string') throw new TypeError('Input is not a string')
41
48
  if (haveNativeBuffer) return deLoose(str, loose, Buffer.from(str)) // faster on ascii on Node.js
42
49
  if (nativeEncoder) return deLoose(str, loose, nativeEncoder.encode(str)) // Node.js, browsers, and Hermes
43
50
  // No reason to use unescape + encodeURIComponent: it's slower than JS on normal engines, and modern Hermes already has TextEncoder