@exodus/bytes 1.0.0-rc.1 → 1.0.0-rc.11

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,3 +1,173 @@
1
- # bytes
1
+ # `@exodus/bytes`
2
2
 
3
- Data structures handling
3
+ `Uint8Array` conversion to and from `base64`, `base32`, `base58`, `hex`, `utf8`, `utf16`, `bech32` and `wif`
4
+
5
+ And a [`TextEncoder` / `TextDecoder` polyfill](#textencoder--textdecoder-polyfill)
6
+
7
+ ## Strict
8
+
9
+ Performs proper input validation, ensures no garbage-in-garbage-out
10
+
11
+ Tested on Node.js, Deno, Bun, browsers (including Servo), Hermes, QuickJS and barebone engines in CI [(how?)](https://github.com/ExodusMovement/test#exodustest)
12
+
13
+ ## Fast
14
+
15
+ * `10-20x` faster than `Buffer` polyfill
16
+ * `2-10x` faster than `iconv-lite`
17
+
18
+ The above was for the js fallback
19
+
20
+ It's up to `100x` when native impl is available \
21
+ e.g. in `utf8fromString` on Hermes / React Native or `fromHex` in Chrome
22
+
23
+ Also:
24
+ * `3-8x` faster than `bs58`
25
+ * `10-30x` faster than `@scure/base` (or `>100x` on Node.js <25)
26
+ * Faster in `utf8toString` / `utf8fromString` than `Buffer` or `TextDecoder` / `TextEncoder` on Node.js
27
+
28
+ See [Performance](./Performance.md) for more info
29
+
30
+ ## TextEncoder / TextDecoder polyfill
31
+
32
+ ```js
33
+ import { TextDecoder, TextEncoder } from '@exodus/bytes/encoding.js'
34
+ ```
35
+
36
+ Less than half the bundle size of [text-encoding](https://npmjs.com/text-encoding), [whatwg-encoding](https://npmjs.com/whatwg-encoding) or [iconv-lite](https://npmjs.com/iconv-lite) (gzipped or not), and [is much faster](#fast).
37
+ See also [lite version](#lite-version).
38
+
39
+ Spec compliant, passing WPT and covered with extra tests.
40
+
41
+ Moreover, tests for this library uncovered [bugs in all major implementations](https://docs.google.com/spreadsheets/d/1pdEefRG6r9fZy61WHGz0TKSt8cO4ISWqlpBN5KntIvQ/edit).
42
+
43
+ [Faster than Node.js native implementation on Node.js](https://github.com/nodejs/node/issues/61041#issuecomment-3649242024).
44
+
45
+ ### Caveat: `TextDecoder` / `TextEncoder` APIs are lossy by default per spec
46
+
47
+ _These are only provided as a compatibility layer, prefer hardened APIs instead in new code._
48
+
49
+ * `TextDecoder` can (and should) be used with `{ fatal: true }` option for all purposes demanding correctness / lossless transforms
50
+
51
+ * `TextEncoder` does not support a fatal mode per spec, it always performs replacement.
52
+
53
+ That is not suitable for hashing, cryptography or consensus applications.\
54
+ Otherwise there would be non-equal strings with equal signatures and hashes — the collision is caused by the lossy transform of a JS string to bytes.
55
+ Those also survive e.g. `JSON.stringify`/`JSON.parse` or being sent over network.
56
+
57
+ Use strict APIs in new applications, see `utf8fromString` / `utf16fromString` below.\
58
+ Those throw on non-well-formed strings by default.
59
+
60
+ ### Lite version
61
+
62
+ If you don't need support for legacy multi-byte encodings, you can use the lite import:
63
+ ```js
64
+ import { TextDecoder, TextEncoder } from '@exodus/bytes/encoding-lite.js'
65
+ ```
66
+
67
+ This reduces the bundle size 9x:\
68
+ from 91 KiB gzipped for `@exodus/bytes/encoding.js` to 10 KiB gzipped for `@exodus/bytes/encoding-lite.js`.\
69
+ (For comparison, `text-encoding` module is 190 KiB gzipped, and `iconv-lite` is 194 KiB gzipped).
70
+
71
+ It still supports `utf-8`, `utf-16le`, `utf-16be` and all single-byte encodings specified by the spec,
72
+ the only difference is support for legacy multi-byte encodings.
73
+
74
+ See [the list of encodings](https://encoding.spec.whatwg.org/#names-and-labels).
75
+
76
+ ## API
77
+
78
+ ### `@exodus/bytes/utf8.js`
79
+
80
+ ##### `utf8fromString(str, format = 'uint8')`
81
+ ##### `utf8fromStringLoose(str, format = 'uint8')`
82
+ ##### `utf8toString(arr)`
83
+ ##### `utf8toStringLoose(arr)`
84
+
85
+ ### `@exodus/bytes/utf16.js`
86
+
87
+ ##### `utf16fromString(str, format = 'uint16')`
88
+ ##### `utf16fromStringLoose(str, format = 'uint16')`
89
+ ##### `utf16toString(arr, 'uint16')`
90
+ ##### `utf16toStringLoose(arr, 'uint16')`
91
+
92
+ ### `@exodus/bytes/single-byte.js`
93
+
94
+ ##### `createSinglebyteDecoder(encoding, loose = false)`
95
+
96
+ Create a decoder for a supported one-byte `encoding`.
97
+
98
+ Returns a function `decode(arr)` that decodes bytes to a string.
99
+
100
+ ### `@exodus/bytes/multi-byte.js`
101
+
102
+ ##### `createMultibyteDecoder(encoding, loose = false)`
103
+
104
+ Create a decoder for a supported legacy multi-byte `encoding`.
105
+
106
+ Returns a function `decode(arr, stream = false)` that decodes bytes to a string.
107
+
108
+ That function will have state while `stream = true` is used.
109
+
110
+ ##### `windows1252toString(arr)`
111
+
112
+ Decode `windows-1252` bytes to a string.
113
+
114
+ Also supports `ascii` and `latin-1` as those are strict subsets of `windows-1252`.
115
+
116
+ There is no loose variant for this encoding, all bytes can be decoded.
117
+
118
+ Same as `windows1252toString = createSinglebyteDecoder('windows-1252')`.
119
+
120
+ ### `@exodus/bytes/hex.js`
121
+
122
+ ##### `toHex(arr)`
123
+ ##### `fromHex(string)`
124
+
125
+ ### `@exodus/bytes/base64.js`
126
+
127
+ ##### `toBase64(arr, { padding = true })`
128
+ ##### `toBase64url(arr, { padding = false })`
129
+ ##### `fromBase64(str, { format = 'uint8', padding = 'both' })`
130
+ ##### `fromBase64url(str, { format = 'uint8', padding = false })`
131
+ ##### `fromBase64any(str, { format = 'uint8', padding = 'both' })`
132
+
133
+ ### `@exodus/bytes/base32.js`
134
+
135
+ ##### `toBase32(arr, { padding = false })`
136
+ ##### `toBase32hex(arr, { padding = false })`
137
+ ##### `fromBase32(str, { format = 'uint8', padding = 'both' })`
138
+ ##### `fromBase32hex(str, { format = 'uint8', padding = 'both' })`
139
+
140
+ ### `@exodus/bytes/bech32.js`
141
+
142
+ ##### `getPrefix(str, limit = 90)`
143
+ ##### `toBech32(prefix, bytes, limit = 90)`
144
+ ##### `fromBech32(str, limit = 90)`
145
+ ##### `toBech32m(prefix, bytes, limit = 90)`
146
+ ##### `fromBech32m(str, limit = 90)`
147
+
148
+ ### `@exodus/bytes/base58.js`
149
+
150
+ ##### `toBase58(arr)`
151
+ ##### `fromBase58(str, format = 'uint8')`
152
+
153
+ ##### `toBase58xrp(arr)`
154
+ ##### `fromBase58xrp(str, format = 'uint8')`
155
+
156
+ ### `@exodus/bytes/base58check.js`
157
+
158
+ ##### `async toBase58check(arr)`
159
+ ##### `toBase58checkSync(arr)`
160
+ ##### `async fromBase58check(str, format = 'uint8')`
161
+ ##### `fromBase58checkSync(str, format = 'uint8')`
162
+ ##### `makeBase58check(hashAlgo, hashAlgoSync)`
163
+
164
+ ### `@exodus/bytes/wif.js`
165
+
166
+ ##### `async fromWifString(string, version)`
167
+ ##### `fromWifStringSync(string, version)`
168
+ ##### `async toWifString({ version, privateKey, compressed })`
169
+ ##### `toWifStringSync({ version, privateKey, compressed })`
170
+
171
+ ## License
172
+
173
+ [MIT](./LICENSE)
package/array.js CHANGED
@@ -2,7 +2,7 @@ import { assertTypedArray } from './assert.js'
2
2
 
3
3
  const { Buffer } = globalThis // Buffer is optional
4
4
 
5
- export function fromTypedArray(arr, format) {
5
+ export function typedView(arr, format) {
6
6
  assertTypedArray(arr)
7
7
  switch (format) {
8
8
  case 'uint8':
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
  }
@@ -12,10 +8,18 @@ const makeMessage = (name, extra) => `Expected${name ? ` ${name} to be` : ''} an
12
8
  const TypedArray = Object.getPrototypeOf(Uint8Array)
13
9
 
14
10
  export function assertTypedArray(arr) {
15
- assert(arr instanceof TypedArray, 'Expected a TypedArray instance')
11
+ if (arr instanceof TypedArray) return
12
+ throw new TypeError('Expected a TypedArray instance')
16
13
  }
17
14
 
18
- export function assertUint8(arr, { name, length, ...rest } = {}) {
15
+ export function assertUint8(arr, options) {
16
+ if (!options) {
17
+ // fast path
18
+ if (arr instanceof Uint8Array) return
19
+ throw new TypeError('Expected an Uint8Array')
20
+ }
21
+
22
+ const { name, length, ...rest } = options
19
23
  assertEmptyRest(rest)
20
24
  if (arr instanceof Uint8Array && (length === undefined || arr.length === length)) return
21
25
  throw new TypeError(makeMessage(name, length === undefined ? '' : ` of size ${Number(length)}`))
package/base32.js ADDED
@@ -0,0 +1,40 @@
1
+ import { assertEmptyRest } from './assert.js'
2
+ import { typedView } from './array.js'
3
+ import * as js from './fallback/base32.js'
4
+
5
+ // See https://datatracker.ietf.org/doc/html/rfc4648
6
+
7
+ // 8 chars per 5 bytes
8
+
9
+ const { E_PADDING } = js
10
+
11
+ export const toBase32 = (arr, { padding = false } = {}) => js.toBase32(arr, false, padding)
12
+ export const toBase32hex = (arr, { padding = false } = {}) => js.toBase32(arr, true, padding)
13
+
14
+ // By default, valid padding is accepted but not required
15
+ export function fromBase32(str, options) {
16
+ if (!options) return fromBase32common(str, false, 'both', 'uint8', null)
17
+ const { format = 'uint8', padding = 'both', ...rest } = options
18
+ return fromBase32common(str, false, padding, format, rest)
19
+ }
20
+
21
+ export function fromBase32hex(str, options) {
22
+ if (!options) return fromBase32common(str, true, 'both', 'uint8', null)
23
+ const { format = 'uint8', padding = 'both', ...rest } = options
24
+ return fromBase32common(str, true, padding, format, rest)
25
+ }
26
+
27
+ function fromBase32common(str, isBase32Hex, padding, format, rest) {
28
+ if (typeof str !== 'string') throw new TypeError('Input is not a string')
29
+ if (rest !== null) assertEmptyRest(rest)
30
+
31
+ if (padding === true) {
32
+ if (str.length % 8 !== 0) throw new SyntaxError(E_PADDING)
33
+ } else if (padding === false) {
34
+ if (str.endsWith('=')) throw new SyntaxError('Did not expect padding in base32 input')
35
+ } else if (padding !== 'both') {
36
+ throw new TypeError('Invalid padding option')
37
+ }
38
+
39
+ return typedView(js.fromBase32(str, isBase32Hex), format)
40
+ }
package/base58.js ADDED
@@ -0,0 +1,220 @@
1
+ import { typedView } from './array.js'
2
+ import { assertUint8 } from './assert.js'
3
+ import { nativeDecoder, nativeEncoder, isHermes } from './fallback/_utils.js'
4
+ import { encodeAscii, decodeAscii } from './fallback/latin1.js'
5
+
6
+ const alphabet58 = [...'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz']
7
+ const alphabetXRP = [...'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz']
8
+ const codes58 = new Uint8Array(alphabet58.map((x) => x.charCodeAt(0)))
9
+ const codesXRP = new Uint8Array(alphabetXRP.map((x) => x.charCodeAt(0)))
10
+
11
+ const _0n = BigInt(0)
12
+ const _1n = BigInt(1)
13
+ const _8n = BigInt(8)
14
+ const _32n = BigInt(32)
15
+ const _58n = BigInt(58)
16
+ const _0xffffffffn = BigInt(0xff_ff_ff_ff)
17
+
18
+ let table // 15 * 82, diagonal, <1kb
19
+ const fromMaps = new Map()
20
+
21
+ const E_CHAR = 'Invalid character in base58 input'
22
+
23
+ const shouldUseBigIntFrom = isHermes // faster only on Hermes, numbers path beats it on normal engines
24
+
25
+ function toBase58core(arr, alphabet, codes) {
26
+ assertUint8(arr)
27
+ const length = arr.length
28
+ if (length === 0) return ''
29
+
30
+ const ZERO = alphabet[0]
31
+ let zeros = 0
32
+ while (zeros < length && arr[zeros] === 0) zeros++
33
+
34
+ if (length > 60) {
35
+ // Slow path. Can be optimized ~10%, but the main factor is /58n division anyway, so doesn't matter much
36
+ let x = _0n
37
+ for (let i = 0; i < arr.length; i++) x = (x << _8n) | BigInt(arr[i])
38
+
39
+ let out = ''
40
+ while (x) {
41
+ const d = x / _58n
42
+ out = alphabet[Number(x - _58n * d)] + out
43
+ x = d
44
+ }
45
+
46
+ return ZERO.repeat(zeros) + out
47
+ }
48
+
49
+ // We run fast mode operations only on short (<=60 bytes) inputs, via precomputation table
50
+ if (!table) {
51
+ table = []
52
+ let x = _1n
53
+ for (let i = 0; i < 15; i++) {
54
+ // Convert x to base 58 digits
55
+ const in58 = []
56
+ let y = x
57
+ while (y) {
58
+ const d = y / _58n
59
+ in58.push(Number(y - _58n * d))
60
+ y = d
61
+ }
62
+
63
+ table.push(new Uint8Array(in58))
64
+ x <<= _32n
65
+ }
66
+ }
67
+
68
+ const res = []
69
+ {
70
+ let j = 0
71
+ // We group each 4 bytes into 32-bit chunks
72
+ // Not using u32arr to not deal with remainder + BE/LE differences
73
+ for (let i = length - 1; i >= 0; i -= 4) {
74
+ let c
75
+ if (i > 2) {
76
+ c = (arr[i] | (arr[i - 1] << 8) | (arr[i - 2] << 16) | (arr[i - 3] << 24)) >>> 0
77
+ } else if (i > 1) {
78
+ c = arr[i] | (arr[i - 1] << 8) | (arr[i - 2] << 16)
79
+ } else {
80
+ c = i === 1 ? arr[i] | (arr[i - 1] << 8) : arr[i]
81
+ }
82
+
83
+ const row = table[j++]
84
+ if (c === 0) continue
85
+ const olen = res.length
86
+ const nlen = row.length
87
+ let k = 0
88
+ for (; k < olen; k++) res[k] += c * row[k]
89
+ while (k < nlen) res.push(c * row[k++])
90
+ }
91
+ }
92
+
93
+ // We can now do a single scan over regular numbers under MAX_SAFE_INTEGER
94
+ // Note: can't use int32 operations on them, as they are outside of 2**32 range
95
+ // This is faster though
96
+ {
97
+ let carry = 0
98
+ let i = 0
99
+ while (i < res.length) {
100
+ const c = res[i] + carry
101
+ carry = Math.floor(c / 58)
102
+ res[i++] = c - carry * 58
103
+ }
104
+
105
+ while (carry) {
106
+ const c = carry
107
+ carry = Math.floor(c / 58)
108
+ res.push(c - carry * 58)
109
+ }
110
+ }
111
+
112
+ if (nativeDecoder) {
113
+ const oa = new Uint8Array(res.length)
114
+ let j = 0
115
+ for (let i = res.length - 1; i >= 0; i--) oa[j++] = codes[res[i]]
116
+ return ZERO.repeat(zeros) + decodeAscii(oa)
117
+ }
118
+
119
+ let out = ''
120
+ for (let i = res.length - 1; i >= 0; i--) out += alphabet[res[i]]
121
+ return ZERO.repeat(zeros) + out
122
+ }
123
+
124
+ function fromBase58core(str, alphabet, codes, format = 'uint8') {
125
+ if (typeof str !== 'string') throw new TypeError('Input is not a string')
126
+ const length = str.length
127
+ if (length === 0) return typedView(new Uint8Array(), format)
128
+
129
+ const zeroC = codes[0]
130
+ let zeros = 0
131
+ while (zeros < length && str.charCodeAt(zeros) === zeroC) zeros++
132
+
133
+ let fromMap = fromMaps.get(alphabet)
134
+ if (!fromMap) {
135
+ fromMap = new Int8Array(256).fill(-1)
136
+ for (let i = 0; i < 58; i++) fromMap[alphabet[i].charCodeAt(0)] = i
137
+ fromMaps.set(alphabet, fromMap)
138
+ }
139
+
140
+ const size = zeros + (((length - zeros + 1) * 3) >> 2) // 3/4 rounded up, larger than ~0.73 coef to fit everything
141
+ const res = new Uint8Array(size)
142
+ let at = size // where is the first significant byte written
143
+
144
+ if (shouldUseBigIntFrom) {
145
+ let x = _0n
146
+
147
+ // nativeEncoder gives a benefit here
148
+ if (nativeEncoder) {
149
+ const codes = encodeAscii(str, E_CHAR)
150
+ for (let i = zeros; i < length; i++) {
151
+ const c = fromMap[codes[i]]
152
+ if (c < 0) throw new SyntaxError(E_CHAR)
153
+ x = x * _58n + BigInt(c)
154
+ }
155
+ } else {
156
+ for (let i = zeros; i < length; i++) {
157
+ const charCode = str.charCodeAt(i)
158
+ const c = fromMap[charCode]
159
+ if (charCode > 255 || c < 0) throw new SyntaxError(E_CHAR)
160
+ x = x * _58n + BigInt(c)
161
+ }
162
+ }
163
+
164
+ while (x) {
165
+ let y = Number(x & _0xffffffffn)
166
+ x >>= 32n
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
+ y >>>= 8
175
+ if (!x && !y) break
176
+ res[--at] = y & 0xff
177
+ }
178
+ } else {
179
+ for (let i = zeros; i < length; i++) {
180
+ const charCode = str.charCodeAt(i)
181
+ let c = fromMap[charCode]
182
+ if (charCode > 255 || c < 0) throw new SyntaxError(E_CHAR)
183
+
184
+ let k = size - 1
185
+ for (;;) {
186
+ if (c === 0 && k < at) break
187
+ c += 58 * res[k]
188
+ res[k] = c & 0xff
189
+ c >>>= 8
190
+ k--
191
+ // unroll a bit
192
+ if (c === 0 && k < at) break
193
+ c += 58 * res[k]
194
+ res[k] = c & 0xff
195
+ c >>>= 8
196
+ k--
197
+ if (c === 0 && k < at) break
198
+ c += 58 * res[k]
199
+ res[k] = c & 0xff
200
+ c >>>= 8
201
+ k--
202
+ if (c === 0 && k < at) break
203
+ c += 58 * res[k]
204
+ res[k] = c & 0xff
205
+ c >>>= 8
206
+ k--
207
+ }
208
+
209
+ at = k + 1
210
+ if (c !== 0 || at < zeros) throw new Error('Unexpected') // unreachable
211
+ }
212
+ }
213
+
214
+ return typedView(res.slice(at - zeros), format) // slice is faster for small sizes than subarray
215
+ }
216
+
217
+ export const toBase58 = (arr) => toBase58core(arr, alphabet58, codes58)
218
+ export const fromBase58 = (str, format) => fromBase58core(str, alphabet58, codes58, format)
219
+ export const toBase58xrp = (arr) => toBase58core(arr, alphabetXRP, codesXRP)
220
+ export const fromBase58xrp = (str, format) => fromBase58core(str, alphabetXRP, codesXRP, format)
package/base58check.js ADDED
@@ -0,0 +1,69 @@
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' // eslint-disable-line @exodus/import/no-deprecated
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 E_CHECKSUM = 'Invalid checksum'
10
+
11
+ // checksum length is 4, i.e. only the first 4 bytes of the hash are used
12
+
13
+ function encodeWithChecksum(arr, checksum) {
14
+ // arr type in already validated in input
15
+ const res = new Uint8Array(arr.length + 4)
16
+ res.set(arr, 0)
17
+ res.set(checksum.subarray(0, 4), arr.length)
18
+ return toBase58(res)
19
+ }
20
+
21
+ function decodeWithChecksum(str) {
22
+ const arr = fromBase58(str) // checks input
23
+ const payloadSize = arr.length - 4
24
+ if (payloadSize < 0) throw new Error(E_CHECKSUM)
25
+ return [arr.subarray(0, payloadSize), arr.subarray(payloadSize)]
26
+ }
27
+
28
+ function assertChecksum(c, r) {
29
+ if ((c[0] ^ r[0]) | (c[1] ^ r[1]) | (c[2] ^ r[2]) | (c[3] ^ r[3])) throw new Error(E_CHECKSUM)
30
+ }
31
+
32
+ export const makeBase58check = (hashAlgo, hashAlgoSync) => {
33
+ const apis = {
34
+ async encode(arr) {
35
+ assertUint8(arr)
36
+ return encodeWithChecksum(arr, await hashAlgo(arr))
37
+ },
38
+ async decode(str, format = 'uint8') {
39
+ const [payload, checksum] = decodeWithChecksum(str)
40
+ assertChecksum(checksum, await hashAlgo(payload))
41
+ return typedView(payload, format)
42
+ },
43
+ }
44
+ if (!hashAlgoSync) return apis
45
+ return {
46
+ ...apis,
47
+ encodeSync(arr) {
48
+ assertUint8(arr)
49
+ return encodeWithChecksum(arr, hashAlgoSync(arr))
50
+ },
51
+ decodeSync(str, format = 'uint8') {
52
+ const [payload, checksum] = decodeWithChecksum(str)
53
+ assertChecksum(checksum, hashAlgoSync(payload))
54
+ return typedView(payload, format)
55
+ },
56
+ }
57
+ }
58
+
59
+ // eslint-disable-next-line @exodus/import/no-deprecated
60
+ const hash256sync = (x) => hashSync('sha256', hashSync('sha256', x, 'uint8'), 'uint8')
61
+ const hash256 = hash256sync // See note at the top
62
+ const {
63
+ encode: toBase58check,
64
+ decode: fromBase58check,
65
+ encodeSync: toBase58checkSync,
66
+ decodeSync: fromBase58checkSync,
67
+ } = makeBase58check(hash256, hash256sync)
68
+
69
+ export { toBase58check, fromBase58check, toBase58checkSync, fromBase58checkSync }