@subsquid/evm-codec 0.3.0 → 1.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.
Files changed (79) hide show
  1. package/lib/codec.d.ts +57 -12
  2. package/lib/codec.d.ts.map +1 -1
  3. package/lib/codec.js.map +1 -1
  4. package/lib/codecs/array.d.ts +1 -3
  5. package/lib/codecs/array.d.ts.map +1 -1
  6. package/lib/codecs/array.js +13 -9
  7. package/lib/codecs/array.js.map +1 -1
  8. package/lib/codecs/primitives.d.ts +2 -17
  9. package/lib/codecs/primitives.d.ts.map +1 -1
  10. package/lib/codecs/primitives.js +46 -152
  11. package/lib/codecs/primitives.js.map +1 -1
  12. package/lib/codecs/struct.d.ts +16 -9
  13. package/lib/codecs/struct.d.ts.map +1 -1
  14. package/lib/codecs/struct.js +53 -40
  15. package/lib/codecs/struct.js.map +1 -1
  16. package/lib/dsl.d.ts +11 -0
  17. package/lib/dsl.d.ts.map +1 -0
  18. package/lib/dsl.js +33 -0
  19. package/lib/dsl.js.map +1 -0
  20. package/lib/index.d.ts +7 -4
  21. package/lib/index.d.ts.map +1 -1
  22. package/lib/index.js +7 -6
  23. package/lib/index.js.map +1 -1
  24. package/lib/sink/bounds.d.ts +21 -0
  25. package/lib/sink/bounds.d.ts.map +1 -0
  26. package/lib/sink/bounds.js +24 -0
  27. package/lib/sink/bounds.js.map +1 -0
  28. package/lib/sink/bytes.d.ts +41 -0
  29. package/lib/sink/bytes.d.ts.map +1 -0
  30. package/lib/sink/bytes.js +261 -0
  31. package/lib/sink/bytes.js.map +1 -0
  32. package/lib/sink/hex.d.ts +33 -0
  33. package/lib/sink/hex.d.ts.map +1 -0
  34. package/lib/sink/hex.js +289 -0
  35. package/lib/sink/hex.js.map +1 -0
  36. package/lib/{src.d.ts → src/bytes.d.ts} +7 -5
  37. package/lib/src/bytes.d.ts.map +1 -0
  38. package/lib/src/bytes.js +161 -0
  39. package/lib/src/bytes.js.map +1 -0
  40. package/lib/src/hex.d.ts +33 -0
  41. package/lib/src/hex.d.ts.map +1 -0
  42. package/lib/src/hex.js +164 -0
  43. package/lib/src/hex.js.map +1 -0
  44. package/lib/util.d.ts +6 -0
  45. package/lib/util.d.ts.map +1 -0
  46. package/lib/util.js +20 -0
  47. package/lib/util.js.map +1 -0
  48. package/package.json +6 -8
  49. package/src/codec.ts +65 -19
  50. package/src/codecs/array.test.ts +87 -0
  51. package/src/codecs/array.ts +6 -11
  52. package/src/codecs/primitives.test.ts +27 -0
  53. package/src/codecs/primitives.ts +87 -208
  54. package/src/codecs/struct.test.ts +69 -0
  55. package/src/codecs/struct.ts +80 -61
  56. package/src/dsl.ts +16 -0
  57. package/src/index.ts +7 -4
  58. package/src/sink/bounds.ts +26 -0
  59. package/src/sink/bytes.test.ts +92 -0
  60. package/src/sink/bytes.ts +290 -0
  61. package/src/sink/hex.ts +311 -0
  62. package/src/src/bytes.test.ts +114 -0
  63. package/src/src/bytes.ts +187 -0
  64. package/src/src/hex.ts +191 -0
  65. package/src/util.ts +19 -0
  66. package/lib/safeToNumber.d.ts +0 -2
  67. package/lib/safeToNumber.d.ts.map +0 -1
  68. package/lib/safeToNumber.js +0 -11
  69. package/lib/safeToNumber.js.map +0 -1
  70. package/lib/sink.d.ts +0 -43
  71. package/lib/sink.d.ts.map +0 -1
  72. package/lib/sink.js +0 -215
  73. package/lib/sink.js.map +0 -1
  74. package/lib/src.d.ts.map +0 -1
  75. package/lib/src.js +0 -141
  76. package/lib/src.js.map +0 -1
  77. package/src/safeToNumber.ts +0 -6
  78. package/src/sink.ts +0 -241
  79. package/src/src.ts +0 -158
@@ -0,0 +1,92 @@
1
+ import {describe, expect, it} from 'vitest'
2
+ import {BytesSink, BytesSrc} from '..'
3
+
4
+ describe('sink', () => {
5
+ it('negative numbers round-trip through Src', () => {
6
+ const sink = new BytesSink(6)
7
+ sink.i8(-1)
8
+ sink.i16(-123)
9
+ sink.i32(-123456)
10
+ sink.i64(-1234567890n)
11
+ sink.i128(-12345678901234567890n)
12
+ sink.i256(-1234567890123456789012345678901234567890n)
13
+
14
+ const src = new BytesSrc(sink.result())
15
+ expect(src.i8()).toBe(-1)
16
+ expect(src.i16()).toBe(-123)
17
+ expect(src.i32()).toBe(-123456)
18
+ expect(src.i64()).toBe(-1234567890n)
19
+ expect(src.i128()).toBe(-12345678901234567890n)
20
+ expect(src.i256()).toBe(-1234567890123456789012345678901234567890n)
21
+ })
22
+
23
+ it('number overflow', () => {
24
+ const sink = new BytesSink(1)
25
+ expect(() => sink.u8(0x1234567890)).toThrowError('78187493520 is out of bounds for uint8[0, 255]')
26
+ expect(() => sink.i8(0x1234567890)).toThrowError('78187493520 is out of bounds for int8[-128, 127]')
27
+ expect(() => sink.u16(0x1234567890)).toThrowError('78187493520 is out of bounds for uint16[0, 65535]')
28
+ expect(() => sink.i16(0x1234567890)).toThrowError('78187493520 is out of bounds for int16[-32768, 32767]')
29
+ expect(() => sink.u32(0x1234567890)).toThrowError('78187493520 is out of bounds for uint32[0, 4294967295]')
30
+ expect(() => sink.i32(0x1234567890)).toThrowError('78187493520 is out of bounds for int32[-2147483648, 2147483647]')
31
+ })
32
+
33
+ it('mixed static types round-trip through Src', () => {
34
+ const sink = new BytesSink(5)
35
+ sink.u8(1)
36
+ sink.i8(-2)
37
+ sink.address('0x1234567890123456789012345678901234567890')
38
+ sink.u256(3n)
39
+ sink.staticBytes(7, Buffer.from('1234567890abcd', 'hex'))
40
+
41
+ const src = new BytesSrc(sink.result())
42
+ expect(src.u8()).toBe(1)
43
+ expect(src.i8()).toBe(-2)
44
+ expect(src.address()).toBe('0x1234567890123456789012345678901234567890')
45
+ expect(src.u256()).toBe(3n)
46
+ expect(src.staticBytes(7)).toStrictEqual(new Uint8Array(Buffer.from('1234567890abcd', 'hex')))
47
+ })
48
+
49
+ describe('string', () => {
50
+ function testString(value: string) {
51
+ const sink = new BytesSink(1)
52
+ sink.openTail()
53
+ sink.string(value)
54
+ sink.closeTail()
55
+ expect(new BytesSrc(sink.result()).string()).toBe(value)
56
+ }
57
+
58
+ it('short string', () => testString('hello'))
59
+ it('32 byte string', () => testString('this string length is 32 bytes!!'))
60
+ it('longer string', () => testString('this string length is 33 bytes!!!'))
61
+ it('UTF', () => testString('привет 👍'))
62
+ })
63
+
64
+ it('bytes', () => {
65
+ const sink = new BytesSink(1)
66
+ sink.openTail()
67
+ const buffer = Buffer.alloc(150)
68
+ buffer.fill('xd')
69
+ sink.bytes(buffer)
70
+ sink.closeTail()
71
+ expect(new BytesSrc(sink.result()).bytes()).toStrictEqual(new Uint8Array(buffer))
72
+ })
73
+
74
+ it('string encodes to canonical 32-byte-aligned ABI layout', () => {
75
+ // Pin the exact on-wire layout for one value so we catch regressions
76
+ // in the offset/length word without needing an external reference
77
+ // implementation on the hot path.
78
+ const sink = new BytesSink(1)
79
+ sink.openTail()
80
+ sink.string('hello')
81
+ sink.closeTail()
82
+ expect(sink.toString()).toBe(
83
+ '0x' +
84
+ // offset to the dynamic region (0x20)
85
+ '0000000000000000000000000000000000000000000000000000000000000020' +
86
+ // length (5)
87
+ '0000000000000000000000000000000000000000000000000000000000000005' +
88
+ // "hello" left-aligned, zero-padded to 32 bytes
89
+ '68656c6c6f000000000000000000000000000000000000000000000000000000',
90
+ )
91
+ })
92
+ })
@@ -0,0 +1,290 @@
1
+ import assert from 'node:assert'
2
+ import {decodeHex, isHex, toHex} from '@subsquid/util-internal-hex'
3
+ import {WORD_SIZE, type Sink} from '../codec'
4
+ import {
5
+ TEXT_ENCODER,
6
+ U256_BASE,
7
+ U8_MAX, U16_MAX, U32_MAX,
8
+ I8_MIN, I8_MAX, I16_MIN, I16_MAX, I32_MIN, I32_MAX,
9
+ U64_MAX_BI, I64_MIN_BI, I64_MAX_BI,
10
+ U128_MAX_BI, I128_MIN_BI, I128_MAX_BI,
11
+ U256_MAX_BI, I256_MIN_BI, I256_MAX_BI,
12
+ } from './bounds'
13
+
14
+ const U64_MASK = 0xffffffffffffffffn
15
+ const U128_MASK = (1n << 128n) - 1n
16
+
17
+ export class BytesSink implements Sink {
18
+ protected pos = 0
19
+ protected buf: Uint8Array
20
+ protected view: DataView
21
+ private stack: {start: number; jumpBackPtr: number; size: number; countWord: boolean}[] = []
22
+
23
+ constructor(fields: number, capacity = 1280) {
24
+ this.stack.push({
25
+ start: 0,
26
+ jumpBackPtr: 0,
27
+ size: fields * WORD_SIZE,
28
+ countWord: false,
29
+ })
30
+ this.buf = new Uint8Array(capacity)
31
+ this.view = new DataView(this.buf.buffer, this.buf.byteOffset, this.buf.byteLength)
32
+ }
33
+
34
+ result(): Uint8Array {
35
+ assert(this.stack.length === 1, 'Cannot get result during dynamic encoding')
36
+ return this.buf.subarray(0, this.size())
37
+ }
38
+
39
+ toString() {
40
+ const size = this.size()
41
+ return toHex(this.buf, 0, size)
42
+ }
43
+
44
+ reserve(additional: number): void {
45
+ if (this.buf.length - this.pos < additional) {
46
+ this.allocate(this.pos + additional)
47
+ }
48
+ }
49
+
50
+ written(): Uint8Array {
51
+ return this.buf.subarray(0, this.pos)
52
+ }
53
+
54
+ mark(): number {
55
+ return this.pos
56
+ }
57
+
58
+ raw(val: Uint8Array | string): void {
59
+ const bytes = typeof val === 'string' ? this.#stringToBytes(val) : val
60
+ this.reserve(bytes.length)
61
+ this.buf.set(bytes, this.pos)
62
+ this.pos += bytes.length
63
+ }
64
+
65
+ utf8(val: string): void {
66
+ this.raw(TEXT_ENCODER.encode(val))
67
+ }
68
+
69
+ padFrom(pos: number): void {
70
+ const rem = (this.pos - pos) % WORD_SIZE
71
+ if (rem === 0) return
72
+ const pad = WORD_SIZE - rem
73
+ this.reserve(pad)
74
+ this.buf.fill(0, this.pos, this.pos + pad)
75
+ this.pos += pad
76
+ }
77
+
78
+ size() {
79
+ return this.stack[this.stack.length - 1].size
80
+ }
81
+
82
+ protected allocate(cap: number): void {
83
+ cap = Math.max(cap, this.buf.length * 2)
84
+ const buf = new Uint8Array(cap)
85
+ buf.set(this.buf)
86
+ this.buf = buf
87
+ this.view = new DataView(this.buf.buffer, this.buf.byteOffset, this.buf.byteLength)
88
+ }
89
+
90
+ u8(val: number) {
91
+ if (val < 0 || val > U8_MAX) this.#oob(val, 'uint8', 0, U8_MAX)
92
+ this.reserve(WORD_SIZE)
93
+ this.pos += WORD_SIZE - 1
94
+ this.view.setUint8(this.pos, val)
95
+ this.pos += 1
96
+ }
97
+
98
+ i8(val: number) {
99
+ if (val < I8_MIN || val > I8_MAX) this.#oob(val, 'int8', I8_MIN, I8_MAX)
100
+ this.#i256(BigInt(val))
101
+ }
102
+
103
+ u16(val: number) {
104
+ if (val < 0 || val > U16_MAX) this.#oob(val, 'uint16', 0, U16_MAX)
105
+ this.reserve(WORD_SIZE)
106
+ this.pos += WORD_SIZE - 2
107
+ this.view.setUint16(this.pos, val, false)
108
+ this.pos += 2
109
+ }
110
+
111
+ i16(val: number) {
112
+ if (val < I16_MIN || val > I16_MAX) this.#oob(val, 'int16', I16_MIN, I16_MAX)
113
+ this.#i256(BigInt(val))
114
+ }
115
+
116
+ u32(val: number) {
117
+ if (val < 0 || val > U32_MAX) this.#oob(val, 'uint32', 0, U32_MAX)
118
+ this.reserve(WORD_SIZE)
119
+ this.pos += WORD_SIZE - 4
120
+ this.view.setUint32(this.pos, val, false)
121
+ this.pos += 4
122
+ }
123
+
124
+ #u32Raw(val: number) {
125
+ this.view.setUint32(this.pos + WORD_SIZE - 4, val, false)
126
+ this.pos += WORD_SIZE
127
+ }
128
+
129
+ i32(val: number) {
130
+ if (val < I32_MIN || val > I32_MAX) this.#oob(val, 'int32', I32_MIN, I32_MAX)
131
+ this.#i256(BigInt(val))
132
+ }
133
+
134
+ u64(val: bigint) {
135
+ if (val < 0n || val > U64_MAX_BI) this.#oob(val, 'uint64', 0n, U64_MAX_BI)
136
+ this.reserve(WORD_SIZE)
137
+ this.pos += WORD_SIZE - 8
138
+ this.view.setBigUint64(this.pos, val, false)
139
+ this.pos += 8
140
+ }
141
+
142
+ i64(val: bigint) {
143
+ if (val < I64_MIN_BI || val > I64_MAX_BI) this.#oob(val, 'int64', I64_MIN_BI, I64_MAX_BI)
144
+ this.#i256(val)
145
+ }
146
+
147
+ private writeU64(val: bigint) {
148
+ this.view.setBigUint64(this.pos, val, false)
149
+ this.pos += 8
150
+ }
151
+
152
+ u128(val: bigint) {
153
+ if (val < 0n || val > U128_MAX_BI) this.#oob(val, 'uint128', 0n, U128_MAX_BI)
154
+ this.reserve(WORD_SIZE)
155
+ this.pos += WORD_SIZE - 16
156
+ this.writeU64(val >> 64n)
157
+ this.writeU64(val & U64_MASK)
158
+ }
159
+
160
+ i128(val: bigint) {
161
+ if (val < I128_MIN_BI || val > I128_MAX_BI) this.#oob(val, 'int128', I128_MIN_BI, I128_MAX_BI)
162
+ this.#i256(val)
163
+ }
164
+
165
+ #u128Raw(val: bigint) {
166
+ this.writeU64(val >> 64n)
167
+ this.writeU64(val & U64_MASK)
168
+ }
169
+
170
+ u256(val: bigint) {
171
+ if (val < 0n || val > U256_MAX_BI) this.#oob(val, 'uint256', 0n, U256_MAX_BI)
172
+ this.reserve(WORD_SIZE)
173
+ this.#u128Raw(val >> 128n)
174
+ this.#u128Raw(val & U128_MASK)
175
+ }
176
+
177
+ i256(val: bigint) {
178
+ if (val < I256_MIN_BI || val > I256_MAX_BI) this.#oob(val, 'int256', I256_MIN_BI, I256_MAX_BI)
179
+ this.#i256(val)
180
+ }
181
+
182
+ #i256(val: bigint) {
183
+ const uval = val >= 0n ? val : val + U256_BASE
184
+ this.reserve(WORD_SIZE)
185
+ this.#u128Raw(uval >> 128n)
186
+ this.#u128Raw(uval & U128_MASK)
187
+ }
188
+
189
+ bytes(val: Uint8Array | string) {
190
+ const bytes = typeof val === 'string' ? this.#stringToBytes(val) : val
191
+ const size = bytes.length
192
+ this.u32(size)
193
+ const reservedSize = Math.ceil(size / WORD_SIZE) * WORD_SIZE
194
+ this.reserve(reservedSize)
195
+ this.buf.set(bytes, this.pos)
196
+ this.pos += reservedSize
197
+ this.#increaseCurrentDataAreaSize(reservedSize + WORD_SIZE)
198
+ }
199
+
200
+ staticBytes(len: number, val: Uint8Array | string) {
201
+ if (len > 32) {
202
+ throw new Error(`bytes${len} is not a valid type`)
203
+ }
204
+ const bytes = typeof val === 'string' ? this.#stringToBytes(val) : val
205
+ if (bytes.length > len) {
206
+ throw new Error(`invalid data size for bytes${len}`)
207
+ }
208
+ this.reserve(WORD_SIZE)
209
+ this.buf.set(bytes, this.pos)
210
+ this.pos += WORD_SIZE
211
+ }
212
+
213
+ #stringToBytes(val: string): Uint8Array {
214
+ if (!isHex(val)) {
215
+ throw new Error(`Expected hex string or Uint8Array, got: ${val}`)
216
+ }
217
+ return decodeHex(val)
218
+ }
219
+
220
+ address(val: string) {
221
+ if (val.length === 42 && val.charCodeAt(0) === 0x30 && val.charCodeAt(1) === 0x78) {
222
+ this.reserve(WORD_SIZE)
223
+ this.buf.fill(0, this.pos, this.pos + 12)
224
+ const view = Buffer.from(this.buf.buffer, this.buf.byteOffset + this.pos + 12, 20)
225
+ if (view.write(val.slice(2), 'hex') === 20) {
226
+ this.pos += WORD_SIZE
227
+ return
228
+ }
229
+ }
230
+ this.u256(BigInt(val))
231
+ }
232
+
233
+ string(val: string) {
234
+ this.bytes(TEXT_ENCODER.encode(val))
235
+ }
236
+
237
+ bool(val: boolean) {
238
+ this.u8(val ? 1 : 0)
239
+ }
240
+
241
+ #oob(val: bigint | number, typeName: string, min: bigint | number, max: bigint | number): never {
242
+ throw new Error(`${val} is out of bounds for ${typeName}[${min}, ${max}]`)
243
+ }
244
+
245
+ openTail(slotsCount = 0) {
246
+ const offset = this.size()
247
+ this.reserve(WORD_SIZE)
248
+ this.#u32Raw(offset)
249
+ const dataAreaStart = this.#currentDataAreaStart()
250
+ this.#pushDataArea(dataAreaStart + offset, slotsCount, false)
251
+ this.pos = dataAreaStart + offset
252
+ }
253
+
254
+ openArray(count: number) {
255
+ const offset = this.size()
256
+ this.reserve(WORD_SIZE)
257
+ this.#u32Raw(offset)
258
+ const dataAreaStart = this.#currentDataAreaStart()
259
+ this.#pushDataArea(dataAreaStart + offset + WORD_SIZE, count, true)
260
+ this.pos = dataAreaStart + offset
261
+ this.reserve(WORD_SIZE)
262
+ this.#u32Raw(count)
263
+ }
264
+
265
+ #currentDataAreaStart() {
266
+ return this.stack[this.stack.length - 1].start
267
+ }
268
+
269
+ #increaseCurrentDataAreaSize(amount: number) {
270
+ this.stack[this.stack.length - 1].size += amount
271
+ }
272
+
273
+ #pushDataArea(dataAreaStart: number, slotsCount: number, countWord: boolean) {
274
+ const size = slotsCount * WORD_SIZE
275
+ this.reserve(dataAreaStart + size)
276
+ this.stack.push({
277
+ start: dataAreaStart,
278
+ jumpBackPtr: this.pos,
279
+ size,
280
+ countWord,
281
+ })
282
+ }
283
+
284
+ closeTail() {
285
+ assert(this.stack.length > 1, 'No dynamic encoding started')
286
+ const {jumpBackPtr, size, countWord} = this.stack.pop()!
287
+ this.#increaseCurrentDataAreaSize(size + (countWord ? WORD_SIZE : 0))
288
+ this.pos = jumpBackPtr
289
+ }
290
+ }
@@ -0,0 +1,311 @@
1
+ import assert from 'node:assert'
2
+ import {isHex} from '@subsquid/util-internal-hex'
3
+ import {WORD_SIZE, type Sink} from '../codec'
4
+ import {
5
+ TEXT_ENCODER,
6
+ U256_BASE,
7
+ U8_MAX, U16_MAX, U32_MAX,
8
+ I8_MIN, I8_MAX, I16_MIN, I16_MAX, I32_MIN, I32_MAX,
9
+ U64_MAX_BI, I64_MIN_BI, I64_MAX_BI,
10
+ U128_MAX_BI, I128_MIN_BI, I128_MAX_BI,
11
+ U256_MAX_BI, I256_MIN_BI, I256_MAX_BI,
12
+ } from './bounds'
13
+
14
+ const HEX_BYTE = 2
15
+ const WORD_HEX = WORD_SIZE * HEX_BYTE
16
+
17
+ const CODE_0 = 48 // '0'
18
+ const CODE_A_UPPER = 65 // 'A'
19
+ const CODE_F_UPPER = 70 // 'F'
20
+
21
+ // Precomputed ASCII codes for hex nibbles 0–15 ('0'–'9', 'a'–'f').
22
+ const NIBBLE_CODE = new Uint8Array(16)
23
+ for (let i = 0; i < 10; i++) NIBBLE_CODE[i] = 48 + i
24
+ for (let i = 10; i < 16; i++) NIBBLE_CODE[i] = 87 + i
25
+
26
+ // At or above this many input bytes the native Buffer.toString('hex') path
27
+ // is cheaper than a per-byte JS loop.
28
+ const BUFFER_HEX_CUTOFF = 64
29
+
30
+ export class HexSink implements Sink {
31
+ private pos = 0
32
+ private buf: Buffer
33
+ private stack: {start: number; jumpBackPtr: number; size: number; countWord: boolean}[] = []
34
+
35
+ constructor(fields: number, capacity = Math.max(fields * WORD_SIZE, 1280)) {
36
+ this.buf = Buffer.alloc(capacity * HEX_BYTE, CODE_0)
37
+ this.stack.push({
38
+ start: 0,
39
+ jumpBackPtr: 0,
40
+ size: fields * WORD_SIZE,
41
+ countWord: false,
42
+ })
43
+ }
44
+
45
+ result(): string {
46
+ assert(this.stack.length === 1, 'Cannot get result during dynamic encoding')
47
+ const chars = this.size() * HEX_BYTE
48
+ return `0x${this.buf.toString('latin1', 0, chars)}`
49
+ }
50
+
51
+ toString(): string {
52
+ return this.result()
53
+ }
54
+
55
+ reserve(additional: number): void {
56
+ const needed = (this.pos + additional) * HEX_BYTE
57
+ if (this.buf.length < needed) {
58
+ this.#allocate(needed)
59
+ }
60
+ }
61
+
62
+ size() {
63
+ return this.stack[this.stack.length - 1].size
64
+ }
65
+
66
+ #allocate(cap: number): void {
67
+ cap = Math.max(cap, this.buf.length * 2)
68
+ const grown = Buffer.alloc(cap, CODE_0)
69
+ grown.set(this.buf)
70
+ this.buf = grown
71
+ }
72
+
73
+ // Right-align `hex` in a 32-byte ABI word at the current position.
74
+ // Left padding is implicit because buf is pre-filled with '0'.
75
+ #writeWordRight(hex: string): void {
76
+ assert(hex.length <= WORD_HEX, `hex string too long for one word: ${hex.length} chars`)
77
+ const buf = this.buf
78
+ const start = this.pos * HEX_BYTE + WORD_HEX - hex.length
79
+ for (let i = 0; i < hex.length; i++) {
80
+ buf[start + i] = hex.charCodeAt(i)
81
+ }
82
+ this.pos += WORD_SIZE
83
+ }
84
+
85
+ // Copy `len` hex chars from `hex` starting at `srcStart` into buf at
86
+ // `dstStart`, lowercasing any A–F so output is canonical lowercase.
87
+ #copyHexLower(hex: string, srcStart: number, dstStart: number, len: number): void {
88
+ const buf = this.buf
89
+ for (let i = 0; i < len; i++) {
90
+ const c = hex.charCodeAt(srcStart + i)
91
+ buf[dstStart + i] = c >= CODE_A_UPPER && c <= CODE_F_UPPER ? c + 32 : c
92
+ }
93
+ }
94
+
95
+ // Encode binary `src` bytes as hex ASCII codes directly into buf at
96
+ // `dstStart` using the NIBBLE_CODE lookup table.
97
+ #encodeBytes(src: Uint8Array, srcLen: number, dstStart: number): void {
98
+ const buf = this.buf
99
+ for (let i = 0; i < srcLen; i++) {
100
+ const b = src[i]
101
+ buf[dstStart + i * 2] = NIBBLE_CODE[b >> 4]
102
+ buf[dstStart + i * 2 + 1] = NIBBLE_CODE[b & 0xf]
103
+ }
104
+ }
105
+
106
+ u8(val: number) {
107
+ if (val < 0 || val > U8_MAX) this.#oob(val, 'uint8', 0, U8_MAX)
108
+ this.reserve(WORD_SIZE)
109
+ this.#writeWordRight(val.toString(16))
110
+ }
111
+
112
+ i8(val: number) {
113
+ if (val < I8_MIN || val > I8_MAX) this.#oob(val, 'int8', I8_MIN, I8_MAX)
114
+ this.#i256(BigInt(val))
115
+ }
116
+
117
+ u16(val: number) {
118
+ if (val < 0 || val > U16_MAX) this.#oob(val, 'uint16', 0, U16_MAX)
119
+ this.reserve(WORD_SIZE)
120
+ this.#writeWordRight(val.toString(16))
121
+ }
122
+
123
+ i16(val: number) {
124
+ if (val < I16_MIN || val > I16_MAX) this.#oob(val, 'int16', I16_MIN, I16_MAX)
125
+ this.#i256(BigInt(val))
126
+ }
127
+
128
+ u32(val: number) {
129
+ if (val < 0 || val > U32_MAX) this.#oob(val, 'uint32', 0, U32_MAX)
130
+ this.reserve(WORD_SIZE)
131
+ this.#writeWordRight(val.toString(16))
132
+ }
133
+
134
+ #u32Raw(val: number) {
135
+ this.#writeWordRight(val.toString(16))
136
+ }
137
+
138
+ i32(val: number) {
139
+ if (val < I32_MIN || val > I32_MAX) this.#oob(val, 'int32', I32_MIN, I32_MAX)
140
+ this.#i256(BigInt(val))
141
+ }
142
+
143
+ u64(val: bigint) {
144
+ if (val < 0n || val > U64_MAX_BI) this.#oob(val, 'uint64', 0n, U64_MAX_BI)
145
+ this.reserve(WORD_SIZE)
146
+ this.#writeWordRight(val.toString(16))
147
+ }
148
+
149
+ i64(val: bigint) {
150
+ if (val < I64_MIN_BI || val > I64_MAX_BI) this.#oob(val, 'int64', I64_MIN_BI, I64_MAX_BI)
151
+ this.#i256(val)
152
+ }
153
+
154
+ u128(val: bigint) {
155
+ if (val < 0n || val > U128_MAX_BI) this.#oob(val, 'uint128', 0n, U128_MAX_BI)
156
+ this.reserve(WORD_SIZE)
157
+ this.#writeWordRight(val.toString(16))
158
+ }
159
+
160
+ i128(val: bigint) {
161
+ if (val < I128_MIN_BI || val > I128_MAX_BI) this.#oob(val, 'int128', I128_MIN_BI, I128_MAX_BI)
162
+ this.#i256(val)
163
+ }
164
+
165
+ u256(val: bigint) {
166
+ if (val < 0n || val > U256_MAX_BI) this.#oob(val, 'uint256', 0n, U256_MAX_BI)
167
+ this.reserve(WORD_SIZE)
168
+ this.#writeWordRight(val.toString(16))
169
+ }
170
+
171
+ i256(val: bigint) {
172
+ if (val < I256_MIN_BI || val > I256_MAX_BI) this.#oob(val, 'int256', I256_MIN_BI, I256_MAX_BI)
173
+ this.#i256(val)
174
+ }
175
+
176
+ #i256(val: bigint) {
177
+ const uval = val >= 0n ? val : val + U256_BASE
178
+ this.reserve(WORD_SIZE)
179
+ this.#writeWordRight(uval.toString(16))
180
+ }
181
+
182
+ bytes(val: Uint8Array | string) {
183
+ if (typeof val === 'string') {
184
+ if (!isHex(val)) {
185
+ throw new Error(`Expected hex string or Uint8Array, got: ${val}`)
186
+ }
187
+ const nibbles = val.length - 2
188
+ const size = nibbles >> 1
189
+ this.u32(size)
190
+ const wordsCount = (size + WORD_SIZE - 1) >> 5
191
+ const reservedSize = wordsCount << 5
192
+ this.reserve(reservedSize)
193
+ const dst = this.pos * HEX_BYTE
194
+ if (nibbles >= BUFFER_HEX_CUTOFF * HEX_BYTE) {
195
+ this.buf.write(val.slice(2).toLowerCase(), dst, nibbles, 'latin1')
196
+ } else {
197
+ this.#copyHexLower(val, 2, dst, nibbles)
198
+ }
199
+ this.pos += reservedSize
200
+ this.#increaseCurrentDataAreaSize(reservedSize + WORD_SIZE)
201
+ } else {
202
+ const size = val.length
203
+ this.u32(size)
204
+ const wordsCount = (size + WORD_SIZE - 1) >> 5
205
+ const reservedSize = wordsCount << 5
206
+ this.reserve(reservedSize)
207
+ const dst = this.pos * HEX_BYTE
208
+ if (size >= BUFFER_HEX_CUTOFF) {
209
+ const hex = Buffer.from(val.buffer, val.byteOffset, size).toString('hex')
210
+ this.buf.write(hex, dst, hex.length, 'latin1')
211
+ } else {
212
+ this.#encodeBytes(val, size, dst)
213
+ }
214
+ this.pos += reservedSize
215
+ this.#increaseCurrentDataAreaSize(reservedSize + WORD_SIZE)
216
+ }
217
+ }
218
+
219
+ staticBytes(len: number, val: Uint8Array | string) {
220
+ if (len > 32) {
221
+ throw new Error(`bytes${len} is not a valid type`)
222
+ }
223
+ this.reserve(WORD_SIZE)
224
+ if (typeof val === 'string') {
225
+ if (!isHex(val)) {
226
+ throw new Error(`Expected hex string or Uint8Array, got: ${val}`)
227
+ }
228
+ const nibbles = val.length - 2
229
+ if (nibbles > len * HEX_BYTE) {
230
+ throw new Error(`invalid data size for bytes${len}`)
231
+ }
232
+ this.#copyHexLower(val, 2, this.pos * HEX_BYTE, nibbles)
233
+ this.pos += WORD_SIZE
234
+ } else {
235
+ if (val.length > len) {
236
+ throw new Error(`invalid data size for bytes${len}`)
237
+ }
238
+ this.#encodeBytes(val, val.length, this.pos * HEX_BYTE)
239
+ this.pos += WORD_SIZE
240
+ }
241
+ }
242
+
243
+ address(val: string) {
244
+ if (val.length === 42 && val.charCodeAt(0) === 0x30 && val.charCodeAt(1) === 0x78) {
245
+ this.reserve(WORD_SIZE)
246
+ // 20-byte address right-aligned in a 32-byte word: skip 12 zero bytes (24 hex chars) of padding
247
+ this.#copyHexLower(val, 2, this.pos * HEX_BYTE + 24, 40)
248
+ this.pos += WORD_SIZE
249
+ return
250
+ }
251
+ this.u256(BigInt(val))
252
+ }
253
+
254
+ string(val: string) {
255
+ this.bytes(TEXT_ENCODER.encode(val))
256
+ }
257
+
258
+ bool(val: boolean) {
259
+ this.u8(val ? 1 : 0)
260
+ }
261
+
262
+ #oob(val: bigint | number, typeName: string, min: bigint | number, max: bigint | number): never {
263
+ throw new Error(`${val} is out of bounds for ${typeName}[${min}, ${max}]`)
264
+ }
265
+
266
+ openTail(slotsCount = 0) {
267
+ const offset = this.size()
268
+ this.reserve(WORD_SIZE)
269
+ this.#u32Raw(offset)
270
+ const dataAreaStart = this.#currentDataAreaStart()
271
+ this.#pushDataArea(dataAreaStart + offset, slotsCount, false)
272
+ this.pos = dataAreaStart + offset
273
+ }
274
+
275
+ openArray(count: number) {
276
+ const offset = this.size()
277
+ this.reserve(WORD_SIZE)
278
+ this.#u32Raw(offset)
279
+ const dataAreaStart = this.#currentDataAreaStart()
280
+ this.#pushDataArea(dataAreaStart + offset + WORD_SIZE, count, true)
281
+ this.pos = dataAreaStart + offset
282
+ this.reserve(WORD_SIZE)
283
+ this.#u32Raw(count)
284
+ }
285
+
286
+ #currentDataAreaStart() {
287
+ return this.stack[this.stack.length - 1].start
288
+ }
289
+
290
+ #increaseCurrentDataAreaSize(amount: number) {
291
+ this.stack[this.stack.length - 1].size += amount
292
+ }
293
+
294
+ #pushDataArea(dataAreaStart: number, slotsCount: number, countWord: boolean) {
295
+ const size = slotsCount * WORD_SIZE
296
+ this.reserve(dataAreaStart + size)
297
+ this.stack.push({
298
+ start: dataAreaStart,
299
+ jumpBackPtr: this.pos,
300
+ size,
301
+ countWord,
302
+ })
303
+ }
304
+
305
+ closeTail() {
306
+ assert(this.stack.length > 1, 'No dynamic encoding started')
307
+ const {jumpBackPtr, size, countWord} = this.stack.pop()!
308
+ this.#increaseCurrentDataAreaSize(size + (countWord ? WORD_SIZE : 0))
309
+ this.pos = jumpBackPtr
310
+ }
311
+ }