@feelyourprotocol/rlp 8141.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/src/errors.ts ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Generic EthereumJS error class with metadata attached
3
+ *
4
+ * Kudos to https://github.com/ChainSafe/lodestar monorepo
5
+ * for the inspiration :-)
6
+ * See: https://github.com/ChainSafe/lodestar/blob/unstable/packages/utils/src/errors.ts
7
+ */
8
+ export type EthereumJSErrorMetaData = Record<string, string | number | null>
9
+ export type EthereumJSErrorObject = {
10
+ message: string
11
+ stack: string
12
+ className: string
13
+ type: EthereumJSErrorMetaData
14
+ }
15
+
16
+ // In order to update all our errors to use `EthereumJSError`, temporarily include the
17
+ // unset error code. All errors throwing this code should be updated to use the relevant
18
+ // error code.
19
+ export const DEFAULT_ERROR_CODE = 'ETHEREUMJS_DEFAULT_ERROR_CODE'
20
+
21
+ /**
22
+ * Generic EthereumJS error with attached metadata
23
+ */
24
+ export class EthereumJSError<T extends { code: string }> extends Error {
25
+ type: T
26
+ constructor(type: T, message?: string, stack?: string) {
27
+ super(message ?? type.code)
28
+ this.type = type
29
+ if (stack !== undefined) this.stack = stack
30
+ }
31
+
32
+ getMetadata(): EthereumJSErrorMetaData {
33
+ return this.type
34
+ }
35
+
36
+ /**
37
+ * Get the metadata and the stacktrace for the error.
38
+ */
39
+ toObject(): EthereumJSErrorObject {
40
+ return {
41
+ type: this.getMetadata(),
42
+ message: this.message ?? '',
43
+ stack: this.stack ?? '',
44
+ className: this.constructor.name,
45
+ }
46
+ }
47
+ }
48
+
49
+ /**
50
+ * @deprecated Use `EthereumJSError` with a set error code instead
51
+ * @param message Optional error message
52
+ * @param stack Optional stack trace
53
+ * @returns
54
+ */
55
+ export function EthereumJSErrorWithoutCode(message?: string, stack?: string) {
56
+ return new EthereumJSError({ code: DEFAULT_ERROR_CODE }, message, stack)
57
+ }
package/src/index.ts ADDED
@@ -0,0 +1,327 @@
1
+ import { EthereumJSErrorWithoutCode } from './errors.ts'
2
+
3
+ export * from './errors.ts'
4
+
5
+ export type Input = string | number | bigint | Uint8Array | Array<Input> | null | undefined
6
+
7
+ export type NestedUint8Array = Array<Uint8Array | NestedUint8Array>
8
+
9
+ export interface Decoded {
10
+ data: Uint8Array | NestedUint8Array
11
+ remainder: Uint8Array
12
+ }
13
+
14
+ /**
15
+ * Parse integers. Check if there is no leading zeros
16
+ * @param v The value to parse
17
+ */
18
+ function decodeLength(v: Uint8Array): number {
19
+ if (v[0] === 0) {
20
+ throw EthereumJSErrorWithoutCode('invalid RLP: extra zeros')
21
+ }
22
+ return parseHexByte(bytesToHex(v))
23
+ }
24
+
25
+ function encodeLength(len: number, offset: number): Uint8Array {
26
+ if (len < 56) {
27
+ return Uint8Array.from([len + offset])
28
+ }
29
+ const hexLength = numberToHex(len)
30
+ const lLength = hexLength.length / 2
31
+ const firstByte = numberToHex(offset + 55 + lLength)
32
+ return Uint8Array.from(hexToBytes(firstByte + hexLength))
33
+ }
34
+
35
+ /**
36
+ * Slices a Uint8Array, throws if the slice goes out-of-bounds of the Uint8Array.
37
+ * E.g. `safeSlice(hexToBytes('aa'), 1, 2)` will throw.
38
+ * @param input
39
+ * @param start
40
+ * @param end
41
+ */
42
+ function safeSlice(input: Uint8Array, start: number, end: number) {
43
+ if (end > input.length) {
44
+ throw EthereumJSErrorWithoutCode(
45
+ 'invalid RLP (safeSlice): end slice of Uint8Array out-of-bounds',
46
+ )
47
+ }
48
+ return input.slice(start, end)
49
+ }
50
+
51
+ /** Decode an input with RLP */
52
+ function _decode(input: Uint8Array): Decoded {
53
+ let length: number, lLength: number, data: Uint8Array, innerRemainder: Uint8Array, d: Decoded
54
+ const decoded = []
55
+ const firstByte = input[0]
56
+
57
+ if (firstByte <= 0x7f) {
58
+ // a single byte whose value is in the [0x00, 0x7f] range, that byte is its own RLP encoding.
59
+ return {
60
+ data: input.slice(0, 1),
61
+ remainder: input.subarray(1),
62
+ }
63
+ } else if (firstByte <= 0xb7) {
64
+ // string is 0-55 bytes long. A single byte with value 0x80 plus the length of the string followed by the string
65
+ // The range of the first byte is [0x80, 0xb7]
66
+ length = firstByte - 0x7f
67
+
68
+ // set 0x80 null to 0
69
+ if (firstByte === 0x80) {
70
+ data = Uint8Array.from([])
71
+ } else {
72
+ data = safeSlice(input, 1, length)
73
+ }
74
+
75
+ if (length === 2 && data[0] < 0x80) {
76
+ throw EthereumJSErrorWithoutCode(
77
+ 'invalid RLP encoding: invalid prefix, single byte < 0x80 are not prefixed',
78
+ )
79
+ }
80
+
81
+ return {
82
+ data,
83
+ remainder: input.subarray(length),
84
+ }
85
+ } else if (firstByte <= 0xbf) {
86
+ // string is greater than 55 bytes long. A single byte with the value (0xb7 plus the length of the length),
87
+ // followed by the length, followed by the string
88
+ lLength = firstByte - 0xb6
89
+ if (input.length - 1 < lLength) {
90
+ throw EthereumJSErrorWithoutCode('invalid RLP: not enough bytes for string length')
91
+ }
92
+ length = decodeLength(safeSlice(input, 1, lLength))
93
+ if (length <= 55) {
94
+ throw EthereumJSErrorWithoutCode('invalid RLP: expected string length to be greater than 55')
95
+ }
96
+ data = safeSlice(input, lLength, length + lLength)
97
+
98
+ return {
99
+ data,
100
+ remainder: input.subarray(length + lLength),
101
+ }
102
+ } else if (firstByte <= 0xf7) {
103
+ // a list between 0-55 bytes long
104
+ length = firstByte - 0xbf
105
+ innerRemainder = safeSlice(input, 1, length)
106
+ while (innerRemainder.length) {
107
+ d = _decode(innerRemainder)
108
+ decoded.push(d.data)
109
+ innerRemainder = d.remainder
110
+ }
111
+
112
+ return {
113
+ data: decoded,
114
+ remainder: input.subarray(length),
115
+ }
116
+ } else {
117
+ // a list over 55 bytes long
118
+ lLength = firstByte - 0xf6
119
+ length = decodeLength(safeSlice(input, 1, lLength))
120
+ if (length < 56) {
121
+ throw EthereumJSErrorWithoutCode('invalid RLP: encoded list too short')
122
+ }
123
+ const totalLength = lLength + length
124
+ if (totalLength > input.length) {
125
+ throw EthereumJSErrorWithoutCode('invalid RLP: total length is larger than the data')
126
+ }
127
+
128
+ innerRemainder = safeSlice(input, lLength, totalLength)
129
+
130
+ while (innerRemainder.length) {
131
+ d = _decode(innerRemainder)
132
+ decoded.push(d.data)
133
+ innerRemainder = d.remainder
134
+ }
135
+
136
+ return {
137
+ data: decoded,
138
+ remainder: input.subarray(totalLength),
139
+ }
140
+ }
141
+ }
142
+
143
+ const cachedHexes = Array.from({ length: 256 }, (_v, i) => i.toString(16).padStart(2, '0'))
144
+ function bytesToHex(uint8a: Uint8Array): string {
145
+ // Pre-caching chars with `cachedHexes` speeds this up 6x
146
+ let hex = ''
147
+ for (let i = 0; i < uint8a.length; i++) {
148
+ hex += cachedHexes[uint8a[i]]
149
+ }
150
+ return hex
151
+ }
152
+
153
+ function parseHexByte(hexByte: string): number {
154
+ const byte = Number.parseInt(hexByte, 16)
155
+ if (Number.isNaN(byte)) throw EthereumJSErrorWithoutCode('Invalid byte sequence')
156
+ return byte
157
+ }
158
+
159
+ // Borrowed from @noble/curves to avoid dependency
160
+ // Original code here - https://github.com/paulmillr/noble-curves/blob/d0a8d2134c5737d9d0aa81be13581cd416ebdeb4/src/abstract/utils.ts#L63-L91
161
+ const asciis = { _0: 48, _9: 57, _A: 65, _F: 70, _a: 97, _f: 102 } as const
162
+ function asciiToBase16(char: number): number | undefined {
163
+ if (char >= asciis._0 && char <= asciis._9) return char - asciis._0
164
+ if (char >= asciis._A && char <= asciis._F) return char - (asciis._A - 10)
165
+ if (char >= asciis._a && char <= asciis._f) return char - (asciis._a - 10)
166
+ return
167
+ }
168
+
169
+ /**
170
+ * @example hexToBytes('0xcafe0123') // Uint8Array.from([0xca, 0xfe, 0x01, 0x23])
171
+ */
172
+ export function hexToBytes(hex: string): Uint8Array {
173
+ if (hex.slice(0, 2) === '0x') hex = hex.slice(0, 2)
174
+ if (typeof hex !== 'string')
175
+ throw EthereumJSErrorWithoutCode('hex string expected, got ' + typeof hex)
176
+ const hl = hex.length
177
+ const al = hl / 2
178
+ if (hl % 2)
179
+ throw EthereumJSErrorWithoutCode('padded hex string expected, got unpadded hex of length ' + hl)
180
+ const array = new Uint8Array(al)
181
+ for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) {
182
+ const n1 = asciiToBase16(hex.charCodeAt(hi))
183
+ const n2 = asciiToBase16(hex.charCodeAt(hi + 1))
184
+ if (n1 === undefined || n2 === undefined) {
185
+ const char = hex[hi] + hex[hi + 1]
186
+ throw EthereumJSErrorWithoutCode(
187
+ 'hex string expected, got non-hex character "' + char + '" at index ' + hi,
188
+ )
189
+ }
190
+ array[ai] = n1 * 16 + n2
191
+ }
192
+ return array
193
+ }
194
+
195
+ /** Concatenates two Uint8Arrays into one. */
196
+ function concatBytes(...arrays: Uint8Array[]): Uint8Array {
197
+ if (arrays.length === 1) return arrays[0]
198
+ const length = arrays.reduce((a, arr) => a + arr.length, 0)
199
+ const result = new Uint8Array(length)
200
+ for (let i = 0, pad = 0; i < arrays.length; i++) {
201
+ const arr = arrays[i]
202
+ result.set(arr, pad)
203
+ pad += arr.length
204
+ }
205
+ return result
206
+ }
207
+
208
+ // Global symbols in both browsers and Node.js since v11
209
+ // See https://github.com/microsoft/TypeScript/issues/31535
210
+ declare const TextEncoder: any
211
+
212
+ function utf8ToBytes(utf: string): Uint8Array {
213
+ return new TextEncoder().encode(utf)
214
+ }
215
+
216
+ /** Transform an integer into its hexadecimal value */
217
+ function numberToHex(integer: number | bigint): string {
218
+ if (integer < 0) {
219
+ throw EthereumJSErrorWithoutCode('Invalid integer as argument, must be unsigned!')
220
+ }
221
+ const hex = integer.toString(16)
222
+ return hex.length % 2 ? `0${hex}` : hex
223
+ }
224
+
225
+ /** Pad a string to be even */
226
+ function padToEven(a: string): string {
227
+ return a.length % 2 ? `0${a}` : a
228
+ }
229
+
230
+ /** Check if a string is prefixed by 0x */
231
+ function isHexString(str: string): boolean {
232
+ return str.length >= 2 && str[0] === '0' && str[1] === 'x'
233
+ }
234
+
235
+ /** Removes 0x from a given String */
236
+ function stripHexPrefix(str: string): string {
237
+ if (typeof str !== 'string') {
238
+ return str
239
+ }
240
+ return isHexString(str) ? str.slice(2) : str
241
+ }
242
+
243
+ /** Transform anything into a Uint8Array */
244
+ function toBytes(v: Input): Uint8Array {
245
+ if (v instanceof Uint8Array) {
246
+ return v
247
+ }
248
+ if (typeof v === 'string') {
249
+ if (isHexString(v)) {
250
+ return hexToBytes(padToEven(stripHexPrefix(v)))
251
+ }
252
+ return utf8ToBytes(v)
253
+ }
254
+ if (typeof v === 'number' || typeof v === 'bigint') {
255
+ if (!v) {
256
+ return Uint8Array.from([])
257
+ }
258
+ return hexToBytes(numberToHex(v))
259
+ }
260
+ if (v === null || v === undefined) {
261
+ return Uint8Array.from([])
262
+ }
263
+ throw EthereumJSErrorWithoutCode('toBytes: received unsupported type ' + typeof v)
264
+ }
265
+
266
+ /**
267
+ * RLP Encoding based on https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/
268
+ * This function takes in data, converts it to Uint8Array if not,
269
+ * and adds a length for recursion.
270
+ * @param input Will be converted to Uint8Array
271
+ * @returns Uint8Array of encoded data
272
+ **/
273
+ export function encode(input: Input): Uint8Array {
274
+ if (Array.isArray(input)) {
275
+ const output: Uint8Array[] = []
276
+ let outputLength = 0
277
+ for (let i = 0; i < input.length; i++) {
278
+ const encoded = encode(input[i])
279
+ output.push(encoded)
280
+ outputLength += encoded.length
281
+ }
282
+ return concatBytes(encodeLength(outputLength, 192), ...output)
283
+ }
284
+ const inputBuf = toBytes(input)
285
+ if (inputBuf.length === 1 && inputBuf[0] < 128) {
286
+ return inputBuf
287
+ }
288
+ return concatBytes(encodeLength(inputBuf.length, 128), inputBuf)
289
+ }
290
+
291
+ /**
292
+ * RLP Decoding based on https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/
293
+ * @param input Will be converted to Uint8Array
294
+ * @param stream Is the input a stream (false by default)
295
+ * @returns decoded Array of Uint8Arrays containing the original message
296
+ **/
297
+ export function decode(input: Input, stream?: false): Uint8Array | NestedUint8Array
298
+ export function decode(input: Input, stream?: true): Decoded
299
+ export function decode(input: Input, stream = false): Uint8Array | NestedUint8Array | Decoded {
300
+ if (typeof input === 'undefined' || input === null || (input as any).length === 0) {
301
+ return Uint8Array.from([])
302
+ }
303
+
304
+ const inputBytes = toBytes(input)
305
+ const decoded = _decode(inputBytes)
306
+
307
+ if (stream) {
308
+ return {
309
+ data: decoded.data,
310
+ remainder: decoded.remainder.slice(),
311
+ }
312
+ }
313
+ if (decoded.remainder.length !== 0) {
314
+ throw EthereumJSErrorWithoutCode('invalid RLP: remainder must be zero')
315
+ }
316
+
317
+ return decoded.data
318
+ }
319
+
320
+ export const utils = {
321
+ bytesToHex,
322
+ concatBytes,
323
+ hexToBytes,
324
+ utf8ToBytes,
325
+ }
326
+
327
+ export const RLP = { encode, decode }