-
Notifications
You must be signed in to change notification settings - Fork 781
/
Copy pathbytes.ts
518 lines (463 loc) · 16.1 KB
/
bytes.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
import { getRandomBytesSync } from 'ethereum-cryptography/random.js'
// eslint-disable-next-line no-restricted-imports
import {
bytesToHex as _bytesToUnprefixedHex,
hexToBytes as nobleH2B,
} from 'ethereum-cryptography/utils.js'
import { assertIsArray, assertIsBytes, assertIsHexString } from './helpers.js'
import { isHexString, padToEven, stripHexPrefix } from './internal.js'
import type { PrefixedHexString, TransformableToBytes } from './types.js'
const BIGINT_0 = BigInt(0)
/**
* @deprecated
*/
export const bytesToUnprefixedHex = _bytesToUnprefixedHex
/**
* Converts a {@link PrefixedHexString} to a {@link Uint8Array}
* @param {PrefixedHexString} hex The 0x-prefixed hex string to convert
* @returns {Uint8Array} The converted bytes
* @throws If the input is not a valid 0x-prefixed hex string
*/
export const hexToBytes = (hex: string) => {
if (!hex.startsWith('0x')) throw new Error('input string must be 0x prefixed')
return nobleH2B(padToEven(stripHexPrefix(hex)))
}
export const unprefixedHexToBytes = (hex: string) => {
if (hex.startsWith('0x')) throw new Error('input string cannot be 0x prefixed')
return nobleH2B(padToEven(hex))
}
export const bytesToHex = (bytes: Uint8Array): PrefixedHexString => {
if (bytes === undefined || bytes.length === 0) return '0x'
const unprefixedHex = bytesToUnprefixedHex(bytes)
return ('0x' + unprefixedHex) as PrefixedHexString
}
// BigInt cache for the numbers 0 - 256*256-1 (two-byte bytes)
const BIGINT_CACHE: bigint[] = []
for (let i = 0; i <= 256 * 256 - 1; i++) {
BIGINT_CACHE[i] = BigInt(i)
}
/**
* Converts a {@link Uint8Array} to a {@link bigint}
* @param {Uint8Array} bytes the bytes to convert
* @returns {bigint}
*/
export const bytesToBigInt = (bytes: Uint8Array, littleEndian = false): bigint => {
if (littleEndian) {
bytes.reverse()
}
const hex = bytesToHex(bytes)
if (hex === '0x') {
return BIGINT_0
}
if (hex.length === 4) {
// If the byte length is 1 (this is faster than checking `bytes.length === 1`)
return BIGINT_CACHE[bytes[0]]
}
if (hex.length === 6) {
return BIGINT_CACHE[bytes[0] * 256 + bytes[1]]
}
return BigInt(hex)
}
/**
* Converts a {@link Uint8Array} to a {@link number}.
* @param {Uint8Array} bytes the bytes to convert
* @return {number}
* @throws If the input number exceeds 53 bits.
*/
export const bytesToInt = (bytes: Uint8Array): number => {
const res = Number(bytesToBigInt(bytes))
if (!Number.isSafeInteger(res)) throw new Error('Number exceeds 53 bits')
return res
}
/******************************************/
/**
* Converts a {@link number} into a {@link PrefixedHexString}
* @param {number} i
* @return {PrefixedHexString}
*/
export const intToHex = (i: number): PrefixedHexString => {
if (!Number.isSafeInteger(i) || i < 0) {
throw new Error(`Received an invalid integer type: ${i}`)
}
return ('0x' + i.toString(16)) as PrefixedHexString
}
/**
* Converts an {@link number} to a {@link Uint8Array}
* @param {Number} i
* @return {Uint8Array}
*/
export const intToBytes = (i: number): Uint8Array => {
const hex = intToHex(i)
return hexToBytes(hex)
}
/**
* Converts a {@link bigint} to a {@link Uint8Array}
* * @param {bigint} num the bigint to convert
* @returns {Uint8Array}
*/
export const bigIntToBytes = (num: bigint, littleEndian = false): Uint8Array => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
const bytes = toBytes(`0x${padToEven(num.toString(16))}`)
return littleEndian ? bytes.reverse() : bytes
}
/**
* Pads a `Uint8Array` with zeros till it has `length` bytes.
* Truncates the beginning or end of input if its length exceeds `length`.
* @param {Uint8Array} msg the value to pad
* @param {number} length the number of bytes the output should be
* @param {boolean} right whether to start padding form the left or right
* @return {Uint8Array}
*/
const setLength = (msg: Uint8Array, length: number, right: boolean): Uint8Array => {
if (right) {
if (msg.length < length) {
return new Uint8Array([...msg, ...new Uint8Array(length - msg.length)])
}
return msg.subarray(0, length)
} else {
if (msg.length < length) {
return new Uint8Array([...new Uint8Array(length - msg.length), ...msg])
}
return msg.subarray(-length)
}
}
/**
* Left Pads a `Uint8Array` with leading zeros till it has `length` bytes.
* Or it truncates the beginning if it exceeds.
* @param {Uint8Array} msg the value to pad
* @param {number} length the number of bytes the output should be
* @return {Uint8Array}
*/
export const setLengthLeft = (msg: Uint8Array, length: number): Uint8Array => {
assertIsBytes(msg)
return setLength(msg, length, false)
}
/**
* Right Pads a `Uint8Array` with trailing zeros till it has `length` bytes.
* it truncates the end if it exceeds.
* @param {Uint8Array} msg the value to pad
* @param {number} length the number of bytes the output should be
* @return {Uint8Array}
*/
export const setLengthRight = (msg: Uint8Array, length: number): Uint8Array => {
assertIsBytes(msg)
return setLength(msg, length, true)
}
/**
* Trims leading zeros from a `Uint8Array`, `number[]` or `string`.
* @param {Uint8Array|number[]|string} a
* @return {Uint8Array|number[]|string}
*/
const stripZeros = <T extends Uint8Array | number[] | string = Uint8Array | number[] | string>(
a: T,
): T => {
let first = a[0]
while (a.length > 0 && first.toString() === '0') {
a = a.slice(1) as T
first = a[0]
}
return a
}
/**
* Trims leading zeros from a `Uint8Array`.
* @param {Uint8Array} a
* @return {Uint8Array}
*/
export const unpadBytes = (a: Uint8Array): Uint8Array => {
assertIsBytes(a)
return stripZeros(a)
}
/**
* Trims leading zeros from an `Array` (of numbers).
* @param {number[]} a
* @return {number[]}
*/
export const unpadArray = (a: number[]): number[] => {
assertIsArray(a)
return stripZeros(a)
}
/**
* Trims leading zeros from a `PrefixedHexString`.
* @param {PrefixedHexString} a
* @return {PrefixedHexString}
*/
export const unpadHex = (a: PrefixedHexString): PrefixedHexString => {
assertIsHexString(a)
return `0x${stripZeros(stripHexPrefix(a))}`
}
export type ToBytesInputTypes =
| PrefixedHexString
| number
| bigint
| Uint8Array
| number[]
| TransformableToBytes
| null
| undefined
/**
* Attempts to turn a value into a `Uint8Array`.
* Inputs supported: `Buffer`, `Uint8Array`, `String` (hex-prefixed), `Number`, null/undefined, `BigInt` and other objects
* with a `toArray()` or `toBytes()` method.
* @param {ToBytesInputTypes} v the value
* @return {Uint8Array}
*/
export const toBytes = (v: ToBytesInputTypes): Uint8Array => {
if (v === null || v === undefined) {
return new Uint8Array()
}
if (Array.isArray(v) || v instanceof Uint8Array) {
return Uint8Array.from(v)
}
if (typeof v === 'string') {
if (!isHexString(v)) {
throw new Error(
`Cannot convert string to Uint8Array. toBytes only supports 0x-prefixed hex strings and this string was given: ${v}`,
)
}
return hexToBytes(v)
}
if (typeof v === 'number') {
return intToBytes(v)
}
if (typeof v === 'bigint') {
if (v < BIGINT_0) {
throw new Error(`Cannot convert negative bigint to Uint8Array. Given: ${v}`)
}
let n = v.toString(16)
if (n.length % 2) n = '0' + n
return unprefixedHexToBytes(n)
}
if (v.toBytes !== undefined) {
// converts a `TransformableToBytes` object to a Uint8Array
return v.toBytes()
}
throw new Error('invalid type')
}
/**
* Interprets a `Uint8Array` as a signed integer and returns a `BigInt`. Assumes 256-bit numbers.
* @param {Uint8Array} num Signed integer value
* @returns {bigint}
*/
export const fromSigned = (num: Uint8Array): bigint => {
return BigInt.asIntN(256, bytesToBigInt(num))
}
/**
* Converts a `BigInt` to an unsigned integer and returns it as a `Uint8Array`. Assumes 256-bit numbers.
* @param {bigint} num
* @returns {Uint8Array}
*/
export const toUnsigned = (num: bigint): Uint8Array => {
return bigIntToBytes(BigInt.asUintN(256, num))
}
/**
* Adds "0x" to a given `string` if it does not already start with "0x".
* @param {string} str
* @return {PrefixedHexString}
*/
export const addHexPrefix = (str: string): PrefixedHexString => {
if (typeof str !== 'string') {
return str
}
return isHexString(str) ? str : `0x${str}`
}
/**
* Shortens a string or Uint8Array's hex string representation to maxLength (default 50).
*
* Examples:
*
* Input: '657468657265756d000000000000000000000000000000000000000000000000'
* Output: '657468657265756d0000000000000000000000000000000000…'
* @param {Uint8Array | string} bytes
* @param {number} maxLength
* @return {string}
*/
export const short = (bytes: Uint8Array | string, maxLength: number = 50): string => {
const byteStr = bytes instanceof Uint8Array ? bytesToHex(bytes) : bytes
const len = byteStr.slice(0, 2) === '0x' ? maxLength + 2 : maxLength
if (byteStr.length <= len) {
return byteStr
}
return byteStr.slice(0, len) + '…'
}
/**
* Checks provided Uint8Array for leading zeroes and throws if found.
*
* Examples:
*
* Valid values: 0x1, 0x, 0x01, 0x1234
* Invalid values: 0x0, 0x00, 0x001, 0x0001
*
* Note: This method is useful for validating that RLP encoded integers comply with the rule that all
* integer values encoded to RLP must be in the most compact form and contain no leading zero bytes
* @param values An object containing string keys and Uint8Array values
* @throws if any provided value is found to have leading zero bytes
*/
export const validateNoLeadingZeroes = (values: { [key: string]: Uint8Array | undefined }) => {
for (const [k, v] of Object.entries(values)) {
if (v !== undefined && v.length > 0 && v[0] === 0) {
throw new Error(`${k} cannot have leading zeroes, received: ${bytesToHex(v)}`)
}
}
}
/**
* Converts a {@link bigint} to a `0x` prefixed hex string
* @param {bigint} num the bigint to convert
* @returns {PrefixedHexString}
*/
export const bigIntToHex = (num: bigint): PrefixedHexString => {
return `0x${num.toString(16)}`
}
/**
* Calculates max bigint from an array of bigints
* @param args array of bigints
*/
export const bigIntMax = (...args: bigint[]) => args.reduce((m, e) => (e > m ? e : m))
/**
* Calculates min BigInt from an array of BigInts
* @param args array of bigints
*/
export const bigIntMin = (...args: bigint[]) => args.reduce((m, e) => (e < m ? e : m))
/**
* Convert value from bigint to an unpadded Uint8Array
* (useful for RLP transport)
* @param {bigint} value the bigint to convert
* @returns {Uint8Array}
*/
export const bigIntToUnpaddedBytes = (value: bigint): Uint8Array => {
return unpadBytes(bigIntToBytes(value))
}
export const bigIntToAddressBytes = (value: bigint, strict: boolean = true): Uint8Array => {
const addressBytes = bigIntToBytes(value)
if (strict && addressBytes.length > 20) {
throw Error(`Invalid address bytes length=${addressBytes.length} strict=${strict}`)
}
// setLength already slices if more than requisite length
return setLengthLeft(addressBytes, 20)
}
/**
* Convert value from number to an unpadded Uint8Array
* (useful for RLP transport)
* @param {number} value the bigint to convert
* @returns {Uint8Array}
*/
export const intToUnpaddedBytes = (value: number): Uint8Array => {
return unpadBytes(intToBytes(value))
}
/**
* Compares two Uint8Arrays and returns a number indicating their order in a sorted array.
*
* @param {Uint8Array} value1 - The first Uint8Array to compare.
* @param {Uint8Array} value2 - The second Uint8Array to compare.
* @returns {number} A positive number if value1 is larger than value2,
* A negative number if value1 is smaller than value2,
* or 0 if value1 and value2 are equal.
*/
export const compareBytes = (value1: Uint8Array, value2: Uint8Array): number => {
const bigIntValue1 = bytesToBigInt(value1)
const bigIntValue2 = bytesToBigInt(value2)
return bigIntValue1 > bigIntValue2 ? 1 : bigIntValue1 < bigIntValue2 ? -1 : 0
}
/**
* Generates a Uint8Array of random bytes of specified length.
*
* @param {number} length - The length of the Uint8Array.
* @returns {Uint8Array} A Uint8Array of random bytes of specified length.
*/
export const randomBytes = (length: number): Uint8Array => {
return getRandomBytesSync(length)
}
/**
* This mirrors the functionality of the `ethereum-cryptography` export except
* it skips the check to validate that every element of `arrays` is indeed a `uint8Array`
* Can give small performance gains on large arrays
* @param {Uint8Array[]} arrays an array of Uint8Arrays
* @returns {Uint8Array} one Uint8Array with all the elements of the original set
* works like `Buffer.concat`
*/
export const concatBytes = (...arrays: Uint8Array[]): Uint8Array => {
if (arrays.length === 1) return arrays[0]
const length = arrays.reduce((a, arr) => a + arr.length, 0)
const result = new Uint8Array(length)
for (let i = 0, pad = 0; i < arrays.length; i++) {
const arr = arrays[i]
result.set(arr, pad)
pad += arr.length
}
return result
}
/**
* @notice Convert a Uint8Array to a 32-bit integer
* @param {Uint8Array} bytes The input Uint8Array from which to read the 32-bit integer.
* @param {boolean} littleEndian True for little-endian, undefined or false for big-endian.
* @return {number} The 32-bit integer read from the input Uint8Array.
*/
export function bytesToInt32(bytes: Uint8Array, littleEndian: boolean = false): number {
if (bytes.length < 4) {
bytes = setLength(bytes, 4, littleEndian)
}
const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
return dataView.getUint32(0, littleEndian)
}
/**
* @notice Convert a Uint8Array to a 64-bit bigint
* @param {Uint8Array} bytes The input Uint8Array from which to read the 64-bit bigint.
* @param {boolean} littleEndian True for little-endian, undefined or false for big-endian.
* @return {bigint} The 64-bit bigint read from the input Uint8Array.
*/
export function bytesToBigInt64(bytes: Uint8Array, littleEndian: boolean = false): bigint {
if (bytes.length < 8) {
bytes = setLength(bytes, 8, littleEndian)
}
const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
return dataView.getBigUint64(0, littleEndian)
}
/**
* @notice Convert a 32-bit integer to a Uint8Array.
* @param {number} value The 32-bit integer to convert.
* @param {boolean} littleEndian True for little-endian, undefined or false for big-endian.
* @return {Uint8Array} A Uint8Array of length 4 containing the integer.
*/
export function int32ToBytes(value: number, littleEndian: boolean = false): Uint8Array {
const buffer = new ArrayBuffer(4)
const dataView = new DataView(buffer)
dataView.setUint32(0, value, littleEndian)
return new Uint8Array(buffer)
}
/**
* @notice Convert a 64-bit bigint to a Uint8Array.
* @param {bigint} value The 64-bit bigint to convert.
* @param {boolean} littleEndian True for little-endian, undefined or false for big-endian.
* @return {Uint8Array} A Uint8Array of length 8 containing the bigint.
*/
export function bigInt64ToBytes(value: bigint, littleEndian: boolean = false): Uint8Array {
const buffer = new ArrayBuffer(8)
const dataView = new DataView(buffer)
dataView.setBigUint64(0, value, littleEndian)
return new Uint8Array(buffer)
}
// eslint-disable-next-line no-restricted-imports
export { bytesToUtf8, equalsBytes, utf8ToBytes } from 'ethereum-cryptography/utils.js'
export function hexToBigInt(input: PrefixedHexString): bigint {
return bytesToBigInt(hexToBytes(isHexString(input) ? input : `0x${input}`))
}
/**
* Compares two byte arrays and returns the count of consecutively matching items from the start.
*
* @function
* @param {Uint8Array} bytes1 - The first Uint8Array to compare.
* @param {Uint8Array} bytes2 - The second Uint8Array to compare.
* @returns {number} The count of consecutively matching items from the start.
*/
export function matchingBytesLength(bytes1: Uint8Array, bytes2: Uint8Array): number {
let count = 0
const minLength = Math.min(bytes1.length, bytes2.length)
for (let i = 0; i < minLength; i++) {
if (bytes1[i] === bytes2[i]) {
count++
} else {
// Break early if a mismatch is found
break
}
}
return count
}